| /* | |
| * Copyright 2024 Rive | |
| */ | |
| // Don't compile this file as part of the "tests" project. | |
| #ifndef TESTING | |
| #include "player.hpp" | |
| #include <optional> | |
| #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/input/gamepad_batch.hpp" | |
| #include "rive/renderer.hpp" | |
| #include "rive/renderer/scoped_autorelease_pool.hpp" | |
| #include "rive/scene.hpp" | |
| #include "rive/text/font_hb.hpp" | |
| #include "rive/text/raw_text.hpp" | |
| #include "rive/viewmodel/viewmodel_instance.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 <condition_variable> | |
| #include <fstream> | |
| #include <mutex> | |
| #include <optional> | |
| #include <thread> | |
| #if defined(RIVE_ANDROID) && !defined(RIVE_UNREAL) | |
| #include "common/rive_android_app.hpp" | |
| #endif | |
| #if (defined(RIVE_IOS) || defined(RIVE_IOS_SIMULATOR)) && !defined(RIVE_UNREAL) | |
| #include "common/rive_ios_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; | |
| }; | |
| struct Player::FrameJob | |
| { | |
| TestingWindow::FrameOptions options; | |
| #ifdef RIVE_CANVAS | |
| rive::cmd::DeferredFrame frame; | |
| #endif | |
| std::vector<uint8_t>* pixels = nullptr; | |
| }; | |
| // One frame in flight at most: submit blocks until the previous frame has | |
| // presented. Every window call happens on this thread while it lives. | |
| struct Player::RenderThread | |
| { | |
| explicit RenderThread(Player* player) : | |
| m_thread([this, player] { run(player); }) | |
| {} | |
| ~RenderThread() | |
| { | |
| { | |
| std::lock_guard lock(m_mutex); | |
| m_quit = true; | |
| } | |
| m_cv.notify_all(); | |
| m_thread.join(); | |
| } | |
| void submit(FrameJob job) | |
| { | |
| std::unique_lock lock(m_mutex); | |
| m_cv.wait(lock, [this] { return idle(); }); | |
| m_pending = std::move(job); | |
| m_cv.notify_all(); | |
| } | |
| // Returns once every submitted frame has presented. | |
| void drain() | |
| { | |
| std::unique_lock lock(m_mutex); | |
| m_cv.wait(lock, [this] { return idle(); }); | |
| } | |
| private: | |
| bool idle() const { return !m_pending.has_value(); } | |
| void run(Player* player) | |
| { | |
| for (;;) | |
| { | |
| std::unique_lock lock(m_mutex); | |
| m_cv.wait(lock, [this] { return m_pending.has_value() || m_quit; }); | |
| if (!m_pending.has_value()) | |
| { | |
| return; | |
| } | |
| lock.unlock(); | |
| { | |
| rive::gpu::ScopedAutoreleasePool autoreleasePool; | |
| player->presentFrame(*m_pending); | |
| } | |
| lock.lock(); | |
| m_pending.reset(); | |
| lock.unlock(); | |
| m_cv.notify_all(); | |
| } | |
| } | |
| std::mutex m_mutex; | |
| std::condition_variable m_cv; | |
| std::optional<FrameJob> m_pending; | |
| bool m_quit = false; | |
| std::thread m_thread; // last, so the state above exists before it runs | |
| }; | |
| Player::Player() : m_fps(std::make_unique<FPSOverlay>()) {} | |
| // Joins before the replay state below goes, for hosts that skip shutdown. | |
| Player::~Player() { m_renderThread = nullptr; } | |
| void Player::shutdown() | |
| { | |
| m_renderThread = nullptr; | |
| #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_viewModelInstance = 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; | |
| // Applied after parsing, since --backend resets the backend params. | |
| std::optional<TestingWindow::BackendParams::PresentMode> presentMode; | |
| 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 (strcmp(argv[i], "--fit") == 0 && i + 1 < argc) | |
| { | |
| const char* fit = argv[++i]; | |
| m_fitSet = true; | |
| m_fit = strcmp(fit, "layout") == 0 ? rive::Fit::layout | |
| : strcmp(fit, "cover") == 0 ? rive::Fit::cover | |
| : strcmp(fit, "fill") == 0 ? rive::Fit::fill | |
| : rive::Fit::contain; | |
| } | |
| else if (strcmp(argv[i], "--present") == 0 && i + 1 < argc) | |
| { | |
| using PM = TestingWindow::BackendParams::PresentMode; | |
| const char* mode = argv[++i]; | |
| if (strcmp(mode, "fifo") == 0) | |
| { | |
| presentMode = PM::fifo; | |
| } | |
| else if (strcmp(mode, "relaxed") == 0) | |
| { | |
| presentMode = PM::fifoRelaxed; | |
| } | |
| else if (strcmp(mode, "mailbox") == 0) | |
| { | |
| presentMode = PM::mailbox; | |
| } | |
| else if (strcmp(mode, "immediate") == 0) | |
| { | |
| presentMode = PM::immediate; | |
| } | |
| else | |
| { | |
| fprintf(stderr, | |
| "--present must be fifo, relaxed, mailbox or " | |
| "immediate, not %s\n", | |
| mode); | |
| abort(); | |
| } | |
| } | |
| else if (strcmp(argv[i], "--threaded") == 0) | |
| { | |
| m_threaded = true; | |
| } | |
| else if (strcmp(argv[i], "--dump") == 0 && i + 1 < argc) | |
| { | |
| m_dumpPath = argv[++i]; | |
| } | |
| else if (strcmp(argv[i], "--dump-frame") == 0 && i + 1 < argc) | |
| { | |
| m_dumpFrame = atoi(argv[++i]); | |
| } | |
| 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 (presentMode) | |
| { | |
| options.backendParams.presentMode = *presentMode; | |
| } | |
| 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); | |
| declareTarget(); | |
| 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_threaded && TestingWindow::Get()->supportsRenderThread()) | |
| { | |
| m_renderThread = std::make_unique<RenderThread>(this); | |
| } | |
| } | |
| #else | |
| if (m_useDeferred) | |
| { | |
| printf("--deferred requires a RIVE_CANVAS build, drawing immediate\n"); | |
| } | |
| #endif | |
| if (m_threaded && m_renderThread == nullptr) | |
| { | |
| printf("--threaded needs deferred replay on a window that presents " | |
| "off the main thread, presenting inline\n"); | |
| } | |
| m_file = rive::File::import(rivBytes, m_factory); | |
| assert(m_file); | |
| m_artboard = m_file->artboardDefault(); | |
| assert(m_artboard); | |
| // A layout artboard is authored to resize, so it fills the window like | |
| // our other hosts rather than letterboxing at its design size. | |
| if (!m_fitSet && m_artboard->style() != nullptr) | |
| { | |
| m_fit = rive::Fit::layout; | |
| } | |
| // Bind the artboard's default view model instance, if it has one, the same | |
| // way a real host runtime would. | |
| m_viewModelInstance = | |
| m_file->createDefaultViewModelInstance(m_artboard.get()); | |
| if (m_viewModelInstance != nullptr) | |
| { | |
| m_artboard->bindViewModelInstance(m_viewModelInstance); | |
| } | |
| auto stateMachine = m_artboard->defaultStateMachine(); | |
| if (!stateMachine && m_artboard->stateMachineCount() > 0) | |
| { | |
| stateMachine = m_artboard->stateMachineAt(0); | |
| } | |
| m_stateMachine = stateMachine.get(); | |
| m_scene = std::move(stateMachine); | |
| if (!m_scene) | |
| { | |
| m_scene = m_artboard->animationAt(0); | |
| } | |
| assert(m_scene); | |
| if (m_viewModelInstance != nullptr) | |
| { | |
| m_scene->bindViewModelInstance(m_viewModelInstance); | |
| } | |
| // 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(); | |
| } | |
| // The state machine takes gamepads as the wire batch our js embedder sends. | |
| static void appendU32(std::vector<uint8_t>& buffer, uint32_t value) | |
| { | |
| for (int i = 0; i < 4; ++i) | |
| { | |
| buffer.push_back(static_cast<uint8_t>(value >> (8 * i))); | |
| } | |
| } | |
| void Player::submitGamepad(const TestingWindow::InputEventData& event) | |
| { | |
| if (m_stateMachine == nullptr) | |
| { | |
| return; | |
| } | |
| const auto& pad = event.metadata.pad; | |
| std::vector<uint8_t> buffer; | |
| appendU32(buffer, rive::kGamepadBatchWireVersion); | |
| if (event.eventType == TestingWindow::InputEvent::GamepadConnected) | |
| { | |
| buffer.push_back( | |
| static_cast<uint8_t>(rive::GamepadRecordType::connected)); | |
| appendU32(buffer, pad.deviceId); | |
| buffer.push_back(0); // standard mapping | |
| buffer.push_back(pad.buttonCount); | |
| buffer.push_back(pad.axisCount); | |
| buffer.push_back(0); // padding | |
| for (int i = 0; i < pad.buttonCount + pad.axisCount; ++i) | |
| { | |
| appendU32(buffer, 0); // every value at rest | |
| } | |
| } | |
| else if (event.eventType == TestingWindow::InputEvent::GamepadDisconnected) | |
| { | |
| buffer.push_back( | |
| static_cast<uint8_t>(rive::GamepadRecordType::disconnected)); | |
| appendU32(buffer, pad.deviceId); | |
| } | |
| else | |
| { | |
| buffer.push_back(static_cast<uint8_t>(rive::GamepadRecordType::update)); | |
| appendU32(buffer, pad.deviceId); | |
| buffer.push_back(1); // one change | |
| buffer.push_back(pad.isAxis ? 1 : 0); | |
| buffer.push_back(pad.index); | |
| uint32_t bits; | |
| memcpy(&bits, &pad.value, sizeof(bits)); | |
| appendU32(buffer, bits); | |
| } | |
| m_stateMachine->submitGamepadsFromBuffer(buffer.data(), buffer.size()); | |
| } | |
| #ifdef RIVE_CANVAS | |
| void Player::declareTarget() | |
| { | |
| TestingWindow* window = TestingWindow::Get(); | |
| TestingWindow::OreTarget target = window->oreTarget(); | |
| m_session->oreContext().setTarget( | |
| target.exposed | |
| ? rive::ore::Context::TargetDesc::color8(window->width(), | |
| window->height(), | |
| target.bgra) | |
| : rive::ore::Context::TargetDesc{}); | |
| } | |
| #endif | |
| void Player::presentFrame(FrameJob& job) | |
| { | |
| #ifdef RIVE_CANVAS | |
| if (m_session != nullptr) | |
| { | |
| TestingWindowFrameSink sink(job.options); | |
| m_replayer->replayFrame(job.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(job.pixels); | |
| } | |
| 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); | |
| if (m_renderThread != nullptr) | |
| { | |
| m_renderThread->drain(); | |
| } | |
| 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; | |
| } | |
| // advanceAndApply drives the artboard through advanceInternal, which | |
| // skips the async-work pump in Artboard::advance; pump it here so scripted | |
| // image decodes resolve (without threads they only run when polled). | |
| m_artboard->pollAsyncWork(); | |
| #ifdef WITH_RIVE_SCRIPTING_WASM | |
| // Scavenges the script nursery, which otherwise grows until it traps. | |
| if (const char* warning = m_file->frameBoundary()) | |
| { | |
| fprintf(stderr, "%s\n", warning); | |
| } | |
| #endif | |
| 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) | |
| { | |
| declareTarget(); | |
| 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"); | |
| if (m_renderThread != nullptr) | |
| { | |
| m_renderThread->drain(); | |
| } | |
| 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_fit == rive::Fit::layout && | |
| (m_artboard->width() != width || m_artboard->height() != height)) | |
| { | |
| m_artboard->width(width); | |
| m_artboard->height(height); | |
| m_artboard->advance(0.0f); | |
| } | |
| 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(m_fit, | |
| 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(); | |
| std::vector<uint8_t> pixels; | |
| const bool dump = !m_dumpPath.empty() && ++m_frameCounter >= m_dumpFrame; | |
| FrameJob job = {.options = frameOptions, | |
| .pixels = dump ? &pixels : nullptr}; | |
| #ifdef RIVE_CANVAS | |
| if (m_session != nullptr) | |
| { | |
| // Snapshot replay is the same path a threaded consumer takes. | |
| job.frame = rive::cmd::snapshotFrame(*m_session); | |
| m_session->resetFrame(); | |
| } | |
| #endif | |
| if (m_renderThread != nullptr) | |
| { | |
| m_renderThread->submit(std::move(job)); | |
| if (dump) | |
| { | |
| m_renderThread->drain(); | |
| } | |
| } | |
| else | |
| { | |
| presentFrame(job); | |
| } | |
| if (dump) | |
| { | |
| uint32_t w = TestingWindow::Get()->width(); | |
| uint32_t h = TestingWindow::Get()->height(); | |
| if (FILE* f = fopen(m_dumpPath.c_str(), "wb")) | |
| { | |
| fwrite(&w, sizeof(w), 1, f); | |
| fwrite(&h, sizeof(h), 1, f); | |
| fwrite(pixels.data(), 1, pixels.size(), f); | |
| fclose(f); | |
| printf("dumped %ux%u frame (%zu bytes) to %s\n", | |
| w, | |
| h, | |
| pixels.size(), | |
| m_dumpPath.c_str()); | |
| } | |
| else | |
| { | |
| fprintf(stderr, | |
| "could not open dump path %s\n", | |
| m_dumpPath.c_str()); | |
| } | |
| m_quit = true; | |
| return false; | |
| } | |
| // 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(m_fit, | |
| 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)) | |
| { | |
| // Only pointer events carry a position in the union. | |
| auto alignedPos = [&]() { | |
| return 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(alignedPos()); | |
| break; | |
| case TestingWindow::InputEvent::MouseDown: | |
| m_scene->pointerDown(alignedPos()); | |
| break; | |
| case TestingWindow::InputEvent::MouseUp: | |
| m_scene->pointerUp(alignedPos()); | |
| break; | |
| case TestingWindow::InputEvent::GamepadConnected: | |
| case TestingWindow::InputEvent::GamepadDisconnected: | |
| case TestingWindow::InputEvent::GamepadChange: | |
| submitGamepad(inputEventData); | |
| 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() | |
| #elif defined(RIVE_IOS) || defined(RIVE_IOS_SIMULATOR) | |
| rive_ios_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 (;;) | |
| { | |
| rive::gpu::ScopedAutoreleasePool autoreleasePool; | |
| if (!player.doFrame()) | |
| { | |
| player_shutdown(); | |
| } | |
| } | |
| #endif | |
| return 0; | |
| } | |
| #endif // !RIVE_UNREAL | |
| #endif |