| /* |
| * Copyright 2025 Rive |
| */ |
| |
| // Don't compile this file as part of the "tests" project. |
| #ifndef TESTING |
| |
| #include "common/testing_window.hpp" |
| #include "rive/artboard.hpp" |
| #include "rive/factory.hpp" |
| #include "rive/text/raw_text.hpp" |
| #include "rive/animation/state_machine_instance.hpp" |
| #include "rive/command_queue.hpp" |
| #include "rive/command_server.hpp" |
| #include "rive/text/font_hb.hpp" |
| #include "assets/roboto_flex.ttf.hpp" |
| #include <stdio.h> |
| #include <fstream> |
| #include <chrono> |
| |
| using namespace rive; |
| |
| std::string rivName; |
| std::vector<uint8_t> rivBytes; |
| auto backend = |
| #ifdef __APPLE__ |
| TestingWindow::Backend::metal; |
| #else |
| TestingWindow::Backend::vk; |
| #endif |
| TestingWindow::BackendParams backendParams; |
| |
| class |
| { |
| public: |
| void push(char key) |
| { |
| std::unique_lock lock(m_mutex); |
| m_keys.push_back(key); |
| m_cv.notify_one(); |
| } |
| |
| char pop() |
| { |
| std::unique_lock lock(m_mutex); |
| while (m_keys.empty()) |
| m_cv.wait(lock); |
| char key = m_keys.front(); |
| m_keys.pop_front(); |
| return key; |
| } |
| |
| private: |
| std::mutex m_mutex; |
| std::condition_variable m_cv; |
| std::deque<char> m_keys; |
| } keyQueue; |
| |
| class SimpleFileListener : public CommandQueue::FileListener |
| { |
| public: |
| virtual void onArtboardsListed( |
| const FileHandle fileHandle, |
| uint64_t requestId, |
| std::vector<std::string> artboardNames) override |
| { |
| // we can guarantee that this is the only listener that will receive |
| // this callback, so it's fine to move the params |
| m_artboardNames = std::move(artboardNames); |
| } |
| |
| std::vector<std::string> m_artboardNames; |
| }; |
| |
| class SimpleArtboardListener : public CommandQueue::ArtboardListener |
| { |
| public: |
| virtual void onStateMachinesListed( |
| const ArtboardHandle fileHandle, |
| uint64_t requestId, |
| std::vector<std::string> stateMachineNames) override |
| { |
| // we can guarantee that this is the only listener that will receive |
| // this callback, so it's fine to move the params |
| m_stateMachineNames = std::move(stateMachineNames); |
| } |
| |
| std::vector<std::string> m_stateMachineNames; |
| }; |
| |
| class SimpleStateMachineListener : public CommandQueue::StateMachineListener |
| {}; |
| |
| std::atomic_bool running = true; |
| std::atomic_bool shouldDraw = true; |
| |
| static void input_thread(rcp<CommandQueue> commandQueue) |
| { |
| for (char key = '\0'; key != 'q'; key = keyQueue.pop()) |
| { |
| if (key == 'p') |
| { |
| shouldDraw.store(!shouldDraw); |
| } |
| } |
| |
| running.store(false); |
| commandQueue->disconnect(); |
| } |
| |
| // this is a seperate thread for testing, it could just as easily be in |
| // input_thread or the main thread |
| static void draw_thread(rcp<CommandQueue> commandQueue) |
| { |
| SimpleFileListener fListener; |
| SimpleArtboardListener aListener; |
| SimpleStateMachineListener stmListener; |
| |
| FileHandle fileHandle = |
| commandQueue->loadFile(std::move(rivBytes), &fListener); |
| |
| ArtboardHandle artboardHandle = |
| commandQueue->instantiateDefaultArtboard(fileHandle, &aListener); |
| |
| StateMachineHandle stateMachineHandle = |
| commandQueue->instantiateDefaultStateMachine(artboardHandle, |
| &stmListener); |
| |
| // TODO: This should really be decoded on the server thread. Do we want to |
| // consider adding FontHandles? |
| auto roboto = HBFont::Decode(assets::roboto_flex_ttf()); |
| |
| auto drawLoop = [artboardHandle, |
| stateMachineHandle, |
| roboto, |
| &fListener, |
| &aListener](DrawKey drawKey, CommandServer* server) { |
| ArtboardInstance* artboard = |
| server->getArtboardInstance(artboardHandle); |
| |
| if (artboard == nullptr) |
| { |
| return; |
| } |
| |
| StateMachineInstance* stateMachine = |
| server->getStateMachineInstance(stateMachineHandle); |
| if (stateMachine == nullptr) |
| { |
| return; |
| } |
| |
| stateMachine->advanceAndApply(1.f / 120); |
| |
| std::unique_ptr<Renderer> renderer = TestingWindow::Get()->beginFrame({ |
| .clearColor = 0xff303030, |
| .doClear = true, |
| }); |
| |
| renderer->save(); |
| |
| auto factory = server->factory(); |
| |
| uint32_t width = TestingWindow::Get()->width(); |
| uint32_t height = TestingWindow::Get()->height(); |
| |
| // Draw the .riv. |
| renderer->save(); |
| renderer->align(Fit::contain, |
| Alignment::center, |
| AABB(0, 0, width, height), |
| artboard->bounds()); |
| artboard->draw(renderer.get()); |
| renderer->restore(); |
| |
| renderer->save(); |
| renderer->translate(20, 0); |
| |
| auto black = factory->makeRenderPaint(); |
| black->color(0xffffffff); |
| |
| for (auto name : fListener.m_artboardNames) |
| { |
| auto paint = factory->makeRenderPaint(); |
| |
| renderer->translate(0, 100); |
| rive::RawText text(factory); |
| text.maxWidth(400); |
| text.sizing(rive::TextSizing::autoWidth); |
| text.append(name + "\n", black, roboto, 72); |
| text.render(renderer.get()); |
| } |
| |
| for (auto name : aListener.m_stateMachineNames) |
| { |
| auto paint = factory->makeRenderPaint(); |
| |
| renderer->translate(0, 100); |
| rive::RawText text(factory); |
| text.maxWidth(400); |
| text.sizing(rive::TextSizing::autoWidth); |
| text.append(name + "\n", black, roboto, 72); |
| text.render(renderer.get()); |
| } |
| |
| renderer->restore(); |
| |
| TestingWindow::Get()->endFrame(); |
| }; |
| |
| auto drawKey = commandQueue->createDrawKey(); |
| |
| commandQueue->requestArtboardNames(fileHandle); |
| commandQueue->requestStateMachineNames(artboardHandle); |
| |
| // 120 fps |
| constexpr auto duration = std::chrono::microseconds(8330); |
| auto lastFrame = std::chrono::high_resolution_clock::now(); |
| while (running.load()) |
| { |
| std::this_thread::sleep_until(lastFrame + duration); |
| lastFrame = std::chrono::high_resolution_clock::now(); |
| if (shouldDraw.load()) |
| commandQueue->draw(drawKey, drawLoop); |
| |
| // should happen in same thread as the listeners |
| commandQueue->processMessages(); |
| } |
| } |
| |
| #ifdef 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 |
| |
| for (int i = 0; i < argc; ++i) |
| { |
| 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 |
| { |
| rivName = argv[i]; |
| std::ifstream rivStream(rivName, std::ios::binary); |
| rivBytes = |
| std::vector<uint8_t>(std::istreambuf_iterator<char>(rivStream), |
| {}); |
| } |
| } |
| if (rivBytes.empty()) |
| { |
| fprintf(stderr, "no .riv file specified"); |
| abort(); |
| } |
| |
| auto commandQueue = make_rcp<CommandQueue>(); |
| std::thread inputThread(input_thread, commandQueue); |
| std::thread drawThread(draw_thread, commandQueue); |
| |
| TestingWindow::Init(backend, |
| backendParams, |
| TestingWindow::Visibility::window); |
| |
| CommandServer server(commandQueue, TestingWindow::Get()->factory()); |
| |
| while (server.processCommands()) |
| { |
| TestingWindow::InputEventData inputEventData; |
| while (TestingWindow::Get()->consumeInputEvent(inputEventData)) |
| { |
| if (inputEventData.eventType == TestingWindow::InputEvent::KeyPress) |
| { |
| keyQueue.push(inputEventData.metadata.key); |
| } |
| } |
| if (TestingWindow::Get()->shouldQuit()) |
| { |
| keyQueue.push('q'); |
| } |
| |
| std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
| } |
| |
| printf("\nShutting down\n"); |
| TestingWindow::Destroy(); |
| inputThread.join(); |
| drawThread.join(); |
| return 0; |
| } |
| |
| #endif |