mirror of
https://github.com/rive-app/rive-cpp.git
synced 2026-01-18 13:11:19 +01:00
* make file ref counted * migrate File to refcnt * add bindable artboard class to keep file reference * feat(unity): support rcp file and BindableArtboard class (#10228) * refactor(apple): use updated file bindable artboard apis (#10229) * update command queue to support bindable artboards * use bindable artboards on command queue * self manage view model instances in js runtime * change remaining viewModelInstanceRuntimes to rcp * refactor(apple): update view model instances to use rcp (#10298) * refactor(unity): support rcp ViewModelInstanceRuntime (#10309) * rebase fix * deprecate getArtboard in favor of getBindableArtboard * fix merge * remove unused lambda capture * fix rive binding * feat(Android): RCP File, VMI, and add bindable artboards (#10456) * refactor: C++ refactors - Import from long (incorrect) -> jlong - Header clang-tidy fix - Use reinterpret_cast instead of C-style cast - Break out some variables instead of long one liners - Use auto - Remove unused env and thisObj # Conflicts: # packages/runtime_android/kotlin/src/main/cpp/src/helpers/general.cpp * docs: Improve documentation on the File class * feat: Support bindable artboard type and RCP VMIs # Conflicts: # packages/runtime_android/kotlin/src/androidTest/java/app/rive/runtime/kotlin/core/RiveDataBindingTest.kt * feat: Support RCP files * refactor: Change from +1/-1 ref to just release * fix: Moved to the more appropriate null pointer for GetStringUTFChars * replace unref with release Co-authored-by: Adam <67035612+damzobridge@users.noreply.github.com> Co-authored-by: David Skuza <david@rive.app> Co-authored-by: Erik <erik@rive.app> Co-authored-by: hernan <hernan@rive.app>
525 lines
16 KiB
C++
525 lines
16 KiB
C++
/*
|
|
* 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/refcnt.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(if (window && window.close) 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
|