| /* |
| * Copyright 2022 Rive |
| */ |
| |
| // Don't compile this file as part of the "tests" project. |
| #ifndef TESTING |
| |
| #include "goldens_arguments.hpp" |
| #include "common/test_harness.hpp" |
| #include "common/tcp_client.hpp" |
| #include "common/rive_mgr.hpp" |
| #include "common/testing_window.hpp" |
| #include "common/write_png_file.hpp" |
| #include "rive/artboard.hpp" |
| #include "rive/renderer.hpp" |
| #include "rive/file.hpp" |
| #include "rive/animation/state_machine_instance.hpp" |
| #include "rive/static_scene.hpp" |
| #include <filesystem> |
| #include <fstream> |
| #include <iostream> |
| |
| #ifdef RIVE_ANDROID |
| #include "common/rive_android_app.hpp" |
| #endif |
| |
| constexpr static int kWindowTargetSize = 1600; |
| |
| GoldensArguments s_args; |
| |
| static bool render_and_dump_png(int cellSize, |
| const char* rivName, |
| rive::Scene* scene) |
| { |
| if (s_args.verbose()) |
| { |
| printf("[goldens] Running %s...\n", rivName); |
| } |
| try |
| { |
| const int frames = s_args.cols() * s_args.rows(); |
| const double duration = scene->durationSeconds(); |
| const double frameDuration = duration / frames; |
| const rive::AABB cellBounds = rive::AABB(0, 0, cellSize, cellSize); |
| |
| // Render the scene in a grid. |
| auto renderer = |
| TestingWindow::Get()->beginFrame({.clearColor = 0xffffffff}); |
| renderer->save(); |
| scene->advanceAndApply(0); |
| for (int y = 0; y < s_args.rows(); ++y) |
| { |
| for (int x = 0; x < s_args.cols(); ++x) |
| { |
| if ((x | y) != 0) |
| { |
| TestingWindow::Get()->endFrame(); |
| TestingWindow::Get()->beginFrame({.doClear = false}); |
| scene->advanceAndApply(frameDuration); |
| } |
| |
| renderer->save(); |
| |
| renderer->translate(x * cellSize, y * cellSize); |
| renderer->align(rive::Fit::cover, |
| rive::Alignment::center, |
| cellBounds, |
| scene->bounds()); |
| scene->draw(renderer.get()); |
| |
| renderer->restore(); |
| } |
| } |
| renderer->restore(); |
| |
| // Save the png. |
| int windowWidth = s_args.cols() * cellSize; |
| int windowHeight = s_args.rows() * cellSize; |
| std::vector<uint8_t> pixels; |
| TestingWindow::Get()->endFrame(&pixels); |
| assert(pixels.size() == windowHeight * windowWidth * 4); |
| std::ostringstream imageName; |
| |
| imageName << std::filesystem::path(rivName) |
| .filename() |
| .stem() |
| .generic_string(); |
| if (s_args.rows() != 1 || s_args.cols() != 1) |
| { |
| imageName << '.' << s_args.cols() << 'x' << s_args.rows() << '.'; |
| } |
| |
| TestHarness::Instance().savePNG({ |
| .name = imageName.str(), |
| .width = static_cast<uint32_t>(windowWidth), |
| .height = static_cast<uint32_t>(windowHeight), |
| .pixels = std::move(pixels), |
| }); |
| |
| if (s_args.verbose()) |
| { |
| printf("[goldens] Sent %s\n", |
| std::filesystem::path(imageName.str()) |
| .replace_extension("png") |
| .generic_string() |
| .c_str()); |
| } |
| |
| if (s_args.interactive()) |
| { |
| // Wait for any key if in interactive mode. |
| TestingWindow::Get()->getKey(); |
| } |
| #ifdef RIVE_ANDROID |
| if (!rive_android_app_poll_once()) |
| { |
| return false; |
| } |
| #endif |
| } |
| catch (const char* msg) |
| { |
| fprintf(stderr, "%s: error: %s\n", rivName, msg); |
| abort(); |
| } |
| catch (...) |
| { |
| fprintf(stderr, "error rendering %s\n", rivName); |
| abort(); |
| } |
| return true; |
| } |
| |
| class RIVLoader |
| { |
| public: |
| RIVLoader(const std::vector<uint8_t>& rivBytes, |
| const char* artboardName, |
| const char* stateMachineName) |
| { |
| m_file = rive::File::import(rivBytes, TestingWindow::Get()->factory()); |
| if (m_file == nullptr) |
| { |
| throw "Bad riv file"; |
| } |
| if (artboardName != nullptr && artboardName[0] != '\0') |
| { |
| m_artboard = m_file->artboardNamed(artboardName); |
| } |
| else |
| { |
| m_artboard = m_file->artboardDefault(); |
| } |
| if (m_artboard == nullptr) |
| { |
| throw "Can't load artboard"; |
| } |
| if (stateMachineName != nullptr && stateMachineName[0] != '\0') |
| { |
| m_scene = m_artboard->stateMachineNamed(stateMachineName); |
| } |
| else |
| { |
| m_scene = m_artboard->defaultStateMachine(); |
| } |
| if (m_scene == nullptr) |
| { |
| // This is a riv without any state machines. Just draw the artboard. |
| m_scene = std::make_unique<rive::StaticScene>(m_artboard.get()); |
| } |
| } |
| |
| rive::Scene* stateMachine() const { return m_scene.get(); } |
| |
| private: |
| std::unique_ptr<rive::File> m_file; |
| std::unique_ptr<rive::ArtboardInstance> m_artboard; |
| std::unique_ptr<rive::Scene> m_scene; |
| }; |
| |
| static bool process_single_golden_file(const std::string file, int cellSize) |
| { |
| std::ifstream stream(file, std::ios::binary); |
| if (!stream.good()) |
| { |
| throw "Bad file"; |
| } |
| |
| RIVLoader riv( |
| std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), |
| s_args.artboard().c_str(), |
| s_args.stateMachine().c_str()); |
| return render_and_dump_png(cellSize, file.c_str(), riv.stateMachine()); |
| } |
| |
| static bool is_riv_file(const std::filesystem::path& file) |
| { |
| return strcmp(file.extension().string().c_str(), ".riv") == 0; |
| } |
| |
| #if defined(RIVE_UNREAL) |
| int goldens_main(int argc, const char* argv[]) |
| #elif defined(RIVE_IOS) || defined(RIVE_IOS_SIMULATOR) |
| int goldens_ios_main(int argc, const char* argv[]) |
| #elif defined(RIVE_ANDROID) |
| int rive_android_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 |
| |
| try |
| { |
| s_args.parse(argc, argv); |
| std::string gpuNameFilter; |
| auto backend = |
| s_args.backend().empty() |
| ? TestingWindow::Backend::gl |
| : TestingWindow::ParseBackend(s_args.backend().c_str(), |
| &gpuNameFilter); |
| auto visibility = s_args.headless() |
| ? TestingWindow::Visibility::headless |
| : TestingWindow::Visibility::window; |
| void* platformWindow = nullptr; |
| #ifdef RIVE_ANDROID |
| if (TestingWindow::IsGL(backend)) |
| { |
| // Android can render directly to the main window in GL. |
| // TOOD: add this support to TestingWindowAndroidVulkan as well. |
| platformWindow = rive_android_app_wait_for_window(); |
| if (platformWindow != nullptr) |
| { |
| visibility = TestingWindow::Visibility::fullscreen; |
| } |
| } |
| #endif |
| TestingWindow::Init(backend, visibility, gpuNameFilter, platformWindow); |
| |
| if (!s_args.testHarness().empty()) |
| { |
| TestHarness::Instance().init( |
| TCPClient::Connect(s_args.testHarness().c_str()), |
| s_args.pngThreads()); |
| } |
| else |
| { |
| TestHarness::Instance().init( |
| std::filesystem::path(s_args.output().c_str()), |
| s_args.pngThreads()); |
| } |
| TestHarness::Instance().setPNGCompression( |
| s_args.fastPNG() ? PNGCompression::fast_rle |
| : PNGCompression::compact); |
| |
| int cellSize = |
| kWindowTargetSize / std::max(s_args.cols(), s_args.rows()); |
| int windowWidth = cellSize * s_args.cols(); |
| int windowHeight = cellSize * s_args.rows(); |
| TestingWindow::Get()->resize(windowWidth, windowHeight); |
| |
| // First check if the --src argument is a TCP server instead of a file. |
| if (TestHarness::Instance().hasTCPConnection()) |
| { |
| // Loop until the server is done sending .rivs. |
| std::string rivName; |
| std::vector<uint8_t> rivBytes; |
| while (TestHarness::Instance().fetchRivFile(rivName, rivBytes)) |
| { |
| RIVLoader riv(rivBytes, |
| nullptr /*default artboard*/, |
| nullptr /*default state machine*/); |
| if (!render_and_dump_png(cellSize, |
| rivName.c_str(), |
| riv.stateMachine())) |
| { |
| return 0; |
| } |
| } |
| } |
| else |
| { |
| const std::filesystem::path& srcPath = |
| std::filesystem::path(s_args.src().c_str()); |
| if (is_riv_file(srcPath)) |
| { |
| // Render a single .riv file. |
| if (!process_single_golden_file(s_args.src().c_str(), cellSize)) |
| { |
| return 0; |
| } |
| } |
| else |
| { |
| // Try to process every riv in the src path dir |
| try |
| { |
| for (const std::filesystem::directory_entry& file : |
| std::filesystem::directory_iterator(s_args.src())) |
| { |
| const std::filesystem::path& filePath = file.path(); |
| if (is_riv_file(filePath)) |
| { |
| if (!process_single_golden_file(filePath.string(), |
| cellSize)) |
| { |
| return 0; |
| } |
| } |
| } |
| } |
| catch (...) |
| { |
| // Not a directory |
| throw "Bad src path"; |
| } |
| } |
| } |
| } |
| catch (const args::Completion&) |
| { |
| return 0; |
| } |
| catch (const args::Help&) |
| { |
| return 0; |
| } |
| catch (const args::ParseError&) |
| { |
| return 1; |
| } |
| catch (args::ValidationError) |
| { |
| return 1; |
| } |
| catch (const char* msg) |
| { |
| fprintf(stderr, "error: %s\n", msg); |
| return -1; |
| } |
| |
| TestingWindow::Destroy(); // Exercise our PLS teardown process now that |
| // we're done. |
| TestHarness::Instance().shutdown(); |
| return 0; |
| } |
| |
| #endif |