blob: d36bba567f4218881dbb609bec14ea0435e951a7 [file] [log] [blame] [edit]
/*
* Copyright 2024 Rive
*/
// Don't compile this file as part of the "tests" project.
#ifndef TESTING
#include "common/test_harness.hpp"
#include "common/testing_window.hpp"
#include "rive/artboard.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/file.hpp"
#include "rive/text/font_hb.hpp"
#include "rive/text/raw_text.hpp"
#include "assets/roboto_flex.ttf.hpp"
#include <stdio.h>
#include <fstream>
#ifdef RIVE_ANDROID
#include "common/rive_android_app.hpp"
#endif
#ifdef __EMSCRIPTEN__
#include "common/rive_wasm_app.hpp"
#include <emscripten/emscripten.h>
#include <emscripten/html5.h>
#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;
}
static int copiesLeft = 0;
static int copiesAbove = 0;
static int copiesRight = 0;
static int copiesBelow = 0;
static int rotations90 = 0;
static int zoomLevel = 0;
static int spacing = 0;
static int monitorIdx = 0;
static bool wireframe = false;
static bool paused = false;
static bool forceFixedDeltaTime = false;
static bool quit = false;
static bool hotloadShaders = false;
static void key_pressed(char key)
{
static int multiplier = 0;
static bool seenDigit = false;
static bool seenBang = false;
if (key >= '0' && key <= '9')
{
multiplier = multiplier * 10 + (key - '0');
seenDigit = true;
return;
}
if (key == '!')
{
seenBang = true;
return;
}
if (!seenDigit)
{
multiplier = seenBang ? 0 : 1;
}
switch (key)
{
case 'h':
case 'H':
update_parameter(copiesLeft, multiplier, key, seenBang);
break;
case 'k':
case 'K':
update_parameter(copiesAbove, multiplier, key, seenBang);
break;
case 'l':
case 'L':
update_parameter(copiesRight, multiplier, key, seenBang);
break;
case 'j':
case 'J':
update_parameter(copiesBelow, multiplier, key, seenBang);
break;
case 'x':
case 'X':
update_parameter(copiesLeft, multiplier, key, seenBang);
update_parameter(copiesRight, multiplier, key, seenBang);
break;
case 'y':
case 'Y':
update_parameter(copiesAbove, multiplier, key, seenBang);
update_parameter(copiesBelow, multiplier, key, seenBang);
break;
case 'r':
case 'R':
update_parameter(rotations90, multiplier, key, seenBang);
break;
case 'z':
case 'Z':
update_parameter(zoomLevel, multiplier, key, seenBang);
break;
case 's':
case 'S':
update_parameter(spacing, multiplier, key, seenBang);
break;
case 'm':
monitorIdx += multiplier;
break;
case 'w':
wireframe = !wireframe;
break;
case 'p':
paused = !paused;
break;
case 'f':
forceFixedDeltaTime = !forceFixedDeltaTime;
break;
case 'q':
case '\x03': // ^C
quit = true;
break;
case '\x1b': // Esc
break;
case '`':
hotloadShaders = true;
break;
default:
// fprintf(stderr, "invalid option: %c\n", key);
// abort();
break;
}
multiplier = 0;
seenDigit = false;
seenBang = false;
}
class Player
{
public:
void init(std::string rivName, std::vector<uint8_t> rivBytes)
{
m_rivName = std::move(rivName);
m_file = rive::File::import(rivBytes, TestingWindow::Get()->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_roboto = HBFont::Decode(assets::roboto_flex_ttf());
m_blackStroke = TestingWindow::Get()->factory()->makeRenderPaint();
m_blackStroke->color(0xff000000);
m_blackStroke->style(rive::RenderPaintStyle::stroke);
m_blackStroke->thickness(4);
m_whiteFill = TestingWindow::Get()->factory()->makeRenderPaint();
m_whiteFill->color(0xffffffff);
m_timeLastFPSUpdate = std::chrono::high_resolution_clock::now();
m_timestampPrevFrame = std::chrono::high_resolution_clock::now();
}
void doFrame()
{
if (quit || TestingWindow::Get()->shouldQuit()
#ifdef RIVE_ANDROID
|| !rive_android_app_poll_once()
#endif
)
{
printf("\nShutting down\n");
TestingWindow::Destroy(); // Exercise our PLS teardown process now
// that we're done.
TestHarness::Instance().shutdown();
#ifdef __EMSCRIPTEN__
emscripten_cancel_main_loop();
EM_ASM(window.close(););
#else
exit(0);
#endif
return;
}
#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 (forceFixedDeltaTime)
{
advanceDeltaTime = 1.0f / 120;
}
m_scene->advanceAndApply(paused ? 0 : advanceDeltaTime);
copiesLeft = std::max(copiesLeft, 0);
copiesAbove = std::max(copiesAbove, 0);
copiesRight = std::max(copiesRight, 0);
copiesBelow = std::max(copiesBelow, 0);
int copyCount =
(copiesLeft + 1 + copiesRight) * (copiesAbove + 1 + copiesBelow);
if (copyCount != lastReportedCopyCount ||
paused != lastReportedPauseState)
{
printf("Drawing %i copies of %s%s at %u x %u\n",
copyCount,
m_rivName.c_str(),
paused ? " (paused)" : "",
TestingWindow::Get()->width(),
TestingWindow::Get()->height());
lastReportedCopyCount = copyCount;
lastReportedPauseState = paused;
}
auto renderer = TestingWindow::Get()->beginFrame({
.clearColor = 0xff303030,
.doClear = true,
.wireframe = wireframe,
});
if (hotloadShaders)
{
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 = rotations90; (i & 3) != 0; --i)
{
renderer->transform(rive::Mat2D(0, 1, -1, 0, width, 0));
std::swap(height, width);
}
if (zoomLevel != 0)
{
float scale = powf(1.25f, 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 = spacing * 5 + 150;
renderer->translate(-spacingPx * copiesLeft, -spacingPx * copiesAbove);
for (int y = -copiesAbove; y <= copiesBelow; ++y)
{
renderer->save();
for (int x = -copiesLeft; x <= copiesRight; ++x)
{
m_artboard->draw(renderer.get());
renderer->translate(spacingPx, 0);
}
renderer->restore();
renderer->translate(0, spacingPx);
}
renderer->restore();
if (m_fpsText != nullptr)
{
// Draw FPS.
renderer->save();
renderer->translate(0, 20);
m_fpsText->render(renderer.get(), m_blackStroke);
m_fpsText->render(renderer.get(), m_whiteFill);
renderer->restore();
}
renderer->restore();
TestingWindow::Get()->endFrame();
// Count FPS.
++m_fpsFrames;
const double elapsedFPSUpdate =
std::chrono::duration_cast<std::chrono::nanoseconds>(
timeNow - m_timeLastFPSUpdate)
.count() /
1e9; // convert to s
if (elapsedFPSUpdate >= 2.0)
{
double fps = m_fpsFrames / elapsedFPSUpdate;
printf("[%.3f FPS]\n", fps);
char fpsRawText[32];
snprintf(fpsRawText, sizeof(fpsRawText), " %.1f FPS ", fps);
m_fpsText = std::make_unique<rive::RawText>(
TestingWindow::Get()->factory());
m_fpsText->maxWidth(width);
#ifdef RIVE_ANDROID
m_fpsText->align(rive::TextAlign::center);
#else
m_fpsText->align(rive::TextAlign::right);
#endif
m_fpsText->sizing(rive::TextSizing::fixed);
m_fpsText->append(fpsRawText, nullptr, m_roboto, 50.f);
m_fpsFrames = 0;
m_timeLastFPSUpdate = 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:
key_pressed(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;
}
}
char key;
while (TestHarness::Instance().peekChar(key))
{
key_pressed(key);
}
}
private:
std::string m_rivName;
rive::rcp<rive::File> m_file;
std::unique_ptr<rive::ArtboardInstance> m_artboard;
std::unique_ptr<rive::Scene> m_scene;
int lastReportedCopyCount = 0;
bool lastReportedPauseState = paused;
rive::rcp<rive::Font> m_roboto;
rive::rcp<rive::RenderPaint> m_blackStroke;
rive::rcp<rive::RenderPaint> m_whiteFill;
std::unique_ptr<rive::RawText> m_fpsText;
int m_fpsFrames = 0;
std::chrono::high_resolution_clock::time_point m_timeLastFPSUpdate;
std::chrono::high_resolution_clock::time_point m_timestampPrevFrame;
};
static Player player;
#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)
#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
std::string rivName;
std::vector<uint8_t> rivBytes;
auto backend =
#ifdef __APPLE__
TestingWindow::Backend::metal;
#else
TestingWindow::Backend::vk;
#endif
auto visibility = TestingWindow::Visibility::fullscreen;
TestingWindow::BackendParams backendParams;
for (int i = 0; i < argc; ++i)
{
if (strcmp(argv[i], "--test_harness") == 0)
{
TestHarness::Instance().init(TCPClient::Connect(argv[++i]), 0);
if (!TestHarness::Instance().fetchRivFile(rivName, rivBytes))
{
fprintf(stderr, "failed to fetch a riv file.");
abort();
}
}
else if (strcmp(argv[i], "--backend") == 0 ||
strcmp(argv[i], "-b") == 0)
{
backend = TestingWindow::ParseBackend(argv[++i], &backendParams);
}
else if (argv[i][0] == '-' &&
argv[i][1] == 'b') // "-bvk" without a space.
{
backend = TestingWindow::ParseBackend(argv[i] + 2, &backendParams);
}
else if (strcmp(argv[i], "--options") == 0 ||
strcmp(argv[i], "-k") == 0)
{
for (const char* k = argv[++i]; *k; ++k)
{
key_pressed(*k);
}
}
else if (argv[i][0] == '-' &&
argv[i][1] == 'k') // "-k1234asdf" without a space.
{
for (const char* k = argv[i] + 2; *k; ++k)
{
key_pressed(*k);
}
}
else if (strcmp(argv[i], "--window") == 0 || strcmp(argv[i], "-w") == 0)
{
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;
}
rivName = argv[i];
std::ifstream rivStream(rivName, std::ios::binary);
rivBytes =
std::vector<uint8_t>(std::istreambuf_iterator<char>(rivStream),
{});
}
}
TestingWindow::Init(backend,
backendParams,
visibility,
#ifdef RIVE_ANDROID
rive_android_app_wait_for_window()
#else
reinterpret_cast<void*>(
static_cast<intptr_t>(monitorIdx))
#endif
);
if (rivBytes.empty())
{
fprintf(stderr, "no .riv file specified");
abort();
}
player.init(std::move(rivName), std::move(rivBytes));
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop([]() { player.doFrame(); }, 0, true);
#else
for (;;)
{
player.doFrame();
}
#endif
return 0;
}
#endif