mirror of
https://github.com/rive-app/rive-cpp.git
synced 2026-01-18 13:11:19 +01:00
feat: Update goldens and player to deploy in the browser (#10453) 827077b899
Set up http and websocket servers in deploy_tests.py that allow us to communicate with the remote wasm app similarly to how we communicate with android & ios devices. Add a "-w" target to check_golds.sh that kicks tests off in the default browser. Co-authored-by: Chris Dalton <99840794+csmartdalton@users.noreply.github.com>
This commit is contained in:
@@ -1 +1 @@
|
||||
c26e82adc9e2fa295b554a1aa0818f7e99d89a38
|
||||
827077b899b109dcb4b5eb57c8e999246f5d44b5
|
||||
|
||||
@@ -20,7 +20,7 @@ std::unique_ptr<FiddleContext> FiddleContext::MakeGLPLS(FiddleContextOptions)
|
||||
#endif
|
||||
|
||||
#define GLFW_INCLUDE_NONE
|
||||
#include "GLFW/glfw3.h"
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
using namespace rive;
|
||||
using namespace rive::gpu;
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
|
||||
#define GLFW_INCLUDE_NONE
|
||||
#define GLFW_EXPOSE_NATIVE_COCOA
|
||||
#include "GLFW/glfw3.h"
|
||||
#include "GLFW/glfw3native.h"
|
||||
#include <GLFW/glfw3.h>
|
||||
#include <GLFW/glfw3native.h>
|
||||
|
||||
using namespace rive;
|
||||
using namespace rive::gpu;
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
#include "gradient.hpp"
|
||||
|
||||
#include "rive/renderer/rive_render_image.hpp"
|
||||
|
||||
namespace rive::gpu
|
||||
{
|
||||
// Ensure the given gradient stops are in a format expected by PLS.
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
|
||||
// Run a microbenchmark repeatedly for a few seconds and print the quickest
|
||||
// time.
|
||||
#ifdef __EMSCRIPTEN__
|
||||
int rive_wasm_main(int argc, const char* const* argv)
|
||||
#else
|
||||
int main(int argc, const char** argv)
|
||||
#endif
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
using clock = std::chrono::high_resolution_clock;
|
||||
@@ -18,8 +22,8 @@ int main(int argc, const char** argv)
|
||||
clock::duration benchDuration = 5s;
|
||||
|
||||
// Parse flags.
|
||||
const char** arg = argv + 1;
|
||||
const char** endarg = argv + argc;
|
||||
const char* const* arg = argv + 1;
|
||||
const char* const* endarg = argv + argc;
|
||||
while (arg + 1 < endarg && *arg[0] == '-')
|
||||
{
|
||||
if (!strcmp(*arg, "--duration") || !strcmp(*arg, "-d"))
|
||||
|
||||
@@ -53,6 +53,16 @@ while :; do
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
-w)
|
||||
TARGET="webbrowser"
|
||||
DEFAULT_BACKEND=gl
|
||||
shift
|
||||
;;
|
||||
-ws)
|
||||
TARGET="webserver"
|
||||
DEFAULT_BACKEND=gl
|
||||
shift
|
||||
;;
|
||||
-R)
|
||||
REBASELINE=true
|
||||
shift
|
||||
@@ -109,6 +119,8 @@ do
|
||||
ID="iossim_$UDID/$BACKEND"
|
||||
elif [[ "$TARGET" == "android" ]]; then
|
||||
ID="android_$SERIAL/$BACKEND"
|
||||
elif [[ "$TARGET" != "host" ]]; then
|
||||
ID="$TARGET/$BACKEND"
|
||||
fi
|
||||
|
||||
if [ "$REBASELINE" == true ]; then
|
||||
|
||||
@@ -230,6 +230,8 @@ static void draw_thread(rcp<CommandQueue> commandQueue)
|
||||
|
||||
#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
|
||||
|
||||
85
tests/common/rive_wasm_app.cpp
Normal file
85
tests/common/rive_wasm_app.cpp
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2025 Rive
|
||||
*/
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
|
||||
#include "common/rive_wasm_app.hpp"
|
||||
|
||||
#include "common/test_harness.hpp"
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
EM_JS(char*, get_window_location, (), {
|
||||
var jsString = window.location.href;
|
||||
var lengthBytes = lengthBytesUTF8(jsString) + 1;
|
||||
var stringOnWasmHeap = _malloc(lengthBytes);
|
||||
stringToUTF8(jsString, stringOnWasmHeap, lengthBytes);
|
||||
return stringOnWasmHeap;
|
||||
});
|
||||
|
||||
EM_JS(char*, get_window_location_hash, (), {
|
||||
var jsString = window.location.hash.substring(1);
|
||||
var lengthBytes = lengthBytesUTF8(jsString) + 1;
|
||||
var stringOnWasmHeap = _malloc(lengthBytes);
|
||||
stringToUTF8(jsString, stringOnWasmHeap, lengthBytes);
|
||||
return stringOnWasmHeap;
|
||||
});
|
||||
|
||||
static void split_string(const std::string& str,
|
||||
const std::string& delimiter,
|
||||
std::vector<std::string>& result)
|
||||
{
|
||||
size_t start = 0;
|
||||
size_t end;
|
||||
|
||||
while ((end = str.find(delimiter, start)) != std::string::npos)
|
||||
{
|
||||
if (end > start)
|
||||
{
|
||||
result.push_back(str.substr(start, end - start));
|
||||
}
|
||||
start = end + delimiter.length();
|
||||
}
|
||||
|
||||
if (str.length() > start)
|
||||
{
|
||||
// Add the last piece
|
||||
result.push_back(str.substr(start));
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
char* location = get_window_location();
|
||||
char* hash = get_window_location_hash();
|
||||
|
||||
// Command line arguments are passed via the window location's hash string.
|
||||
//
|
||||
// e.g., "http://localhost/my_app.html#--backend%20gl%20--another%20arg"
|
||||
//
|
||||
std::vector<std::string> hashStrs = {location};
|
||||
split_string(hash, "%20", hashStrs);
|
||||
|
||||
free(hash);
|
||||
free(location);
|
||||
|
||||
std::vector<const char*> hashArgs;
|
||||
for (const std::string& str : hashStrs)
|
||||
{
|
||||
hashArgs.push_back(str.c_str());
|
||||
}
|
||||
|
||||
return rive_wasm_main(hashArgs.size(), hashArgs.data());
|
||||
}
|
||||
|
||||
extern "C" void rive_print_message_on_server(const char* msg)
|
||||
{
|
||||
// stdout and stderr get redirected here and forwarded to the server for
|
||||
// logging.
|
||||
TestHarness::Instance().printMessageOnServer(msg);
|
||||
}
|
||||
|
||||
#endif
|
||||
9
tests/common/rive_wasm_app.hpp
Normal file
9
tests/common/rive_wasm_app.hpp
Normal file
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* Copyright 2025 Rive
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// Primary entrypoint for rive wasm tests. Each individual test is expected
|
||||
// to implement this method in wasm (instead of "main()").
|
||||
extern int rive_wasm_main(int argc, const char* const* argv);
|
||||
@@ -5,6 +5,17 @@
|
||||
#include "common/stacktrace.hpp"
|
||||
#include <assert.h>
|
||||
|
||||
#if defined(NO_REDIRECT_OUTPUT) || defined(__EMSCRIPTEN__)
|
||||
|
||||
namespace stacktrace
|
||||
{
|
||||
void replace_signal_handlers(SignalFunc signalFunc,
|
||||
ExitFunc atExitFunc) noexcept
|
||||
{}
|
||||
}; // namespace stacktrace
|
||||
|
||||
#else
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#include <dbghelp.h>
|
||||
@@ -55,8 +66,6 @@ const char* strsignal(int signo)
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef NO_REDIRECT_OUTPUT
|
||||
|
||||
namespace stacktrace
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
@@ -285,9 +294,5 @@ void replace_signal_handlers(SignalFunc inSignalFunc,
|
||||
|
||||
#endif // defined _WIN32
|
||||
|
||||
#else // NO_REDIRECT_OUTPUT is defined
|
||||
void replace_signal_handlers(SignalFunc signalFunc,
|
||||
ExitFunc atExitFunc) noexcept
|
||||
{}
|
||||
#endif // ifdef NO_REDIRECT_OUTPUT
|
||||
};
|
||||
}; // namespace stacktrace
|
||||
#endif // defined(NO_REDIRECT_OUTPUT) || defined(__EMSCRIPTEN__)
|
||||
|
||||
@@ -43,7 +43,10 @@ static bool is_socket_valid(SOCKET sockfd)
|
||||
|
||||
static void close_socket(SOCKET sockfd)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
#ifdef __EMSCRIPTEN__
|
||||
constexpr static int WEBSOCKET_NORMAL_CLOSURE = 1000;
|
||||
emscripten_websocket_close(sockfd, WEBSOCKET_NORMAL_CLOSURE, "finished");
|
||||
#elif defined(_WIN32)
|
||||
closesocket(sockfd);
|
||||
#else
|
||||
close(sockfd);
|
||||
@@ -51,12 +54,10 @@ static void close_socket(SOCKET sockfd)
|
||||
}
|
||||
|
||||
TCPClient::TCPClient(const char* serverAddress /*server:port*/, bool* success) :
|
||||
m_serverAddress(serverAddress)
|
||||
m_serverAddress(serverAddress), m_sockfd(invalid_socket())
|
||||
{
|
||||
*success = false;
|
||||
|
||||
m_sockfd = invalid_socket();
|
||||
|
||||
char hostname[256];
|
||||
uint16_t port;
|
||||
if (sscanf(serverAddress, "%255[^:]:%hu", hostname, &port) != 2)
|
||||
@@ -64,13 +65,30 @@ TCPClient::TCPClient(const char* serverAddress /*server:port*/, bool* success) :
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// WASM has to use websockets instead of TCP.
|
||||
if (!emscripten_websocket_is_supported())
|
||||
{
|
||||
fprintf(stderr,
|
||||
"ERROR: WebSockets are not supported in this browser.\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
auto url = std::string("ws://") + serverAddress;
|
||||
EmscriptenWebSocketCreateAttributes attr = {
|
||||
.url = url.c_str(),
|
||||
.protocols = NULL,
|
||||
.createOnMainThread = EM_TRUE,
|
||||
};
|
||||
|
||||
m_sockfd = emscripten_websocket_new(&attr);
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
WSADATA wsaData;
|
||||
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
|
||||
{
|
||||
fprintf(stderr, "WSAStartup() failed.\n");
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -79,6 +97,7 @@ TCPClient::TCPClient(const char* serverAddress /*server:port*/, bool* success) :
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = inet_addr(hostname);
|
||||
m_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
#endif
|
||||
|
||||
if (!is_socket_valid(m_sockfd))
|
||||
{
|
||||
@@ -90,6 +109,29 @@ TCPClient::TCPClient(const char* serverAddress /*server:port*/, bool* success) :
|
||||
abort();
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_websocket_set_onopen_callback(m_sockfd, this, OnWebSocketOpen);
|
||||
emscripten_websocket_set_onmessage_callback(m_sockfd,
|
||||
this,
|
||||
OnWebSocketMessage);
|
||||
emscripten_websocket_set_onerror_callback(m_sockfd, this, OnWebSocketError);
|
||||
|
||||
while (m_webSocketStatus != WebSocketStatus::open)
|
||||
{
|
||||
if (m_webSocketStatus == WebSocketStatus::error)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"Unable to connect to WebSocket at %s\n",
|
||||
url.c_str());
|
||||
abort();
|
||||
}
|
||||
// Busy wait until our connection is established.
|
||||
//
|
||||
// NOTE: emscripten_sleep() is an async operation that yields control
|
||||
// back to the browser to process.
|
||||
emscripten_sleep(10);
|
||||
}
|
||||
#else
|
||||
if (connect(m_sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0)
|
||||
{
|
||||
fprintf(stderr,
|
||||
@@ -99,6 +141,7 @@ TCPClient::TCPClient(const char* serverAddress /*server:port*/, bool* success) :
|
||||
strerror(GetLastError()));
|
||||
abort();
|
||||
}
|
||||
#endif
|
||||
|
||||
*success = true;
|
||||
}
|
||||
@@ -127,6 +170,16 @@ std::unique_ptr<TCPClient> TCPClient::clone() const
|
||||
|
||||
uint32_t TCPClient::send(const char* data, uint32_t size)
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
if (emscripten_websocket_send_binary(m_sockfd,
|
||||
const_cast<char*>(data),
|
||||
size) < 0)
|
||||
{
|
||||
fprintf(stderr, "Failed to send %u bytes to websocket.\n", size);
|
||||
abort();
|
||||
}
|
||||
return size;
|
||||
#else
|
||||
size_t sent = ::send(m_sockfd, data, size, 0);
|
||||
if (sent == -1)
|
||||
{
|
||||
@@ -134,12 +187,29 @@ uint32_t TCPClient::send(const char* data, uint32_t size)
|
||||
abort();
|
||||
}
|
||||
return rive::math::lossless_numeric_cast<uint32_t>(sent);
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t TCPClient::recv(char* buff, uint32_t size)
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
while (m_serverMessages.size() < size)
|
||||
{
|
||||
// Busy wait until the server sends us our data. This isn't ideal, but
|
||||
// we always expect immediate responses with our testing protocol.
|
||||
//
|
||||
// NOTE: emscripten_sleep() is an async operation that yields control
|
||||
// back to the browser to process.
|
||||
emscripten_sleep(10);
|
||||
}
|
||||
std::copy(m_serverMessages.begin(), m_serverMessages.begin() + size, buff);
|
||||
m_serverMessages.erase(m_serverMessages.begin(),
|
||||
m_serverMessages.begin() + size);
|
||||
return size;
|
||||
#else
|
||||
return rive::math::lossless_numeric_cast<uint32_t>(
|
||||
::recv(m_sockfd, buff, size, 0));
|
||||
#endif
|
||||
}
|
||||
|
||||
void TCPClient::sendall(const void* data, size_t size)
|
||||
@@ -208,4 +278,39 @@ std::string TCPClient::recvString()
|
||||
recvall(str.data(), length);
|
||||
return str;
|
||||
}
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EM_BOOL TCPClient::OnWebSocketOpen(int eventType,
|
||||
const EmscriptenWebSocketOpenEvent* e,
|
||||
void* userData)
|
||||
{
|
||||
auto this_ = reinterpret_cast<TCPClient*>(userData);
|
||||
assert(e->socket == this_->m_sockfd);
|
||||
this_->m_webSocketStatus = WebSocketStatus::open;
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL TCPClient::OnWebSocketMessage(int eventType,
|
||||
const EmscriptenWebSocketMessageEvent* e,
|
||||
void* userData)
|
||||
{
|
||||
auto this_ = reinterpret_cast<TCPClient*>(userData);
|
||||
assert(e->socket == this_->m_sockfd);
|
||||
this_->m_serverMessages.insert(this_->m_serverMessages.end(),
|
||||
e->data,
|
||||
e->data + e->numBytes);
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
EM_BOOL TCPClient::OnWebSocketError(int eventType,
|
||||
const EmscriptenWebSocketErrorEvent* e,
|
||||
void* userData)
|
||||
{
|
||||
auto this_ = reinterpret_cast<TCPClient*>(userData);
|
||||
assert(e->socket == this_->m_sockfd);
|
||||
this_->m_webSocketStatus = WebSocketStatus::error;
|
||||
return EM_TRUE;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
#include <string>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten/websocket.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <deque>
|
||||
#define SOCKET EMSCRIPTEN_WEBSOCKET_T
|
||||
#elif defined(_WIN32)
|
||||
#include "WinSock2.h"
|
||||
#else
|
||||
#include <unistd.h>
|
||||
@@ -64,4 +69,26 @@ private:
|
||||
|
||||
const std::string m_serverAddress;
|
||||
SOCKET m_sockfd;
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
static EM_BOOL OnWebSocketOpen(int eventType,
|
||||
const EmscriptenWebSocketOpenEvent* e,
|
||||
void* userData);
|
||||
static EM_BOOL OnWebSocketMessage(int eventType,
|
||||
const EmscriptenWebSocketMessageEvent*,
|
||||
void* userData);
|
||||
static EM_BOOL OnWebSocketError(int eventType,
|
||||
const EmscriptenWebSocketErrorEvent*,
|
||||
void* userData);
|
||||
|
||||
enum class WebSocketStatus
|
||||
{
|
||||
pendingConnection,
|
||||
open,
|
||||
error,
|
||||
};
|
||||
|
||||
WebSocketStatus m_webSocketStatus = WebSocketStatus::pendingConnection;
|
||||
std::deque<uint8_t> m_serverMessages;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -25,9 +25,7 @@ constexpr static uint32_t REQUEST_TYPE_CLAIM_GM_TEST = 1;
|
||||
constexpr static uint32_t REQUEST_TYPE_FETCH_RIV_FILE = 2;
|
||||
constexpr static uint32_t REQUEST_TYPE_GET_INPUT = 3;
|
||||
constexpr static uint32_t REQUEST_TYPE_CANCEL_INPUT = 4;
|
||||
#ifndef NO_REDIRECT_OUTPUT
|
||||
constexpr static uint32_t REQUEST_TYPE_PRINT_MESSAGE = 5;
|
||||
#endif
|
||||
constexpr static uint32_t REQUEST_TYPE_DISCONNECT = 6;
|
||||
constexpr static uint32_t REQUEST_TYPE_APPLICATION_CRASH = 7;
|
||||
|
||||
@@ -66,10 +64,13 @@ void TestHarness::init(std::unique_ptr<TCPClient> tcpClient,
|
||||
|
||||
initStdioThread();
|
||||
|
||||
// We don't compile with emscripten pthreads.
|
||||
#ifndef __EMSCRIPTEN__
|
||||
for (size_t i = 0; i < pngThreadCount; ++i)
|
||||
{
|
||||
m_encodeThreads.emplace_back(EncodePNGThread, this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void TestHarness::init(std::filesystem::path outputDir, size_t pngThreadCount)
|
||||
@@ -92,8 +93,7 @@ void TestHarness::init(std::filesystem::path outputDir, size_t pngThreadCount)
|
||||
|
||||
void TestHarness::initStdioThread()
|
||||
{
|
||||
#ifndef NO_REDIRECT_OUTPUT
|
||||
|
||||
#if !defined(NO_REDIRECT_OUTPUT) && !defined(__EMSCRIPTEN__)
|
||||
#ifndef _WIN32
|
||||
// Make stdout & stderr line buffered. (This is not supported on Windows.)
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
@@ -111,7 +111,7 @@ void TestHarness::initStdioThread()
|
||||
|
||||
void TestHarness::monitorStdIOThread()
|
||||
{
|
||||
#ifndef NO_REDIRECT_OUTPUT
|
||||
#if !defined(NO_REDIRECT_OUTPUT) && !defined(__EMSCRIPTEN__)
|
||||
assert(m_initialized);
|
||||
|
||||
std::unique_ptr<TCPClient> threadTCPClient;
|
||||
@@ -163,6 +163,124 @@ void send_png_data_chunk(png_structp png, png_bytep data, png_size_t length)
|
||||
|
||||
void flush_png_data(png_structp png) {}
|
||||
|
||||
static void save_png_impl(ImageSaveArgs args,
|
||||
const std::filesystem::path& outputDir,
|
||||
PNGCompression pngCompression,
|
||||
TCPClient* tcpClient)
|
||||
{
|
||||
assert(args.width > 0);
|
||||
assert(args.height > 0);
|
||||
std::string pngName = args.name + ".png";
|
||||
|
||||
if (tcpClient == nullptr)
|
||||
{
|
||||
// We aren't connect to a test harness. Just save a file.
|
||||
auto destination = outputDir;
|
||||
destination /= pngName;
|
||||
destination.make_preferred();
|
||||
WritePNGFile(args.pixels.data(),
|
||||
args.width,
|
||||
args.height,
|
||||
true,
|
||||
destination.generic_string().c_str(),
|
||||
pngCompression);
|
||||
return;
|
||||
}
|
||||
|
||||
tcpClient->send4(REQUEST_TYPE_IMAGE_UPLOAD);
|
||||
tcpClient->sendString(pngName);
|
||||
|
||||
auto png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
|
||||
if (!png)
|
||||
{
|
||||
fprintf(stderr, "TestHarness: png_create_write_struct failed\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
// RLE with SUB gets best performance with our content.
|
||||
png_set_compression_level(png, 6);
|
||||
png_set_compression_strategy(png, Z_RLE);
|
||||
png_set_compression_strategy(png, Z_RLE);
|
||||
png_set_filter(png, 0, PNG_FILTER_SUB);
|
||||
|
||||
auto info = png_create_info_struct(png);
|
||||
if (!info)
|
||||
{
|
||||
fprintf(stderr, "TestHarness: png_create_info_struct failed\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
if (setjmp(png_jmpbuf(png)))
|
||||
{
|
||||
fprintf(stderr, "TestHarness: Error during init_io\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
png_set_write_fn(png, tcpClient, &send_png_data_chunk, &flush_png_data);
|
||||
|
||||
// Write header.
|
||||
if (setjmp(png_jmpbuf(png)))
|
||||
{
|
||||
fprintf(stderr, "TestHarness: Error during writing header\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
png_set_IHDR(png,
|
||||
info,
|
||||
args.width,
|
||||
args.height,
|
||||
8,
|
||||
PNG_COLOR_TYPE_RGB_ALPHA,
|
||||
PNG_INTERLACE_NONE,
|
||||
PNG_COMPRESSION_TYPE_BASE,
|
||||
PNG_FILTER_TYPE_BASE);
|
||||
|
||||
png_write_info(png, info);
|
||||
|
||||
// Write bytes.
|
||||
if (setjmp(png_jmpbuf(png)))
|
||||
{
|
||||
fprintf(stderr, "TestHarness: Error during writing bytes\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
std::vector<uint8_t*> rows(args.height);
|
||||
for (uint32_t y = 0; y < args.height; ++y)
|
||||
{
|
||||
rows[y] = args.pixels.data() + (args.height - 1 - y) * args.width * 4;
|
||||
}
|
||||
png_write_image(png, rows.data());
|
||||
|
||||
// End write.
|
||||
if (setjmp(png_jmpbuf(png)))
|
||||
{
|
||||
fprintf(stderr, "TestHarness: Error during end of write");
|
||||
abort();
|
||||
}
|
||||
|
||||
png_write_end(png, NULL);
|
||||
png_destroy_write_struct(&png, &info);
|
||||
|
||||
tcpClient->sendHandshake();
|
||||
tcpClient->recvHandshake();
|
||||
}
|
||||
|
||||
void TestHarness::savePNG(ImageSaveArgs args)
|
||||
{
|
||||
assert(m_initialized);
|
||||
if (!m_encodeThreads.empty())
|
||||
{
|
||||
m_encodeQueue.push(std::move(args));
|
||||
}
|
||||
else
|
||||
{
|
||||
save_png_impl(std::move(args),
|
||||
m_outputDir,
|
||||
m_pngCompression,
|
||||
m_primaryTCPClient.get());
|
||||
}
|
||||
}
|
||||
|
||||
void TestHarness::encodePNGThread()
|
||||
{
|
||||
assert(m_initialized);
|
||||
@@ -181,106 +299,10 @@ void TestHarness::encodePNGThread()
|
||||
{
|
||||
break;
|
||||
}
|
||||
assert(args.width > 0);
|
||||
assert(args.height > 0);
|
||||
std::string pngName = args.name + ".png";
|
||||
|
||||
if (threadTCPClient == nullptr)
|
||||
{
|
||||
// We aren't connect to a test harness. Just save a file.
|
||||
auto destination = m_outputDir;
|
||||
destination /= pngName;
|
||||
destination.make_preferred();
|
||||
WritePNGFile(args.pixels.data(),
|
||||
args.width,
|
||||
args.height,
|
||||
true,
|
||||
destination.generic_string().c_str(),
|
||||
m_pngCompression);
|
||||
continue;
|
||||
}
|
||||
|
||||
threadTCPClient->send4(REQUEST_TYPE_IMAGE_UPLOAD);
|
||||
threadTCPClient->sendString(pngName);
|
||||
|
||||
auto png =
|
||||
png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
|
||||
if (!png)
|
||||
{
|
||||
fprintf(stderr, "TestHarness: png_create_write_struct failed\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
// RLE with SUB gets best performance with our content.
|
||||
png_set_compression_level(png, 6);
|
||||
png_set_compression_strategy(png, Z_RLE);
|
||||
png_set_compression_strategy(png, Z_RLE);
|
||||
png_set_filter(png, 0, PNG_FILTER_SUB);
|
||||
|
||||
auto info = png_create_info_struct(png);
|
||||
if (!info)
|
||||
{
|
||||
fprintf(stderr, "TestHarness: png_create_info_struct failed\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
if (setjmp(png_jmpbuf(png)))
|
||||
{
|
||||
fprintf(stderr, "TestHarness: Error during init_io\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
png_set_write_fn(png,
|
||||
threadTCPClient.get(),
|
||||
&send_png_data_chunk,
|
||||
&flush_png_data);
|
||||
|
||||
// Write header.
|
||||
if (setjmp(png_jmpbuf(png)))
|
||||
{
|
||||
fprintf(stderr, "TestHarness: Error during writing header\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
png_set_IHDR(png,
|
||||
info,
|
||||
args.width,
|
||||
args.height,
|
||||
8,
|
||||
PNG_COLOR_TYPE_RGB_ALPHA,
|
||||
PNG_INTERLACE_NONE,
|
||||
PNG_COMPRESSION_TYPE_BASE,
|
||||
PNG_FILTER_TYPE_BASE);
|
||||
|
||||
png_write_info(png, info);
|
||||
|
||||
// Write bytes.
|
||||
if (setjmp(png_jmpbuf(png)))
|
||||
{
|
||||
fprintf(stderr, "TestHarness: Error during writing bytes\n");
|
||||
abort();
|
||||
}
|
||||
|
||||
std::vector<uint8_t*> rows(args.height);
|
||||
for (uint32_t y = 0; y < args.height; ++y)
|
||||
{
|
||||
rows[y] =
|
||||
args.pixels.data() + (args.height - 1 - y) * args.width * 4;
|
||||
}
|
||||
png_write_image(png, rows.data());
|
||||
|
||||
// End write.
|
||||
if (setjmp(png_jmpbuf(png)))
|
||||
{
|
||||
fprintf(stderr, "TestHarness: Error during end of write");
|
||||
abort();
|
||||
}
|
||||
|
||||
png_write_end(png, NULL);
|
||||
png_destroy_write_struct(&png, &info);
|
||||
|
||||
threadTCPClient->sendHandshake();
|
||||
threadTCPClient->recvHandshake();
|
||||
save_png_impl(std::move(args),
|
||||
m_outputDir,
|
||||
m_pngCompression,
|
||||
threadTCPClient.get());
|
||||
}
|
||||
|
||||
if (threadTCPClient != nullptr)
|
||||
@@ -321,6 +343,15 @@ bool TestHarness::fetchRivFile(std::string& name, std::vector<uint8_t>& bytes)
|
||||
return true;
|
||||
}
|
||||
|
||||
void TestHarness::printMessageOnServer(const char* msg)
|
||||
{
|
||||
if (m_primaryTCPClient != nullptr)
|
||||
{
|
||||
m_primaryTCPClient->send4(REQUEST_TYPE_PRINT_MESSAGE);
|
||||
m_primaryTCPClient->sendString(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void TestHarness::inputPumpThread()
|
||||
{
|
||||
assert(m_initialized);
|
||||
@@ -354,6 +385,10 @@ void TestHarness::inputPumpThread()
|
||||
|
||||
bool TestHarness::peekChar(char& key)
|
||||
{
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// We don't compile with emscripten pthreads.
|
||||
return false;
|
||||
#endif
|
||||
if (m_primaryTCPClient == nullptr)
|
||||
{
|
||||
return false;
|
||||
@@ -397,7 +432,7 @@ void TestHarness::shutdown()
|
||||
|
||||
void TestHarness::shutdownStdioThread()
|
||||
{
|
||||
#ifndef NO_REDIRECT_OUTPUT
|
||||
#if !defined(NO_REDIRECT_OUTPUT) && !defined(__EMSCRIPTEN__)
|
||||
if (m_savedStdout != 0 || m_savedStderr != 0)
|
||||
{
|
||||
// Restore stdout and stderr.
|
||||
|
||||
@@ -43,14 +43,7 @@ public:
|
||||
m_pngCompression = compression;
|
||||
}
|
||||
|
||||
void savePNG(ImageSaveArgs args)
|
||||
{
|
||||
assert(m_initialized);
|
||||
if (!m_encodeThreads.empty())
|
||||
{
|
||||
m_encodeQueue.push(std::move(args));
|
||||
}
|
||||
}
|
||||
void savePNG(ImageSaveArgs args);
|
||||
|
||||
// Only returns true the on the first server request for a given name.
|
||||
// Prevents gms from running more than once in a multi-process execution.
|
||||
@@ -60,6 +53,10 @@ public:
|
||||
// thread.)
|
||||
bool fetchRivFile(std::string& name, std::vector<uint8_t>& bytes);
|
||||
|
||||
// Sends a message to be printed on the server's stdout, e.g., for
|
||||
// forwarding the client's stdout.
|
||||
void printMessageOnServer(const char* msg);
|
||||
|
||||
// Returns true if there is an input character to process from the server.
|
||||
bool peekChar(char& key);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include "testing_window.hpp"
|
||||
|
||||
#ifdef RIVE_TOOLS_NO_GL
|
||||
#if defined(RIVE_TOOLS_NO_GL) || defined(__EMSCRIPTEN__)
|
||||
|
||||
TestingWindow* TestingWindow::MakeEGL(Backend backend,
|
||||
const BackendParams& backendParams,
|
||||
|
||||
@@ -21,9 +21,7 @@ TestingWindow* TestingWindow::MakeFiddleContext(Backend,
|
||||
#include <queue>
|
||||
|
||||
#define GLFW_INCLUDE_NONE
|
||||
#define GLFW_NATIVE_INCLUDE_NONE
|
||||
#include "GLFW/glfw3.h"
|
||||
#include <GLFW/glfw3native.h>
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
using namespace rive;
|
||||
using namespace rive::gpu;
|
||||
@@ -142,7 +140,7 @@ public:
|
||||
#elif defined(_WIN32)
|
||||
glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE,
|
||||
GLFW_ANGLE_PLATFORM_TYPE_D3D11);
|
||||
#else
|
||||
#elif !defined(__EMSCRIPTEN__)
|
||||
glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE,
|
||||
GLFW_ANGLE_PLATFORM_TYPE_VULKAN);
|
||||
#endif
|
||||
@@ -227,7 +225,9 @@ public:
|
||||
abort();
|
||||
}
|
||||
glfwMakeContextCurrent(m_glfwWindow);
|
||||
#ifndef __EMSCRIPTEN__
|
||||
glfwSwapInterval(0);
|
||||
#endif
|
||||
|
||||
glfwSetKeyCallback(m_glfwWindow, key_callback);
|
||||
glfwSetCursorPosCallback(m_glfwWindow, mouse_position_callback);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import argparse
|
||||
import atexit
|
||||
import glob
|
||||
import http.server
|
||||
import os
|
||||
import platform
|
||||
import queue
|
||||
@@ -65,7 +66,8 @@ parser.add_argument("-m", "--match",
|
||||
help="`match` patter for gms")
|
||||
parser.add_argument("-t", "--target",
|
||||
default="host",
|
||||
choices=["host", "android", "ios", "iossim", "unreal", "unreal_android"],
|
||||
choices=["host", "android", "ios", "iossim", "unreal",
|
||||
"unreal_android", "webbrowser", "webserver"],
|
||||
help="which platform to run on")
|
||||
parser.add_argument("-a", "--android-arch",
|
||||
default="arm64",
|
||||
@@ -74,6 +76,9 @@ parser.add_argument("-u", "--ios_udid",
|
||||
type=str,
|
||||
default=None,
|
||||
help="unique id of iOS device to run on (--target=ios or iossim)")
|
||||
parser.add_argument("-c", "--webclient",
|
||||
default=None,
|
||||
help="executable to launch when --target=webserver")
|
||||
parser.add_argument("-k", "--options",
|
||||
type=str,
|
||||
default=None,
|
||||
@@ -171,6 +176,99 @@ def get_local_ip():
|
||||
s.close()
|
||||
return ip
|
||||
|
||||
# Simple http server for web-based targets.
|
||||
def start_http_server(directory, server_ip):
|
||||
import functools
|
||||
import http.server
|
||||
|
||||
http_port_holder = []
|
||||
http_port_ready_event = threading.Event()
|
||||
|
||||
class COOPHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def end_headers(self):
|
||||
# Serve cross-origin isolated pages so SharedArrayBuffer is defined
|
||||
# for emscripten POSIX emulation.
|
||||
self.send_header("Cross-Origin-Opener-Policy", "same-origin")
|
||||
self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
|
||||
super().end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
# Suppress default HTTP logs like:
|
||||
# 127.0.0.1 - - [22/Aug/2025 13:37:00] "GET /index.html HTTP/1.1" 200 -
|
||||
pass
|
||||
|
||||
handler = functools.partial(COOPHandler, directory=directory)
|
||||
|
||||
def run_http_server():
|
||||
with socketserver.TCPServer((server_ip, 0), handler) as httpd:
|
||||
http_port_holder.append(httpd.server_address[1])
|
||||
http_port_ready_event.set()
|
||||
httpd.serve_forever()
|
||||
|
||||
thread = threading.Thread(target=run_http_server, daemon=True)
|
||||
thread.start()
|
||||
|
||||
http_port_ready_event.wait() # wait until server is ready
|
||||
return (server_ip, http_port_holder[0])
|
||||
|
||||
# Simple websocket <-> TCP bridge for web-based targets.
|
||||
def start_websocket_bridge(tcp_server_address):
|
||||
import asyncio
|
||||
import threading
|
||||
import socket
|
||||
import websockets
|
||||
|
||||
websocket_port_holder = []
|
||||
port_ready_event = threading.Event()
|
||||
|
||||
async def handle_websocket(websocket):
|
||||
reader, writer = await asyncio.open_connection(*tcp_server_address)
|
||||
|
||||
async def tcp_to_ws():
|
||||
try:
|
||||
while True:
|
||||
data = await reader.read(1024)
|
||||
if not data:
|
||||
break
|
||||
await websocket.send(data)
|
||||
except:
|
||||
raise
|
||||
|
||||
async def ws_to_tcp():
|
||||
try:
|
||||
async for message in websocket:
|
||||
if isinstance(message, str):
|
||||
message = message.encode()
|
||||
writer.write(message)
|
||||
await writer.drain()
|
||||
except:
|
||||
raise
|
||||
|
||||
await asyncio.gather(tcp_to_ws(), ws_to_tcp())
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
|
||||
async def run_server(sock):
|
||||
async with websockets.serve(handle_websocket, sock=sock):
|
||||
await asyncio.Future() # run forever
|
||||
|
||||
def server_thread():
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.bind(('0.0.0.0', 0)) # OS assigns port
|
||||
sock.listen(5)
|
||||
sock.setblocking(False)
|
||||
|
||||
websocket_port_holder.append(sock.getsockname()[1])
|
||||
port_ready_event.set()
|
||||
|
||||
asyncio.run(run_server(sock))
|
||||
|
||||
thread = threading.Thread(target=server_thread, daemon=True)
|
||||
thread.start()
|
||||
|
||||
port_ready_event.wait()
|
||||
return (tcp_server_address[0], websocket_port_holder[0])
|
||||
|
||||
# Simple TCP server for Rive tools.
|
||||
class ToolServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
daemon_threads = True
|
||||
@@ -207,6 +305,19 @@ class ToolServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
||||
hostname, port = self.server_address # find out what port we were given
|
||||
subprocess.Popen(["adb", "reverse", "tcp:%s" % port, "tcp:%s" % port],
|
||||
stdout=subprocess.DEVNULL)
|
||||
print("TestHarness server running on %s:%u" % self.server_address, flush=True)
|
||||
|
||||
if args.target.startswith("web"):
|
||||
self.server_address = start_websocket_bridge(self.server_address)
|
||||
print("TestHarness server bridged to WebSocket on %s:%u" % self.server_address,
|
||||
flush=True)
|
||||
|
||||
self.http_address = start_http_server(args.builddir,
|
||||
self.server_address[0])
|
||||
print("HTTP server running from %s on %s:%u" % (args.builddir,
|
||||
*self.http_address),
|
||||
flush=True)
|
||||
|
||||
|
||||
# Simple utility to wait until a TCP client tells the server it has finished.
|
||||
def wait_for_shutdown_event(self, timeout=threading.TIMEOUT_MAX):
|
||||
@@ -345,7 +456,7 @@ class TestHarnessRequestHandler(socketserver.BaseRequestHandler):
|
||||
|
||||
|
||||
# If we aren't deploying to the host, update the given command to deploy on its intended target.
|
||||
def update_cmd_to_deploy_on_target(cmd):
|
||||
def update_cmd_to_deploy_on_target(cmd, test_harness_server):
|
||||
dirname = os.path.dirname(cmd[0])
|
||||
toolname = os.path.basename(cmd[0])
|
||||
|
||||
@@ -389,6 +500,16 @@ def update_cmd_to_deploy_on_target(cmd):
|
||||
cmd = [toolname] + cmd[1:]
|
||||
return ["xcrun", "simctl", "launch", args.ios_udid, "rive.app.golden-test-app"] + cmd
|
||||
|
||||
elif args.target.startswith("web"):
|
||||
if args.target == "webbrowser":
|
||||
client = ["python3", "-m", "webbrowser", "-t"]
|
||||
elif args.webclient:
|
||||
client = [args.webclient]
|
||||
else:
|
||||
client = ["echo", "\nPlease navigate your web client to:\n\n"]
|
||||
return client + ["http://%s:%u/%s.html#%s" % (*test_harness_server.http_address,
|
||||
toolname,
|
||||
'%20'.join(cmd[1:]))]
|
||||
else:
|
||||
assert(args.target == "host")
|
||||
return cmd
|
||||
@@ -399,14 +520,15 @@ def launch_gms(test_harness_server):
|
||||
tool = tool + ".exe"
|
||||
cmd = [tool,
|
||||
"--backend", args.backend,
|
||||
"--test_harness", "%s:%u" % test_harness_server.server_address,
|
||||
"--headless",
|
||||
"-p%i" % args.png_threads]
|
||||
"--test_harness", "%s:%u" % test_harness_server.server_address]
|
||||
if not args.target.startswith("web"):
|
||||
cmd = cmd + ["--headless",
|
||||
"-p%i" % args.png_threads];
|
||||
if args.match:
|
||||
cmd = cmd + ["--match", args.match];
|
||||
if args.verbose:
|
||||
cmd = cmd + ["--verbose"];
|
||||
cmd = update_cmd_to_deploy_on_target(cmd)
|
||||
cmd = update_cmd_to_deploy_on_target(cmd, test_harness_server)
|
||||
|
||||
procs = [CheckProcess(cmd) for i in range(0, args.jobs_per_tool)]
|
||||
for proc in procs:
|
||||
@@ -436,12 +558,13 @@ def launch_goldens(test_harness_server):
|
||||
"--test_harness", "%s:%u" % test_harness_server.server_address,
|
||||
"--backend", args.backend,
|
||||
"--rows", str(args.rows),
|
||||
"--cols", str(args.cols),
|
||||
"--headless",
|
||||
"-p%i" % args.png_threads]
|
||||
"--cols", str(args.cols)]
|
||||
if not args.target.startswith("web"):
|
||||
cmd = cmd + ["--headless",
|
||||
"-p%i" % args.png_threads];
|
||||
if args.verbose:
|
||||
cmd = cmd + ["--verbose"];
|
||||
cmd = update_cmd_to_deploy_on_target(cmd)
|
||||
cmd = update_cmd_to_deploy_on_target(cmd, test_harness_server)
|
||||
|
||||
procs = [CheckProcess(cmd) for i in range(0, args.jobs_per_tool)]
|
||||
for proc in procs:
|
||||
@@ -462,7 +585,7 @@ def launch_player(test_harness_server):
|
||||
"--backend", args.backend]
|
||||
if args.options:
|
||||
cmd += ["--options", args.options]
|
||||
cmd = update_cmd_to_deploy_on_target(cmd)
|
||||
cmd = update_cmd_to_deploy_on_target(cmd, test_harness_server)
|
||||
|
||||
rivsqueue.put(args.src)
|
||||
player = CheckProcess(cmd)
|
||||
@@ -527,6 +650,12 @@ def main():
|
||||
args.builddir = os.path.join("out", "debug")
|
||||
# unreal is currently always rhi, we may have seperate rhi types in the future like rhi_metal etc..
|
||||
args.backend = 'rhi'
|
||||
elif args.target.startswith("web"):
|
||||
args.jobs_per_tool = 1
|
||||
if args.builddir == None:
|
||||
args.builddir = f"out/wasm_debug"
|
||||
if args.backend == None:
|
||||
args.backend = "gl"
|
||||
else:
|
||||
assert(args.target == "host")
|
||||
if args.builddir == None:
|
||||
@@ -670,11 +799,12 @@ def main():
|
||||
|
||||
with (ToolServer(TestHarnessRequestHandler) as test_harness_server):
|
||||
test_harness_server.serve_forever_async()
|
||||
print("TestHarness server running on %s:%u" % test_harness_server.server_address,
|
||||
flush=True)
|
||||
|
||||
# On mobile we can't launch >1 instance of the app at a time.
|
||||
serial_deploy = not args.server_only and ("ios" in args.target or args.target == "android" or args.target == "unreal")
|
||||
# On many targets we can't launch >1 instance of the app at a time.
|
||||
serial_deploy = not args.server_only and ("ios" in args.target or
|
||||
args.target == "android" or
|
||||
"unreal" in args.target or
|
||||
args.target.startswith("web"))
|
||||
procs = []
|
||||
|
||||
def keyboard_interrupt_handler(signal, frame):
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
#include <sys/system_properties.h>
|
||||
#endif
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include "common/rive_wasm_app.hpp"
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
#include <functional>
|
||||
|
||||
using namespace rivegm;
|
||||
@@ -220,6 +225,10 @@ static void dumpGMs(const std::string& match, bool interactive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// Yield control back to the browser so it can process its event loop.
|
||||
emscripten_sleep(1);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -307,6 +316,8 @@ extern "C" int gms_main(int argc, const char* argv[])
|
||||
int gms_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
|
||||
@@ -415,6 +426,9 @@ int main(int argc, const char* argv[])
|
||||
TestingWindow::Destroy(); // Exercise our PLS teardown process now that
|
||||
// we're done.
|
||||
TestHarness::Instance().shutdown();
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EM_ASM(window.close(););
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
23
tests/gm/gms.html
Normal file
23
tests/gm/gms.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<canvas id="canvas"></canvas>
|
||||
<script>
|
||||
window.onhashchange = function() { location.reload(); }
|
||||
function printMessageOnServer(text) {
|
||||
const lengthBytes = lengthBytesUTF8(text) + 1;
|
||||
const stringOnWasmHeap = Module['_malloc'](lengthBytes);
|
||||
stringToUTF8(text, stringOnWasmHeap, lengthBytes);
|
||||
Module['_rive_print_message_on_server'](stringOnWasmHeap);
|
||||
Module['_free'](stringOnWasmHeap);
|
||||
}
|
||||
var Module = {
|
||||
'canvas': document.getElementById("canvas"),
|
||||
'print': function(text) {
|
||||
printMessageOnServer(text + '\n');
|
||||
console.log(text);
|
||||
},
|
||||
'printErr': function(text) {
|
||||
printMessageOnServer(text + '\n');
|
||||
console.error(text);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<script src="gms.js"></script>
|
||||
15
tests/gm/gms_parallel.html
Normal file
15
tests/gm/gms_parallel.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
Experiment to run the tests in multiple popup windows.
|
||||
No perf improvement has been observed.
|
||||
-->
|
||||
<script>
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
const x = (i % 2) * 450;
|
||||
const y = Math.floor(i / 2) * 550;
|
||||
window.open(
|
||||
`gms.html${window.location.hash}`,
|
||||
`gms_${i + 1}`,
|
||||
`width=${400},height=${400},top=${y},left=${x},toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no`
|
||||
);
|
||||
}
|
||||
</script>
|
||||
@@ -24,19 +24,24 @@ public:
|
||||
void onDraw(rive::Renderer* renderer) override
|
||||
{
|
||||
Rand rand;
|
||||
ColorInt colors[N];
|
||||
float stops[N];
|
||||
std::vector<ColorInt> colors(N);
|
||||
std::vector<float> stops(N);
|
||||
for (size_t i = 0; i < N; ++i)
|
||||
{
|
||||
colors[i] = rand.u32() | 0xff808080;
|
||||
stops[i] = std::round(rand.f32() * W) / W;
|
||||
}
|
||||
std::sort(stops, stops + N);
|
||||
std::sort(stops.begin(), stops.end());
|
||||
|
||||
Paint paint;
|
||||
paint->shader(TestingWindow::Get()
|
||||
->factory()
|
||||
->makeLinearGradient(0, 0, W, 0, colors, stops, N));
|
||||
paint->shader(
|
||||
TestingWindow::Get()->factory()->makeLinearGradient(0,
|
||||
0,
|
||||
W,
|
||||
0,
|
||||
colors.data(),
|
||||
stops.data(),
|
||||
N));
|
||||
|
||||
Path fullscreen = PathBuilder().addRect({0, 0, W, H}).detach();
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
#include "common/rive_android_app.hpp"
|
||||
#endif
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include "common/rive_wasm_app.hpp"
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
constexpr static int kWindowTargetSize = 1600;
|
||||
|
||||
GoldensArguments s_args;
|
||||
@@ -124,6 +129,10 @@ static bool render_and_dump_png(int cellSize,
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// Yield control back to the browser so it can process its event loop.
|
||||
emscripten_sleep(1);
|
||||
#endif
|
||||
}
|
||||
catch (const char* msg)
|
||||
@@ -224,6 +233,8 @@ int goldens_main(int argc, const char* argv[])
|
||||
int goldens_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
|
||||
@@ -366,6 +377,9 @@ int main(int argc, const char* argv[])
|
||||
TestingWindow::Destroy(); // Exercise our PLS teardown process now that
|
||||
// we're done.
|
||||
TestHarness::Instance().shutdown();
|
||||
#ifdef __EMSCRIPTEN__
|
||||
EM_ASM(window.close(););
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
23
tests/goldens/goldens.html
Normal file
23
tests/goldens/goldens.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<canvas id="canvas"></canvas>
|
||||
<script>
|
||||
window.onhashchange = function() { location.reload(); }
|
||||
function printMessageOnServer(text) {
|
||||
const lengthBytes = lengthBytesUTF8(text) + 1;
|
||||
const stringOnWasmHeap = Module['_malloc'](lengthBytes);
|
||||
stringToUTF8(text, stringOnWasmHeap, lengthBytes);
|
||||
Module['_rive_print_message_on_server'](stringOnWasmHeap);
|
||||
Module['_free'](stringOnWasmHeap);
|
||||
}
|
||||
var Module = {
|
||||
'canvas': document.getElementById("canvas"),
|
||||
'print': function(text) {
|
||||
printMessageOnServer(text + '\n');
|
||||
console.log(text);
|
||||
},
|
||||
'printErr': function(text) {
|
||||
printMessageOnServer(text + '\n');
|
||||
console.error(text);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<script src="goldens.js"></script>
|
||||
15
tests/goldens/goldens_parallel.html
Normal file
15
tests/goldens/goldens_parallel.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!--
|
||||
Experiment to run the tests in multiple popup windows.
|
||||
No perf improvement has been observed.
|
||||
-->
|
||||
<script>
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
const x = (i % 2) * 450;
|
||||
const y = Math.floor(i / 2) * 550;
|
||||
window.open(
|
||||
`goldens.html${window.location.hash}`,
|
||||
`goldens_${i + 1}`,
|
||||
`width=${400},height=${400},top=${y},left=${x},toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,status=no`
|
||||
);
|
||||
}
|
||||
</script>
|
||||
@@ -20,6 +20,12 @@
|
||||
#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)
|
||||
@@ -134,10 +140,278 @@ static void key_pressed(char key)
|
||||
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
|
||||
@@ -147,6 +421,7 @@ int main(int argc, const char* argv[])
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
setvbuf(stderr, NULL, _IONBF, 0);
|
||||
#endif
|
||||
|
||||
std::string rivName;
|
||||
std::vector<uint8_t> rivBytes;
|
||||
auto backend =
|
||||
@@ -157,6 +432,7 @@ int main(int argc, const char* argv[])
|
||||
#endif
|
||||
auto visibility = TestingWindow::Visibility::fullscreen;
|
||||
TestingWindow::BackendParams backendParams;
|
||||
|
||||
for (int i = 0; i < argc; ++i)
|
||||
{
|
||||
if (strcmp(argv[i], "--test_harness") == 0)
|
||||
@@ -224,230 +500,23 @@ int main(int argc, const char* argv[])
|
||||
#endif
|
||||
);
|
||||
|
||||
// Load the riv file.
|
||||
if (rivBytes.empty())
|
||||
{
|
||||
fprintf(stderr, "no .riv file specified");
|
||||
abort();
|
||||
}
|
||||
rive::rcp<rive::File> file =
|
||||
rive::File::import(rivBytes, TestingWindow::Get()->factory());
|
||||
assert(file);
|
||||
std::unique_ptr<rive::ArtboardInstance> artboard = file->artboardDefault();
|
||||
assert(artboard);
|
||||
std::unique_ptr<rive::Scene> scene = artboard->defaultStateMachine();
|
||||
if (!scene)
|
||||
{
|
||||
scene = artboard->animationAt(0);
|
||||
}
|
||||
assert(scene);
|
||||
|
||||
int lastReportedCopyCount = 0;
|
||||
bool lastReportedPauseState = paused;
|
||||
player.init(std::move(rivName), std::move(rivBytes));
|
||||
|
||||
// Setup FPS.
|
||||
rive::rcp<rive::Font> roboto = HBFont::Decode(assets::roboto_flex_ttf());
|
||||
rive::rcp<rive::RenderPaint> blackStroke =
|
||||
TestingWindow::Get()->factory()->makeRenderPaint();
|
||||
blackStroke->color(0xff000000);
|
||||
blackStroke->style(rive::RenderPaintStyle::stroke);
|
||||
blackStroke->thickness(4);
|
||||
rive::rcp<rive::RenderPaint> whiteFill =
|
||||
TestingWindow::Get()->factory()->makeRenderPaint();
|
||||
whiteFill->color(0xffffffff);
|
||||
std::unique_ptr<rive::RawText> fpsText;
|
||||
int fpsFrames = 0;
|
||||
std::chrono::time_point timeLastFPSUpdate =
|
||||
std::chrono::high_resolution_clock::now();
|
||||
std::chrono::time_point timestampPrevFrame =
|
||||
std::chrono::high_resolution_clock::now();
|
||||
|
||||
while (!quit && !TestingWindow::Get()->shouldQuit())
|
||||
{
|
||||
std::chrono::time_point timeNow =
|
||||
std::chrono::high_resolution_clock::now();
|
||||
const double elapsedS =
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
timeNow - timestampPrevFrame)
|
||||
.count() /
|
||||
1e9; // convert to s
|
||||
timestampPrevFrame = timeNow;
|
||||
|
||||
float advanceDeltaTime = static_cast<float>(elapsedS);
|
||||
if (forceFixedDeltaTime)
|
||||
{
|
||||
advanceDeltaTime = 1.0f / 120;
|
||||
}
|
||||
|
||||
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,
|
||||
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),
|
||||
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)
|
||||
{
|
||||
artboard->draw(renderer.get());
|
||||
renderer->translate(spacingPx, 0);
|
||||
}
|
||||
renderer->restore();
|
||||
renderer->translate(0, spacingPx);
|
||||
}
|
||||
renderer->restore();
|
||||
|
||||
if (fpsText != nullptr)
|
||||
{
|
||||
// Draw FPS.
|
||||
renderer->save();
|
||||
renderer->translate(0, 20);
|
||||
fpsText->render(renderer.get(), blackStroke);
|
||||
fpsText->render(renderer.get(), whiteFill);
|
||||
renderer->restore();
|
||||
}
|
||||
|
||||
renderer->restore();
|
||||
TestingWindow::Get()->endFrame();
|
||||
|
||||
// Count FPS.
|
||||
++fpsFrames;
|
||||
const double elapsedFPSUpdate =
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
timeNow - timeLastFPSUpdate)
|
||||
.count() /
|
||||
1e9; // convert to s
|
||||
if (elapsedFPSUpdate >= 2.0)
|
||||
{
|
||||
double fps = fpsFrames / elapsedFPSUpdate;
|
||||
printf("[%.3f FPS]\n", fps);
|
||||
|
||||
char fpsRawText[32];
|
||||
snprintf(fpsRawText, sizeof(fpsRawText), " %.1f FPS ", fps);
|
||||
fpsText = std::make_unique<rive::RawText>(
|
||||
TestingWindow::Get()->factory());
|
||||
fpsText->maxWidth(width);
|
||||
#ifdef RIVE_ANDROID
|
||||
fpsText->align(rive::TextAlign::center);
|
||||
#ifdef __EMSCRIPTEN__
|
||||
emscripten_set_main_loop([]() { player.doFrame(); }, 0, true);
|
||||
#else
|
||||
fpsText->align(rive::TextAlign::right);
|
||||
#endif
|
||||
fpsText->sizing(rive::TextSizing::fixed);
|
||||
fpsText->append(fpsRawText, nullptr, roboto, 50.f);
|
||||
|
||||
fpsFrames = 0;
|
||||
timeLastFPSUpdate = timeNow;
|
||||
}
|
||||
|
||||
const rive::Mat2D alignmentMat =
|
||||
computeAlignment(rive::Fit::contain,
|
||||
rive::Alignment::center,
|
||||
rive::AABB(0, 0, width, height),
|
||||
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:
|
||||
scene->pointerMove(mousePosAligned);
|
||||
break;
|
||||
|
||||
case TestingWindow::InputEvent::MouseDown:
|
||||
scene->pointerDown(mousePosAligned);
|
||||
break;
|
||||
|
||||
case TestingWindow::InputEvent::MouseUp:
|
||||
scene->pointerUp(mousePosAligned);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
char key;
|
||||
while (TestHarness::Instance().peekChar(key))
|
||||
{
|
||||
key_pressed(key);
|
||||
}
|
||||
|
||||
#ifdef RIVE_ANDROID
|
||||
if (!rive_android_app_poll_once())
|
||||
{
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
for (;;)
|
||||
{
|
||||
player.doFrame();
|
||||
}
|
||||
#endif
|
||||
|
||||
printf("\nShutting down\n");
|
||||
TestingWindow::Destroy(); // Exercise our PLS teardown process now that
|
||||
// we're done.
|
||||
TestHarness::Instance().shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
32
tests/player/player.html
Normal file
32
tests/player/player.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<style>
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: black;
|
||||
}
|
||||
</style>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script>
|
||||
window.onhashchange = function() { location.reload(); }
|
||||
function printMessageOnServer(text) {
|
||||
const lengthBytes = lengthBytesUTF8(text) + 1;
|
||||
const stringOnWasmHeap = Module['_malloc'](lengthBytes);
|
||||
stringToUTF8(text, stringOnWasmHeap, lengthBytes);
|
||||
Module['_rive_print_message_on_server'](stringOnWasmHeap);
|
||||
Module['_free'](stringOnWasmHeap);
|
||||
}
|
||||
var Module = {
|
||||
'canvas': document.getElementById("canvas"),
|
||||
'print': function(text) {
|
||||
printMessageOnServer(text + '\n');
|
||||
console.log(text);
|
||||
},
|
||||
'printErr': function(text) {
|
||||
printMessageOnServer(text + '\n');
|
||||
console.error(text);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<script src="player.js"></script>
|
||||
@@ -14,6 +14,10 @@ do
|
||||
do
|
||||
defines({ 'RIVE_UNREAL' })
|
||||
end
|
||||
filter('system:emscripten')
|
||||
do
|
||||
files({ 'gm/gms.html' })
|
||||
end
|
||||
end
|
||||
|
||||
rive_tools_project('goldens', 'RiveTool')
|
||||
@@ -24,11 +28,19 @@ do
|
||||
do
|
||||
defines({ 'RIVE_UNREAL' })
|
||||
end
|
||||
filter('system:emscripten')
|
||||
do
|
||||
files({ 'goldens/goldens.html' })
|
||||
end
|
||||
end
|
||||
|
||||
rive_tools_project('player', 'RiveTool')
|
||||
do
|
||||
files({ 'player/player.cpp' })
|
||||
filter('system:emscripten')
|
||||
do
|
||||
files({ 'player/player.html' })
|
||||
end
|
||||
end
|
||||
|
||||
rive_tools_project('command_buffer_example', 'RiveTool')
|
||||
|
||||
@@ -77,7 +77,6 @@ function rive_tools_project(name, project_kind)
|
||||
'include',
|
||||
RIVE_PLS_DIR .. '/glad',
|
||||
RIVE_PLS_DIR .. '/glad/include',
|
||||
RIVE_RUNTIME_DIR .. '/skia/dependencies/glfw/include',
|
||||
yoga,
|
||||
libpng,
|
||||
zlib,
|
||||
@@ -92,6 +91,13 @@ function rive_tools_project(name, project_kind)
|
||||
dofile(RIVE_PLS_DIR .. '/rive_vk_bootstrap/bootstrap_project.lua')
|
||||
end
|
||||
|
||||
filter({ 'system:windows or macosx or linux', 'options:not for_unreal' })
|
||||
do
|
||||
externalincludedirs({
|
||||
RIVE_RUNTIME_DIR .. '/skia/dependencies/glfw/include',
|
||||
})
|
||||
end
|
||||
|
||||
filter('options:with-skia')
|
||||
do
|
||||
includedirs({
|
||||
@@ -181,113 +187,115 @@ function rive_tools_project(name, project_kind)
|
||||
'rive_sheenbidi',
|
||||
'miniaudio',
|
||||
})
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'options:not no_rive_jpeg' })
|
||||
do
|
||||
links({
|
||||
'libjpeg',
|
||||
})
|
||||
end
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'options:not no_rive_jpeg' })
|
||||
do
|
||||
links({
|
||||
'libjpeg',
|
||||
})
|
||||
end
|
||||
|
||||
if ndk then
|
||||
relative_ndk = ndk
|
||||
if string.sub(ndk, 1, 1) == '/' then
|
||||
-- An absolute file path wasn't working with premake.
|
||||
local current_path = string.gmatch(path.getabsolute('.'), '([^\\/]+)')
|
||||
for dir in current_path do
|
||||
relative_ndk = '../' .. relative_ndk
|
||||
end
|
||||
filter({})
|
||||
|
||||
if ndk then
|
||||
relative_ndk = ndk
|
||||
if string.sub(ndk, 1, 1) == '/' then
|
||||
-- An absolute file path wasn't working with premake.
|
||||
local current_path = string.gmatch(path.getabsolute('.'), '([^\\/]+)')
|
||||
for dir in current_path do
|
||||
relative_ndk = '../' .. relative_ndk
|
||||
end
|
||||
files({
|
||||
relative_ndk .. '/sources/android/native_app_glue/android_native_app_glue.c',
|
||||
})
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'system:windows' })
|
||||
do
|
||||
libdirs({
|
||||
RIVE_RUNTIME_DIR .. '/skia/dependencies/glfw_build/src/Release',
|
||||
})
|
||||
links({
|
||||
'glfw3',
|
||||
'opengl32',
|
||||
'd3d11',
|
||||
'd3d12',
|
||||
'dxguid',
|
||||
'dxgi',
|
||||
'Dbghelp',
|
||||
'd3dcompiler',
|
||||
'ws2_32',
|
||||
})
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'system:macosx' })
|
||||
do
|
||||
libdirs({ RIVE_RUNTIME_DIR .. '/skia/dependencies/glfw_build/src' })
|
||||
links({
|
||||
'glfw3',
|
||||
'Metal.framework',
|
||||
'QuartzCore.framework',
|
||||
'Cocoa.framework',
|
||||
'CoreGraphics.framework',
|
||||
'CoreFoundation.framework',
|
||||
'CoreMedia.framework',
|
||||
'CoreServices.framework',
|
||||
'IOKit.framework',
|
||||
'Security.framework',
|
||||
'OpenGL.framework',
|
||||
'bz2',
|
||||
'iconv',
|
||||
'lzma',
|
||||
'z', -- lib av format
|
||||
})
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'system:linux' })
|
||||
do
|
||||
libdirs({ RIVE_RUNTIME_DIR .. '/skia/dependencies/glfw_build/src' })
|
||||
links({ 'glfw3', 'm', 'z', 'dl', 'pthread', 'GL' })
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'system:android' })
|
||||
do
|
||||
links({ 'EGL', 'GLESv3', 'log' })
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'options:with-dawn' })
|
||||
do
|
||||
libdirs({
|
||||
RIVE_PLS_DIR .. '/dependencies/dawn/out/release/obj/src/dawn',
|
||||
RIVE_PLS_DIR .. '/dependencies/dawn/out/release/obj/src/dawn/native',
|
||||
RIVE_PLS_DIR .. '/dependencies/dawn/out/release/obj/src/dawn/platform',
|
||||
RIVE_PLS_DIR .. '/dependencies/dawn/out/release/obj/src/dawn/platform',
|
||||
})
|
||||
links({
|
||||
'winmm',
|
||||
'webgpu_dawn',
|
||||
'dawn_native_static',
|
||||
'dawn_proc_static',
|
||||
'dawn_platform_static',
|
||||
})
|
||||
end
|
||||
|
||||
filter({
|
||||
'kind:ConsoleApp or SharedLib or WindowedApp',
|
||||
'options:with-dawn',
|
||||
'system:windows',
|
||||
files({
|
||||
relative_ndk .. '/sources/android/native_app_glue/android_native_app_glue.c',
|
||||
})
|
||||
do
|
||||
links({ 'dxguid' })
|
||||
end
|
||||
end
|
||||
|
||||
filter({
|
||||
'kind:ConsoleApp or SharedLib or WindowedApp',
|
||||
'options:with-dawn',
|
||||
'system:macosx',
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'system:windows' })
|
||||
do
|
||||
libdirs({
|
||||
RIVE_RUNTIME_DIR .. '/skia/dependencies/glfw_build/src/Release',
|
||||
})
|
||||
do
|
||||
links({ 'IOSurface.framework' })
|
||||
end
|
||||
links({
|
||||
'glfw3',
|
||||
'opengl32',
|
||||
'd3d11',
|
||||
'd3d12',
|
||||
'dxguid',
|
||||
'dxgi',
|
||||
'Dbghelp',
|
||||
'd3dcompiler',
|
||||
'ws2_32',
|
||||
})
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'system:macosx' })
|
||||
do
|
||||
libdirs({ RIVE_RUNTIME_DIR .. '/skia/dependencies/glfw_build/src' })
|
||||
links({
|
||||
'glfw3',
|
||||
'Metal.framework',
|
||||
'QuartzCore.framework',
|
||||
'Cocoa.framework',
|
||||
'CoreGraphics.framework',
|
||||
'CoreFoundation.framework',
|
||||
'CoreMedia.framework',
|
||||
'CoreServices.framework',
|
||||
'IOKit.framework',
|
||||
'Security.framework',
|
||||
'OpenGL.framework',
|
||||
'bz2',
|
||||
'iconv',
|
||||
'lzma',
|
||||
'z', -- lib av format
|
||||
})
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'system:linux' })
|
||||
do
|
||||
libdirs({ RIVE_RUNTIME_DIR .. '/skia/dependencies/glfw_build/src' })
|
||||
links({ 'glfw3', 'm', 'z', 'dl', 'pthread', 'GL' })
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'system:android' })
|
||||
do
|
||||
links({ 'EGL', 'GLESv3', 'log' })
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'options:with-dawn' })
|
||||
do
|
||||
libdirs({
|
||||
RIVE_PLS_DIR .. '/dependencies/dawn/out/release/obj/src/dawn',
|
||||
RIVE_PLS_DIR .. '/dependencies/dawn/out/release/obj/src/dawn/native',
|
||||
RIVE_PLS_DIR .. '/dependencies/dawn/out/release/obj/src/dawn/platform',
|
||||
RIVE_PLS_DIR .. '/dependencies/dawn/out/release/obj/src/dawn/platform',
|
||||
})
|
||||
links({
|
||||
'winmm',
|
||||
'webgpu_dawn',
|
||||
'dawn_native_static',
|
||||
'dawn_proc_static',
|
||||
'dawn_platform_static',
|
||||
})
|
||||
end
|
||||
|
||||
filter({
|
||||
'kind:ConsoleApp or SharedLib or WindowedApp',
|
||||
'options:with-dawn',
|
||||
'system:windows',
|
||||
})
|
||||
do
|
||||
links({ 'dxguid' })
|
||||
end
|
||||
|
||||
filter({
|
||||
'kind:ConsoleApp or SharedLib or WindowedApp',
|
||||
'options:with-dawn',
|
||||
'system:macosx',
|
||||
})
|
||||
do
|
||||
links({ 'IOSurface.framework' })
|
||||
end
|
||||
|
||||
filter({ 'kind:ConsoleApp or SharedLib or WindowedApp', 'options:with-skia' })
|
||||
@@ -295,7 +303,44 @@ function rive_tools_project(name, project_kind)
|
||||
links({ 'skia', 'rive_skia_renderer' })
|
||||
end
|
||||
|
||||
filter('system:emscripten')
|
||||
do
|
||||
targetextension('.js')
|
||||
linkoptions({
|
||||
'-sEXPORTED_FUNCTIONS=_main,_rive_print_message_on_server,_malloc,_free',
|
||||
'-sEXPORTED_RUNTIME_METHODS=ccall,cwrap',
|
||||
'-sENVIRONMENT=web',
|
||||
'-sUSE_GLFW=3',
|
||||
'-sMIN_WEBGL_VERSION=2',
|
||||
'-sMAX_WEBGL_VERSION=2',
|
||||
'-sASYNCIFY',
|
||||
'-sASYNCIFY_IMPORTS="[async_sleep, wasi_snapshot_preview1.fd_write]"',
|
||||
'-sASYNCIFY_STACK_SIZE=16384',
|
||||
'-sGL_TESTING',
|
||||
'-lwebsocket.js',
|
||||
})
|
||||
end
|
||||
|
||||
filter({ 'system:emscripten', 'options:with-webgpu', 'options:not with_wagyu' })
|
||||
do
|
||||
linkoptions({
|
||||
'-sUSE_WEBGPU',
|
||||
})
|
||||
end
|
||||
|
||||
filter('files:**.html')
|
||||
do
|
||||
buildmessage('Copying %{file.relpath} to %{cfg.targetdir}')
|
||||
buildcommands({ 'cp %{file.relpath} %{cfg.targetdir}/%{file.name}' })
|
||||
buildoutputs({ '%{cfg.targetdir}/%{file.name}' })
|
||||
end
|
||||
|
||||
filter({})
|
||||
|
||||
if RIVE_WAGYU_PORT then
|
||||
buildoptions({ RIVE_WAGYU_PORT })
|
||||
linkoptions({ RIVE_WAGYU_PORT })
|
||||
end
|
||||
end
|
||||
|
||||
rive_tools_project('tools_common', 'StaticLib')
|
||||
@@ -340,5 +385,10 @@ do
|
||||
})
|
||||
end
|
||||
|
||||
filter('system:emscripten')
|
||||
do
|
||||
files({ 'common/rive_wasm_app.cpp' })
|
||||
end
|
||||
|
||||
filter({})
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user