blob: eeee32d227c42ff061ceb7de9008099e25a26afc [file]
// Imports a riv the way a web host does: import, let the page prepare the
// script modules, start the scripts, then run an artboard for a few frames.
#include "rive/animation/state_machine_instance.hpp"
#include "rive/artboard.hpp"
#include "rive/file.hpp"
#include "rive/renderer/ore/cmd/ore_deferred_context.hpp"
#include "rive/scroll_event.hpp"
#include "rive/wasm/wasm_lane_policy.hpp"
#include "rive/wasm/wasm_processors.hpp"
#include "rive/wasm/wasm_scripting_vm.hpp"
#include "utils/no_op_factory.hpp"
#include "utils/no_op_renderer.hpp"
#include <emscripten/emscripten.h>
#include <emscripten/stack.h>
using namespace rive;
static uint32_t byteSum(Span<const uint8_t> bytes)
{
uint32_t sum = 0;
for (uint8_t byte : bytes)
{
sum += byte;
}
return sum;
}
// Records what scripts send the GPU, as a host that replays it later does.
struct RecordingFactory : NoOpFactory
{
ore::cmd::DeferredOreContext recorder{ore::ReplayCaps{}};
size_t buffersPrinted = 0;
ore::Context* ore() override { return &recorder; }
// Prints each buffer made or updated since the last call.
void printBuffers()
{
const auto& stream = recorder.stream();
ore::cmd::OreCommandReader reader(
stream.commandBytes(),
stream.blobBytes());
ore::cmd::CommandType type;
size_t seen = 0;
while (reader.next(type))
{
if (type == ore::cmd::CommandType::makeBuffer)
{
reader.read<ore::cmd::MakeResourcePOD>();
auto desc = reader.read<ore::cmd::BufferDescPOD>();
if (seen++ >= buffersPrinted)
{
printf(
"gpu buffer of %u bytes%s sum %u\n",
desc.size,
desc.immutable ? " immutable" : "",
byteSum(reader.blobAt(desc.data.offset, desc.data.size)));
}
}
else if (type == ore::cmd::CommandType::bufferUpdate)
{
auto update = reader.read<ore::cmd::BufferUpdatePOD>();
if (seen++ >= buffersPrinted)
{
printf(
"gpu upload at %u of %u bytes sum %u\n",
update.offset,
update.bytes.size,
byteSum(reader.blobAt(update.bytes.offset, update.bytes.size)));
}
}
else
{
reader.skip(ore::cmd::orePayloadSizeOf(type));
}
}
buffersPrinted = seen;
}
};
static RecordingFactory s_factory;
static rcp<File> s_file;
static bool s_scroll = false;
static bool s_errorSink = false;
static std::unique_ptr<ArtboardInstance> s_artboard;
static std::unique_ptr<StateMachineInstance> s_machine;
static rcp<ViewModelInstance> s_data;
static void advanceFrames(int frames)
{
NoOpRenderer renderer;
for (int i = 0; i < frames; i++)
{
if (s_machine != nullptr)
{
s_machine->advanceAndApply(1.0f / 60.0f);
}
else
{
s_artboard->advance(1.0f / 60.0f);
}
s_artboard->draw(&renderer);
s_file->frameBoundary();
s_factory.printBuffers();
}
}
extern "C"
{
EMSCRIPTEN_KEEPALIVE int harness_import(const uint8_t* bytes, uint32_t size)
{
s_file = File::import(Span<const uint8_t>(bytes, size), &s_factory);
if (s_file != nullptr && s_errorSink)
{
for (auto& vm : s_file->wasmVMs())
{
vm->onError([](const std::string& line) {
printf("error sink: %s\n", line.c_str());
});
}
}
return s_file != nullptr;
}
// Gives the files imported after it an error sink, as rive-cli does.
EMSCRIPTEN_KEEPALIVE void harness_error_sink() { s_errorSink = true; }
// Writes a string at appAddr through the module memory seam and reads it
// back as a host op would.
EMSCRIPTEN_KEEPALIVE void harness_seam(uint32_t appAddr)
{
WasmScriptingVM* vm = s_file->wasmScriptingVM();
bool wrote = vm != nullptr && vm->copyToModule(appAddr, "seam", 5);
const char* text = wrote ? vm->resolveModuleString(appAddr) : nullptr;
printf("seam %s\n", text != nullptr ? text : "failed");
}
// Returns how many script VMs are running.
EMSCRIPTEN_KEEPALIVE int harness_run(int frames)
{
s_file->startScripts();
printf("script vms kept: %zu\n", s_file->wasmVMs().size());
int running = 0;
for (auto& vm : s_file->wasmVMs())
{
running += vm->valid() ? 1 : 0;
}
// The machine points into the artboard, so it goes first.
s_machine = nullptr;
s_artboard = s_file->artboardDefault();
if (s_artboard == nullptr)
{
return -1;
}
s_machine = s_artboard->defaultStateMachine();
s_data = s_file->createDefaultViewModelInstance(s_artboard.get());
if (s_machine != nullptr && s_data != nullptr)
{
s_machine->bindViewModelInstance(s_data);
}
advanceFrames(frames);
printf("artboard drew %d frames\n", frames);
if (s_scroll && s_machine != nullptr)
{
ScrollEvent event;
event.delta = Vec2D(0.0f, -30.0f);
event.phase = ScrollPhase::update;
event.precise = true;
HitResult hit = s_machine
->pointerScroll(Vec2D(50.0f, 60.0f), event, 1.5f, 2);
printf("scroll %s\n", hit != HitResult::none ? "claimed" : "declined");
}
int alive = 0;
for (auto& vm : s_file->wasmVMs())
{
alive += vm->valid() ? 1 : 0;
if (!vm->lastTrap().message.empty())
{
printf("last trap: %s\n", vm->lastTrap().message.c_str());
}
}
printf("script vms alive after the frames: %d\n", alive);
return running;
}
// Runs frames with only this much stack left, as deeply nested script
// and host calls would.
EMSCRIPTEN_KEEPALIVE int harness_run_low_on_stack(int frames, int left)
{
if (emscripten_stack_get_free() <= (size_t)left)
{
return harness_run(frames);
}
volatile char pad[1024];
pad[0] = 0;
return harness_run_low_on_stack(frames, left) + pad[0];
}
// More frames on the artboard harness_run started, for a page that lets
// time pass between them.
EMSCRIPTEN_KEEPALIVE void harness_frames(int frames)
{
if (s_artboard != nullptr)
{
advanceFrames(frames);
}
}
// Renders blocks of 128 frames from frame 0 through each script audio
// processor, as an audio thread would, and prints what came out, with
// an FNV-1a hash of its bytes.
EMSCRIPTEN_KEEPALIVE void harness_audio(int blocks)
{
int index = 0;
for (auto& vm : s_file->wasmVMs())
{
WasmProcessors* processors = vm->processors();
if (processors == nullptr)
{
continue;
}
for (auto& processor : processors->lanes())
{
uint32_t channels = processor->format().channels;
std::vector<float> block(128 * channels);
double left = 0;
double right = 0;
uint32_t hash = 2166136261u;
for (int i = 0; i < blocks; i++)
{
processor->render(block.data(), 128, (uint64_t)i * 128);
for (size_t f = 0; f < 128; f++)
{
left += block[f * channels];
right += block[f * channels + channels - 1];
}
auto bytes = (const uint8_t*)block.data();
for (size_t b = 0; b < block.size() * sizeof(float); b++)
{
hash = (hash ^ bytes[b]) * 16777619u;
}
}
printf(
"processor %d state %u sums %.2f %.2f hash %08x\n",
index++,
(uint32_t)processor->state(),
left,
right,
hash);
}
}
}
// Renders blocks of 128 frames through each processor between frames, as
// an offline host would, moving the clock the main lane reads past them.
EMSCRIPTEN_KEEPALIVE void harness_audio_pump(int blocks)
{
uint64_t frame;
uint32_t rate;
WasmProcessors::offlineClock(frame, rate);
std::vector<float> block;
for (auto& vm : s_file->wasmVMs())
{
if (WasmProcessors* processors = vm->processors())
{
for (auto& processor : processors->lanes())
{
block.resize(128 * processor->format().channels);
for (int i = 0; i < blocks; i++)
{
processor->render(block.data(), 128, frame + (uint64_t)i * 128);
}
}
}
}
WasmProcessors::setOfflineFrame(frame + (uint64_t)blocks * 128);
}
// Whether the artboard still needs advances for script work.
EMSCRIPTEN_KEEPALIVE int harness_pending()
{
return s_artboard != nullptr && s_artboard->hasPendingAsyncWork();
}
// Sends one scroll over the artboard after the frames.
EMSCRIPTEN_KEEPALIVE void harness_scroll() { s_scroll = true; }
// Starts only modules the page can bound, as an editor does.
EMSCRIPTEN_KEEPALIVE void harness_require_bounds()
{
WasmScriptingVM::requireBounds(true);
}
// The harness pulls processor blocks itself, so they start without a
// worklet.
EMSCRIPTEN_KEEPALIVE void harness_render_audio_offline()
{
WasmProcessors::setRenderOffline(true);
}
// A loaded machine can hold a block past the default limit, which would
// drop a processor no run means to drop.
EMSCRIPTEN_KEEPALIVE void harness_audio_hard_blocks(uint32_t blocks)
{
ScriptingLimits limits = ScriptingLimits::get();
limits.audioHardBlocks = blocks;
ScriptingLimits::set(limits);
}
// Before the pool first runs.
EMSCRIPTEN_KEEPALIVE void harness_job_threads(uint32_t threads)
{
ScriptingLimits limits = ScriptingLimits::get();
limits.jobThreads = threads;
ScriptingLimits::set(limits);
}
// Before the file loads; 0 runs its calls unbounded.
EMSCRIPTEN_KEEPALIVE void harness_call_budget(uint32_t ms)
{
ScriptingLimits limits = ScriptingLimits::get();
limits.mainCallBudgetMs = ms;
ScriptingLimits::set(limits);
}
EMSCRIPTEN_KEEPALIVE void harness_parallel_safe()
{
ScriptingLimits limits = ScriptingLimits::get();
limits.parallelSafeMode = true;
ScriptingLimits::set(limits);
}
EMSCRIPTEN_KEEPALIVE uintptr_t harness_stack()
{
return emscripten_stack_get_current();
}
EMSCRIPTEN_KEEPALIVE void harness_release()
{
s_machine = nullptr;
s_artboard = nullptr;
s_data = nullptr;
s_file = nullptr;
}
}