blob: 6a7ceb3dd3900f7d18a56dad7c725e2962305cfe [file]
#ifdef WITH_RIVE_SCRIPTING_WASM
#include "rive/wasm/module_tier_ladder.hpp"
#include "rive/wasm/wamr_state_transplant.hpp"
#include "rive/wasm/wasm_scripting_vm.hpp"
#include "rive/wasm/prelinked_aot.hpp"
#if WASM_ENABLE_PRELINKED_AOT != 0
// AOT_MAGIC_NUMBER / AOT_CURRENT_VERSION for container validation; same
// internal-header precedent as wamr_state_transplant.cpp.
#include "aot_runtime.h"
#endif
#include "rive/animation/linear_animation.hpp"
#include "rive/animation/linear_animation_instance.hpp"
#include "rive/animation/listener_invocation.hpp"
#include "rive/animation/state_machine_instance.hpp"
#include "rive/artboard.hpp"
#include "rive/assets/blob_asset.hpp"
#include "rive/custom_property_boolean.hpp"
#include "rive/custom_property_color.hpp"
#include "rive/assets/manifest_asset.hpp"
#include "rive/custom_property.hpp"
#include "rive/custom_property_number.hpp"
#include "rive/custom_property_string.hpp"
#include "rive/drawable.hpp"
#ifdef WITH_RIVE_AUDIO
#include "rive/assets/audio_asset.hpp"
#include "rive/audio/audio_engine.hpp"
#include "rive/audio/audio_sound.hpp"
#include "rive/audio/audio_source.hpp"
#endif
#include "rive/bones/root_bone.hpp"
#include "rive/constraints/constraint.hpp"
#include "rive/container_component.hpp"
#include "rive/math/transform_components.hpp"
#include "rive/node.hpp"
#include "rive/shapes/path.hpp"
#include "rive/transform_component.hpp"
#include "rive/viewmodel/viewmodel_instance_symbol_list_index.hpp"
#include "rive/wasm/artboard_wire.hpp"
#include "rive/assets/font_asset.hpp"
#include "rive/assets/image_asset.hpp"
#include "rive/assets/shader_asset.hpp"
#include "rive/async/work_pool.hpp"
#include "rive/data_bind/data_context.hpp"
#include "rive/data_bind/data_values/data_value.hpp"
#include "rive/data_bind/data_values/data_value_boolean.hpp"
#include "rive/data_bind/data_values/data_value_color.hpp"
#include "rive/data_bind/data_values/data_value_number.hpp"
#include "rive/data_bind/data_values/data_value_string.hpp"
#include "rive/factory.hpp"
#include "rive/file.hpp"
#include "rive/shapes/paint/feather.hpp"
#include "rive/shapes/paint/fill.hpp"
#include "rive/shapes/paint/image_sampler.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/shapes/paint/solid_color.hpp"
#include "rive/shapes/paint/stroke.hpp"
#include "rive/math/raw_path.hpp"
#include "rive/math/contour_measure.hpp"
#include "rive/math/path_measure.hpp"
#include "rive/renderer.hpp"
#include "rive/scripted/scripted_object.hpp"
#include "rive/viewmodel/viewmodel.hpp"
#include "rive/viewmodel/viewmodel_instance.hpp"
#include "rive/viewmodel/viewmodel_instance_boolean.hpp"
#include "rive/viewmodel/viewmodel_instance_number.hpp"
#include "rive/viewmodel/viewmodel_instance_string.hpp"
#include "rive/viewmodel/data_enum.hpp"
#include "rive/viewmodel/data_enum_value.hpp"
#include "rive/viewmodel/viewmodel_instance_color.hpp"
#include "rive/viewmodel/viewmodel_instance_enum.hpp"
#include "rive/viewmodel/viewmodel_property_enum.hpp"
#include "rive/viewmodel/viewmodel_instance_trigger.hpp"
#include "rive/viewmodel/viewmodel_instance_list.hpp"
#include "rive/viewmodel/viewmodel_instance_list_item.hpp"
#include "rive/viewmodel/viewmodel_instance_viewmodel.hpp"
#include "rive/viewmodel/viewmodel_instance_asset_blob.hpp"
#include "rive/viewmodel/viewmodel_instance_asset_font.hpp"
#include "rive/viewmodel/viewmodel_instance_asset_image.hpp"
#include "rive/wasm/data_convert_wire.hpp"
#include "rive/wasm/gamepad_wire.hpp"
#include "rive/wasm/listener_wire.hpp"
#include "rive/wasm/path_effect_wire.hpp"
#ifdef RIVE_DECODERS
#include "rive/decoders/bitmap_decoder.hpp"
#include <chrono>
#include <sys/stat.h>
#endif
#ifdef RIVE_CANVAS
#include "rive/renderer/render_context.hpp"
#include "rive/renderer/render_canvas.hpp"
#include "rive/renderer/cmd/deferred_canvas_host.hpp"
#include "rive/renderer/rive_render_image.hpp"
#endif
#if defined(RIVE_CANVAS) && defined(RIVE_ORE)
#include "rive/renderer/ore/ore_context.hpp"
#include "rive/renderer/ore/ore_script_guards.hpp"
#include "rive/renderer/ore/ore_buffer.hpp"
#include "rive/renderer/ore/ore_texture.hpp"
#include "rive/renderer/ore/ore_sampler.hpp"
#include "rive/renderer/ore/ore_shader_module.hpp"
#include "rive/renderer/ore/ore_bind_group_layout.hpp"
#include "rive/renderer/ore/ore_bind_group.hpp"
#include "rive/renderer/ore/ore_pipeline.hpp"
#include "rive/renderer/ore/cmd/ore_deferred_render_pass.hpp"
#include "rive/renderer/cmd/deferred_render_resource.hpp"
#include <algorithm>
#include <cassert>
#endif
#include "wasm_export.h"
#include <chrono>
#include <string.h>
#include <time.h>
using namespace rive;
namespace
{
constexpr char kLongjmpTag[] = "rive-longjmp";
WasmScriptingVM* vmFromEnv(wasm_exec_env_t env)
{
return static_cast<WasmScriptingVM*>(wasm_runtime_get_user_data(env));
}
// Module start runs inside wasm_runtime_instantiate, before an exec env
// exists to carry the vm, so natives called from there resolve none. rasc
// runs every script's top level there, and its output is the one thing a
// host still needs; the sink for that window is parked here.
thread_local const std::function<void(const char*, size_t)>* s_bootPrint =
nullptr;
// The VM whose module is starting, for the debug probes its top level hits.
thread_local WasmScriptingVM* s_booting = nullptr;
thread_local WasmScriptingVM::BootHook s_bootHook;
} // namespace
struct rive::WasmScriptingVMNatives
{
static void print(WasmScriptingVM* vm, const char* data, size_t size);
};
// Loaded modules are immutable and shared: fast-interp translation costs
// ~11ms per load while instantiation is microseconds, and every file
// instance of the same content reloads identical bytes. Entries live for
// the process; the byte buffer must outlive the module (wasm_runtime_load
// keeps referencing it).
static void pregrowAotMemory(wasm_module_inst_t instance);
struct SharedWasmModule
{
// The buffer WAMR loads from and references for the module's lifetime;
// the loader null-terminates import/export names in place, so these
// bytes are unfit to recompile.
std::vector<uint8_t> bytes;
// Pristine pre-load copy handed to wamrc for the AOT lane.
std::vector<uint8_t> pristineBytes;
wasm_module_t module = nullptr;
// Artifact-backed entries hand every later VM their real tier; without
// this a cache hit reports interp while running compiled code.
WasmScriptingVM::ExecutionTier tier =
WasmScriptingVM::ExecutionTier::interp;
};
static std::unordered_map<uint64_t, SharedWasmModule>& sharedModuleCache()
{
static std::unordered_map<uint64_t, SharedWasmModule> cache;
return cache;
}
struct WasmScriptingVM::WamrState
{
// Backing for an artifact loaded by a tier swap; wasm_runtime_load keeps
// referencing it.
std::vector<uint8_t> artifactBytes;
wasm_module_t module = nullptr;
bool ownsModule = true;
wasm_module_inst_t instance = nullptr;
wasm_exec_env_t execEnv = nullptr;
uint32_t callDepth = 0;
// Export names reach callModule as literals, so the pointer is the key;
// the owned copy lets debug builds catch a caller that reuses a buffer.
struct ExportSlot
{
const char* key = nullptr;
std::string name;
wasm_function_inst_t function = nullptr;
};
ExportSlot exportCache[64];
wasm_function_inst_t lookupExport(const char* name)
{
ExportSlot& slot = exportCache[(reinterpret_cast<uintptr_t>(name) *
0x9E3779B97F4A7C15ull) >>
58];
if (slot.key != name)
{
wasm_function_inst_t function =
wasm_runtime_lookup_function(instance, name);
if (function == nullptr)
{
return nullptr;
}
slot = {name, name, function};
}
assert(slot.name == name);
return slot.function;
}
~WamrState()
{
if (execEnv != nullptr)
{
wasm_runtime_destroy_exec_env(execEnv);
}
if (instance != nullptr)
{
wasm_runtime_deinstantiate(instance);
}
if (module != nullptr && ownsModule)
{
wasm_runtime_unload(module);
}
}
};
#if defined(RIVE_CANVAS) && defined(RIVE_ORE)
namespace
{
ore::Context* gpuOreContext(WasmScriptingVM* vm);
}
#endif
#ifdef RIVE_CANVAS
namespace
{
void canvasEndFrameImpl(WasmScriptingVM* vm, uint32_t canvas);
}
#endif
uint32_t WasmScriptingVM::callModule(const char* name,
uint32_t argc,
uint32_t* argv)
{
uint32_t result = 0;
callModuleChecked(name, argc, argv, &result);
return result;
}
WasmScriptingVM::ScriptCallScope::ScriptCallScope(WasmScriptingVM* vm) :
m_vm(vm)
{
if (vm->m_debugHooks != nullptr)
{
vm->m_debugHooks->onCallBegin(*vm);
}
// The outermost scope sweeps what any nested call left open.
if (vm->m_state->callDepth++ != 0)
{
return;
}
#if defined(RIVE_CANVAS) && defined(RIVE_ORE)
ore::Context* oreContext = gpuOreContext(vm);
m_passToken = oreContext != nullptr ? oreContext->nextRenderPassToken() : 0;
#endif
#ifdef RIVE_CANVAS
m_frameToken = vm->m_nextCanvasFrameToken;
#endif
}
WasmScriptingVM::ScriptCallScope::~ScriptCallScope()
{
if (--m_vm->m_state->callDepth == 0)
{
#if defined(RIVE_CANVAS) && defined(RIVE_ORE)
ore::Context* oreContext = gpuOreContext(m_vm);
if (oreContext != nullptr &&
oreContext->finishOpenRenderPassesFrom(m_passToken) != 0)
{
fprintf(stderr,
"GPU render pass left open at script return. Call finish() "
"on render passes before returning.\n");
}
#endif
#ifdef RIVE_CANVAS
std::vector<uint32_t> openFrames =
m_vm->takeOpenCanvasFramesFrom(m_frameToken);
for (uint32_t canvas : openFrames)
{
canvasEndFrameImpl(m_vm, canvas);
}
if (!openFrames.empty())
{
fprintf(stderr,
"Canvas frame left open at script return. Call endFrame() "
"before returning.\n");
}
#endif
}
if (m_vm->m_debugHooks != nullptr)
{
// Still set here when the call trapped; the caller clears it.
const char* trap =
m_vm->m_state != nullptr && m_vm->m_state->instance != nullptr
? wasm_runtime_get_exception(m_vm->m_state->instance)
: nullptr;
m_vm->m_debugHooks->onCallEnd(*m_vm, trap);
}
}
WasmScriptingVM::CallOutcome WasmScriptingVM::callModuleChecked(
const char* name,
uint32_t argc,
uint32_t* argv,
uint32_t* result)
{
wasm_module_inst_t inst = m_state->instance;
wasm_function_inst_t f = m_state->lookupExport(name);
if (f == nullptr)
{
// Callers probing optional exports read the outcome; a plain
// callModule folds this to zero, so probe before relying on it.
return CallOutcome::missing;
}
uint32_t buf[8] = {0};
for (uint32_t i = 0; i < argc; i++)
{
buf[i] = argv[i];
}
bool ok;
{
ScriptCallScope callScope(this);
ok = m_state->callDepth > 1
? wasm_runtime_call_wasm_nested(m_state->execEnv, f, argc, buf)
: wasm_runtime_call_wasm(m_state->execEnv, f, argc, buf);
}
if (!ok)
{
// A silent fold hides real traps; name them so a script that dies
// mid-call is diagnosable instead of a mystery no-op.
const char* exception =
fullTrapMessage(wasm_runtime_get_exception(inst));
if (exception != nullptr)
{
fprintf(stderr, "wasm call trapped in %s: %s\n", name, exception);
#if WASM_ENABLE_DUMP_CALL_STACK != 0
wasm_runtime_dump_call_stack(m_state->execEnv);
#endif
if (m_leakWarningCount > 0 && !m_leakTrapContextPrinted)
{
// A bare trap after leak warnings is almost always the
// memory ceiling; say so once for hosts that dropped the
// warning strings.
m_leakTrapContextPrinted = true;
fprintf(stderr,
"wasm call trapped after %u script heap leak "
"warnings; the module likely hit its wasmMaxPages "
"ceiling\n",
m_leakWarningCount);
}
wasm_runtime_clear_exception(inst);
}
return CallOutcome::trapped;
}
*result = buf[0];
return CallOutcome::ok;
}
void WasmScriptingVM::unregisterOpenCanvasFrame(uint32_t canvas)
{
for (size_t i = 0; i < m_openCanvasFrames.size(); i++)
{
if (m_openCanvasFrames[i].canvas == canvas)
{
m_openCanvasFrames.erase(m_openCanvasFrames.begin() + i);
return;
}
}
}
std::vector<uint32_t> WasmScriptingVM::takeOpenCanvasFramesFrom(uint64_t token)
{
std::vector<uint32_t> taken;
size_t keep = 0;
for (size_t i = 0; i < m_openCanvasFrames.size(); i++)
{
if (m_openCanvasFrames[i].token >= token)
{
taken.push_back(m_openCanvasFrames[i].canvas);
}
else
{
m_openCanvasFrames[keep++] = m_openCanvasFrames[i];
}
}
m_openCanvasFrames.resize(keep);
return taken;
}
void* WasmScriptingVM::resolveModulePtr(uint32_t appAddr, uint32_t size)
{
wasm_module_inst_t inst = m_state->instance;
if (!wasm_runtime_validate_app_addr(inst, appAddr, size))
{
return nullptr;
}
return wasm_runtime_addr_app_to_native(inst, appAddr);
}
void WasmScriptingVM::raiseModuleError(const char* message)
{
// The runtime's exception buffer truncates long messages (shader
// compiler output); keep the full text for trap reporting.
m_moduleErrorDetail = message;
wasm_runtime_set_exception(m_state->instance, message);
}
const char* WasmScriptingVM::fullTrapMessage(const char* exception) const
{
if (exception == nullptr || m_moduleErrorDetail.empty())
{
return exception;
}
const char* text = exception;
constexpr char kPrefix[] = "Exception: ";
if (strncmp(text, kPrefix, sizeof(kPrefix) - 1) == 0)
{
text += sizeof(kPrefix) - 1;
}
// Substitute only when the exception is a truncation of the detail, so
// an unrelated later trap keeps its own message. An empty remainder
// matches every prefix and must not adopt stale detail.
if (*text != '\0' &&
m_moduleErrorDetail.compare(0, strlen(text), text) == 0)
{
return m_moduleErrorDetail.c_str();
}
return exception;
}
void WasmScriptingVMNatives::print(WasmScriptingVM* vm,
const char* data,
size_t size)
{
const std::function<void(const char*, size_t)>* sink =
vm != nullptr ? &vm->m_print : s_bootPrint;
if (sink != nullptr && *sink)
{
(*sink)(data, size);
}
else
{
fwrite(data, 1, size, stdout);
}
}
namespace
{
// A string argument as the impl cores read it, UTF-8. A module that hands
// over its own UTF-16 has it transcoded here, on the stack for anything the
// size of a name.
class WasmStringArg
{
public:
WasmStringArg(WasmScriptingVM* vm, const char* bytes, uint32_t byteCount) :
m_data(bytes), m_size(byteCount)
{
// Module start calls in before the vm rides on the exec env.
WasmScriptingVM* owner = vm != nullptr ? vm : s_booting;
if (owner == nullptr || !owner->utf16Strings() || bytes == nullptr)
{
return;
}
auto in = reinterpret_cast<const uint8_t*>(bytes);
uint32_t units = byteCount / 2;
// A unit is three bytes at most, a surrogate pair four for its two.
char* out = m_inline;
if ((size_t)units * 3 > sizeof(m_inline))
{
m_heap.resize((size_t)units * 3);
out = &m_heap[0];
}
m_data = out;
auto unitAt = [in](uint32_t i) {
return (uint32_t)in[i * 2] | (uint32_t)in[i * 2 + 1] << 8;
};
for (uint32_t i = 0; i < units; i++)
{
uint32_t code = unitAt(i);
if (code >= 0xD800 && code < 0xDC00 && i + 1 < units)
{
uint32_t low = unitAt(i + 1);
if (low >= 0xDC00 && low < 0xE000)
{
code = 0x10000 + ((code - 0xD800) << 10) + (low - 0xDC00);
i++;
}
}
// A lone surrogate encodes as it stands, as the module's own
// encoder did.
if (code < 0x80)
{
*out++ = (char)code;
}
else if (code < 0x800)
{
*out++ = (char)(0xC0 | code >> 6);
*out++ = (char)(0x80 | (code & 0x3F));
}
else if (code < 0x10000)
{
*out++ = (char)(0xE0 | code >> 12);
*out++ = (char)(0x80 | (code >> 6 & 0x3F));
*out++ = (char)(0x80 | (code & 0x3F));
}
else
{
*out++ = (char)(0xF0 | code >> 18);
*out++ = (char)(0x80 | (code >> 12 & 0x3F));
*out++ = (char)(0x80 | (code >> 6 & 0x3F));
*out++ = (char)(0x80 | (code & 0x3F));
}
}
m_size = (uint32_t)(out - m_data);
}
const char* data() const { return m_data; }
uint32_t size() const { return m_size; }
private:
const char* m_data;
uint32_t m_size;
char m_inline[192];
std::string m_heap;
};
// Prototypes, descriptor PODs, and registration tables for the rive_*_v1
// namespaces come from the binding IDL (src/wasm/idl/bindings.py); the
// declarations pin each implementation below to the contract signature.
#include "wasm_natives_gen.hpp"
// --- emscripten setjmp/longjmp glue -----------------------------------------
void throwLongjmpNative(wasm_exec_env_t env)
{
wasm_runtime_set_exception(wasm_runtime_get_module_inst(env), kLongjmpTag);
}
void invokeViiNative(wasm_exec_env_t env,
uint32_t index,
uint32_t a1,
uint32_t a2)
{
WasmScriptingVM* vm = vmFromEnv(env);
wasm_module_inst_t inst = wasm_runtime_get_module_inst(env);
uint32_t sp = vm->callModule("emscripten_stack_get_current", 0, nullptr);
uint32_t argv[2] = {a1, a2};
if (wasm_runtime_call_indirect(env, index, 2, argv))
{
return;
}
const char* exception = wasm_runtime_get_exception(inst);
if (exception != nullptr && strstr(exception, kLongjmpTag) != nullptr)
{
wasm_runtime_clear_exception(inst);
uint32_t restoreArgs[1] = {sp};
vm->callModule("_emscripten_stack_restore", 1, restoreArgs);
uint32_t threwArgs[2] = {1, 0};
vm->callModule("setThrew", 2, threwArgs);
return;
}
// Genuine trap: leave the exception set so it propagates outward.
}
// --- remaining emscripten env imports ---------------------------------------
void abortJs(wasm_exec_env_t env)
{
wasm_runtime_set_exception(wasm_runtime_get_module_inst(env),
"script module abort");
}
void memcpyJs(wasm_exec_env_t env, uint32_t dst, uint32_t src, uint32_t n)
{
WasmScriptingVM* vm = vmFromEnv(env);
void* dstPtr = vm->resolveModulePtr(dst, n);
void* srcPtr = vm->resolveModulePtr(src, n);
if (dstPtr == nullptr || srcPtr == nullptr)
{
wasm_runtime_set_exception(wasm_runtime_get_module_inst(env),
"script module memcpy out of bounds");
return;
}
memmove(dstPtr, srcPtr, n);
}
double getNow(wasm_exec_env_t env)
{
// Same dev hook as dateNow: clock() feeds Luau's default RNG seed. A
// pinned clock also parks the module's execution budget.
if (getenv("RIVE_WASM_FIXED_DATE") != nullptr)
{
return 0;
}
auto now = std::chrono::steady_clock::now().time_since_epoch();
return std::chrono::duration<double, std::milli>(now).count();
}
double dateNow(wasm_exec_env_t env)
{
// Dev hook: a pinned date (epoch ms) keeps wasm lanes reproducible when
// harnesses A/B the same module over wall-clock-seeded content.
static const char* fixed = getenv("RIVE_WASM_FIXED_DATE");
if (fixed != nullptr)
{
return atof(fixed);
}
auto now = std::chrono::system_clock::now().time_since_epoch();
return std::chrono::duration<double, std::milli>(now).count();
}
// sbrk's growth request: enlarge linear memory to cover `size` bytes.
// The module's declared maximum still caps the growth.
uint32_t resizeHeap(wasm_exec_env_t env, uint32_t size)
{
wasm_module_inst_t inst = wasm_runtime_get_module_inst(env);
wasm_memory_inst_t memory = wasm_runtime_get_memory(inst, 0);
if (memory == nullptr)
{
return 0;
}
const uint64_t kPageSize = 65536;
uint64_t wantPages = ((uint64_t)size + kPageSize - 1) / kPageSize;
uint64_t curPages = wasm_memory_get_cur_page_count(memory);
if (wantPages <= curPages)
{
return 1;
}
return wasm_runtime_enlarge_memory(inst, wantPages - curPages) ? 1 : 0;
}
static bool localTime(double epochSeconds, time_t& at, struct tm& local)
{
at = (time_t)epochSeconds;
#ifdef _WIN32
return localtime_s(&local, &at) == 0;
#else
return localtime_r(&at, &local) != nullptr;
#endif
}
// Seconds the local rendering sits ahead of UTC.
static int32_t utcOffsetOf(struct tm local, time_t at)
{
#ifdef _WIN32
return (int32_t)(_mkgmtime(&local) - at);
#else
return (int32_t)(timegm(&local) - at);
#endif
}
// --- emscripten time imports: the Luau VM blob's os.date and os.time walk
// through these, so they serve the host's real calendar. Without WASM_BIGINT
// the module passes time_t as two i32 halves. struct tm is musl's wasm32
// layout: nine ints, the gmtoff long, then tm_zone, which the module's own
// libc fills.
static time_t moduleTime(uint32_t low, uint32_t high)
{
return (time_t)(((uint64_t)high << 32) | low);
}
static void writeTm(wasm_exec_env_t env,
uint32_t tmPtr,
const struct tm& time,
int32_t gmtoff)
{
int32_t* fields = (int32_t*)vmFromEnv(env)->resolveModulePtr(tmPtr, 44);
if (fields == nullptr)
{
return;
}
fields[0] = time.tm_sec;
fields[1] = time.tm_min;
fields[2] = time.tm_hour;
fields[3] = time.tm_mday;
fields[4] = time.tm_mon;
fields[5] = time.tm_year;
fields[6] = time.tm_wday;
fields[7] = time.tm_yday;
fields[8] = time.tm_isdst;
fields[9] = gmtoff;
}
void gmtimeJs(wasm_exec_env_t env, uint32_t low, uint32_t high, uint32_t tmPtr)
{
time_t at = moduleTime(low, high);
struct tm utc;
#ifdef _WIN32
if (gmtime_s(&utc, &at) != 0)
#else
if (gmtime_r(&at, &utc) == nullptr)
#endif
{
return;
}
writeTm(env, tmPtr, utc, 0);
}
void localtimeJs(wasm_exec_env_t env,
uint32_t low,
uint32_t high,
uint32_t tmPtr)
{
time_t at;
struct tm local;
if (!localTime((double)moduleTime(low, high), at, local))
{
return;
}
writeTm(env, tmPtr, local, utcOffsetOf(local, at));
}
// The module's tzset: seconds west of UTC, whether the zone observes
// daylight saving, and the two zone names into its 17 byte buffers.
void tzsetJs(wasm_exec_env_t env,
uint32_t timezonePtr,
uint32_t daylightPtr,
uint32_t stdNamePtr,
uint32_t dstNamePtr)
{
WasmScriptingVM* vm = vmFromEnv(env);
#ifdef _WIN32
_tzset();
long west = 0;
int observesDst = 0;
_get_timezone(&west);
_get_daylight(&observesDst);
const char* names[2] = {_tzname[0], _tzname[1]};
#elif defined(RIVE_NX)
// nnSdk's libc has no tz globals, so read the zone off January and July
// of this year, standard time being whichever sits further west.
long west = 0;
int observesDst = 0;
const char* names[2] = {"", ""};
time_t at;
struct tm probe[2];
if (localTime((double)time(nullptr), at, probe[0]))
{
int year = probe[0].tm_year;
int32_t offset[2];
bool ok = true;
for (int i = 0; i < 2 && ok; i++)
{
struct tm start{};
start.tm_year = year;
start.tm_mon = i * 6;
start.tm_mday = 1;
ok = localTime((double)timegm(&start), at, probe[i]);
offset[i] = utcOffsetOf(probe[i], at);
}
if (ok)
{
int std = offset[1] < offset[0] ? 1 : 0;
west = -offset[std];
observesDst = offset[0] != offset[1];
names[0] = probe[std].tm_zone;
names[1] = probe[observesDst ? 1 - std : std].tm_zone;
}
}
#else
tzset();
long west = timezone;
int observesDst = daylight;
const char* names[2] = {tzname[0], tzname[1]};
#endif
if (int32_t* out = (int32_t*)vm->resolveModulePtr(timezonePtr, 4))
{
*out = (int32_t)west;
}
if (int32_t* out = (int32_t*)vm->resolveModulePtr(daylightPtr, 4))
{
*out = observesDst;
}
uint32_t namePtrs[2] = {stdNamePtr, dstNamePtr};
for (int i = 0; i < 2; i++)
{
if (char* out = (char*)vm->resolveModulePtr(namePtrs[i], 17))
{
strncpy(out, names[i] != nullptr ? names[i] : "", 16);
out[16] = '\0';
}
}
}
uint32_t strftimeNative(wasm_exec_env_t env,
uint32_t out,
uint32_t capacity,
uint32_t format,
uint32_t tmPtr)
{
WasmScriptingVM* vm = vmFromEnv(env);
wasm_module_inst_t inst = wasm_runtime_get_module_inst(env);
char* outPtr = (char*)vm->resolveModulePtr(out, capacity);
const int32_t* fields = (const int32_t*)vm->resolveModulePtr(tmPtr, 44);
if (outPtr == nullptr || fields == nullptr ||
!wasm_runtime_validate_app_str_addr(inst, format))
{
return 0;
}
struct tm time = {};
time.tm_sec = fields[0];
time.tm_min = fields[1];
time.tm_hour = fields[2];
time.tm_mday = fields[3];
time.tm_mon = fields[4];
time.tm_year = fields[5];
time.tm_wday = fields[6];
time.tm_yday = fields[7];
time.tm_isdst = fields[8];
#ifndef _WIN32
time.tm_gmtoff = fields[9];
// The zone name lives in module memory, so it must be translated before
// the host's %Z reads it.
uint32_t zone = (uint32_t)fields[10];
if (zone != 0 && wasm_runtime_validate_app_str_addr(inst, zone))
{
time.tm_zone = (char*)wasm_runtime_addr_app_to_native(inst, zone);
}
#endif
return (uint32_t)strftime(
outPtr,
capacity,
(const char*)wasm_runtime_addr_app_to_native(inst, format),
&time);
}
// Older libc++ routes stream formatting through the locale variant.
uint32_t strftimeLNative(wasm_exec_env_t env,
uint32_t out,
uint32_t capacity,
uint32_t format,
uint32_t tmPtr,
uint32_t locale)
{
return strftimeNative(env, out, capacity, format, tmPtr);
}
// A module assert would otherwise trap with no message.
void assertFail(wasm_exec_env_t env,
uint32_t message,
uint32_t file,
uint32_t line,
uint32_t function)
{
WasmScriptingVM* vm = vmFromEnv(env);
auto text = [vm](uint32_t ptr) {
const char* str =
ptr != 0 ? (const char*)vm->resolveModulePtr(ptr, 1) : nullptr;
return str != nullptr ? str : "?";
};
fprintf(stderr,
"module assertion failed: %s (%s:%u, %s)\n",
text(message),
text(file),
line,
text(function));
wasm_runtime_set_exception(wasm_runtime_get_module_inst(env),
"module assertion failed");
}
// The blob's print goes through fd_write; intercept it so script output
// reaches the host's sink instead of the process stdout. The signature must
// match the libc-wasi builtin's "(i*i*)i": wamrc resolves WASI imports
// against the builtin table at compile time and inlines the app-to-native
// conversion for the '*' params, so an int-only registration receives
// native pointers under AOT while the interpreter honors the raw ints.
uint32_t fdWrite(wasm_exec_env_t env,
uint32_t fd,
uint32_t* iovs,
uint32_t iovcnt,
uint32_t* nwritten)
{
WasmScriptingVM* vm = vmFromEnv(env);
wasm_module_inst_t inst = wasm_runtime_get_module_inst(env);
if (!wasm_runtime_validate_native_addr(inst, iovs, (uint64_t)iovcnt * 8) ||
!wasm_runtime_validate_native_addr(inst, nwritten, 4))
{
return 28; // EINVAL
}
uint32_t total = 0;
for (uint32_t i = 0; i < iovcnt; i++)
{
uint32_t* io = iovs + i * 2;
uint32_t ptr = io[0], len = io[1];
const char* data =
len != 0 ? (const char*)vm->resolveModulePtr(ptr, len) : nullptr;
if (data != nullptr)
{
// Module stderr carries script errors; keep it out of the print
// sink so a capturing harness cannot swallow them.
if (fd == 2)
{
fwrite(data, 1, len, stderr);
}
else
{
WasmScriptingVMNatives::print(vm, data, len);
}
total += len;
}
}
*nwritten = total;
return 0;
}
// vm_host externalizes a handful of rive C++ methods as env imports that
// the runtime path never reaches (the live GPU path goes through the
// rive_gpu_v1 natives, not these direct calls). The fast interpreter
// tolerates an unresolved import, trapping only on an actual call, but
// wamrc bakes a null call target into the AOT that faults the moment a
// static initializer takes its address. Registering inert stubs gives the
// AOT valid pointers. Signatures mirror vm_host's import types.
uint32_t deadEnvImport_i_i(wasm_exec_env_t, uint32_t) { return 0; }
void deadCoreBytesDeserialize(wasm_exec_env_t, uint32_t, uint32_t) {}
void deadCoreStringDeserialize(wasm_exec_env_t, uint32_t, uint32_t) {}
void deadBeginFrame(wasm_exec_env_t, uint32_t, uint32_t) {}
void deadFlush(wasm_exec_env_t, uint32_t, uint32_t) {}
void deadAddFileAsset(wasm_exec_env_t, uint32_t, uint32_t) {}
uint32_t deadEnvImport_ii_i(wasm_exec_env_t, uint32_t, uint32_t) { return 0; }
void deadEnvImport_iiii(wasm_exec_env_t, uint32_t, uint32_t, uint32_t, uint32_t)
{}
// __cxa_throw never returns; a no-op stub falls through to the module's
// `unreachable` and corrupts the shadow stack under AOT. Trap instead so
// the call unwinds like the interpreter's unlinked-import path, and name
// the thrown type from its Itanium type_info (wasm32: __type_name at +4).
// Registered with libc-builtin's "(**i)": wamrc knows that table and
// inlines the pointer conversion, so an int-only registration would
// receive native pointers under AOT.
void cxaThrowNative(wasm_exec_env_t env,
void* thrown,
void* tinfo,
uint32_t dest)
{
(void)thrown;
(void)dest;
wasm_module_inst_t inst = wasm_runtime_get_module_inst(env);
const char* typeName = "?";
if (tinfo != nullptr && wasm_runtime_validate_native_addr(inst, tinfo, 8))
{
uint32_t nameApp = ((uint32_t*)tinfo)[1];
if (wasm_runtime_validate_app_str_addr(inst, nameApp))
{
typeName =
(const char*)wasm_runtime_addr_app_to_native(inst, nameApp);
}
}
fprintf(stderr, "[wasm] C++ exception thrown: %s\n", typeName);
wasm_runtime_set_exception(inst, "c++ exception");
}
NativeSymbol kEnvNatives[] = {
{"invoke_vii", (void*)invokeViiNative, "(iii)", nullptr},
{"_ZN4rive12CoreUintType11deserializeERNS_12BinaryReaderE",
(void*)deadEnvImport_i_i,
"(i)i",
nullptr},
{"_ZN4rive13CoreBytesType11deserializeERNS_12BinaryReaderE",
(void*)deadCoreBytesDeserialize,
"(ii)",
nullptr},
{"_ZN4rive14CoreStringType11deserializeERNS_12BinaryReaderE",
(void*)deadCoreStringDeserialize,
"(ii)",
nullptr},
{"_ZN4rive3gpu13RenderContext24makeDeferredRenderCanvasEjj",
(void*)deadEnvImport_iiii,
"(iiii)",
nullptr},
{"_ZN4rive3gpu13RenderContext16makeRenderCanvasEjj",
(void*)deadEnvImport_iiii,
"(iiii)",
nullptr},
{"_ZN4rive3gpu13RenderContext10beginFrameERKNS1_15FrameDescriptorE",
(void*)deadBeginFrame,
"(ii)",
nullptr},
{"_ZN4rive12RiveRendererC1EPNS_3gpu13RenderContextE",
(void*)deadEnvImport_ii_i,
"(ii)i",
nullptr},
{"_ZN4rive3gpu13RenderContext5flushERKNS1_14FlushResourcesE",
(void*)deadFlush,
"(ii)",
nullptr},
{"_ZN4rive17BackboardImporter12addFileAssetENS_3rcpINS_9FileAssetEEE",
(void*)deadAddFileAsset,
"(ii)",
nullptr},
{"__cxa_throw", (void*)cxaThrowNative, "(**i)", nullptr},
{"_emscripten_throw_longjmp", (void*)throwLongjmpNative, "()", nullptr},
{"_abort_js", (void*)abortJs, "()", nullptr},
{"_emscripten_memcpy_js", (void*)memcpyJs, "(iii)", nullptr},
{"emscripten_get_now", (void*)getNow, "()F", nullptr},
{"emscripten_date_now", (void*)dateNow, "()F", nullptr},
{"emscripten_resize_heap", (void*)resizeHeap, "(i)i", nullptr},
{"strftime", (void*)strftimeNative, "(iiii)i", nullptr},
{"strftime_l", (void*)strftimeLNative, "(iiiii)i", nullptr},
{"_tzset_js", (void*)tzsetJs, "(iiii)", nullptr},
{"_localtime_js", (void*)localtimeJs, "(iii)", nullptr},
{"_gmtime_js", (void*)gmtimeJs, "(iii)", nullptr},
{"__assert_fail", (void*)assertFail, "(iiii)", nullptr},
};
// Module streams are not seekable. The blob imports the emscripten-legalized
// form, offset i64 split across two i32s.
uint32_t fdSeek(wasm_exec_env_t env,
uint32_t fd,
uint32_t offsetLow,
uint32_t offsetHigh,
uint32_t whence,
uint32_t newOffsetPtr)
{
return 70; // WASI ESPIPE
}
NativeSymbol kWasiNatives[] = {
{"fd_write", (void*)fdWrite, "(i*i*)i", nullptr},
{"fd_seek", (void*)fdSeek, "(iiiii)i", nullptr},
};
// --- rive_path/paint/renderer_v1: handle-backed render objects --------------
// The module's ModuleRenderPath mirror; geometry arrives through update and
// rebuilds the real render path.
struct HostPath
{
rcp<RenderPath> path;
};
struct HostPaint
{
rcp<RenderPaint> paint;
};
uint32_t pathNewImpl(WasmScriptingVM* vm)
{
if (vm == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::path,
new HostPath());
}
void pathUpdateImpl(WasmScriptingVM* vm,
uint32_t handle,
const uint8_t* verbs,
uint32_t verbCount,
const float* points,
uint32_t floatCount,
uint32_t fillRule)
{
if (vm == nullptr)
{
return;
}
auto hostPath = static_cast<HostPath*>(
vm->handles().resolve(handle, WasmScriptingVM::HandleTable::Tag::path));
if (hostPath == nullptr)
{
return;
}
RawPath rawPath(Span<const PathVerb>((const PathVerb*)verbs, verbCount),
Span<const Vec2D>((const Vec2D*)points, floatCount / 2));
hostPath->path = vm->factory()->makeRenderPath(rawPath, (FillRule)fillRule);
}
void pathEffectResultImpl(WasmScriptingVM* vm,
const uint8_t* verbs,
uint32_t verbCount,
const float* points,
uint32_t floatCount)
{
if (vm == nullptr || vm->pathEffectOut() == nullptr)
{
return;
}
RawPath rawPath(Span<const PathVerb>((const PathVerb*)verbs, verbCount),
Span<const Vec2D>((const Vec2D*)points, floatCount / 2));
vm->pathEffectOut()->addPath(rawPath);
}
// One handle kind for the three measure shapes: a whole path measure, a
// contour iterator (measures nothing itself) and a contour it produced.
// The geometry is copied so the module's path can change underneath.
struct HostMeasure
{
std::unique_ptr<RawPath> source;
std::unique_ptr<PathMeasure> path;
std::unique_ptr<ContourMeasureIter> iter;
rcp<ContourMeasure> contour;
RawPath segment;
float length() const
{
return path ? path->length() : contour ? contour->length() : 0.0f;
}
bool isClosed() const
{
return path ? path->isClosed() : contour ? contour->isClosed() : false;
}
ContourMeasure::PosTan posTan(float distance) const
{
if (path)
{
auto at = path->atDistance(distance);
return {at.pos, at.tan};
}
if (contour)
{
return contour->getPosTan(distance);
}
return {};
}
void getSegment(float start, float end, RawPath* dst, bool startWithMove)
{
if (path)
{
path->getSegment(start, end, dst, startWithMove);
}
else if (contour)
{
contour->getSegment(start, end, dst, startWithMove);
}
}
};
HostMeasure* resolveMeasure(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostMeasure*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::measure));
}
std::unique_ptr<RawPath> copyGeometry(const uint8_t* verbs,
uint32_t verbCount,
const float* points,
uint32_t floatCount)
{
return std::make_unique<RawPath>(
Span<const PathVerb>((const PathVerb*)verbs, verbCount),
Span<const Vec2D>((const Vec2D*)points, floatCount / 2));
}
uint32_t measurePathNewImpl(WasmScriptingVM* vm,
const uint8_t* verbs,
uint32_t verbCount,
const float* points,
uint32_t floatCount)
{
if (vm == nullptr)
{
return 0;
}
auto measure = new HostMeasure();
measure->source = copyGeometry(verbs, verbCount, points, floatCount);
measure->path = std::make_unique<PathMeasure>(measure->source.get());
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::measure,
measure);
}
uint32_t measureContoursNewImpl(WasmScriptingVM* vm,
const uint8_t* verbs,
uint32_t verbCount,
const float* points,
uint32_t floatCount)
{
if (vm == nullptr)
{
return 0;
}
auto measure = new HostMeasure();
measure->source = copyGeometry(verbs, verbCount, points, floatCount);
measure->iter = std::make_unique<ContourMeasureIter>(measure->source.get());
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::measure,
measure);
}
uint32_t measureContourNextImpl(WasmScriptingVM* vm, uint32_t handle)
{
HostMeasure* iter = resolveMeasure(vm, handle);
if (iter == nullptr || !iter->iter)
{
return 0;
}
rcp<ContourMeasure> next = iter->iter->next();
if (next == nullptr)
{
return 0;
}
auto measure = new HostMeasure();
measure->contour = std::move(next);
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::measure,
measure);
}
float measureLengthImpl(WasmScriptingVM* vm, uint32_t handle)
{
HostMeasure* measure = resolveMeasure(vm, handle);
return measure != nullptr ? measure->length() : 0.0f;
}
uint32_t measureIsClosedImpl(WasmScriptingVM* vm, uint32_t handle)
{
HostMeasure* measure = resolveMeasure(vm, handle);
return measure != nullptr && measure->isClosed() ? 1 : 0;
}
void measurePosTanImpl(WasmScriptingVM* vm,
uint32_t handle,
float distance,
float* out,
uint32_t outCount)
{
HostMeasure* measure = resolveMeasure(vm, handle);
if (measure == nullptr || out == nullptr || outCount < 4)
{
return;
}
auto posTan = measure->posTan(distance);
out[0] = posTan.pos.x;
out[1] = posTan.pos.y;
out[2] = posTan.tan.x;
out[3] = posTan.tan.y;
}
void measureWarpImpl(WasmScriptingVM* vm,
uint32_t handle,
float x,
float y,
float* out,
uint32_t outCount)
{
HostMeasure* measure = resolveMeasure(vm, handle);
if (measure == nullptr || out == nullptr || outCount < 2)
{
return;
}
auto posTan = measure->posTan(x);
out[0] = posTan.pos.x - posTan.tan.y * y;
out[1] = posTan.pos.y + posTan.tan.x * y;
}
uint32_t measureExtractImpl(WasmScriptingVM* vm,
uint32_t handle,
float startDistance,
float endDistance,
uint32_t startWithMove)
{
HostMeasure* measure = resolveMeasure(vm, handle);
if (measure == nullptr)
{
return 0;
}
measure->segment.rewind();
measure->getSegment(startDistance,
endDistance,
&measure->segment,
startWithMove != 0);
return (uint32_t)measure->segment.verbs().size();
}
uint32_t measureExtractReadImpl(WasmScriptingVM* vm,
uint32_t handle,
uint8_t* verbs,
uint32_t verbCount,
float* points,
uint32_t floatCount)
{
HostMeasure* measure = resolveMeasure(vm, handle);
if (measure == nullptr)
{
return 0;
}
const RawPath& segment = measure->segment;
uint32_t floats = (uint32_t)segment.points().size() * 2;
if (verbCount < segment.verbs().size() || floatCount < floats)
{
return 0;
}
memcpy(verbs, segment.verbs().data(), segment.verbs().size());
memcpy(points, segment.points().data(), floats * sizeof(float));
return floats;
}
void measureReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete resolveMeasure(vm, handle);
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::measure);
}
void pathReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostPath*>(
vm->handles().resolve(handle, WasmScriptingVM::HandleTable::Tag::path));
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::path);
}
uint32_t paintNewImpl(WasmScriptingVM* vm)
{
if (vm == nullptr)
{
return 0;
}
auto hostPaint = new HostPaint{vm->factory()->makeRenderPaint()};
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::paint,
hostPaint);
}
void paintReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostPaint*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::paint));
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::paint);
}
RenderPaint* resolvePaint(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
auto hostPaint = static_cast<HostPaint*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::paint));
return hostPaint != nullptr ? hostPaint->paint.get() : nullptr;
}
void paintStyleImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value)
{
if (auto paint = resolvePaint(vm, handle))
{
paint->style((RenderPaintStyle)value);
}
}
void paintColorImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value)
{
if (auto paint = resolvePaint(vm, handle))
{
paint->color(value);
}
}
void paintThicknessImpl(WasmScriptingVM* vm, uint32_t handle, float value)
{
if (auto paint = resolvePaint(vm, handle))
{
paint->thickness(value);
}
}
void paintJoinImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value)
{
if (auto paint = resolvePaint(vm, handle))
{
paint->join((StrokeJoin)value);
}
}
void paintCapImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value)
{
if (auto paint = resolvePaint(vm, handle))
{
paint->cap((StrokeCap)value);
}
}
void paintBlendModeImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value)
{
if (auto paint = resolvePaint(vm, handle))
{
paint->blendMode((BlendMode)value);
}
}
void paintFeatherImpl(WasmScriptingVM* vm, uint32_t handle, float value)
{
if (auto paint = resolvePaint(vm, handle))
{
paint->feather(value);
}
}
struct HostShader
{
rcp<RenderShader> shader;
};
uint32_t shaderLinearImpl(WasmScriptingVM* vm,
float sx,
float sy,
float ex,
float ey,
uint32_t colorsPtr,
uint32_t stopsPtr,
uint32_t count)
{
if (vm == nullptr)
{
return 0;
}
auto colors = (const ColorInt*)vm->resolveModulePtr(colorsPtr, count * 4);
auto stops = (const float*)vm->resolveModulePtr(stopsPtr, count * 4);
if (colors == nullptr || stops == nullptr)
{
return 0;
}
auto shader =
vm->factory()->makeLinearGradient(sx, sy, ex, ey, colors, stops, count);
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::shader,
new HostShader{std::move(shader)});
}
uint32_t shaderRadialImpl(WasmScriptingVM* vm,
float cx,
float cy,
float radius,
uint32_t colorsPtr,
uint32_t stopsPtr,
uint32_t count)
{
if (vm == nullptr)
{
return 0;
}
auto colors = (const ColorInt*)vm->resolveModulePtr(colorsPtr, count * 4);
auto stops = (const float*)vm->resolveModulePtr(stopsPtr, count * 4);
if (colors == nullptr || stops == nullptr)
{
return 0;
}
auto shader =
vm->factory()->makeRadialGradient(cx, cy, radius, colors, stops, count);
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::shader,
new HostShader{std::move(shader)});
}
void shaderReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostShader*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::shader));
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::shader);
}
void paintShaderImpl(WasmScriptingVM* vm,
uint32_t paintHandle,
uint32_t shaderHandle)
{
auto paint = resolvePaint(vm, paintHandle);
if (vm == nullptr || paint == nullptr)
{
return;
}
auto hostShader = static_cast<HostShader*>(
vm->handles().resolve(shaderHandle,
WasmScriptingVM::HandleTable::Tag::shader));
paint->shader(hostShader != nullptr ? hostShader->shader : nullptr);
}
// The object's file asset of type T by name; accept skips matches that
// carry nothing, the way the Luau lookups keep scanning past them.
template <typename T>
T* findFileAsset(WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t length,
bool (*accept)(T*) = nullptr)
{
if (vm == nullptr)
{
return nullptr;
}
auto object = static_cast<ScriptedObject*>(
vm->handles().resolve(objectHandle,
WasmScriptingVM::HandleTable::Tag::object));
if (object == nullptr || object->scriptAsset() == nullptr ||
object->scriptAsset()->file() == nullptr)
{
return nullptr;
}
std::string key(name, length);
for (const auto& asset : object->scriptAsset()->file()->assets())
{
if (!asset->is<T>() || asset->name() != key)
{
continue;
}
T* match = asset->template as<T>();
if (accept == nullptr || accept(match))
{
return match;
}
}
return nullptr;
}
struct HostImage
{
rcp<RenderImage> image;
#ifdef RIVE_CANVAS
// A canvas image outlives its canvas only through this.
rcp<gpu::RenderCanvas> sourceCanvas;
#endif
};
HostImage* resolveImage(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostImage*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::image));
}
uint32_t imageFromAssetImpl(WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t length)
{
auto asset = findFileAsset<ImageAsset>(vm, objectHandle, name, length);
if (asset == nullptr || asset->renderImage() == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::image,
new HostImage{ref_rcp(asset->renderImage())});
}
uint32_t imageWidthImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveImage(vm, handle);
return host != nullptr ? (uint32_t)host->image->width() : 0;
}
uint32_t imageHeightImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveImage(vm, handle);
return host != nullptr ? (uint32_t)host->image->height() : 0;
}
void imageReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostImage*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::image));
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::image);
}
uint32_t imageDecodeImpl(WasmScriptingVM* vm,
const uint8_t* bytes,
uint32_t byteCount,
uint32_t token)
{
if (vm == nullptr || bytes == nullptr || byteCount == 0)
{
return 0;
}
return vm->startImageDecode(bytes, byteCount, token) ? 1 : 0;
}
void imageDecodeCancelImpl(WasmScriptingVM* vm, uint32_t token)
{
if (vm != nullptr)
{
vm->cancelImageDecode(token);
}
}
#if defined(RIVE_CANVAS) && defined(RIVE_ORE)
struct HostGpuCanvas
{
rcp<gpu::RenderCanvas> canvas;
rcp<ore::TextureView> colorView;
};
struct HostGpuPass
{
std::unique_ptr<ore::RenderPass> pass;
bool pipelineSet = false;
};
struct HostGpuBuffer
{
rcp<ore::Buffer> buffer;
};
struct HostGpuTexture
{
rcp<ore::Texture> texture;
};
struct HostGpuSampler
{
rcp<ore::Sampler> sampler;
};
struct HostGpuTextureView
{
rcp<ore::TextureView> view;
};
struct HostGpuShaderModule
{
rcp<ore::ShaderModule> shaderModule;
};
struct HostGpuBindGroupLayout
{
rcp<ore::BindGroupLayout> layout;
};
struct HostGpuBindGroup
{
rcp<ore::BindGroup> bindGroup;
};
struct HostGpuPipeline
{
rcp<ore::Pipeline> pipeline;
};
ore::Context* gpuOreContext(WasmScriptingVM* vm)
{
if (vm == nullptr || vm->factory() == nullptr)
{
return nullptr;
}
if (auto* recording = vm->factory()->ore())
{
return recording;
}
Factory* renderContext = vm->factory()->renderContext();
return renderContext != nullptr ? renderContext->ore() : nullptr;
}
// The Lua bindings raise lastError as a script error; the module lane traps
// with the same text so the author reads the cause instead of a 0 handle.
uint32_t gpuRejected(WasmScriptingVM* vm,
ore::Context* context,
const char* what)
{
std::string message = std::string(what) + ": " +
(context->lastError().empty() ? "creation failed"
: context->lastError());
vm->raiseModuleError(message.c_str());
return 0;
}
uint32_t gpuCanvasNewImpl(WasmScriptingVM* vm, uint32_t width, uint32_t height)
{
if (vm == nullptr || vm->factory() == nullptr || width == 0 || height == 0)
{
return 0;
}
auto* renderContext =
static_cast<gpu::RenderContext*>(vm->factory()->renderContext());
ore::Context* oreContext = gpuOreContext(vm);
if (renderContext == nullptr || oreContext == nullptr)
{
return 0;
}
auto canvas = vm->factory()->deferredCanvasHost() != nullptr
? renderContext->makeDeferredRenderCanvas(width, height)
: renderContext->makeRenderCanvas(width, height);
if (canvas == nullptr)
{
return 0;
}
auto colorView = oreContext->wrapCanvasTexture(canvas.get());
if (colorView == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::gpuCanvas,
new HostGpuCanvas{std::move(canvas), std::move(colorView)});
}
void gpuCanvasReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostGpuCanvas*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuCanvas));
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::gpuCanvas);
}
uint32_t gpuCanvasColorViewImpl(WasmScriptingVM* vm,
uint32_t canvasHandle,
uint32_t* props,
uint32_t propCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (vm == nullptr || oreContext == nullptr || propCount < 4)
{
return 0;
}
auto host = static_cast<HostGpuCanvas*>(
vm->handles().resolve(canvasHandle,
WasmScriptingVM::HandleTable::Tag::gpuCanvas));
if (host == nullptr)
{
return 0;
}
props[0] = host->canvas->width();
props[1] = host->canvas->height();
props[2] = (uint32_t)oreContext->canvasTargetFormat();
props[3] = 1;
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuTextureView,
new HostGpuTextureView{host->colorView});
}
uint32_t gpuCanvasImageImpl(WasmScriptingVM* vm, uint32_t canvasHandle)
{
if (vm == nullptr)
{
return 0;
}
auto host = static_cast<HostGpuCanvas*>(
vm->handles().resolve(canvasHandle,
WasmScriptingVM::HandleTable::Tag::gpuCanvas));
if (host == nullptr || host->canvas == nullptr ||
host->canvas->renderImage() == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::image,
new HostImage{ref_rcp(static_cast<RenderImage*>(
host->canvas->renderImage())),
host->canvas});
}
uint32_t gpuCanvasResizeImpl(WasmScriptingVM* vm,
uint32_t canvasHandle,
uint32_t width,
uint32_t height,
uint32_t* props,
uint32_t propCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (vm == nullptr || vm->factory() == nullptr || oreContext == nullptr ||
width == 0 || height == 0 || propCount < 4)
{
return 0;
}
auto host = static_cast<HostGpuCanvas*>(
vm->handles().resolve(canvasHandle,
WasmScriptingVM::HandleTable::Tag::gpuCanvas));
if (host == nullptr)
{
return 0;
}
auto* renderContext =
static_cast<gpu::RenderContext*>(vm->factory()->renderContext());
if (renderContext == nullptr)
{
return 0;
}
auto canvas = vm->factory()->deferredCanvasHost() != nullptr
? renderContext->makeDeferredRenderCanvas(width, height)
: renderContext->makeRenderCanvas(width, height);
if (canvas == nullptr)
{
return 0;
}
auto colorView = oreContext->wrapCanvasTexture(canvas.get());
if (colorView == nullptr)
{
return 0;
}
host->canvas = std::move(canvas);
host->colorView = std::move(colorView);
props[0] = host->canvas->width();
props[1] = host->canvas->height();
props[2] = (uint32_t)oreContext->canvasTargetFormat();
props[3] = 1;
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuTextureView,
new HostGpuTextureView{host->colorView});
}
uint32_t gpuFeaturesImpl(WasmScriptingVM* vm, uint32_t* out, uint32_t outCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (vm == nullptr || oreContext == nullptr || outCount < 19)
{
return 0;
}
// With ReplayCaps a recording context answers from shipped data; an
// unknown snapshot must raise module-side instead of guessing.
if (!oreContext->featuresKnown())
{
return ~0u;
}
const ore::Features& f = oreContext->features();
out[0] = f.bc ? 1 : 0;
out[1] = f.etc2 ? 1 : 0;
out[2] = f.astc ? 1 : 0;
out[3] = (uint32_t)f.maxTextureSize2D;
out[4] = (uint32_t)f.maxTextureSizeCube;
out[5] = (uint32_t)f.maxTextureSize3D;
out[6] = f.anisotropicFiltering ? 1 : 0;
out[7] = f.texture3D ? 1 : 0;
out[8] = f.textureArrays ? 1 : 0;
out[9] = f.colorBufferFloat ? 1 : 0;
out[10] = f.colorBufferHalfFloat ? 1 : 0;
out[11] = f.perTargetBlend ? 1 : 0;
out[12] = f.perTargetWriteMask ? 1 : 0;
out[13] = f.drawBaseInstance ? 1 : 0;
out[14] = f.depthBiasClamp ? 1 : 0;
out[15] = (uint32_t)f.maxColorAttachments;
out[16] = (uint32_t)f.maxUniformBufferSize;
out[17] = (uint32_t)f.maxSamplers;
out[18] = (uint32_t)f.maxSamples;
return 19;
}
uint32_t gpuPassBeginImpl(WasmScriptingVM* vm,
const rive_gpu_pass_desc_v1* podDesc,
uint32_t descByteCount,
const rive_gpu_pass_color_attachment_v1* colors,
uint32_t colorByteCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (vm == nullptr || oreContext == nullptr ||
descByteCount < sizeof(*podDesc) ||
colorByteCount % sizeof(*colors) != 0 ||
podDesc->colorCount != colorByteCount / sizeof(*colors) ||
podDesc->colorCount > 4)
{
return 0;
}
auto resolveView = [vm](uint32_t handle) -> ore::TextureView* {
auto host = static_cast<HostGpuTextureView*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::gpuTextureView));
return host != nullptr ? host->view.get() : nullptr;
};
ore::RenderPassDesc desc;
desc.colorCount = podDesc->colorCount;
for (uint32_t i = 0; i < podDesc->colorCount; i++)
{
auto& out = desc.colorAttachments[i];
out.view = resolveView(colors[i].view);
if (out.view == nullptr)
{
return 0;
}
out.resolveTarget = colors[i].resolveTarget != 0
? resolveView(colors[i].resolveTarget)
: nullptr;
out.loadOp = (ore::LoadOp)colors[i].loadOp;
out.storeOp = (ore::StoreOp)colors[i].storeOp;
out.clearColor = {colors[i].clearR,
colors[i].clearG,
colors[i].clearB,
colors[i].clearA};
}
if (podDesc->depthView != 0)
{
desc.depthStencil.view = resolveView(podDesc->depthView);
if (desc.depthStencil.view == nullptr)
{
return 0;
}
}
desc.depthStencil.depthLoadOp = (ore::LoadOp)podDesc->depthLoadOp;
desc.depthStencil.depthStoreOp = (ore::StoreOp)podDesc->depthStoreOp;
desc.depthStencil.depthClearValue = podDesc->depthClearValue;
desc.depthStencil.stencilLoadOp = (ore::LoadOp)podDesc->stencilLoadOp;
desc.depthStencil.stencilStoreOp = (ore::StoreOp)podDesc->stencilStoreOp;
desc.depthStencil.stencilClearValue = podDesc->stencilClearValue;
auto pass = ore::cmd::beginRecordedRenderPass(*oreContext, desc);
if (pass == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuPass,
new HostGpuPass{std::move(pass)});
}
uint32_t gpuImageViewImpl(WasmScriptingVM* vm,
uint32_t imageHandle,
uint32_t width,
uint32_t height)
{
ore::Context* oreContext = gpuOreContext(vm);
if (vm == nullptr || oreContext == nullptr)
{
return 0;
}
auto host = static_cast<HostImage*>(
vm->handles().resolve(imageHandle,
WasmScriptingVM::HandleTable::Tag::image));
if (host == nullptr)
{
return 0;
}
// Same dispatch as Image:view() in the Luau binding.
rcp<ore::TextureView> view;
if (!oreContext->isRecording())
{
if (host->sourceCanvas != nullptr)
{
view = oreContext->wrapCanvasSampleView(host->sourceCanvas.get());
}
else if (auto* riveImage =
lite_rtti_cast<RiveRenderImage*>(host->image.get());
riveImage != nullptr && riveImage->getTexture() != nullptr)
{
view = oreContext->wrapRiveTexture(riveImage->getTexture(),
host->image->width(),
host->image->height());
}
}
else if (auto* deferredImage =
lite_rtti_cast<cmd::DeferredRenderImage*>(host->image.get()))
{
view = oreContext->recordWrapImageView(deferredImage->id(),
host->image->width(),
host->image->height());
}
else
{
view = oreContext->recordWrapCanvasImage(host->image.get(),
host->image->width(),
host->image->height());
}
if (view == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuTextureView,
new HostGpuTextureView{std::move(view)});
}
HostGpuPass* resolveHostPass(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostGpuPass*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuPass));
}
ore::RenderPass* resolvePass(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveHostPass(vm, handle);
return host != nullptr ? host->pass.get() : nullptr;
}
// The draw guards the Luau binding raises, with the same text so both lanes
// read the same cause. Returns false after raising.
bool gpuPassDrawAllowed(WasmScriptingVM* vm,
HostGpuPass* host,
const char* what,
int32_t baseVertex,
uint32_t firstInstance)
{
if (!host->pipelineSet)
{
vm->raiseModuleError(ore::kGuardSetPipelineBeforeDraw);
return false;
}
ore::Context* oreContext = gpuOreContext(vm);
bool featuresKnown = oreContext != nullptr && oreContext->featuresKnown();
if (featuresKnown && !oreContext->features().drawBaseInstance)
{
char message[160];
if (baseVertex != 0)
{
snprintf(message,
sizeof(message),
ore::kGuardBaseVertexFormat,
what,
baseVertex);
vm->raiseModuleError(message);
return false;
}
if (firstInstance > 0)
{
snprintf(message,
sizeof(message),
ore::kGuardFirstInstanceFormat,
what,
firstInstance);
vm->raiseModuleError(message);
return false;
}
}
return true;
}
void gpuPassSetPipelineImpl(WasmScriptingVM* vm,
uint32_t passHandle,
uint32_t pipelineHandle)
{
auto* host = resolveHostPass(vm, passHandle);
auto pipeline = static_cast<HostGpuPipeline*>(
vm->handles().resolve(pipelineHandle,
WasmScriptingVM::HandleTable::Tag::gpuPipeline));
if (host == nullptr || pipeline == nullptr)
{
return;
}
// An attachment compat failure no-ops the backend and later draws crash
// in the driver, so it surfaces here the way the Luau binding raises it.
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext != nullptr)
{
oreContext->clearLastError();
}
host->pass->setPipeline(pipeline->pipeline.get());
if (oreContext != nullptr && !oreContext->lastError().empty())
{
gpuRejected(vm, oreContext, "setPipeline");
return;
}
host->pipelineSet = true;
}
void gpuPassSetVertexBufferImpl(WasmScriptingVM* vm,
uint32_t passHandle,
uint32_t slot,
uint32_t bufferHandle,
uint32_t offset)
{
auto* pass = resolvePass(vm, passHandle);
auto buffer = static_cast<HostGpuBuffer*>(
vm->handles().resolve(bufferHandle,
WasmScriptingVM::HandleTable::Tag::gpuBuffer));
if (pass == nullptr || buffer == nullptr)
{
return;
}
if (slot >= ore::kMaxVertexBufferSlots)
{
char message[96];
snprintf(message,
sizeof(message),
ore::kGuardVertexSlotRangeFormat,
ore::kMaxVertexBufferSlots - 1,
slot);
vm->raiseModuleError(message);
return;
}
pass->setVertexBuffer(slot, buffer->buffer.get(), offset);
}
void gpuPassSetIndexBufferImpl(WasmScriptingVM* vm,
uint32_t passHandle,
uint32_t bufferHandle,
uint32_t indexFormat,
uint32_t offset)
{
auto* pass = resolvePass(vm, passHandle);
auto buffer = static_cast<HostGpuBuffer*>(
vm->handles().resolve(bufferHandle,
WasmScriptingVM::HandleTable::Tag::gpuBuffer));
if (pass == nullptr || buffer == nullptr)
{
return;
}
pass->setIndexBuffer(buffer->buffer.get(),
(ore::IndexFormat)indexFormat,
offset);
}
void gpuPassSetBindGroupImpl(WasmScriptingVM* vm,
uint32_t passHandle,
uint32_t groupIndex,
uint32_t bindGroupHandle,
const uint32_t* dynamicOffsets,
uint32_t dynamicOffsetByteCount)
{
auto* pass = resolvePass(vm, passHandle);
auto bindGroup = static_cast<HostGpuBindGroup*>(
vm->handles().resolve(bindGroupHandle,
WasmScriptingVM::HandleTable::Tag::gpuBindGroup));
if (pass == nullptr || bindGroup == nullptr ||
dynamicOffsetByteCount % sizeof(uint32_t) != 0)
{
return;
}
uint32_t count = dynamicOffsetByteCount / (uint32_t)sizeof(uint32_t);
pass->setBindGroup(groupIndex,
bindGroup->bindGroup.get(),
count != 0 ? dynamicOffsets : nullptr,
count);
}
void gpuPassSetViewportImpl(WasmScriptingVM* vm,
uint32_t passHandle,
float x,
float y,
float width,
float height,
float minDepth,
float maxDepth)
{
if (auto* pass = resolvePass(vm, passHandle))
{
pass->setViewport(x, y, width, height, minDepth, maxDepth);
}
}
void gpuPassSetScissorImpl(WasmScriptingVM* vm,
uint32_t passHandle,
uint32_t x,
uint32_t y,
uint32_t width,
uint32_t height)
{
if (auto* pass = resolvePass(vm, passHandle))
{
pass->setScissorRect(x, y, width, height);
}
}
void gpuPassSetStencilReferenceImpl(WasmScriptingVM* vm,
uint32_t passHandle,
uint32_t ref)
{
if (auto* pass = resolvePass(vm, passHandle))
{
pass->setStencilReference(ref);
}
}
void gpuPassSetBlendColorImpl(WasmScriptingVM* vm,
uint32_t passHandle,
float r,
float g,
float b,
float a)
{
if (auto* pass = resolvePass(vm, passHandle))
{
pass->setBlendColor(r, g, b, a);
}
}
void gpuPassDrawImpl(WasmScriptingVM* vm,
uint32_t passHandle,
uint32_t vertexCount,
uint32_t instanceCount,
uint32_t firstVertex,
uint32_t firstInstance)
{
auto* host = resolveHostPass(vm, passHandle);
if (host == nullptr ||
!gpuPassDrawAllowed(vm, host, "draw", 0, firstInstance))
{
return;
}
host->pass->draw(vertexCount, instanceCount, firstVertex, firstInstance);
}
void gpuPassDrawIndexedImpl(WasmScriptingVM* vm,
uint32_t passHandle,
uint32_t indexCount,
uint32_t instanceCount,
uint32_t firstIndex,
int32_t baseVertex,
uint32_t firstInstance)
{
auto* host = resolveHostPass(vm, passHandle);
if (host == nullptr ||
!gpuPassDrawAllowed(vm, host, "drawIndexed", baseVertex, firstInstance))
{
return;
}
host->pass->drawIndexed(indexCount,
instanceCount,
firstIndex,
baseVertex,
firstInstance);
}
void gpuPassFinishImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
auto host = static_cast<HostGpuPass*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuPass));
if (host == nullptr || host->pass == nullptr)
{
return;
}
if (!host->pass->isFinished())
{
host->pass->finish();
}
}
void gpuPassReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostGpuPass*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuPass));
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::gpuPass);
}
uint32_t gpuBufferNewImpl(WasmScriptingVM* vm,
uint32_t usage,
uint32_t sizeInBytes,
uint32_t immutable,
const uint8_t* data,
uint32_t dataCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr || sizeInBytes == 0 ||
(dataCount != 0 && dataCount != sizeInBytes))
{
return 0;
}
ore::BufferDesc desc;
desc.usage = (ore::BufferUsage)usage;
desc.size = sizeInBytes;
desc.immutable = immutable != 0;
desc.data = dataCount != 0 ? data : nullptr;
oreContext->clearLastError();
auto buffer = oreContext->makeBuffer(desc);
if (buffer == nullptr)
{
return gpuRejected(vm, oreContext, "GPUBuffer");
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuBuffer,
new HostGpuBuffer{std::move(buffer)});
}
void gpuBufferUpdateImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t dstOffset,
const uint8_t* data,
uint32_t dataCount)
{
if (vm == nullptr)
{
return;
}
auto host = static_cast<HostGpuBuffer*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuBuffer));
if (host == nullptr || dataCount == 0 ||
uint64_t(dstOffset) + dataCount > host->buffer->size())
{
return;
}
host->buffer->update(data, dataCount, dstOffset);
}
void gpuBufferReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostGpuBuffer*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuBuffer));
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::gpuBuffer);
}
uint32_t gpuTextureNewImpl(WasmScriptingVM* vm,
const rive_gpu_texture_desc_v1* podDesc,
uint32_t descByteCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr || descByteCount < sizeof(*podDesc))
{
return 0;
}
ore::TextureDesc desc;
desc.width = podDesc->width;
desc.height = podDesc->height;
desc.depthOrArrayLayers = podDesc->depthOrArrayLayers;
desc.format = (ore::TextureFormat)podDesc->format;
desc.type = (ore::TextureType)podDesc->textureType;
desc.renderTarget = podDesc->renderTarget != 0;
desc.numMipmaps = podDesc->numMipmaps;
desc.sampleCount = podDesc->sampleCount;
oreContext->clearLastError();
auto texture = oreContext->makeTexture(desc);
if (texture == nullptr)
{
return gpuRejected(vm, oreContext, "GPUTexture");
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuTexture,
new HostGpuTexture{std::move(texture)});
}
void gpuTextureUploadImpl(WasmScriptingVM* vm,
uint32_t handle,
const rive_gpu_texture_upload_v1* region,
uint32_t regionByteCount,
const uint8_t* data,
uint32_t dataCount)
{
if (vm == nullptr || regionByteCount < sizeof(*region) || dataCount == 0)
{
return;
}
auto host = static_cast<HostGpuTexture*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuTexture));
if (host == nullptr)
{
return;
}
ore::TextureDataDesc upload;
upload.data = data;
upload.dataSize = dataCount;
upload.bytesPerRow = region->bytesPerRow;
upload.rowsPerImage = region->rowsPerImage;
upload.mipLevel = region->mipLevel;
upload.layer = region->layer;
upload.x = region->x;
upload.y = region->y;
upload.z = region->z;
upload.width = region->width;
upload.height = region->height;
upload.depth = region->depth;
std::string error;
if (!host->texture->upload(upload, &error))
{
vm->raiseModuleError(error.c_str());
}
}
void gpuTextureReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostGpuTexture*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuTexture));
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::gpuTexture);
}
uint32_t gpuSamplerNewImpl(WasmScriptingVM* vm,
const rive_gpu_sampler_desc_v1* podDesc,
uint32_t descByteCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr || descByteCount < sizeof(*podDesc))
{
return 0;
}
ore::SamplerDesc desc;
desc.minFilter = (ore::Filter)podDesc->minFilter;
desc.magFilter = (ore::Filter)podDesc->magFilter;
desc.mipmapFilter = (ore::Filter)podDesc->mipmapFilter;
desc.wrapU = (ore::WrapMode)podDesc->wrapU;
desc.wrapV = (ore::WrapMode)podDesc->wrapV;
desc.wrapW = (ore::WrapMode)podDesc->wrapW;
// The guest sends ~0 for "no comparison sampler".
desc.compare = podDesc->compare == 0xFFFFFFFFu
? ore::CompareFunction::none
: (ore::CompareFunction)podDesc->compare;
desc.minLod = podDesc->minLod;
desc.maxLod = podDesc->maxLod;
desc.maxAnisotropy = podDesc->maxAnisotropy;
oreContext->clearLastError();
auto sampler = oreContext->makeSampler(desc);
if (sampler == nullptr)
{
return gpuRejected(vm, oreContext, "GPUSampler");
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuSampler,
new HostGpuSampler{std::move(sampler)});
}
void gpuSamplerReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostGpuSampler*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuSampler));
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::gpuSampler);
}
uint32_t gpuTextureViewNewImpl(WasmScriptingVM* vm,
uint32_t textureHandle,
const rive_gpu_texture_view_desc_v1* podDesc,
uint32_t descByteCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr || descByteCount < sizeof(*podDesc))
{
return 0;
}
auto texture = static_cast<HostGpuTexture*>(
vm->handles().resolve(textureHandle,
WasmScriptingVM::HandleTable::Tag::gpuTexture));
if (texture == nullptr)
{
return 0;
}
ore::TextureViewDesc desc;
desc.texture = texture->texture.get();
desc.dimension = (ore::TextureViewDimension)podDesc->dimension;
desc.aspect = (ore::TextureAspect)podDesc->aspect;
desc.baseMipLevel = podDesc->baseMipLevel;
desc.mipCount = podDesc->mipCount;
desc.baseLayer = podDesc->baseLayer;
desc.layerCount = podDesc->layerCount;
oreContext->clearLastError();
auto view = oreContext->makeTextureView(desc);
if (view == nullptr)
{
return gpuRejected(vm, oreContext, "GPUTextureView");
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuTextureView,
new HostGpuTextureView{std::move(view)});
}
void gpuTextureViewReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostGpuTextureView*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::gpuTextureView));
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::gpuTextureView);
}
uint32_t gpuShaderTargetImpl(WasmScriptingVM* vm)
{
ore::Context* oreContext = gpuOreContext(vm);
return oreContext != nullptr ? (uint32_t)oreContext->shaderTarget()
: (uint32_t)ore::ShaderTarget::wgsl;
}
uint32_t gpuShaderAssetBytesImpl(WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t nameLength,
uint8_t* out,
uint32_t outCount)
{
if (vm == nullptr)
{
return 0;
}
#ifdef WITH_RIVE_TOOLS
// Editor path first, matching the Luau lane: RSTBs compiled during
// requestWasmVM shadow whatever a file asset carries.
{
const std::vector<uint8_t>* rstb =
vm->findShaderRstb(std::string(name, nameLength));
if (rstb != nullptr)
{
if (rstb->size() <= outCount)
{
memcpy(out, rstb->data(), rstb->size());
}
return (uint32_t)rstb->size();
}
}
#endif
auto asset = findFileAsset<ShaderAsset>(vm, objectHandle, name, nameLength);
if (asset == nullptr)
{
return 0;
}
auto rstb = asset->rstb();
if (rstb.size() <= outCount)
{
memcpy(out, rstb.data(), rstb.size());
}
return (uint32_t)rstb.size();
}
uint32_t gpuShaderAssetIdImpl(WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t nameLength)
{
auto asset = findFileAsset<ShaderAsset>(vm, objectHandle, name, nameLength);
return asset != nullptr ? asset->assetId() : 0;
}
uint32_t gpuShaderModuleNewImpl(WasmScriptingVM* vm,
const rive_gpu_shader_module_desc_v1* podDesc,
uint32_t descByteCount,
const uint8_t* blob,
uint32_t blobCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr || descByteCount < sizeof(*podDesc))
{
return 0;
}
uint64_t total = uint64_t(podDesc->codeSize) + podDesc->hlslSourceSize +
podDesc->hlslEntryPointSize + podDesc->bindingMapSize +
podDesc->glFixupSize;
if (total > blobCount)
{
return 0;
}
const uint8_t* cursor = blob;
auto slice = [&cursor](uint32_t size) {
const uint8_t* begin = cursor;
cursor += size;
return size != 0 ? begin : nullptr;
};
ore::ShaderModuleDesc desc;
desc.code = slice(podDesc->codeSize);
desc.codeSize = podDesc->codeSize;
desc.language = (ore::ShaderLanguage)podDesc->language;
desc.stage = (ore::ShaderStage)podDesc->stage;
// hlslSource and entry point are null-terminated strings host side.
std::string hlslSource;
std::string hlslEntryPoint;
if (const uint8_t* bytes = slice(podDesc->hlslSourceSize))
{
hlslSource.assign((const char*)bytes, podDesc->hlslSourceSize);
desc.hlslSource = hlslSource.c_str();
desc.hlslSourceSize = podDesc->hlslSourceSize;
}
if (const uint8_t* bytes = slice(podDesc->hlslEntryPointSize))
{
hlslEntryPoint.assign((const char*)bytes, podDesc->hlslEntryPointSize);
desc.hlslEntryPoint = hlslEntryPoint.c_str();
}
desc.bindingMapBytes = slice(podDesc->bindingMapSize);
desc.bindingMapSize = podDesc->bindingMapSize;
desc.glFixupBytes = slice(podDesc->glFixupSize);
desc.glFixupSize = podDesc->glFixupSize;
desc.shaderAssetId = podDesc->shaderAssetId;
// Texture-sampler pairs ride the desc so deferred replay rebuilds them;
// resolved host side from the asset like the Luau lane, not the wire.
std::vector<uint8_t> pairBytes;
if (podDesc->shaderAssetId != 0 && vm->file() != nullptr)
{
for (const auto& asset : vm->file()->assets())
{
if (asset->is<ShaderAsset>() &&
asset->assetId() == podDesc->shaderAssetId)
{
auto pairs = asset->as<ShaderAsset>()->textureSamplerPairs();
pairBytes.reserve(pairs.size() * 4);
for (size_t i = 0; i < pairs.size(); i++)
{
pairBytes.push_back(pairs[i].texGroup);
pairBytes.push_back(pairs[i].texBinding);
pairBytes.push_back(pairs[i].sampGroup);
pairBytes.push_back(pairs[i].sampBinding);
}
break;
}
}
}
desc.texSamplerPairBytes = pairBytes.empty() ? nullptr : pairBytes.data();
desc.texSamplerPairSize = (uint32_t)pairBytes.size();
oreContext->clearLastError();
auto shaderModule = oreContext->makeShaderModule(desc);
if (shaderModule == nullptr)
{
return gpuRejected(vm, oreContext, "Shader");
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::gpuShaderModule,
new HostGpuShaderModule{std::move(shaderModule)});
}
void gpuShaderModuleReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostGpuShaderModule*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::gpuShaderModule));
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::gpuShaderModule);
}
uint32_t gpuBindGroupLayoutNewImpl(
WasmScriptingVM* vm,
uint32_t groupIndex,
const rive_gpu_bind_group_layout_entry_v1* entries,
uint32_t entryByteCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr || entryByteCount % sizeof(*entries) != 0)
{
return 0;
}
uint32_t count = entryByteCount / (uint32_t)sizeof(*entries);
std::vector<ore::BindGroupLayoutEntry> resolved(count);
for (uint32_t i = 0; i < count; i++)
{
auto& out = resolved[i];
const auto& in = entries[i];
out.binding = in.binding;
out.kind = (ore::BindingKind)in.kind;
out.visibility.mask = (uint8_t)in.visibility;
out.hasDynamicOffset = in.hasDynamicOffset != 0;
out.textureViewDim = (ore::TextureViewDimension)in.textureViewDim;
out.textureSampleType =
(ore::BindGroupLayoutEntry::SampleType)in.textureSampleType;
out.textureMultisampled = in.textureMultisampled != 0;
out.minBindingSize = in.minBindingSize;
out.nativeSlotVS = in.nativeSlotVS;
out.nativeSlotFS = in.nativeSlotFS;
out.nativeSlotCS = in.nativeSlotCS;
out.samplerNonFiltering = in.samplerNonFiltering != 0;
}
ore::BindGroupLayoutDesc desc;
desc.groupIndex = groupIndex;
desc.entries = resolved.data();
desc.entryCount = count;
oreContext->clearLastError();
auto layout = oreContext->makeBindGroupLayout(desc);
if (layout == nullptr)
{
return gpuRejected(vm, oreContext, "GPUBindGroupLayout");
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::gpuBindGroupLayout,
new HostGpuBindGroupLayout{std::move(layout)});
}
void gpuBindGroupLayoutReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostGpuBindGroupLayout*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::gpuBindGroupLayout));
vm->handles().release(
handle,
WasmScriptingVM::HandleTable::Tag::gpuBindGroupLayout);
}
uint32_t gpuBindGroupLayoutFromShaderImpl(WasmScriptingVM* vm,
uint32_t shaderModule,
uint32_t groupIndex,
const uint32_t* dynamicUBOs,
uint32_t dynamicUBOCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr)
{
return 0;
}
auto host = static_cast<HostGpuShaderModule*>(vm->handles().resolve(
shaderModule,
WasmScriptingVM::HandleTable::Tag::gpuShaderModule));
if (host == nullptr)
{
return 0;
}
auto layout = ore::makeBindGroupLayoutFromShader(*oreContext,
host->shaderModule.get(),
groupIndex,
dynamicUBOs,
dynamicUBOCount);
if (layout == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::gpuBindGroupLayout,
new HostGpuBindGroupLayout{std::move(layout)});
}
uint32_t gpuBindGroupNewImpl(WasmScriptingVM* vm,
uint32_t layoutHandle,
const rive_gpu_bind_group_ubo_v1* ubos,
uint32_t uboByteCount,
const rive_gpu_bind_group_texture_v1* textures,
uint32_t textureByteCount,
const rive_gpu_bind_group_sampler_v1* samplers,
uint32_t samplerByteCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr || uboByteCount % sizeof(*ubos) != 0 ||
textureByteCount % sizeof(*textures) != 0 ||
samplerByteCount % sizeof(*samplers) != 0)
{
return 0;
}
auto layout = static_cast<HostGpuBindGroupLayout*>(vm->handles().resolve(
layoutHandle,
WasmScriptingVM::HandleTable::Tag::gpuBindGroupLayout));
if (layout == nullptr)
{
return 0;
}
ore::BindGroupDesc desc;
desc.layout = layout->layout.get();
uint32_t uboCount = uboByteCount / (uint32_t)sizeof(*ubos);
std::vector<ore::BindGroupDesc::UBOEntry> uboEntries(uboCount);
for (uint32_t i = 0; i < uboCount; i++)
{
auto buffer = static_cast<HostGpuBuffer*>(vm->handles().resolve(
ubos[i].buffer,
WasmScriptingVM::HandleTable::Tag::gpuBuffer));
if (buffer == nullptr)
{
return 0;
}
uboEntries[i].slot = ubos[i].slot;
uboEntries[i].buffer = buffer->buffer.get();
uboEntries[i].offset = ubos[i].offset;
uboEntries[i].size = ubos[i].size;
}
desc.ubos = uboEntries.data();
desc.uboCount = uboCount;
uint32_t texCount = textureByteCount / (uint32_t)sizeof(*textures);
std::vector<ore::BindGroupDesc::TexEntry> texEntries(texCount);
for (uint32_t i = 0; i < texCount; i++)
{
auto view = static_cast<HostGpuTextureView*>(vm->handles().resolve(
textures[i].view,
WasmScriptingVM::HandleTable::Tag::gpuTextureView));
if (view == nullptr)
{
return 0;
}
texEntries[i].slot = textures[i].slot;
texEntries[i].view = view->view.get();
}
desc.textures = texEntries.data();
desc.textureCount = texCount;
uint32_t sampCount = samplerByteCount / (uint32_t)sizeof(*samplers);
std::vector<ore::BindGroupDesc::SampEntry> sampEntries(sampCount);
for (uint32_t i = 0; i < sampCount; i++)
{
auto sampler = static_cast<HostGpuSampler*>(vm->handles().resolve(
samplers[i].sampler,
WasmScriptingVM::HandleTable::Tag::gpuSampler));
if (sampler == nullptr)
{
return 0;
}
sampEntries[i].slot = samplers[i].slot;
sampEntries[i].sampler = sampler->sampler.get();
}
desc.samplers = sampEntries.data();
desc.samplerCount = sampCount;
oreContext->clearLastError();
auto bindGroup = oreContext->makeBindGroup(desc);
if (bindGroup == nullptr)
{
return gpuRejected(vm, oreContext, "GPUBindGroup");
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuBindGroup,
new HostGpuBindGroup{std::move(bindGroup)});
}
void gpuBindGroupReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostGpuBindGroup*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuBindGroup));
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::gpuBindGroup);
}
uint32_t gpuPipelineNewImpl(WasmScriptingVM* vm,
const rive_gpu_pipeline_desc_v1* podDesc,
uint32_t descByteCount,
const uint8_t* blob,
uint32_t blobCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr || descByteCount < sizeof(*podDesc))
{
return 0;
}
uint64_t total =
uint64_t(podDesc->vertexEntrySize) + podDesc->fragmentEntrySize +
uint64_t(podDesc->colorCount) * sizeof(rive_gpu_color_target_v1) +
uint64_t(podDesc->vertexBufferCount) *
sizeof(rive_gpu_vertex_buffer_layout_v1) +
uint64_t(podDesc->attributeCount) *
sizeof(rive_gpu_vertex_attribute_v1) +
uint64_t(podDesc->bindGroupLayoutCount) * sizeof(uint32_t);
if (total > blobCount || podDesc->colorCount > 4 ||
podDesc->bindGroupLayoutCount > ore::kMaxBindGroups)
{
return 0;
}
const uint8_t* cursor = blob;
auto slice = [&cursor](uint32_t size) {
const uint8_t* begin = cursor;
cursor += size;
return size != 0 ? begin : nullptr;
};
ore::PipelineDesc desc;
auto vertexModule = static_cast<HostGpuShaderModule*>(vm->handles().resolve(
podDesc->vertexModule,
WasmScriptingVM::HandleTable::Tag::gpuShaderModule));
auto fragmentModule =
static_cast<HostGpuShaderModule*>(vm->handles().resolve(
podDesc->fragmentModule,
WasmScriptingVM::HandleTable::Tag::gpuShaderModule));
desc.vertexModule =
vertexModule != nullptr ? vertexModule->shaderModule.get() : nullptr;
desc.fragmentModule = fragmentModule != nullptr
? fragmentModule->shaderModule.get()
: nullptr;
std::string vertexEntry;
std::string fragmentEntry;
if (const uint8_t* bytes = slice(podDesc->vertexEntrySize))
{
vertexEntry.assign((const char*)bytes, podDesc->vertexEntrySize);
desc.vertexEntryPoint = vertexEntry.c_str();
}
if (const uint8_t* bytes = slice(podDesc->fragmentEntrySize))
{
fragmentEntry.assign((const char*)bytes, podDesc->fragmentEntrySize);
desc.fragmentEntryPoint = fragmentEntry.c_str();
}
auto colorTargets = (const rive_gpu_color_target_v1*)slice(
podDesc->colorCount * (uint32_t)sizeof(rive_gpu_color_target_v1));
desc.colorCount = podDesc->colorCount;
for (uint32_t i = 0; i < podDesc->colorCount; i++)
{
auto& out = desc.colorTargets[i];
out.format = (ore::TextureFormat)colorTargets[i].format;
out.blendEnabled = colorTargets[i].blendEnabled != 0;
out.blend.srcColor = (ore::BlendFactor)colorTargets[i].srcColor;
out.blend.dstColor = (ore::BlendFactor)colorTargets[i].dstColor;
out.blend.colorOp = (ore::BlendOp)colorTargets[i].colorOp;
out.blend.srcAlpha = (ore::BlendFactor)colorTargets[i].srcAlpha;
out.blend.dstAlpha = (ore::BlendFactor)colorTargets[i].dstAlpha;
out.blend.alphaOp = (ore::BlendOp)colorTargets[i].alphaOp;
out.writeMask = (ore::ColorWriteMask)colorTargets[i].writeMask;
}
auto bufferPods = (const rive_gpu_vertex_buffer_layout_v1*)slice(
podDesc->vertexBufferCount *
(uint32_t)sizeof(rive_gpu_vertex_buffer_layout_v1));
auto attributePods = (const rive_gpu_vertex_attribute_v1*)slice(
podDesc->attributeCount *
(uint32_t)sizeof(rive_gpu_vertex_attribute_v1));
std::vector<ore::VertexAttribute> attributes(podDesc->attributeCount);
for (uint32_t i = 0; i < podDesc->attributeCount; i++)
{
attributes[i].format = (ore::VertexFormat)attributePods[i].format;
attributes[i].offset = attributePods[i].offset;
attributes[i].shaderSlot = attributePods[i].shaderSlot;
}
std::vector<ore::VertexBufferLayout> buffers(podDesc->vertexBufferCount);
uint32_t attributeCursor = 0;
for (uint32_t i = 0; i < podDesc->vertexBufferCount; i++)
{
uint32_t count = bufferPods[i].attributeCount;
if (uint64_t(attributeCursor) + count > podDesc->attributeCount)
{
return 0;
}
buffers[i].stride = bufferPods[i].stride;
buffers[i].stepMode = (ore::VertexStepMode)bufferPods[i].stepMode;
buffers[i].attributes = attributes.data() + attributeCursor;
buffers[i].attributeCount = count;
attributeCursor += count;
}
desc.vertexBuffers = buffers.data();
desc.vertexBufferCount = podDesc->vertexBufferCount;
auto layoutHandles = (const uint32_t*)slice(podDesc->bindGroupLayoutCount *
(uint32_t)sizeof(uint32_t));
ore::BindGroupLayout* layouts[ore::kMaxBindGroups] = {};
for (uint32_t i = 0; i < podDesc->bindGroupLayoutCount; i++)
{
if (layoutHandles[i] == 0)
{
continue;
}
auto layout =
static_cast<HostGpuBindGroupLayout*>(vm->handles().resolve(
layoutHandles[i],
WasmScriptingVM::HandleTable::Tag::gpuBindGroupLayout));
if (layout == nullptr)
{
return 0;
}
layouts[i] = layout->layout.get();
}
desc.bindGroupLayouts = layouts;
desc.bindGroupLayoutCount = podDesc->bindGroupLayoutCount;
desc.topology = (ore::PrimitiveTopology)podDesc->topology;
desc.indexFormat = (ore::IndexFormat)podDesc->indexFormat;
desc.cullMode = (ore::CullMode)podDesc->cullMode;
desc.winding = (ore::FaceWinding)podDesc->winding;
desc.depthStencil.format = (ore::TextureFormat)podDesc->depthFormat;
desc.depthStencil.depthCompare =
(ore::CompareFunction)podDesc->depthCompare;
desc.depthStencil.depthWriteEnabled = podDesc->depthWriteEnabled != 0;
desc.depthStencil.depthBias = (int32_t)podDesc->depthBias;
desc.depthStencil.depthBiasSlopeScale = podDesc->depthBiasSlopeScale;
desc.depthStencil.depthBiasClamp = podDesc->depthBiasClamp;
desc.stencilFront.compare =
(ore::CompareFunction)podDesc->stencilFrontCompare;
desc.stencilFront.failOp = (ore::StencilOp)podDesc->stencilFrontFailOp;
desc.stencilFront.depthFailOp =
(ore::StencilOp)podDesc->stencilFrontDepthFailOp;
desc.stencilFront.passOp = (ore::StencilOp)podDesc->stencilFrontPassOp;
desc.stencilBack.compare =
(ore::CompareFunction)podDesc->stencilBackCompare;
desc.stencilBack.failOp = (ore::StencilOp)podDesc->stencilBackFailOp;
desc.stencilBack.depthFailOp =
(ore::StencilOp)podDesc->stencilBackDepthFailOp;
desc.stencilBack.passOp = (ore::StencilOp)podDesc->stencilBackPassOp;
desc.stencilReadMask = (uint8_t)podDesc->stencilReadMask;
desc.stencilWriteMask = (uint8_t)podDesc->stencilWriteMask;
desc.sampleCount = podDesc->sampleCount;
oreContext->clearLastError();
auto pipeline = oreContext->makePipeline(desc);
if (pipeline == nullptr)
{
return gpuRejected(vm, oreContext, "GPUPipeline");
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuPipeline,
new HostGpuPipeline{std::move(pipeline)});
}
void gpuPipelineReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostGpuPipeline*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::gpuPipeline));
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::gpuPipeline);
}
#else
// Builds without the canvas renderer keep the namespace linkable; scripts
// see the same nil/no-op surface the Luau backend presents there.
uint32_t gpuCanvasNewImpl(WasmScriptingVM*, uint32_t, uint32_t) { return 0; }
void gpuCanvasReleaseImpl(WasmScriptingVM*, uint32_t) {}
uint32_t gpuCanvasColorViewImpl(WasmScriptingVM*, uint32_t, uint32_t*, uint32_t)
{
return 0;
}
uint32_t gpuCanvasImageImpl(WasmScriptingVM*, uint32_t) { return 0; }
uint32_t gpuCanvasResizeImpl(WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
uint32_t*,
uint32_t)
{
return 0;
}
uint32_t gpuFeaturesImpl(WasmScriptingVM*, uint32_t*, uint32_t) { return 0; }
uint32_t gpuPassBeginImpl(WasmScriptingVM*,
const rive_gpu_pass_desc_v1*,
uint32_t,
const rive_gpu_pass_color_attachment_v1*,
uint32_t)
{
return 0;
}
void gpuPassSetPipelineImpl(WasmScriptingVM*, uint32_t, uint32_t) {}
void gpuPassSetVertexBufferImpl(WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
uint32_t)
{}
void gpuPassSetIndexBufferImpl(WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
uint32_t)
{}
void gpuPassSetBindGroupImpl(WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
const uint32_t*,
uint32_t)
{}
void gpuPassSetViewportImpl(WasmScriptingVM*,
uint32_t,
float,
float,
float,
float,
float,
float)
{}
void gpuPassSetScissorImpl(WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
uint32_t,
uint32_t)
{}
void gpuPassSetStencilReferenceImpl(WasmScriptingVM*, uint32_t, uint32_t) {}
void gpuPassSetBlendColorImpl(WasmScriptingVM*,
uint32_t,
float,
float,
float,
float)
{}
void gpuPassDrawImpl(WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
uint32_t,
uint32_t)
{}
void gpuPassDrawIndexedImpl(WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
uint32_t,
int32_t,
uint32_t)
{}
void gpuPassFinishImpl(WasmScriptingVM*, uint32_t) {}
void gpuPassReleaseImpl(WasmScriptingVM*, uint32_t) {}
uint32_t gpuImageViewImpl(WasmScriptingVM*, uint32_t, uint32_t, uint32_t)
{
return 0;
}
uint32_t gpuBufferNewImpl(WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
const uint8_t*,
uint32_t)
{
return 0;
}
void gpuBufferUpdateImpl(WasmScriptingVM*,
uint32_t,
uint32_t,
const uint8_t*,
uint32_t)
{}
void gpuBufferReleaseImpl(WasmScriptingVM*, uint32_t) {}
uint32_t gpuTextureNewImpl(WasmScriptingVM*,
const rive_gpu_texture_desc_v1*,
uint32_t)
{
return 0;
}
void gpuTextureUploadImpl(WasmScriptingVM*,
uint32_t,
const rive_gpu_texture_upload_v1*,
uint32_t,
const uint8_t*,
uint32_t)
{}
void gpuTextureReleaseImpl(WasmScriptingVM*, uint32_t) {}
uint32_t gpuSamplerNewImpl(WasmScriptingVM*,
const rive_gpu_sampler_desc_v1*,
uint32_t)
{
return 0;
}
void gpuSamplerReleaseImpl(WasmScriptingVM*, uint32_t) {}
uint32_t gpuTextureViewNewImpl(WasmScriptingVM*,
uint32_t,
const rive_gpu_texture_view_desc_v1*,
uint32_t)
{
return 0;
}
void gpuTextureViewReleaseImpl(WasmScriptingVM*, uint32_t) {}
uint32_t gpuShaderTargetImpl(WasmScriptingVM*) { return 0; }
uint32_t gpuShaderAssetBytesImpl(WasmScriptingVM*,
uint32_t,
const char*,
uint32_t,
uint8_t*,
uint32_t)
{
return 0;
}
uint32_t gpuShaderAssetIdImpl(WasmScriptingVM*, uint32_t, const char*, uint32_t)
{
return 0;
}
uint32_t gpuShaderModuleNewImpl(WasmScriptingVM*,
const rive_gpu_shader_module_desc_v1*,
uint32_t,
const uint8_t*,
uint32_t)
{
return 0;
}
void gpuShaderModuleReleaseImpl(WasmScriptingVM*, uint32_t) {}
uint32_t gpuBindGroupLayoutNewImpl(WasmScriptingVM*,
uint32_t,
const rive_gpu_bind_group_layout_entry_v1*,
uint32_t)
{
return 0;
}
void gpuBindGroupLayoutReleaseImpl(WasmScriptingVM*, uint32_t) {}
uint32_t gpuBindGroupLayoutFromShaderImpl(WasmScriptingVM*,
uint32_t,
uint32_t,
const uint32_t*,
uint32_t)
{
return 0;
}
uint32_t gpuBindGroupNewImpl(WasmScriptingVM*,
uint32_t,
const rive_gpu_bind_group_ubo_v1*,
uint32_t,
const rive_gpu_bind_group_texture_v1*,
uint32_t,
const rive_gpu_bind_group_sampler_v1*,
uint32_t)
{
return 0;
}
void gpuBindGroupReleaseImpl(WasmScriptingVM*, uint32_t) {}
uint32_t gpuPipelineNewImpl(WasmScriptingVM*,
const rive_gpu_pipeline_desc_v1*,
uint32_t,
const uint8_t*,
uint32_t)
{
return 0;
}
void gpuPipelineReleaseImpl(WasmScriptingVM*, uint32_t) {}
#endif
#ifdef RIVE_CANVAS
// Frames go through the deferred canvas host, the only path that can open
// inside the screen frame already open around the draw.
struct HostCanvas
{
rcp<gpu::RenderCanvas> canvas;
// A size requested before the device bound, allocated on first use.
uint32_t pendingWidth = 0;
uint32_t pendingHeight = 0;
cmd::DeferredCanvasHost* frameHost = nullptr;
uint32_t rendererHandle = 0;
};
HostCanvas* resolveCanvas(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostCanvas*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::canvas));
}
// Allocates a pending size once a device is there to allocate against.
// False only when a device is present and refuses.
bool canvasSatisfyPending(WasmScriptingVM* vm, HostCanvas* host)
{
if (host->pendingWidth == 0 || host->pendingHeight == 0)
{
return true;
}
auto* renderContext =
static_cast<gpu::RenderContext*>(vm->factory()->renderContext());
if (renderContext == nullptr)
{
return true;
}
// A refused size leaves the previous backing in place.
rcp<gpu::RenderCanvas> canvas =
vm->factory()->deferredCanvasHost() != nullptr
? renderContext->makeDeferredRenderCanvas(host->pendingWidth,
host->pendingHeight)
: renderContext->makeRenderCanvas(host->pendingWidth,
host->pendingHeight);
host->pendingWidth = 0;
host->pendingHeight = 0;
if (canvas == nullptr)
{
return false;
}
host->canvas = std::move(canvas);
return true;
}
uint32_t canvasNewImpl(WasmScriptingVM* vm, uint32_t width, uint32_t height)
{
if (vm == nullptr || vm->factory() == nullptr)
{
return 0;
}
auto* host = new HostCanvas();
if (width != 0 && height != 0)
{
host->pendingWidth = width;
host->pendingHeight = height;
// A size only waits when a recording host will bind a device later.
if (!canvasSatisfyPending(vm, host) ||
(host->canvas == nullptr &&
vm->factory()->deferredCanvasHost() == nullptr))
{
delete host;
return 0;
}
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::canvas, host);
}
void canvasReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto* host = resolveCanvas(vm, handle);
if (host == nullptr)
{
return;
}
canvasEndFrameImpl(vm, handle);
delete host;
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::canvas);
}
uint32_t canvasWidthImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto* host = resolveCanvas(vm, handle);
if (host == nullptr)
{
return 0;
}
canvasSatisfyPending(vm, host);
return host->canvas != nullptr ? host->canvas->width() : host->pendingWidth;
}
uint32_t canvasHeightImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto* host = resolveCanvas(vm, handle);
if (host == nullptr)
{
return 0;
}
canvasSatisfyPending(vm, host);
return host->canvas != nullptr ? host->canvas->height()
: host->pendingHeight;
}
uint32_t canvasResizeImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t width,
uint32_t height)
{
auto* host = resolveCanvas(vm, handle);
if (host == nullptr || host->frameHost != nullptr)
{
return 0;
}
if (width == 0 || height == 0)
{
host->canvas = nullptr;
host->pendingWidth = 0;
host->pendingHeight = 0;
return 1;
}
// An unchanged size would churn a new texture per frame.
if (host->canvas != nullptr && host->canvas->width() == width &&
host->canvas->height() == height)
{
return 1;
}
host->pendingWidth = width;
host->pendingHeight = height;
return canvasSatisfyPending(vm, host) ? 1 : 0;
}
uint32_t canvasImageImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto* host = resolveCanvas(vm, handle);
if (host == nullptr)
{
return 0;
}
canvasSatisfyPending(vm, host);
if (host->canvas == nullptr || host->canvas->renderImage() == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::image,
new HostImage{ref_rcp(static_cast<RenderImage*>(
host->canvas->renderImage())),
host->canvas});
}
uint32_t canvasBeginFrameImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t clearColor)
{
auto* host = resolveCanvas(vm, handle);
if (host == nullptr || host->frameHost != nullptr)
{
return 0;
}
canvasSatisfyPending(vm, host);
cmd::DeferredCanvasHost* frameHost = vm->factory()->deferredCanvasHost();
if (host->canvas == nullptr || frameHost == nullptr)
{
return 0;
}
Renderer* renderer =
frameHost->beginCanvasContent(host->canvas.get(), clearColor);
if (renderer == nullptr)
{
return 0;
}
host->frameHost = frameHost;
host->rendererHandle =
vm->handles().mint(WasmScriptingVM::HandleTable::Tag::renderer,
renderer);
vm->registerOpenCanvasFrame(handle);
return host->rendererHandle;
}
void canvasEndFrameImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto* host = resolveCanvas(vm, handle);
if (host == nullptr || host->frameHost == nullptr)
{
return;
}
vm->unregisterOpenCanvasFrame(handle);
// Releasing bumps the generation so a stashed renderer goes stale.
vm->handles().release(host->rendererHandle,
WasmScriptingVM::HandleTable::Tag::renderer);
host->rendererHandle = 0;
host->frameHost->endCanvasContent(host->canvas.get());
host->frameHost = nullptr;
}
#else
uint32_t canvasNewImpl(WasmScriptingVM*, uint32_t, uint32_t) { return 0; }
void canvasReleaseImpl(WasmScriptingVM*, uint32_t) {}
uint32_t canvasWidthImpl(WasmScriptingVM*, uint32_t) { return 0; }
uint32_t canvasHeightImpl(WasmScriptingVM*, uint32_t) { return 0; }
uint32_t canvasResizeImpl(WasmScriptingVM*, uint32_t, uint32_t, uint32_t)
{
return 0;
}
uint32_t canvasImageImpl(WasmScriptingVM*, uint32_t) { return 0; }
uint32_t canvasBeginFrameImpl(WasmScriptingVM*, uint32_t, uint32_t)
{
return 0;
}
void canvasEndFrameImpl(WasmScriptingVM*, uint32_t) {}
#endif
struct HostBuffer
{
rcp<RenderBuffer> buffer;
};
HostBuffer* resolveBuffer(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostBuffer*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::buffer));
}
uint32_t bufferNewImpl(WasmScriptingVM* vm,
uint32_t bufferType,
uint32_t flags,
uint32_t sizeInBytes)
{
if (vm == nullptr)
{
return 0;
}
auto buffer = vm->factory()->makeRenderBuffer((RenderBufferType)bufferType,
(RenderBufferFlags)flags,
sizeInBytes);
if (buffer == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::buffer,
new HostBuffer{std::move(buffer)});
}
void bufferUpdateImpl(WasmScriptingVM* vm,
uint32_t handle,
const uint8_t* bytes,
uint32_t byteCount)
{
auto host = resolveBuffer(vm, handle);
if (host == nullptr || byteCount != host->buffer->sizeInBytes())
{
return;
}
void* data = host->buffer->map();
if (data != nullptr)
{
memcpy(data, bytes, byteCount);
host->buffer->unmap();
}
}
void bufferReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostBuffer*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::buffer));
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::buffer);
}
Renderer* resolveRenderer(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<Renderer*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::renderer));
}
void rendererSaveImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (auto renderer = resolveRenderer(vm, handle))
{
renderer->save();
auto& saves = vm->visitSaves();
saves.open += renderer == saves.renderer ? 1 : 0;
}
}
void rendererRestoreImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (auto renderer = resolveRenderer(vm, handle))
{
renderer->restore();
auto& saves = vm->visitSaves();
saves.open -= renderer == saves.renderer ? 1 : 0;
}
}
void rendererTransformImpl(WasmScriptingVM* vm,
uint32_t handle,
float xx,
float xy,
float yx,
float yy,
float tx,
float ty)
{
if (auto renderer = resolveRenderer(vm, handle))
{
renderer->transform(Mat2D(xx, xy, yx, yy, tx, ty));
}
}
void rendererDrawPathImpl(WasmScriptingVM* vm,
uint32_t rendererHandle,
uint32_t pathHandle,
uint32_t paintHandle)
{
auto renderer = resolveRenderer(vm, rendererHandle);
auto paint = resolvePaint(vm, paintHandle);
if (vm == nullptr || renderer == nullptr || paint == nullptr)
{
return;
}
auto hostPath = static_cast<HostPath*>(
vm->handles().resolve(pathHandle,
WasmScriptingVM::HandleTable::Tag::path));
if (hostPath == nullptr || hostPath->path == nullptr)
{
return;
}
renderer->drawPath(hostPath->path.get(), paint);
}
void rendererDrawImageImpl(WasmScriptingVM* vm,
uint32_t rendererHandle,
uint32_t imageHandle,
uint32_t samplerKey,
uint32_t blend,
float opacity)
{
auto renderer = resolveRenderer(vm, rendererHandle);
auto host = resolveImage(vm, imageHandle);
if (renderer == nullptr || host == nullptr)
{
return;
}
renderer->drawImage(host->image.get(),
ImageSampler::SamplerFromKey((uint8_t)samplerKey),
(BlendMode)blend,
opacity);
}
void rendererDrawImageMeshImpl(WasmScriptingVM* vm,
uint32_t rendererHandle,
uint32_t imageHandle,
uint32_t samplerKey,
uint32_t vertexHandle,
uint32_t uvHandle,
uint32_t indexHandle,
uint32_t blend,
float opacity)
{
auto renderer = resolveRenderer(vm, rendererHandle);
auto image = resolveImage(vm, imageHandle);
auto vertex = resolveBuffer(vm, vertexHandle);
auto uv = resolveBuffer(vm, uvHandle);
auto index = resolveBuffer(vm, indexHandle);
if (renderer == nullptr || image == nullptr || vertex == nullptr ||
uv == nullptr || index == nullptr)
{
return;
}
renderer->drawImageMesh(
image->image.get(),
ImageSampler::SamplerFromKey((uint8_t)samplerKey),
vertex->buffer,
uv->buffer,
index->buffer,
(uint32_t)(vertex->buffer->sizeInBytes() / sizeof(Vec2D)),
(uint32_t)(index->buffer->sizeInBytes() / sizeof(uint16_t)),
(BlendMode)blend,
opacity);
}
void rendererClipPathImpl(WasmScriptingVM* vm,
uint32_t rendererHandle,
uint32_t pathHandle)
{
auto renderer = resolveRenderer(vm, rendererHandle);
if (vm == nullptr || renderer == nullptr)
{
return;
}
auto hostPath = static_cast<HostPath*>(
vm->handles().resolve(pathHandle,
WasmScriptingVM::HandleTable::Tag::path));
if (hostPath == nullptr || hostPath->path == nullptr)
{
return;
}
renderer->clipPath(hostPath->path.get());
}
void rendererModulateColorImpl(WasmScriptingVM* vm,
uint32_t rendererHandle,
uint32_t color,
uint32_t replace)
{
auto renderer = resolveRenderer(vm, rendererHandle);
if (renderer != nullptr)
{
renderer->modulateColor(color, replace != 0);
}
}
void rtLogImpl(WasmScriptingVM* vm,
int32_t level,
const char* message,
uint32_t length)
{
WasmScriptingVMNatives::print(vm, message, length);
WasmScriptingVMNatives::print(vm, "\n", 1);
}
void rtBudgetExceededImpl(WasmScriptingVM* vm, uint32_t ms)
{
fprintf(stderr,
"script error: execution exceeded %u millisecond timeout\n",
ms);
// Terminates like a trap when the native returns, so the caller's
// failed-op handling engages.
vm->raiseModuleError("execution exceeded timeout");
}
uint32_t rtUtcOffsetImpl(WasmScriptingVM* vm, double epochSeconds)
{
time_t at;
struct tm local;
return localTime(epochSeconds, at, local) ? (uint32_t)utcOffsetOf(local, at)
: 0;
}
uint32_t rtIsDstImpl(WasmScriptingVM* vm, double epochSeconds)
{
time_t at;
struct tm local;
return localTime(epochSeconds, at, local) && local.tm_isdst > 0 ? 1 : 0;
}
uint32_t rtZoneNameImpl(WasmScriptingVM* vm,
double epochSeconds,
char* buffer,
uint32_t capacity)
{
time_t at;
struct tm local;
if (!localTime(epochSeconds, at, local))
{
return 0;
}
char name[64];
size_t length = strftime(name, sizeof(name), "%Z", &local);
memcpy(buffer, name, length < capacity ? length : capacity);
return (uint32_t)length;
}
// Module start has no exec env to carry the vm; the probes its top level
// hits belong to the VM being booted.
static WasmDebugHooks* debugHooksFor(WasmScriptingVM*& vm)
{
if (vm == nullptr)
{
vm = s_booting;
}
return vm != nullptr ? vm->debugHooks() : nullptr;
}
void rtDebugEnterImpl(WasmScriptingVM* vm, uint32_t function, uint32_t line)
{
if (WasmDebugHooks* hooks = debugHooksFor(vm))
{
hooks->onEnter(*vm, function, line);
}
}
uint32_t rtDebugLineImpl(WasmScriptingVM* vm, uint32_t line)
{
WasmDebugHooks* hooks = debugHooksFor(vm);
return hooks != nullptr && hooks->onLine(*vm, line) ? 1 : 0;
}
void rtDebugLeaveImpl(WasmScriptingVM* vm)
{
if (WasmDebugHooks* hooks = debugHooksFor(vm))
{
hooks->onLeave(*vm);
}
}
void rtMarkNeedsUpdateImpl(WasmScriptingVM* vm, uint32_t objectHandle)
{
if (vm == nullptr)
{
return;
}
auto object = static_cast<ScriptedObject*>(
vm->handles().resolve(objectHandle,
WasmScriptingVM::HandleTable::Tag::object));
if (object != nullptr)
{
object->markNeedsUpdate();
}
}
// --- rive_data_v1: view model instances and their values, pinned per handle
struct HostViewModelInstance
{
rcp<ViewModelInstance> instance;
};
// Forwards core value change notifications into the module's listener
// registry by token.
struct HostValueDelegate : public ViewModelInstanceValueDelegate
{
WasmScriptingVM* vm = nullptr;
uint32_t token = 0;
void valueChanged() override { vm->notifyDataValueChanged(token); }
};
struct HostInstanceValue
{
rcp<ViewModelInstanceValue> value;
HostValueDelegate* delegate = nullptr;
~HostInstanceValue()
{
if (delegate != nullptr)
{
value->removeDelegate(delegate);
delete delegate;
}
}
};
void dataConvertResultImpl(WasmScriptingVM* vm,
uint32_t kind,
float number,
uint32_t booleanValue,
uint32_t color,
const char* value,
uint32_t length)
{
ScriptBackend::ScriptDataResult* out =
vm != nullptr ? vm->convertResultOut() : nullptr;
if (out == nullptr)
{
return;
}
switch (kind)
{
case DataConvertWire::kindNumber:
out->kind = ScriptBackend::ScriptDataResult::Kind::number;
out->number = number;
break;
case DataConvertWire::kindString:
out->kind = ScriptBackend::ScriptDataResult::Kind::string;
out->string.assign(value, length);
break;
case DataConvertWire::kindBoolean:
out->kind = ScriptBackend::ScriptDataResult::Kind::boolean;
out->boolean = booleanValue != 0;
break;
case DataConvertWire::kindColor:
out->kind = ScriptBackend::ScriptDataResult::Kind::color;
out->color = (int)color;
break;
default:
break;
}
}
uint32_t dataViewModelImpl(WasmScriptingVM* vm, uint32_t objectHandle)
{
if (vm == nullptr)
{
return 0;
}
auto object = static_cast<ScriptedObject*>(
vm->handles().resolve(objectHandle,
WasmScriptingVM::HandleTable::Tag::object));
if (object == nullptr || object->dataContext() == nullptr)
{
return 0;
}
auto instance = object->dataContext()->mainViewModelInstance();
if (instance == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{std::move(instance)});
}
void dataVmiReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostViewModelInstance*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::viewModelInstance));
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::viewModelInstance);
}
template <typename T>
uint32_t mintInstanceValue(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
if (vm == nullptr)
{
return 0;
}
auto host = static_cast<HostViewModelInstance*>(vm->handles().resolve(
vmiHandle,
WasmScriptingVM::HandleTable::Tag::viewModelInstance));
if (host == nullptr)
{
return 0;
}
auto value = host->instance->propertyValue(std::string(name, length));
if (value == nullptr || !value->template is<T>())
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::instanceValue,
new HostInstanceValue{ref_rcp(value)});
}
ViewModel* findViewModel(WasmScriptingVM* vm, const char* name, uint32_t length)
{
if (vm == nullptr || vm->viewModels() == nullptr)
{
return nullptr;
}
std::string key(name, length);
for (ViewModel* viewModel : *vm->viewModels())
{
if (viewModel != nullptr && viewModel->name() == key)
{
return viewModel;
}
}
return nullptr;
}
uint32_t dataHasViewModelImpl(WasmScriptingVM* vm,
const char* name,
uint32_t nameLength)
{
return findViewModel(vm, name, nameLength) != nullptr ? 1 : 0;
}
uint32_t dataNewViewModelImpl(WasmScriptingVM* vm,
const char* name,
uint32_t nameLength,
const char* templateName,
uint32_t templateLength)
{
ViewModel* viewModel = findViewModel(vm, name, nameLength);
if (vm == nullptr || viewModel == nullptr)
{
return 0;
}
auto instance = templateLength != 0
? viewModel->createFromInstance(
std::string(templateName, templateLength))
: viewModel->createInstance();
if (instance == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{std::move(instance)});
}
uint32_t dataVmiNumberImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceNumber>(vm,
vmiHandle,
name,
length);
}
uint32_t dataVmiBooleanImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceBoolean>(vm,
vmiHandle,
name,
length);
}
uint32_t dataVmiStringImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceString>(vm,
vmiHandle,
name,
length);
}
uint32_t dataVmiTriggerImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceTrigger>(vm,
vmiHandle,
name,
length);
}
uint32_t dataVmiColorImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceColor>(vm,
vmiHandle,
name,
length);
}
uint32_t dataVmiViewModelImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceViewModel>(vm,
vmiHandle,
name,
length);
}
void dataPropReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostInstanceValue*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::instanceValue));
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::instanceValue);
}
template <typename T>
T* resolveInstanceValue(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
auto host = static_cast<HostInstanceValue*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::instanceValue));
if (host == nullptr || !host->value->template is<T>())
{
return nullptr;
}
return host->value->template as<T>();
}
float dataNumberGetImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto number = resolveInstanceValue<ViewModelInstanceNumber>(vm, handle);
return number != nullptr ? number->propertyValue() : 0.0f;
}
void dataNumberSetImpl(WasmScriptingVM* vm, uint32_t handle, float value)
{
auto number = resolveInstanceValue<ViewModelInstanceNumber>(vm, handle);
if (number != nullptr)
{
number->propertyValue(value);
}
}
uint32_t dataBooleanGetImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto boolean = resolveInstanceValue<ViewModelInstanceBoolean>(vm, handle);
return boolean != nullptr && boolean->propertyValue() ? 1 : 0;
}
void dataBooleanSetImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value)
{
auto boolean = resolveInstanceValue<ViewModelInstanceBoolean>(vm, handle);
if (boolean != nullptr)
{
boolean->propertyValue(value != 0);
}
}
uint32_t dataStringGetImpl(WasmScriptingVM* vm,
uint32_t handle,
char* buffer,
uint32_t capacity)
{
auto string = resolveInstanceValue<ViewModelInstanceString>(vm, handle);
if (string == nullptr)
{
return 0;
}
const std::string& value = string->propertyValue();
size_t copied = value.size() < capacity ? value.size() : capacity;
memcpy(buffer, value.data(), copied);
return (uint32_t)value.size();
}
void dataStringSetImpl(WasmScriptingVM* vm,
uint32_t handle,
const char* value,
uint32_t length)
{
auto string = resolveInstanceValue<ViewModelInstanceString>(vm, handle);
if (string != nullptr)
{
string->propertyValue(std::string(value, length));
}
}
void dataTriggerFireImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto trigger = resolveInstanceValue<ViewModelInstanceTrigger>(vm, handle);
if (trigger != nullptr)
{
trigger->trigger();
}
}
uint32_t dataColorGetImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto color = resolveInstanceValue<ViewModelInstanceColor>(vm, handle);
return color != nullptr ? (uint32_t)color->propertyValue() : 0;
}
void dataColorSetImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value)
{
auto color = resolveInstanceValue<ViewModelInstanceColor>(vm, handle);
if (color != nullptr)
{
color->propertyValue((int)value);
}
}
uint32_t dataVmiEnumImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceEnum>(vm,
vmiHandle,
name,
length);
}
uint32_t dataEnumGetImpl(WasmScriptingVM* vm,
uint32_t handle,
char* buffer,
uint32_t capacity)
{
auto value = resolveInstanceValue<ViewModelInstanceEnum>(vm, handle);
if (value == nullptr || value->viewModelProperty() == nullptr ||
!value->viewModelProperty()->is<ViewModelPropertyEnum>())
{
return 0;
}
auto dataEnum =
value->viewModelProperty()->as<ViewModelPropertyEnum>()->dataEnum();
if (dataEnum == nullptr)
{
return 0;
}
auto values = dataEnum->values();
uint32_t index = value->propertyValue();
if (index >= values.size())
{
return 0;
}
const std::string& key = values[index]->key();
size_t copied = key.size() < capacity ? key.size() : capacity;
memcpy(buffer, key.data(), copied);
return (uint32_t)key.size();
}
void dataEnumSetImpl(WasmScriptingVM* vm,
uint32_t handle,
const char* value,
uint32_t length)
{
auto instanceValue =
resolveInstanceValue<ViewModelInstanceEnum>(vm, handle);
if (instanceValue != nullptr)
{
instanceValue->value(std::string(value, length));
}
}
uint32_t dataVmiListImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceList>(vm,
vmiHandle,
name,
length);
}
uint32_t dataListLengthImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
return list != nullptr ? (uint32_t)list->listItems().size() : 0;
}
void dataListPushImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t vmiHandle)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
if (vm == nullptr || list == nullptr)
{
return;
}
auto host = static_cast<HostViewModelInstance*>(vm->handles().resolve(
vmiHandle,
WasmScriptingVM::HandleTable::Tag::viewModelInstance));
if (host == nullptr)
{
return;
}
auto item = make_rcp<ViewModelInstanceListItem>();
item->viewModelInstance(host->instance);
list->addItem(std::move(item));
}
uint32_t mintRemovedItem(WasmScriptingVM* vm,
rcp<ViewModelInstanceListItem> item)
{
if (vm == nullptr || item == nullptr ||
item->viewModelInstance() == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{item->viewModelInstance()});
}
uint32_t dataListPopImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
return list != nullptr ? mintRemovedItem(vm, list->pop()) : 0;
}
uint32_t dataListShiftImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
return list != nullptr ? mintRemovedItem(vm, list->shift()) : 0;
}
void dataListClearImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
if (list != nullptr)
{
list->removeAllItems();
}
}
void dataListSwapImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t index1,
uint32_t index2)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
if (list != nullptr)
{
list->swap(index1, index2);
}
}
rcp<ViewModelInstance> resolveVmiArg(WasmScriptingVM* vm, uint32_t vmiHandle)
{
if (vm == nullptr)
{
return nullptr;
}
auto host = static_cast<HostViewModelInstance*>(vm->handles().resolve(
vmiHandle,
WasmScriptingVM::HandleTable::Tag::viewModelInstance));
return host != nullptr ? host->instance : nullptr;
}
void dataListInsertImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t vmiHandle,
uint32_t index)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
auto instance = resolveVmiArg(vm, vmiHandle);
if (list == nullptr || instance == nullptr)
{
return;
}
auto item = make_rcp<ViewModelInstanceListItem>();
item->viewModelInstance(std::move(instance));
list->addItemAt(std::move(item), (int)index);
}
void dataListRemoveImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t vmiHandle)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
auto instance = resolveVmiArg(vm, vmiHandle);
if (list == nullptr || instance == nullptr)
{
return;
}
for (const auto& item : list->listItems())
{
if (item->viewModelInstance() == instance)
{
list->removeItem(item);
break;
}
}
}
void dataListRemoveAtImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t index)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
if (list != nullptr)
{
list->removeItem((int)index);
}
}
void dataViewModelSetImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t vmiHandle)
{
auto value = resolveInstanceValue<ViewModelInstanceViewModel>(vm, handle);
auto instance = resolveVmiArg(vm, vmiHandle);
if (value == nullptr || instance == nullptr ||
value->parentViewModelInstance() == nullptr)
{
return;
}
value->parentViewModelInstance()->replaceViewModelByProperty(
value,
std::move(instance));
}
uint32_t dataViewModelGetImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto value = resolveInstanceValue<ViewModelInstanceViewModel>(vm, handle);
if (vm == nullptr || value == nullptr)
{
return 0;
}
auto reference = value->referenceViewModelInstance();
if (reference == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{std::move(reference)});
}
uint32_t dataVmiPropertyImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length,
uint32_t* kindOut,
uint32_t kindCount)
{
if (kindCount > 0)
{
kindOut[0] = DataPropertyWire::kindNone;
}
if (vm == nullptr)
{
return 0;
}
auto host = static_cast<HostViewModelInstance*>(vm->handles().resolve(
vmiHandle,
WasmScriptingVM::HandleTable::Tag::viewModelInstance));
if (host == nullptr)
{
return 0;
}
auto value = host->instance->propertyValue(std::string(name, length));
if (value == nullptr)
{
return 0;
}
uint32_t kind = DataPropertyWire::kindNone;
switch (value->coreType())
{
case ViewModelInstanceNumberBase::typeKey:
kind = DataPropertyWire::kindNumber;
break;
case ViewModelInstanceBooleanBase::typeKey:
kind = DataPropertyWire::kindBoolean;
break;
case ViewModelInstanceStringBase::typeKey:
kind = DataPropertyWire::kindString;
break;
case ViewModelInstanceTriggerBase::typeKey:
kind = DataPropertyWire::kindTrigger;
break;
case ViewModelInstanceColorBase::typeKey:
kind = DataPropertyWire::kindColor;
break;
case ViewModelInstanceViewModelBase::typeKey:
kind = DataPropertyWire::kindViewModel;
break;
case ViewModelInstanceListBase::typeKey:
kind = DataPropertyWire::kindList;
break;
case ViewModelInstanceEnumBase::typeKey:
kind = DataPropertyWire::kindEnum;
break;
case ViewModelInstanceAssetImageBase::typeKey:
kind = DataPropertyWire::kindImage;
break;
case ViewModelInstanceAssetFontBase::typeKey:
kind = DataPropertyWire::kindFont;
break;
case ViewModelInstanceAssetBlobBase::typeKey:
kind = DataPropertyWire::kindBlob;
break;
case ViewModelInstanceSymbolListIndexBase::typeKey:
if (kindCount > 0)
{
kindOut[0] = DataPropertyWire::kindSymbolListIndex;
}
if (kindCount > 1)
{
kindOut[1] =
(uint32_t)value->as<ViewModelInstanceSymbolListIndex>()
->propertyValue();
}
return 0;
default:
return 0;
}
if (kindCount > 0)
{
kindOut[0] = kind;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::instanceValue,
new HostInstanceValue{ref_rcp(value)});
}
uint32_t dataVmiInstanceImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t nameLength)
{
if (vm == nullptr)
{
return 0;
}
auto host = static_cast<HostViewModelInstance*>(vm->handles().resolve(
vmiHandle,
WasmScriptingVM::HandleTable::Tag::viewModelInstance));
if (host == nullptr || host->instance->viewModel() == nullptr)
{
return 0;
}
ViewModel* viewModel = host->instance->viewModel();
auto instance =
nameLength != 0
? viewModel->createFromInstance(std::string(name, nameLength))
: nullptr;
if (instance == nullptr)
{
instance = viewModel->createInstance();
}
if (instance == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{std::move(instance)});
}
uint32_t dataVmiSymbolIndexImpl(WasmScriptingVM* vm, uint32_t vmiHandle)
{
if (vm == nullptr)
{
return ~0u;
}
auto host = static_cast<HostViewModelInstance*>(vm->handles().resolve(
vmiHandle,
WasmScriptingVM::HandleTable::Tag::viewModelInstance));
if (host == nullptr)
{
return ~0u;
}
auto prop = host->instance->propertyValue(SymbolType::itemIndex);
if (prop != nullptr && prop->is<ViewModelInstanceSymbolListIndex>())
{
return (uint32_t)prop->as<ViewModelInstanceSymbolListIndex>()
->propertyValue();
}
return ~0u;
}
uint32_t dataVmiEqualImpl(WasmScriptingVM* vm, uint32_t a, uint32_t b)
{
auto lhs = resolveVmiArg(vm, a);
auto rhs = resolveVmiArg(vm, b);
return lhs != nullptr && lhs == rhs ? 1 : 0;
}
void dataListRemoveAllOfImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t vmiHandle)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
auto instance = resolveVmiArg(vm, vmiHandle);
if (list != nullptr && instance != nullptr)
{
list->removeAllItemsWithViewModelInstance(instance.get());
}
}
uint32_t dataListGetImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t index)
{
auto list = resolveInstanceValue<ViewModelInstanceList>(vm, handle);
if (list == nullptr || index >= list->listItems().size())
{
return 0;
}
return mintRemovedItem(vm, list->listItems()[index]);
}
uint32_t dataEnumValuesImpl(WasmScriptingVM* vm,
uint32_t handle,
char* buffer,
uint32_t capacity)
{
auto value = resolveInstanceValue<ViewModelInstanceEnum>(vm, handle);
if (value == nullptr || value->viewModelProperty() == nullptr ||
!value->viewModelProperty()->is<ViewModelPropertyEnum>())
{
return 0;
}
auto dataEnum =
value->viewModelProperty()->as<ViewModelPropertyEnum>()->dataEnum();
if (dataEnum == nullptr)
{
return 0;
}
std::string joined;
for (auto& entry : dataEnum->values())
{
if (!joined.empty())
{
joined += '\n';
}
joined += entry->key();
}
size_t copied = joined.size() < capacity ? joined.size() : capacity;
memcpy(buffer, joined.data(), copied);
return (uint32_t)joined.size();
}
uint32_t dataRootViewModelImpl(WasmScriptingVM* vm, uint32_t objectHandle)
{
if (vm == nullptr)
{
return 0;
}
auto object = static_cast<ScriptedObject*>(
vm->handles().resolve(objectHandle,
WasmScriptingVM::HandleTable::Tag::object));
if (object == nullptr || object->dataContext() == nullptr)
{
return 0;
}
auto instance = object->dataContext()->rootViewModelInstance();
if (instance == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{std::move(instance)});
}
uint32_t dataGlobalViewModelImpl(WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t nameLength)
{
if (vm == nullptr)
{
return 0;
}
auto object = static_cast<ScriptedObject*>(
vm->handles().resolve(objectHandle,
WasmScriptingVM::HandleTable::Tag::object));
if (object == nullptr || object->scriptAsset() == nullptr ||
object->scriptAsset()->file() == nullptr ||
object->dataContext() == nullptr)
{
return 0;
}
std::string key(name, nameLength);
auto instance = object->dataContext()->resolveGlobalViewModel(
object->scriptAsset()->file(),
key.c_str());
if (instance == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{std::move(instance)});
}
uint32_t dataGlobalViewModelNamesImpl(WasmScriptingVM* vm,
uint32_t objectHandle,
char* buffer,
uint32_t capacity)
{
if (vm == nullptr)
{
return 0;
}
auto object = static_cast<ScriptedObject*>(
vm->handles().resolve(objectHandle,
WasmScriptingVM::HandleTable::Tag::object));
if (object == nullptr || object->scriptAsset() == nullptr ||
object->scriptAsset()->file() == nullptr)
{
return 0;
}
std::string joined;
for (auto& name : object->scriptAsset()->file()->globalViewModelNames())
{
if (!joined.empty())
{
joined += '\n';
}
joined += name;
}
size_t copied = joined.size() < capacity ? joined.size() : capacity;
memcpy(buffer, joined.data(), copied);
return (uint32_t)joined.size();
}
struct HostDataContext
{
rcp<DataContext> context;
};
uint32_t dataContextImpl(WasmScriptingVM* vm, uint32_t objectHandle)
{
if (vm == nullptr)
{
return 0;
}
auto object = static_cast<ScriptedObject*>(
vm->handles().resolve(objectHandle,
WasmScriptingVM::HandleTable::Tag::object));
if (object == nullptr || object->dataContext() == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::dataContext,
new HostDataContext{object->dataContext()});
}
uint32_t dataContextParentImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return 0;
}
auto host = static_cast<HostDataContext*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::dataContext));
if (host == nullptr || host->context->parent() == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::dataContext,
new HostDataContext{host->context->parent()});
}
uint32_t dataContextViewModelImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return 0;
}
auto host = static_cast<HostDataContext*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::dataContext));
if (host == nullptr)
{
return 0;
}
auto instance = host->context->mainViewModelInstance();
if (instance == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{std::move(instance)});
}
void dataContextReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostDataContext*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::dataContext));
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::dataContext);
}
// --- rive_artboard_v1: host-owned artboard inputs ---------------------------
// Mirrors ScriptReffedArtboard: the host owns the instance, its default state
// machine, and the bound view model instance; module userdata share it by ref.
struct HostArtboard : public RefCnt<HostArtboard>
{
File* file = nullptr;
std::unique_ptr<ArtboardInstance> artboard;
std::unique_ptr<StateMachineInstance> stateMachine;
rcp<ViewModelInstance> viewModelInstance;
rcp<DataContext> parentDataContext;
HostArtboard(File* fileValue,
std::unique_ptr<ArtboardInstance>&& artboardInstance,
rcp<ViewModelInstance> boundInstance,
rcp<DataContext> parent) :
file(fileValue),
artboard(std::move(artboardInstance)),
stateMachine(artboard->defaultStateMachine()),
parentDataContext(std::move(parent))
{
// A scripted artboard is a root: nothing hosts it in another
// artboard's focus tree, so it owns its FocusManager and builds its
// own focus tree.
artboard->buildFocusTree(artboard->ensureFocusManager(), nullptr);
viewModelInstance = boundInstance != nullptr
? std::move(boundInstance)
: file->createViewModelInstance(artboard.get());
if (stateMachine != nullptr && viewModelInstance != nullptr)
{
if (parentDataContext != nullptr)
{
auto dataContext = make_rcp<DataContext>(viewModelInstance);
dataContext->parent(parentDataContext);
stateMachine->bindDataContext(dataContext);
}
else
{
stateMachine->bindViewModelInstance(viewModelInstance);
}
}
}
~HostArtboard()
{
// State machine before artboard; its destructor touches the artboard.
stateMachine = nullptr;
artboard = nullptr;
}
bool advance(float seconds)
{
if (stateMachine != nullptr)
{
// Bound view models advance with the host frame, not here.
return stateMachine->advanceAndApply(seconds, false);
}
return artboard->advance(seconds);
}
};
HostArtboard* resolveArtboard(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostArtboard*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::artboard));
}
void artboardReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveArtboard(vm, handle);
if (vm == nullptr || host == nullptr)
{
return;
}
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::artboard);
host->unref();
}
uint32_t artboardAdvanceImpl(WasmScriptingVM* vm,
uint32_t handle,
float seconds)
{
auto host = resolveArtboard(vm, handle);
return host != nullptr && host->advance(seconds) ? 1 : 0;
}
void artboardDrawImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t rendererHandle)
{
auto host = resolveArtboard(vm, handle);
if (vm == nullptr || host == nullptr)
{
return;
}
auto renderer = static_cast<Renderer*>(
vm->handles().resolve(rendererHandle,
WasmScriptingVM::HandleTable::Tag::renderer));
if (renderer != nullptr)
{
host->artboard->drawInternal(renderer);
}
}
namespace
{
struct WasmDrawVisit
{
WasmScriptingVM* vm;
bool failed = false;
};
void wasmDrawVisitor(void* context, Drawable* drawable, Renderer* renderer)
{
auto visit = static_cast<WasmDrawVisit*>(context);
if (visit->failed)
{
drawable->draw(renderer);
return;
}
auto& handles = visit->vm->handles();
uint32_t handle =
handles.mint(WasmScriptingVM::HandleTable::Tag::drawable, drawable);
auto& saves = visit->vm->visitSaves();
WasmScriptingVM::VisitSaves outer = saves;
saves = {renderer, 0};
visit->failed = !visit->vm->notifyDrawVisit(handle);
if (visit->failed)
{
for (; saves.open > 0; saves.open--)
{
renderer->restore();
}
}
saves = outer;
handles.release(handle, WasmScriptingVM::HandleTable::Tag::drawable);
}
Drawable* resolveDrawable(WasmScriptingVM* vm, uint32_t handle)
{
return vm == nullptr ? nullptr
: static_cast<Drawable*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::drawable));
}
CustomProperty* resolveDrawableProperty(WasmScriptingVM* vm,
uint32_t handle,
uint32_t key)
{
auto drawable = resolveDrawable(vm, handle);
return drawable != nullptr ? drawable->customProperty(key) : nullptr;
}
} // namespace
void artboardDrawVisitImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t rendererHandle)
{
auto host = resolveArtboard(vm, handle);
auto renderer = resolveRenderer(vm, rendererHandle);
if (host == nullptr || renderer == nullptr)
{
return;
}
WasmDrawVisit visit = {vm};
host->artboard->drawInternal(renderer, wasmDrawVisitor, &visit, host->file);
}
void artboardDrawModulatedImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t rendererHandle,
uint32_t key)
{
auto host = resolveArtboard(vm, handle);
auto renderer = resolveRenderer(vm, rendererHandle);
if (host != nullptr && renderer != nullptr)
{
host->artboard->drawModulated(renderer, key, host->file);
}
}
void artboardDrawableDrawImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t rendererHandle)
{
auto drawable = resolveDrawable(vm, handle);
auto renderer = resolveRenderer(vm, rendererHandle);
if (drawable != nullptr && renderer != nullptr)
{
drawable->draw(renderer);
}
}
uint32_t artboardPropertyKeyImpl(WasmScriptingVM* vm,
uint32_t handle,
const char* name,
uint32_t length)
{
auto host = resolveArtboard(vm, handle);
return File::customPropertyKey(host != nullptr ? host->file : nullptr,
name,
length);
}
uint32_t artboardDrawableValueImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t key,
uint32_t* out,
uint32_t outCount)
{
auto property = resolveDrawableProperty(vm, handle, key);
if (property == nullptr)
{
return 0;
}
CustomPropertyKind kind = property->kind();
uint32_t bits = 0;
switch (kind)
{
case CustomPropertyKind::number:
{
float value = property->as<CustomPropertyNumber>()->propertyValue();
memcpy(&bits, &value, sizeof(bits));
break;
}
case CustomPropertyKind::boolean:
bits = property->as<CustomPropertyBoolean>()->propertyValue();
break;
case CustomPropertyKind::color:
bits = property->as<CustomPropertyColor>()->propertyValue();
break;
default:
// No reader takes the others' bits, the kind answers has().
break;
}
if (outCount > 0)
{
out[0] = bits;
}
return (uint32_t)kind + 1;
}
uint32_t artboardDrawableStringImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t key,
char* out,
uint32_t outCount)
{
auto property = resolveDrawableProperty(vm, handle, key);
if (property == nullptr || !property->is<CustomPropertyString>())
{
return ~0u;
}
const std::string& value =
property->as<CustomPropertyString>()->propertyValue();
memcpy(out, value.data(), std::min<size_t>(value.size(), outCount));
return (uint32_t)value.size();
}
#ifdef WITH_RIVE_TOOLS
uint32_t artboardDrawablePropertiesImpl(WasmScriptingVM* vm,
uint32_t handle,
char* out,
uint32_t outCount)
{
auto drawable = resolveDrawable(vm, handle);
auto file =
drawable != nullptr ? drawable->artboard()->drawVisitorFile() : nullptr;
auto manifest = file != nullptr ? file->manifest() : nullptr;
if (manifest == nullptr)
{
return 0;
}
std::string packed;
for (auto child : drawable->children())
{
auto property = CustomProperty::tagging(child);
if (property == nullptr)
{
continue;
}
packed.push_back((char)property->kind());
packed.append(manifest->resolveName(property->nameId()));
packed.push_back('\0');
}
memcpy(out, packed.data(), std::min<size_t>(packed.size(), outCount));
return (uint32_t)packed.size();
}
#endif
uint32_t artboardInstanceImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t vmiHandle)
{
auto host = resolveArtboard(vm, handle);
if (vm == nullptr || host == nullptr)
{
return 0;
}
auto clone = host->artboard->instance();
clone->frameOrigin(false);
auto vmi = vmiHandle != 0 ? resolveVmiArg(vm, vmiHandle) : nullptr;
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::artboard,
new HostArtboard(host->file,
std::move(clone),
std::move(vmi),
host->parentDataContext));
}
uint32_t artboardDataImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveArtboard(vm, handle);
if (vm == nullptr || host == nullptr || host->viewModelInstance == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{host->viewModelInstance});
}
float artboardWidthImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveArtboard(vm, handle);
return host != nullptr ? host->artboard->width() : 0.0f;
}
float artboardHeightImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveArtboard(vm, handle);
return host != nullptr ? host->artboard->height() : 0.0f;
}
void artboardSetWidthImpl(WasmScriptingVM* vm, uint32_t handle, float value)
{
auto host = resolveArtboard(vm, handle);
if (host != nullptr)
{
host->artboard->width(value);
}
}
void artboardSetHeightImpl(WasmScriptingVM* vm, uint32_t handle, float value)
{
auto host = resolveArtboard(vm, handle);
if (host != nullptr)
{
host->artboard->height(value);
}
}
uint32_t artboardFrameOriginImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveArtboard(vm, handle);
return host != nullptr && host->artboard->frameOrigin() ? 1 : 0;
}
void artboardSetFrameOriginImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t value)
{
auto host = resolveArtboard(vm, handle);
if (host != nullptr)
{
host->artboard->frameOrigin(value != 0);
}
}
void artboardBoundsImpl(WasmScriptingVM* vm,
uint32_t handle,
float* out,
uint32_t outCount)
{
auto host = resolveArtboard(vm, handle);
if (host == nullptr || outCount < 4)
{
return;
}
const AABB& bounds = host->artboard->bounds();
out[0] = bounds.min().x;
out[1] = bounds.min().y;
out[2] = bounds.max().x;
out[3] = bounds.max().y;
}
uint32_t artboardPointerEventImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t kind,
uint32_t pointerId,
float x,
float y)
{
auto host = resolveArtboard(vm, handle);
if (host == nullptr || host->stateMachine == nullptr)
{
return 0;
}
Vec2D position(x, y);
switch (kind)
{
case ArtboardWire::pointerDown:
return (uint32_t)(int)host->stateMachine->pointerDown(
position,
(uint8_t)pointerId);
case ArtboardWire::pointerMove:
return (uint32_t)(int)host->stateMachine->pointerMove(
position,
0,
(uint8_t)pointerId);
case ArtboardWire::pointerUp:
return (uint32_t)(int)host->stateMachine->pointerUp(
position,
(uint8_t)pointerId);
case ArtboardWire::pointerExit:
return (uint32_t)(int)host->stateMachine->pointerExit(
position,
(uint8_t)pointerId);
}
return 0;
}
// Animations pin their artboard so the instance they play into outlives them.
struct HostAnimation
{
rcp<HostArtboard> owner;
std::unique_ptr<LinearAnimationInstance> animation;
float duration() const
{
return (float)animation->duration() / (float)animation->fps();
}
};
uint32_t artboardAnimationImpl(WasmScriptingVM* vm,
uint32_t handle,
const char* name,
uint32_t length)
{
auto host = resolveArtboard(vm, handle);
if (vm == nullptr || host == nullptr)
{
return 0;
}
auto animation = host->artboard->animationNamed(std::string(name, length));
if (animation == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::animation,
new HostAnimation{ref_rcp(host), std::move(animation)});
}
HostAnimation* resolveAnimation(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostAnimation*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::animation));
}
void artboardAnimationReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveAnimation(vm, handle);
if (vm == nullptr || host == nullptr)
{
return;
}
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::animation);
delete host;
}
float artboardAnimationDurationImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveAnimation(vm, handle);
return host != nullptr ? host->duration() : 0.0f;
}
uint32_t artboardAnimationAdvanceImpl(WasmScriptingVM* vm,
uint32_t handle,
float seconds)
{
auto host = resolveAnimation(vm, handle);
if (host == nullptr)
{
return 0;
}
bool advanced = host->animation->advance(seconds);
host->animation->apply();
return advanced ? 1 : 0;
}
void artboardAnimationSetTimeImpl(WasmScriptingVM* vm,
uint32_t handle,
float value,
uint32_t mode)
{
auto host = resolveAnimation(vm, handle);
if (host == nullptr)
{
return;
}
float seconds = value;
if (mode == ArtboardWire::timeFrames)
{
seconds = value / (float)host->animation->fps();
}
else if (mode == ArtboardWire::timePercentage)
{
seconds = value * host->duration();
}
host->animation->time(
host->animation->animation()->globalToLocalSeconds(seconds));
host->animation->apply();
}
// Nodes pin their artboard; the component pointer lives inside its instance.
struct HostNode
{
rcp<HostArtboard> owner;
TransformComponent* component;
};
uint32_t artboardNodeImpl(WasmScriptingVM* vm,
uint32_t handle,
const char* name,
uint32_t length)
{
auto host = resolveArtboard(vm, handle);
if (vm == nullptr || host == nullptr)
{
return 0;
}
auto component =
host->artboard->find<TransformComponent>(std::string(name, length));
if (component == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::node,
new HostNode{ref_rcp(host), component});
}
HostNode* resolveNode(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostNode*>(
vm->handles().resolve(handle, WasmScriptingVM::HandleTable::Tag::node));
}
void artboardNodeReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveNode(vm, handle);
if (vm == nullptr || host == nullptr)
{
return;
}
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::node);
delete host;
}
void artboardNodeTransformImpl(WasmScriptingVM* vm,
uint32_t handle,
float* out,
uint32_t outCount)
{
auto host = resolveNode(vm, handle);
if (host == nullptr || outCount < 5)
{
return;
}
out[0] = host->component->x();
out[1] = host->component->y();
out[2] = host->component->rotation();
out[3] = host->component->scaleX();
out[4] = host->component->scaleY();
}
// x/y writes only land on Node and RootBone, like the Luau lane.
void nodeSetX(TransformComponent* component, float value)
{
if (component->is<Node>())
{
component->as<Node>()->x(value);
}
else if (component->is<RootBone>())
{
component->as<RootBone>()->x(value);
}
}
void nodeSetY(TransformComponent* component, float value)
{
if (component->is<Node>())
{
component->as<Node>()->y(value);
}
else if (component->is<RootBone>())
{
component->as<RootBone>()->y(value);
}
}
void artboardNodeSetImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t field,
float v0,
float v1)
{
auto host = resolveNode(vm, handle);
if (host == nullptr)
{
return;
}
TransformComponent* component = host->component;
switch (field)
{
case ArtboardWire::nodeX:
nodeSetX(component, v0);
break;
case ArtboardWire::nodeY:
nodeSetY(component, v0);
break;
case ArtboardWire::nodeRotation:
component->rotation(v0);
break;
case ArtboardWire::nodeScaleX:
component->scaleX(v0);
break;
case ArtboardWire::nodeScaleY:
component->scaleY(v0);
break;
case ArtboardWire::nodePosition:
nodeSetX(component, v0);
nodeSetY(component, v1);
break;
case ArtboardWire::nodeScale:
component->scaleX(v0);
component->scaleY(v1);
break;
}
}
void artboardNodeWorldTransformImpl(WasmScriptingVM* vm,
uint32_t handle,
float* out,
uint32_t outCount)
{
auto host = resolveNode(vm, handle);
if (host == nullptr || outCount < 6)
{
return;
}
const Mat2D& world = host->component->worldTransform();
for (int i = 0; i < 6; i++)
{
out[i] = world[i];
}
}
void artboardNodeSetWorldTransformImpl(WasmScriptingVM* vm,
uint32_t handle,
const float* values,
uint32_t floatCount)
{
auto host = resolveNode(vm, handle);
if (host == nullptr || floatCount < 6)
{
return;
}
host->component->mutableWorldTransform() =
Mat2D(values[0], values[1], values[2], values[3], values[4], values[5]);
}
void artboardNodeDecomposeImpl(WasmScriptingVM* vm,
uint32_t handle,
const float* values,
uint32_t floatCount)
{
auto host = resolveNode(vm, handle);
if (host == nullptr || floatCount < 6)
{
return;
}
TransformComponent* component = host->component;
Mat2D world =
getParentWorld(*component).invertOrIdentity() *
Mat2D(values[0], values[1], values[2], values[3], values[4], values[5]);
TransformComponents components = world.decompose();
nodeSetX(component, components.x());
nodeSetY(component, components.y());
component->scaleX(components.scaleX());
component->scaleY(components.scaleY());
component->rotation(components.rotation());
}
uint32_t artboardNodePathVerbsImpl(WasmScriptingVM* vm,
uint32_t handle,
uint8_t* out,
uint32_t outCount)
{
auto host = resolveNode(vm, handle);
if (host == nullptr || !host->component->is<Path>())
{
return ~0u;
}
const RawPath& raw = host->component->as<Path>()->rawPath();
size_t count = raw.verbs().size();
if (count <= outCount)
{
memcpy(out, raw.verbs().data(), count);
}
return (uint32_t)count;
}
uint32_t artboardNodePathPointsImpl(WasmScriptingVM* vm,
uint32_t handle,
float* out,
uint32_t outCount)
{
auto host = resolveNode(vm, handle);
if (host == nullptr || !host->component->is<Path>())
{
return ~0u;
}
const RawPath& raw = host->component->as<Path>()->rawPath();
size_t count = raw.points().size() * 2;
if (count <= outCount)
{
memcpy(out, raw.points().data(), count * sizeof(float));
}
return (uint32_t)count;
}
uint32_t artboardNodePaintImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t* out,
uint32_t outCount)
{
auto host = resolveNode(vm, handle);
if (host == nullptr || !host->component->is<ShapePaint>() || outCount < 7)
{
return 0;
}
// The same snapshot ScriptedPaintData(const ShapePaint*) takes.
auto shapePaint = host->component->as<ShapePaint>();
uint32_t style = (uint32_t)RenderPaintStyle::fill;
uint32_t join = (uint32_t)StrokeJoin::miter;
uint32_t cap = (uint32_t)StrokeCap::butt;
float thickness = 1.0f;
float featherStrength = 0.0f;
uint32_t color = 0xFF000000;
if (shapePaint->is<Stroke>())
{
auto stroke = shapePaint->as<Stroke>();
style = (uint32_t)RenderPaintStyle::stroke;
thickness = stroke->thickness();
cap = stroke->cap();
join = stroke->join();
}
for (auto& child : shapePaint->children())
{
if (child->is<SolidColor>())
{
color = child->as<SolidColor>()->colorValue();
break;
}
}
if (shapePaint->feather() != nullptr)
{
featherStrength = shapePaint->feather()->strength();
}
out[0] = style;
out[1] = join;
out[2] = cap;
out[3] = (uint32_t)shapePaint->blendModeValue();
out[4] = color;
memcpy(&out[5], &thickness, sizeof(float));
memcpy(&out[6], &featherStrength, sizeof(float));
return 1;
}
uint32_t artboardNodeChildrenImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t* out,
uint32_t outCount)
{
auto host = resolveNode(vm, handle);
if (host == nullptr || !host->component->is<ContainerComponent>())
{
return 0;
}
auto& children = host->component->as<ContainerComponent>()->children();
uint32_t count = 0;
for (auto child : children)
{
if (child->is<TransformComponent>())
{
count++;
}
}
if (count > outCount)
{
return count;
}
uint32_t index = 0;
for (auto child : children)
{
if (child->is<TransformComponent>())
{
out[index++] = vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::node,
new HostNode{host->owner, child->as<TransformComponent>()});
}
}
return count;
}
uint32_t artboardNodeParentImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveNode(vm, handle);
if (host == nullptr)
{
return 0;
}
auto parent = host->component->parent();
if (parent == nullptr || !parent->is<TransformComponent>())
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::node,
new HostNode{host->owner, parent->as<TransformComponent>()});
}
// --- rive_data_v1 asset properties (image/font/blob) ------------------------
struct HostFont
{
rcp<Font> font;
};
uint32_t dataVmiImageImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceAssetImage>(vm,
vmiHandle,
name,
length);
}
uint32_t dataVmiFontImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceAssetFont>(vm,
vmiHandle,
name,
length);
}
uint32_t dataVmiBlobImpl(WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue<ViewModelInstanceAssetBlob>(vm,
vmiHandle,
name,
length);
}
// Mirrors ScriptedPropertyImage::pushValue: the instance's embedded asset
// first, else the id-bound asset through the file registry.
uint32_t dataImageGetImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto value = resolveInstanceValue<ViewModelInstanceAssetImage>(vm, handle);
if (vm == nullptr || value == nullptr)
{
return 0;
}
RenderImage* renderImage = nullptr;
if (auto asset = value->asset())
{
renderImage = asset->renderImage();
}
if (renderImage == nullptr && vm->file() != nullptr)
{
auto fileAsset = vm->file()->asset(value->propertyValue());
if (fileAsset != nullptr && fileAsset->is<ImageAsset>())
{
renderImage = fileAsset->as<ImageAsset>()->renderImage();
}
}
if (renderImage == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::image,
new HostImage{ref_rcp(renderImage)});
}
void dataImageSetImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t imageHandle)
{
auto value = resolveInstanceValue<ViewModelInstanceAssetImage>(vm, handle);
if (value == nullptr)
{
return;
}
auto host = resolveImage(vm, imageHandle);
value->value(host != nullptr ? host->image.get() : nullptr);
}
uint32_t dataFontGetImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto value = resolveInstanceValue<ViewModelInstanceAssetFont>(vm, handle);
if (vm == nullptr || value == nullptr)
{
return 0;
}
rcp<Font> font;
if (auto asset = value->asset())
{
font = asset->font();
}
if (font == nullptr && vm->file() != nullptr)
{
auto fileAsset = vm->file()->asset(value->propertyValue());
if (fileAsset != nullptr && fileAsset->is<FontAsset>())
{
font = fileAsset->as<FontAsset>()->font();
}
}
if (font == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::font,
new HostFont{std::move(font)});
}
void dataFontSetImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t fontHandle)
{
auto value = resolveInstanceValue<ViewModelInstanceAssetFont>(vm, handle);
if (vm == nullptr || value == nullptr)
{
return;
}
auto host = static_cast<HostFont*>(
vm->handles().resolve(fontHandle,
WasmScriptingVM::HandleTable::Tag::font));
value->value(host != nullptr ? host->font.get() : nullptr);
}
void dataFontReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return;
}
delete static_cast<HostFont*>(
vm->handles().resolve(handle, WasmScriptingVM::HandleTable::Tag::font));
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::font);
}
// Mirrors ScriptedPropertyBlob::pushValue: a non-null instance asset (even
// empty) wins, else the id-bound asset through the file registry.
rcp<FileAsset> resolveBlobAsset(WasmScriptingVM* vm, uint32_t handle)
{
auto value = resolveInstanceValue<ViewModelInstanceAssetBlob>(vm, handle);
if (vm == nullptr || value == nullptr)
{
return nullptr;
}
if (auto asset = value->asset())
{
return asset;
}
if (vm->file() != nullptr)
{
auto fileAsset = vm->file()->asset(value->propertyValue());
if (fileAsset != nullptr && fileAsset->is<BlobAsset>())
{
return fileAsset;
}
}
return nullptr;
}
uint32_t blobAssetBytesImpl(WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t nameLength,
uint8_t* out,
uint32_t outCount)
{
auto asset = findFileAsset<BlobAsset>(
vm,
objectHandle,
name,
nameLength,
[](BlobAsset* blob) { return !blob->bytes().empty(); });
if (asset == nullptr)
{
return 0;
}
auto bytes = asset->bytes();
if (bytes.size() <= outCount)
{
memcpy(out, bytes.data(), bytes.size());
}
return (uint32_t)bytes.size();
}
// --- rive_audio_v1 ---------------------------------------------------------
#ifdef WITH_RIVE_AUDIO
struct HostAudioSource
{
rcp<AudioSource> source;
};
struct HostAudioSound
{
rcp<AudioSound> sound;
};
HostAudioSource* resolveAudioSource(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostAudioSource*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::audioSource));
}
HostAudioSound* resolveAudioSound(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostAudioSound*>(
vm->handles().resolve(handle,
WasmScriptingVM::HandleTable::Tag::audioSound));
}
// The Luau play and playFrame shapes: relative times offset from the
// engine clock, the sound minted at the artboard-free volume of 1. Tools
// builds refuse to play while playback is paused.
uint32_t audioPlaySound(WasmScriptingVM* vm, rcp<AudioSound> sound)
{
if (sound == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::audioSound,
new HostAudioSound{std::move(sound)});
}
rcp<AudioEngine> audioPlayEngine(WasmScriptingVM* vm, HostAudioSource* host)
{
if (host == nullptr)
{
return nullptr;
}
#ifdef WITH_RIVE_TOOLS
if (!vm->isPlaying())
{
return nullptr;
}
#endif
return AudioEngine::RuntimeEngine(true);
}
uint32_t audioPlaySeconds(WasmScriptingVM* vm,
uint32_t sourceHandle,
float seconds,
bool relative)
{
auto host = resolveAudioSource(vm, sourceHandle);
auto engine = audioPlayEngine(vm, host);
if (engine == nullptr)
{
return 0;
}
if (relative)
{
seconds += engine->timeInSeconds();
}
return audioPlaySound(vm, engine->playSeconds(host->source, seconds, 0, 0));
}
uint32_t audioPlayFrames(WasmScriptingVM* vm,
uint32_t sourceHandle,
double frames,
bool relative)
{
auto host = resolveAudioSource(vm, sourceHandle);
auto engine = audioPlayEngine(vm, host);
if (engine == nullptr)
{
return 0;
}
uint64_t startTime = (uint64_t)frames;
if (relative)
{
startTime += engine->timeInFrames();
}
return audioPlaySound(vm, engine->play(host->source, startTime, 0, 0));
}
#endif
uint32_t audioSourceImpl(WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t nameLength)
{
#ifdef WITH_RIVE_AUDIO
auto asset = findFileAsset<AudioAsset>(
vm,
objectHandle,
name,
nameLength,
[](AudioAsset* audio) { return audio->audioSource() != nullptr; });
if (asset == nullptr)
{
return 0;
}
return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::audioSource,
new HostAudioSource{asset->audioSource()});
#endif
return 0;
}
void audioSourceReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSource(vm, handle);
if (host == nullptr)
{
return;
}
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::audioSource);
delete host;
#endif
}
float audioSourceDurationImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSource(vm, handle);
return host != nullptr ? host->source->duration() : 0.0f;
#else
return 0.0f;
#endif
}
uint32_t audioSourceSampleRateImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSource(vm, handle);
return host != nullptr ? host->source->sampleRate() : 0;
#else
return 0;
#endif
}
uint32_t audioSourceChannelsImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSource(vm, handle);
return host != nullptr ? host->source->channels() : 0;
#else
return 0;
#endif
}
uint32_t audioPlayImpl(WasmScriptingVM* vm, uint32_t source)
{
#ifdef WITH_RIVE_AUDIO
return audioPlaySeconds(vm, source, 0.0f, true);
#else
return 0;
#endif
}
uint32_t audioPlayAtTimeImpl(WasmScriptingVM* vm,
uint32_t source,
float seconds)
{
#ifdef WITH_RIVE_AUDIO
return audioPlaySeconds(vm, source, seconds, false);
#else
return 0;
#endif
}
uint32_t audioPlayInTimeImpl(WasmScriptingVM* vm,
uint32_t source,
float seconds)
{
#ifdef WITH_RIVE_AUDIO
return audioPlaySeconds(vm, source, seconds, true);
#else
return 0;
#endif
}
uint32_t audioPlayAtFrameImpl(WasmScriptingVM* vm,
uint32_t source,
double frame)
{
#ifdef WITH_RIVE_AUDIO
return audioPlayFrames(vm, source, frame, false);
#else
return 0;
#endif
}
uint32_t audioPlayInFrameImpl(WasmScriptingVM* vm,
uint32_t source,
double frame)
{
#ifdef WITH_RIVE_AUDIO
return audioPlayFrames(vm, source, frame, true);
#else
return 0;
#endif
}
float audioTimeImpl(WasmScriptingVM* vm)
{
#ifdef WITH_RIVE_AUDIO
auto engine = AudioEngine::RuntimeEngine(true);
return engine != nullptr ? engine->timeInSeconds() : 0.0f;
#else
return 0.0f;
#endif
}
double audioTimeFrameImpl(WasmScriptingVM* vm)
{
#ifdef WITH_RIVE_AUDIO
auto engine = AudioEngine::RuntimeEngine(true);
return engine != nullptr ? (double)engine->timeInFrames() : 0.0;
#else
return 0.0;
#endif
}
uint32_t audioSampleRateImpl(WasmScriptingVM* vm)
{
#ifdef WITH_RIVE_AUDIO
auto engine = AudioEngine::RuntimeEngine(true);
return engine != nullptr ? engine->sampleRate() : 0;
#else
return 0;
#endif
}
void audioSoundReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
if (host == nullptr)
{
return;
}
vm->handles().release(handle,
WasmScriptingVM::HandleTable::Tag::audioSound);
delete host;
#endif
}
void audioSoundPlayImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
if (host != nullptr)
{
host->sound->play();
}
#endif
}
void audioSoundPauseImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
if (host != nullptr)
{
host->sound->pause();
}
#endif
}
void audioSoundResumeImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
if (host != nullptr)
{
host->sound->resume();
}
#endif
}
void audioSoundStopImpl(WasmScriptingVM* vm,
uint32_t handle,
uint32_t fadeFrames)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
if (host != nullptr)
{
host->sound->stop(fadeFrames);
}
#endif
}
uint32_t audioSoundSeekImpl(WasmScriptingVM* vm, uint32_t handle, float seconds)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
return host != nullptr && host->sound->seekSeconds(seconds) ? 1 : 0;
#else
return 0;
#endif
}
uint32_t audioSoundSeekFrameImpl(WasmScriptingVM* vm,
uint32_t handle,
double frame)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
return host != nullptr && host->sound->seek((uint64_t)frame) ? 1 : 0;
#else
return 0;
#endif
}
uint32_t audioSoundCompletedImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
return host != nullptr && host->sound->completed() ? 1 : 0;
#else
return 0;
#endif
}
float audioSoundTimeImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
return host != nullptr ? host->sound->timeInSeconds() : 0.0f;
#else
return 0.0f;
#endif
}
double audioSoundTimeFrameImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
return host != nullptr ? (double)host->sound->timeInFrames() : 0.0;
#else
return 0.0;
#endif
}
float audioSoundVolumeImpl(WasmScriptingVM* vm, uint32_t handle)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
return host != nullptr ? host->sound->volume() : 0.0f;
#else
return 0.0f;
#endif
}
void audioSoundSetVolumeImpl(WasmScriptingVM* vm, uint32_t handle, float value)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSound(vm, handle);
if (host != nullptr)
{
host->sound->volume(value);
}
#endif
}
uint32_t dataBlobPresentImpl(WasmScriptingVM* vm, uint32_t handle)
{
return resolveBlobAsset(vm, handle) != nullptr ? 1 : 0;
}
uint32_t dataBlobGetImpl(WasmScriptingVM* vm,
uint32_t handle,
uint8_t* buffer,
uint32_t capacity)
{
auto asset = resolveBlobAsset(vm, handle);
if (asset == nullptr)
{
return 0;
}
auto bytes = asset->as<BlobAsset>()->bytes();
size_t copied = bytes.size() < capacity ? bytes.size() : capacity;
memcpy(buffer, bytes.data(), copied);
return (uint32_t)bytes.size();
}
uint32_t dataBlobNameImpl(WasmScriptingVM* vm,
uint32_t handle,
char* buffer,
uint32_t capacity)
{
auto asset = resolveBlobAsset(vm, handle);
if (asset == nullptr)
{
return 0;
}
const std::string& name = asset->name();
size_t copied = name.size() < capacity ? name.size() : capacity;
memcpy(buffer, name.data(), copied);
return (uint32_t)name.size();
}
void dataBlobSetImpl(WasmScriptingVM* vm,
uint32_t handle,
const uint8_t* bytes,
uint32_t byteCount)
{
auto value = resolveInstanceValue<ViewModelInstanceAssetBlob>(vm, handle);
if (value == nullptr)
{
return;
}
auto asset = make_rcp<BlobAsset>();
SimpleArray<uint8_t> data(bytes, byteCount);
asset->decode(data, nullptr);
value->value(asset.get());
}
void dataBlobClearImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto value = resolveInstanceValue<ViewModelInstanceAssetBlob>(vm, handle);
if (value != nullptr)
{
value->value(nullptr);
}
}
HostInstanceValue* resolveHostValue(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostInstanceValue*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::instanceValue));
}
void dataWatchImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t token)
{
auto host = resolveHostValue(vm, handle);
if (host == nullptr || host->delegate != nullptr)
{
return;
}
host->delegate = new HostValueDelegate();
host->delegate->vm = vm;
host->delegate->token = token;
host->value->addDelegate(host->delegate);
}
void dataUnwatchImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveHostValue(vm, handle);
if (host == nullptr || host->delegate == nullptr)
{
return;
}
host->value->removeDelegate(host->delegate);
delete host->delegate;
host->delegate = nullptr;
}
bool ensureRuntime()
{
static bool initialized = false;
static bool ok = false;
if (initialized)
{
return ok;
}
initialized = true;
RuntimeInitArgs args;
memset(&args, 0, sizeof(args));
args.mem_alloc_type = Alloc_With_System_Allocator;
if (!wasm_runtime_full_init(&args))
{
return false;
}
wasm_runtime_register_natives("env",
kEnvNatives,
sizeof(kEnvNatives) / sizeof(NativeSymbol));
wasm_runtime_register_natives("wasi_snapshot_preview1",
kWasiNatives,
sizeof(kWasiNatives) / sizeof(NativeSymbol));
if (!registerRiveBindingNatives())
{
return false;
}
ok = true;
return ok;
}
} // namespace
uint32_t WasmScriptingVM::HandleTable::mint(Tag tag, void* object)
{
uint32_t slot;
if (!freeSlots.empty())
{
slot = freeSlots.back();
freeSlots.pop_back();
}
else
{
slot = (uint32_t)slots.size();
slots.push_back({});
}
slots[slot].tag = tag;
slots[slot].object = object;
// Handle 0 is never valid; slots bias by one.
return ((slot + 1) & 0xffffff) | ((uint32_t)slots[slot].generation << 24);
}
void* WasmScriptingVM::HandleTable::resolve(uint32_t handle, Tag tag) const
{
uint32_t slot = (handle & 0xffffff);
if (slot == 0 || slot > slots.size())
{
return nullptr;
}
const Slot& entry = slots[slot - 1];
if (entry.tag != tag || entry.generation != (uint8_t)(handle >> 24))
{
return nullptr;
}
return entry.object;
}
void WasmScriptingVM::HandleTable::release(uint32_t handle, Tag tag)
{
uint32_t slot = (handle & 0xffffff);
if (slot == 0 || slot > slots.size())
{
return;
}
Slot& entry = slots[slot - 1];
// Tag mismatch must not free the slot: a mistyped guest release would
// stale the correctly-typed handle and leak its host wrapper.
if (entry.generation != (uint8_t)(handle >> 24) || entry.tag != tag)
{
return;
}
entry.tag = Tag::empty;
entry.object = nullptr;
entry.generation++;
freeSlots.push_back(slot - 1);
}
int WasmScriptingVM::sm_defaultTimeoutMs = 200;
WasmScriptingVM::WasmScriptingVM() = default;
std::unique_ptr<WasmScriptingVM> WasmScriptingVM::make(
Span<const uint8_t> module,
Factory* factory,
std::string& outError,
std::function<void(const char*, size_t)> print)
{
std::unique_ptr<WasmScriptingVM> vm(new WasmScriptingVM());
vm->m_factory = factory;
vm->m_print = std::move(print);
if (s_bootHook)
{
s_bootHook(*vm, module);
}
if (!vm->init(module))
{
outError = vm->m_lastError;
return nullptr;
}
return vm;
}
void WasmScriptingVM::callDraw(ScriptedObject* object,
int selfRef,
Renderer* renderer)
{
// The handle is scoped to this call; releasing bumps the generation so a
// stashed renderer goes stale instead of ghost drawing.
uint32_t handle = m_handles.mint(HandleTable::Tag::renderer, renderer);
uint32_t args[3] = {m_L, (uint32_t)selfRef, handle};
callModule("host_obj_draw", 3, args);
m_handles.release(handle, HandleTable::Tag::renderer);
}
WasmScriptingVM::~WasmScriptingVM()
{
if (m_debugHooks != nullptr)
{
m_debugHooks->onDetach(*this);
m_debugHooks = nullptr;
}
// Flag in-flight decodes cancelled before teardown so a later poll
// cannot call back into this dead VM, mirroring the Luau backend's
// shutdownAsyncForState.
if (m_decodeOwnerId != 0)
{
auto& pool = getGlobalWorkPoolIfExists();
if (pool != nullptr)
{
pool->cancelAllForOwner(m_decodeOwnerId);
}
}
// Coverage measurement: RIVE_WASM_EXEC_STATS=1 dumps how many frames ran
// compiled, interpreted, and as guard-exit continuations.
if (getenv("RIVE_WASM_EXEC_STATS") != nullptr && m_state != nullptr &&
m_state->execEnv != nullptr)
{
uint32_t args[1] = {0};
uint32_t compiled = callModule("host_exec_stats", 1, args);
args[0] = 1;
uint32_t interp = callModule("host_exec_stats", 1, args);
args[0] = 2;
uint32_t guardExits = callModule("host_exec_stats", 1, args);
fprintf(stderr,
"wasm exec stats: compiled=%u interp=%u guardExits=%u\n",
compiled,
interp,
guardExits);
}
// The module never runs guest finalizers at teardown, so host objects
// behind live handles are swept here; instance values in particular must
// detach their delegates before the VM goes away.
for (HandleTable::Slot& slot : m_handles.slots)
{
switch (slot.tag)
{
case HandleTable::Tag::instanceValue:
delete static_cast<HostInstanceValue*>(slot.object);
break;
case HandleTable::Tag::viewModelInstance:
delete static_cast<HostViewModelInstance*>(slot.object);
break;
case HandleTable::Tag::path:
delete static_cast<HostPath*>(slot.object);
break;
case HandleTable::Tag::measure:
delete static_cast<HostMeasure*>(slot.object);
break;
case HandleTable::Tag::paint:
delete static_cast<HostPaint*>(slot.object);
break;
case HandleTable::Tag::shader:
delete static_cast<HostShader*>(slot.object);
break;
case HandleTable::Tag::image:
delete static_cast<HostImage*>(slot.object);
break;
case HandleTable::Tag::font:
delete static_cast<HostFont*>(slot.object);
break;
case HandleTable::Tag::buffer:
delete static_cast<HostBuffer*>(slot.object);
break;
case HandleTable::Tag::dataContext:
delete static_cast<HostDataContext*>(slot.object);
break;
case HandleTable::Tag::artboard:
// Refcounted: animations and nodes may still hold it.
static_cast<HostArtboard*>(slot.object)->unref();
break;
case HandleTable::Tag::animation:
delete static_cast<HostAnimation*>(slot.object);
break;
case HandleTable::Tag::node:
delete static_cast<HostNode*>(slot.object);
break;
#ifdef WITH_RIVE_AUDIO
case HandleTable::Tag::audioSource:
delete static_cast<HostAudioSource*>(slot.object);
break;
case HandleTable::Tag::audioSound:
delete static_cast<HostAudioSound*>(slot.object);
break;
#endif
#ifdef RIVE_CANVAS
case HandleTable::Tag::canvas:
delete static_cast<HostCanvas*>(slot.object);
break;
#endif
#if defined(RIVE_CANVAS) && defined(RIVE_ORE)
case HandleTable::Tag::gpuPass:
delete static_cast<HostGpuPass*>(slot.object);
break;
case HandleTable::Tag::gpuCanvas:
delete static_cast<HostGpuCanvas*>(slot.object);
break;
case HandleTable::Tag::gpuBuffer:
delete static_cast<HostGpuBuffer*>(slot.object);
break;
case HandleTable::Tag::gpuTexture:
delete static_cast<HostGpuTexture*>(slot.object);
break;
case HandleTable::Tag::gpuSampler:
delete static_cast<HostGpuSampler*>(slot.object);
break;
case HandleTable::Tag::gpuTextureView:
delete static_cast<HostGpuTextureView*>(slot.object);
break;
case HandleTable::Tag::gpuShaderModule:
delete static_cast<HostGpuShaderModule*>(slot.object);
break;
case HandleTable::Tag::gpuBindGroupLayout:
delete static_cast<HostGpuBindGroupLayout*>(slot.object);
break;
case HandleTable::Tag::gpuBindGroup:
delete static_cast<HostGpuBindGroup*>(slot.object);
break;
case HandleTable::Tag::gpuPipeline:
delete static_cast<HostGpuPipeline*>(slot.object);
break;
#endif
default:
break;
}
slot.tag = HandleTable::Tag::empty;
slot.object = nullptr;
}
}
bool WasmScriptingVM::notifyDrawVisit(uint32_t drawable)
{
uint32_t args[2] = {m_L, drawable};
return valid() && callModule("host_draw_visit", 2, args) != 0;
}
void WasmScriptingVM::notifyDataValueChanged(uint32_t token)
{
if (!valid())
{
return;
}
uint32_t args[2] = {m_L, token};
callModule("host_data_value_changed", 2, args);
}
// --- context:decodeImage over the module ABI ---------------------------------
#ifdef RIVE_DECODERS
namespace
{
// Mirrors the Luau backend's ImageDecodeTask (lua_image_decode.cpp): same
// Bitmap::decode, same premultiply, same error string, same WorkPool, so the
// wasm lane settles on the same advance as the Luau lane.
class WasmImageDecodeTask : public WorkTask
{
public:
std::vector<uint8_t> m_encodedData;
WasmScriptingVM* m_vm = nullptr;
uint32_t m_token = 0;
std::unique_ptr<Bitmap> m_bitmap;
bool execute() override
{
m_bitmap = Bitmap::decode(m_encodedData.data(), m_encodedData.size());
if (!m_bitmap)
{
m_errorMessage = "failed to decode image data";
return false;
}
if (m_bitmap->pixelFormat() != Bitmap::PixelFormat::RGBAPremul)
{
m_bitmap->pixelFormat(Bitmap::PixelFormat::RGBAPremul);
}
return true;
}
void onComplete() override
{
if (m_vm != nullptr)
{
m_vm->resolveImageDecode(m_token,
m_bitmap->width(),
m_bitmap->height(),
m_bitmap->bytes(),
m_bitmap->width() * m_bitmap->height() *
4);
}
m_bitmap.reset();
m_encodedData.clear();
m_encodedData.shrink_to_fit();
}
void onError(const std::string& error) override
{
if (m_vm != nullptr)
{
m_vm->rejectImageDecode(m_token, error.c_str());
}
}
void onCancel() override { m_vm = nullptr; }
};
} // namespace
#endif // RIVE_DECODERS
bool WasmScriptingVM::startImageDecode(const uint8_t* bytes,
uint32_t byteCount,
uint32_t token)
{
#ifndef RIVE_DECODERS
return false;
#else
if (m_decodeOwnerId == 0)
{
m_decodeOwnerId = WorkPool::nextOwnerId();
}
auto task = make_rcp<WasmImageDecodeTask>();
task->m_encodedData.assign(bytes, bytes + byteCount);
task->m_vm = this;
task->m_token = token;
task->setOwnerId(m_decodeOwnerId);
m_pendingDecodes[token] = task;
getGlobalWorkPool()->submit(std::move(task));
return true;
#endif
}
void WasmScriptingVM::cancelImageDecode(uint32_t token)
{
auto it = m_pendingDecodes.find(token);
if (it != m_pendingDecodes.end())
{
it->second->cancel();
m_pendingDecodes.erase(it);
}
}
void WasmScriptingVM::deliverDecodeResult(const DecodeResult& result)
{
if (result.ok)
{
uint32_t byteCount = (uint32_t)result.pixels.size();
uint32_t sizeArgs[1] = {byteCount};
uint32_t pixelsPtr = callModule("malloc", 1, sizeArgs);
if (pixelsPtr == 0)
{
DecodeResult failure;
failure.token = result.token;
failure.error = "failed to allocate decoded pixels";
deliverDecodeResult(failure);
return;
}
memcpy(resolveModulePtr(pixelsPtr, byteCount),
result.pixels.data(),
byteCount);
uint32_t args[6] = {m_L,
result.token,
result.width,
result.height,
pixelsPtr,
byteCount};
callModule("host_image_decoded", 6, args);
guestFree(pixelsPtr);
return;
}
uint32_t messagePtr = guestString(result.error.c_str());
if (messagePtr == 0)
{
return;
}
uint32_t args[3] = {m_L, result.token, messagePtr};
callModule("host_image_decode_failed", 3, args);
guestFree(messagePtr);
}
void WasmScriptingVM::resolveImageDecode(uint32_t token,
uint32_t width,
uint32_t height,
const uint8_t* pixels,
uint32_t byteCount)
{
m_pendingDecodes.erase(token);
if (!valid())
{
return;
}
DecodeResult result;
result.ok = true;
result.token = token;
result.width = width;
result.height = height;
result.pixels = Span<const uint8_t>(pixels, byteCount);
deliverDecodeResult(result);
}
void WasmScriptingVM::rejectImageDecode(uint32_t token, const char* message)
{
m_pendingDecodes.erase(token);
if (!valid())
{
return;
}
DecodeResult result;
result.token = token;
result.error = message;
deliverDecodeResult(result);
}
void WasmScriptingVM::setBootHook(BootHook hook)
{
s_bootHook = std::move(hook);
}
void WasmScriptingVM::setTimeoutMs(int ms)
{
m_timeoutMs = ms;
if (valid())
{
uint32_t args[2] = {m_L, (uint32_t)ms};
callModule("host_set_timeout", 2, args);
}
}
void WasmScriptingVM::advanceDetachedViewModels()
{
// Only detached roots; instances with parents are already reached
// through the bound tree or their detached-root ancestor.
auto advanceDetached = [](const rcp<ViewModelInstance>& instance) {
if (instance != nullptr && !instance->hasParents())
{
instance->advanced();
}
};
for (auto& slot : m_handles.slots)
{
if (slot.tag == HandleTable::Tag::viewModelInstance)
{
advanceDetached(
static_cast<HostViewModelInstance*>(slot.object)->instance);
}
else if (slot.tag == HandleTable::Tag::artboard)
{
// Artboard inputs keep their bound instance advancing, like the
// Luau context's tracked instances.
advanceDetached(
static_cast<HostArtboard*>(slot.object)->viewModelInstance);
}
}
}
#if WASM_ENABLE_PRELINKED_AOT != 0
// Builds the section list for a module whose AOT text is linked into this
// binary and loads it. The relocation section is dropped: the native linker
// already applied every fixup, which is what makes the text usable without
// runtime executable memory.
static wasm_module_t loadPrelinkedModule(const PrelinkedAotModule& prelinked,
char* error,
uint32_t errorSize)
{
constexpr uint32_t kSectionText = 2;
constexpr uint32_t kSectionRelocation = 5;
// .aot container: 8 byte magic+version header, then type/size framed
// sections. The from-sections loader never sees the header, so an
// artifact from an incompatible wamrc must be rejected here.
if (prelinked.aotSize < 8)
{
snprintf(error, errorSize, "prelinked aot container too small");
return nullptr;
}
uint32_t magic;
uint32_t version;
memcpy(&magic, prelinked.aot, 4);
memcpy(&version, prelinked.aot + 4, 4);
if (magic != AOT_MAGIC_NUMBER || version != AOT_CURRENT_VERSION)
{
snprintf(error,
errorSize,
"prelinked aot container magic/version mismatch "
"(%08x v%u, runtime expects v%u)",
magic,
version,
(uint32_t)AOT_CURRENT_VERSION);
return nullptr;
}
const uint8_t* p = prelinked.aot + 8;
const uint8_t* end = prelinked.aot + prelinked.aotSize;
std::vector<wasm_section_t> sections;
while (true)
{
// The container aligns every u32 read; section headers land on
// 4 byte boundaries relative to the (aligned) container base.
// Compare as integers first: the aligned address may lie past end,
// where pointer arithmetic is undefined.
uintptr_t aligned = ((uintptr_t)p + 3) & ~(uintptr_t)3;
if (aligned > (uintptr_t)end || (uintptr_t)end - aligned < 8)
{
break;
}
p = (const uint8_t*)aligned;
uint32_t type;
uint32_t size;
memcpy(&type, p, 4);
memcpy(&size, p + 4, 4);
p += 8;
if (size > (size_t)(end - p))
{
snprintf(error, errorSize, "prelinked aot container truncated");
return nullptr;
}
if (type != kSectionRelocation)
{
wasm_section_t section = {};
section.section_type = (int)type;
if (type == kSectionText)
{
section.section_body = const_cast<uint8_t*>(prelinked.text);
section.section_body_size = (uint32_t)prelinked.textSize;
}
else
{
section.section_body = const_cast<uint8_t*>(p);
section.section_body_size = size;
}
sections.push_back(section);
}
p += size;
}
if (sections.empty())
{
snprintf(error, errorSize, "prelinked aot container empty");
return nullptr;
}
for (size_t i = 0; i + 1 < sections.size(); i++)
{
sections[i].next = &sections[i + 1];
}
sections.back().next = nullptr;
return wasm_runtime_load_from_sections(sections.data(),
true,
error,
errorSize);
}
#endif
bool WasmScriptingVM::init(Span<const uint8_t> module)
{
if (!ensureRuntime())
{
m_lastError = "wamr runtime init failed";
return false;
}
// wasm_runtime_load keeps referencing the buffer, so hold a copy.
m_moduleBytes.assign(module.begin(), module.end());
// Snapshot before load: the loader mutates m_moduleBytes in place
// (null-terminating names), which corrupts it for wamrc. The AOT lane
// compiles from this pristine copy instead.
std::vector<uint8_t> pristineBytes(module.begin(), module.end());
// One content hash serves the dev AOT artifact lookup and the shared
// module cache key; the key folds in which artifact actually loads so
// interp and AOT lanes never share an entry.
uint64_t moduleKey = 0xcbf29ce484222325ull;
for (uint8_t byte : m_moduleBytes)
{
moduleKey = (moduleKey ^ byte) * 0x100000001b3ull;
}
m_state = std::make_unique<WamrState>();
m_moduleKey = moduleKey;
char error[256] = {0};
// Dev AOT lane: with RIVE_WASM_AOT_CACHE set, swap in a wamrc-compiled
// artifact when one exists for these bytes, else dump the wasm so it can
// be compiled offline. wasm_runtime_load sniffs the AOT magic.
char aotPath[1024] = {0};
bool haveAot = false;
bool haveHwAot = false;
bool haveO0Aot = false;
if (const char* cacheDir = getenv("RIVE_WASM_AOT_CACHE"))
{
#ifdef RIVE_WASM_HW_BOUNDS
// Hw artifacts (wamrc --bounds-checks=0 --stack-bounds-checks=1)
// rely on the guard-page trap handler, so only builds carrying it
// ever probe the .hw.aot name; sw-only builds cannot load them.
snprintf(aotPath,
sizeof(aotPath),
"%s/%016llx.hw.aot",
cacheDir,
(unsigned long long)moduleKey);
struct stat hwAotStat;
haveHwAot = stat(aotPath, &hwAotStat) == 0;
#endif
if (!haveHwAot)
{
snprintf(aotPath,
sizeof(aotPath),
"%s/%016llx.aot",
cacheDir,
(unsigned long long)moduleKey);
struct stat aotStat;
haveAot = stat(aotPath, &aotStat) == 0;
}
if (!haveAot && !haveHwAot)
{
char dumpPath[1024];
snprintf(dumpPath,
sizeof(dumpPath),
"%s/%016llx.wasm",
cacheDir,
(unsigned long long)moduleKey);
struct stat dumpStat;
if (stat(dumpPath, &dumpStat) != 0)
{
if (FILE* dump = fopen(dumpPath, "wb"))
{
fwrite(m_moduleBytes.data(), 1, m_moduleBytes.size(), dump);
fclose(dump);
fprintf(stderr,
"wasm aot: dumped %s for wamrc\n",
dumpPath);
}
}
}
}
// Ladder cache: content already compiled in a prior session or by a
// background schedule boots straight onto the artifact - the
// undo/reopen-instant and permanently-hot-vm_host path.
// Only the documented values opt into sync boot; any other string (a
// typo, an empty value) leaves the module on the async ladder rather
// than silently blocking the load for seconds.
const char* syncEnv = getenv("RIVE_WASM_AOT_SYNC");
bool syncO0 = syncEnv != nullptr && strcmp(syncEnv, "o0") == 0;
bool syncO3 = syncEnv != nullptr && strcmp(syncEnv, "o3") == 0;
// Debug boot ignores any faster artifact a prior run left in a shared
// cache; the point is to run -O0, so only the sync-o0 path below acts.
bool debugBoot = syncO0;
if (!haveAot && !haveHwAot)
{
auto& ladder = ModuleTierLadder::instance();
if (ladder.enabled())
{
#ifdef RIVE_WASM_HW_BOUNDS
std::string hwPath =
debugBoot ? std::string()
: ladder.artifactPath(moduleKey, TierSpecies::hw);
if (!hwPath.empty() && hwPath.size() < sizeof(aotPath))
{
memcpy(aotPath, hwPath.c_str(), hwPath.size() + 1);
haveHwAot = true;
}
#endif
if (!haveHwAot && !debugBoot)
{
std::string path =
ladder.artifactPath(moduleKey, TierSpecies::o3);
if (!path.empty() && path.size() < sizeof(aotPath))
{
memcpy(aotPath, path.c_str(), path.size() + 1);
haveAot = true;
}
}
// Sync AOT boot, opted in by RIVE_WASM_AOT_SYNC=o3|o0: on a
// cache miss, compile on this thread and boot the artifact
// instead of the interpreter. Project-sized modules compile in
// seconds and the interpreter is never representative of real
// performance; hosts with large modules stay on the async
// ladder by not setting this.
if (!haveAot && !haveHwAot && (syncO0 || syncO3))
{
#ifdef RIVE_WASM_HW_BOUNDS
TierSpecies top = TierSpecies::hw;
#else
TierSpecies top = TierSpecies::o3;
#endif
TierSpecies species = syncO0 ? TierSpecies::o0 : top;
auto syncStart = std::chrono::steady_clock::now();
std::string path = ladder.compileSync(
moduleKey,
Span<const uint8_t>(pristineBytes.data(),
pristineBytes.size()),
species);
if (!path.empty() && path.size() < sizeof(aotPath))
{
memcpy(aotPath, path.c_str(), path.size() + 1);
haveHwAot = species == TierSpecies::hw;
haveO0Aot = species == TierSpecies::o0;
haveAot = !haveHwAot && !haveO0Aot;
m_tierPinned = m_tierPinned || syncO0;
auto syncMs =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - syncStart)
.count();
fprintf(stderr,
"wasm aot: sync compiled %s in %lld ms\n",
path.c_str(),
(long long)syncMs);
}
else
{
fprintf(stderr,
"wasm aot: sync compile failed, booting interp\n");
}
}
}
}
#if WASM_ENABLE_PRELINKED_AOT != 0
// Offline-compiled code linked into this binary; the only AOT shape on
// platforms without runtime executable memory. The file lanes above never
// fire there, but an explicit artifact still wins if one appears.
const PrelinkedAotModule* prelinked =
haveAot || haveHwAot || haveO0Aot
? nullptr
: findPrelinkedAotModule(moduleKey, m_moduleBytes.size());
if (prelinked != nullptr)
{
moduleKey ^= 0x94d049bb133111ebull;
}
#endif
if (haveAot || haveHwAot || haveO0Aot)
{
moduleKey ^= 0x9e3779b97f4a7c15ull;
}
if (haveHwAot)
{
// Distinct key: hw and sw artifacts of the same bytes must never
// share a cached module, the flag below lives on the module.
moduleKey ^= 0xc2b2ae3d27d4eb4full;
}
if (haveO0Aot)
{
// Same rule for the -O0 species: its cached module reports aotO0
// so later VMs still schedule the -O3 upgrade.
moduleKey ^= 0x27d4eb2f165667c5ull;
}
auto& cache = sharedModuleCache();
auto adoptCached = [&](SharedWasmModule& entry) {
m_state->module = entry.module;
m_state->ownsModule = false;
m_tier = entry.tier;
// The VM's own copy is redundant against the cache entry, but the
// tier ladder still needs the pristine bytes; entries live for the
// process.
m_scheduleBytes = Span<const uint8_t>(entry.pristineBytes.data(),
entry.pristineBytes.size());
m_moduleBytes.clear();
};
bool adoptedFromCache = false;
auto cached = cache.find(moduleKey);
if (cached != cache.end())
{
adoptCached(cached->second);
adoptedFromCache = true;
}
else
{
if (haveAot || haveHwAot || haveO0Aot)
{
if (FILE* aot = fopen(aotPath, "rb"))
{
fseek(aot, 0, SEEK_END);
long size = ftell(aot);
fseek(aot, 0, SEEK_SET);
m_moduleBytes.resize(size);
size_t read = fread(m_moduleBytes.data(), 1, size, aot);
fclose(aot);
fprintf(stderr,
"wasm aot: loaded %s (%zu bytes)\n",
aotPath,
read);
m_tier =
haveO0Aot ? ExecutionTier::aotO0 : ExecutionTier::aotO3;
}
}
#if WASM_ENABLE_PRELINKED_AOT != 0
if (prelinked != nullptr)
{
m_state->module =
loadPrelinkedModule(*prelinked, error, sizeof(error));
if (m_state->module != nullptr)
{
m_tier = ExecutionTier::aotO3;
fprintf(stderr,
"wasm aot: prelinked module %016llx (%zu byte text)\n",
(unsigned long long)m_moduleKey,
prelinked->textSize);
}
else
{
// A stale or incompatible bake must not fail the file; drop
// to the wasm bytes under the plain key.
fprintf(stderr,
"wasm aot: prelinked module rejected (%s); "
"falling back\n",
error);
prelinked = nullptr;
moduleKey ^= 0x94d049bb133111ebull;
auto fallback = cache.find(moduleKey);
if (fallback != cache.end())
{
adoptCached(fallback->second);
adoptedFromCache = true;
}
}
}
if (m_state->module == nullptr)
#endif
{
if (!haveAot && !haveHwAot && !haveO0Aot)
{
// The load below rewrites the buffer in place; wamrc needs
// the module as it is now.
ModuleTierLadder::instance().stagePristine(
m_moduleKey,
Span<const uint8_t>(m_moduleBytes.data(),
m_moduleBytes.size()));
}
m_state->module = wasm_runtime_load(m_moduleBytes.data(),
(uint32_t)m_moduleBytes.size(),
error,
sizeof(error));
}
if (m_state->module != nullptr && !adoptedFromCache)
{
if (haveHwAot)
{
wasm_runtime_set_module_hw_bounds(m_state->module, true);
}
SharedWasmModule entry;
entry.bytes = std::move(m_moduleBytes);
entry.pristineBytes = std::move(pristineBytes);
entry.module = m_state->module;
entry.tier = m_tier;
auto inserted = cache.emplace(moduleKey, std::move(entry));
m_state->ownsModule = false;
m_scheduleBytes = Span<const uint8_t>(
inserted.first->second.pristineBytes.data(),
inserted.first->second.pristineBytes.size());
}
}
if (m_state->module == nullptr)
{
m_lastError = std::string("module load failed: ") + error;
return false;
}
// Unresolved function imports link fine and trap only when first called,
// with no diagnostic; report them here where the failure is actionable.
int32_t importCount = wasm_runtime_get_import_count(m_state->module);
for (int32_t i = 0; i < importCount; i++)
{
wasm_import_t import;
wasm_runtime_get_import_type(m_state->module, i, &import);
if (import.kind == WASM_IMPORT_EXPORT_KIND_FUNC && !import.linked)
{
m_unresolvedImports.push_back(std::string(import.module_name) +
"." + import.name);
}
}
// Read off the module, not the instance: module start runs every
// script's top level, which already passes strings.
int32_t exportCount = wasm_runtime_get_export_count(m_state->module);
for (int32_t i = 0; i < exportCount && !m_utf16Strings; i++)
{
wasm_export_t moduleExport;
wasm_runtime_get_export_type(m_state->module, i, &moduleExport);
m_utf16Strings = strcmp(moduleExport.name, "__riveUtf16Strings") == 0;
}
s_bootPrint = &m_print;
s_booting = this;
// Module start is a call into the module like any other: the top
// level's probes need the bracket, and its trap is the instantiate
// error.
if (m_debugHooks != nullptr)
{
m_debugHooks->onCallBegin(*this);
}
m_state->instance = wasm_runtime_instantiate(m_state->module,
512 * 1024,
0,
error,
sizeof(error));
if (m_debugHooks != nullptr)
{
m_debugHooks->onCallEnd(*this,
m_state->instance == nullptr ? error : nullptr);
}
s_booting = nullptr;
s_bootPrint = nullptr;
if (m_state->instance == nullptr)
{
m_lastError = std::string("module instantiate failed: ") + error;
return false;
}
if (m_tier != ExecutionTier::interp && !haveHwAot)
{
pregrowAotMemory(m_state->instance);
}
m_state->execEnv =
wasm_runtime_create_exec_env(m_state->instance, 512 * 1024);
if (m_state->execEnv == nullptr)
{
m_lastError = "exec env creation failed";
return false;
}
wasm_runtime_set_user_data(m_state->execEnv, this);
// The leak watch for rasc-linked modules (riveRegister marks one): the
// stub baseline never frees, and sustained growth on a collecting
// module is references accumulating.
const char* leakEnv = getenv("RIVE_WASM_LEAK_WARN");
m_leakWatch = wasm_runtime_lookup_function(m_state->instance,
"riveRegister") != nullptr &&
(leakEnv == nullptr || strcmp(leakEnv, "0") != 0);
m_collectedRuntime =
wasm_runtime_lookup_function(m_state->instance, "__riveCollected") !=
nullptr;
m_handleWatch = wasm_runtime_lookup_function(m_state->instance,
"riveRegister") != nullptr &&
(leakEnv == nullptr || strcmp(leakEnv, "0") != 0);
// Frame collector modules scavenge at every boundary instead of
// rewinding or finalizing.
m_frameMinor = wasm_runtime_lookup_function(m_state->instance,
"__riveFrameMinor") != nullptr;
m_frameMajorsProbe =
wasm_runtime_lookup_function(m_state->instance, "__riveFrameMajors") !=
nullptr;
// Stub modules report their bump position; page counts go blind once
// the aot lanes pregrow to wasmMaxPages.
m_heapUsedProbe = wasm_runtime_lookup_function(m_state->instance,
"__riveHeapUsed") != nullptr;
callModule("__wasm_call_ctors", 0, nullptr);
m_L = callModule("host_newstate", 0, nullptr);
if (m_L == 0)
{
m_lastError = "host_newstate failed";
return false;
}
uint32_t largs[1] = {m_L};
// Same execution budget as the Luau backend's timed pcall.
uint32_t timeoutArgs[2] = {m_L, (uint32_t)m_timeoutMs};
callModule("host_set_timeout", 2, timeoutArgs);
callModule("host_install_require", 1, largs);
callModule("host_install_rive_math", 1, largs);
callModule("host_install_rive_input", 1, largs);
callModule("host_install_rive_renderer", 1, largs);
callModule("host_install_rive_data", 1, largs);
callModule("host_install_rive_gpu", 1, largs);
// Last like the Luau backend's lualibs order, and before host_seal_env
// so the Promise/async/await globals do not clear the safe-env flag.
callModule("host_install_rive_promise", 1, largs);
m_embeddedModules = (int)callModule("host_register_embedded", 1, largs);
callModule("host_seal_env", 1, largs);
return true;
}
void WasmScriptingVM::scheduleTierCompiles(const std::string& laneId)
{
auto& ladder = ModuleTierLadder::instance();
if (!ladder.enabled() || m_tier == ExecutionTier::aotO3 || m_tierPinned)
{
return;
}
Span<const uint8_t> bytes = m_scheduleBytes;
if (bytes.size() == 0)
{
bytes = Span<const uint8_t>(m_moduleBytes.data(), m_moduleBytes.size());
}
if (bytes.size() == 0)
{
return;
}
ladder.schedule(laneId, m_moduleKey, bytes);
}
bool WasmScriptingVM::maybeUpgradeTier()
{
auto& ladder = ModuleTierLadder::instance();
if (!ladder.enabled() || m_tier == ExecutionTier::aotO3 || m_tierPinned)
{
return false;
}
ExecutionTier target = ExecutionTier::aotO3;
bool hwBounds = false;
std::string path;
#ifdef RIVE_WASM_HW_BOUNDS
path = ladder.artifactPath(m_moduleKey, TierSpecies::hw);
hwBounds = !path.empty();
#endif
if (path.empty())
{
path = ladder.artifactPath(m_moduleKey, TierSpecies::o3);
}
if (path.empty() && m_tier < ExecutionTier::aotO0)
{
target = ExecutionTier::aotO0;
path = ladder.artifactPath(m_moduleKey, TierSpecies::o0);
}
if (path.empty())
{
return false;
}
FILE* f = fopen(path.c_str(), "rb");
if (f == nullptr)
{
return false;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
std::vector<uint8_t> artifact(size);
size_t read = fread(artifact.data(), 1, size, f);
fclose(f);
if (read != (size_t)size)
{
return false;
}
std::string error;
if (!applyTierArtifact(
Span<const uint8_t>(artifact.data(), artifact.size()),
target,
error,
hwBounds))
{
fprintf(stderr, "wasm tier swap failed: %s\n", error.c_str());
return false;
}
return true;
}
const char* WasmScriptingVM::frameBoundary()
{
if (!valid())
{
return nullptr;
}
if (m_frameMinor)
{
uint32_t promoted = callModule("__riveFrameMinor", 0, nullptr);
if (getenv("RIVE_FRAME_GC_DEBUG") != nullptr)
{
fprintf(stderr, "frameMinor promoted=%u\n", promoted);
callModule("__riveFrameVerify", 0, nullptr);
}
if (!m_frameMinorAnnounced)
{
m_frameMinorAnnounced = true;
return "script frame gc: scavenging per frame";
}
if (const char* warning = handleLeakWarning())
{
return warning;
}
return heapGrowthWarning();
}
if (const char* warning = handleLeakWarning())
{
return warning;
}
return heapGrowthWarning();
}
const char* WasmScriptingVM::heapGrowthWarning()
{
if (!m_leakWatch || !m_advancedOnce)
{
return nullptr;
}
wasm_memory_inst_t memory =
wasm_runtime_get_default_memory(m_state->instance);
if (memory == nullptr)
{
return nullptr;
}
// The stub bump position beats page counts when available: pregrown
// aot memory never grows.
uint32_t pages =
m_heapUsedProbe
? std::max(1u, callModule("__riveHeapUsed", 0, nullptr) >> 16)
: (uint32_t)wasm_memory_get_cur_page_count(memory);
if (m_leakBaselinePages == 0)
{
m_leakBaselinePages = pages;
m_leakFirstBaselinePages = pages;
return nullptr;
}
m_leakFrames++;
m_leakTotalFrames++;
// 8MB past baseline over at least two seconds of frames: far beyond any
// one-time warmup we have measured, reached in seconds by a per-frame
// leak (box2d hand-optimized leaked ~2.1MB/s). The watch re-arms after
// each warning, so a leaking session keeps hearing about it every 8MB
// instead of dying hours after a single line scrolled away.
constexpr uint32_t kLeakWarnPages = 128;
constexpr uint32_t kLeakWarnMinFrames = 120;
if (m_leakFrames < kLeakWarnMinFrames ||
pages < m_leakBaselinePages + kLeakWarnPages)
{
return nullptr;
}
if (m_collectedRuntime && !m_leakArmedCollected)
{
// Linear memory never shrinks, so a collected runtime's one-time
// spike would read as growth forever. Demand a second growing
// window before the first warning.
m_leakArmedCollected = true;
m_leakBaselinePages = pages;
m_leakFrames = 0;
return nullptr;
}
uint32_t grownMB = (pages - m_leakBaselinePages) / 16;
uint32_t frames = m_leakFrames;
m_leakBaselinePages = pages;
m_leakFrames = 0;
m_leakWarningCount++;
// Time to the wasmMaxPages trap from the average growth rate at 60fps.
// Growth arrives in page-doubling steps, so this is an estimate.
char projection[96] = {0};
uint32_t maxPages = (uint32_t)wasm_memory_get_max_page_count(memory);
double pagesPerFrame =
(double)(pages - m_leakFirstBaselinePages) / (double)m_leakTotalFrames;
if (maxPages > pages && pagesPerFrame > 0.0)
{
double minutes =
(double)(maxPages - pages) / pagesPerFrame / (60.0 * 60.0);
snprintf(projection,
sizeof(projection),
"; at this rate every frame traps in roughly %.0f minutes",
minutes < 1.0 ? 1.0 : minutes);
}
char buffer[384];
if (m_collectedRuntime)
{
snprintf(buffer,
sizeof(buffer),
"script heap grew %uMB over %u frames%s; the collector is "
"running, so something is accumulating references (a "
"growing array, map, or cache). RIVE_WASM_LEAK_WARN=0 "
"silences this.",
grownMB,
frames,
projection);
}
else
{
snprintf(buffer,
sizeof(buffer),
"script heap grew %uMB over %u frames%s; the stub runtime "
"never frees, so per-frame allocations leak. Set "
"wasmRuntime: frame. RIVE_WASM_LEAK_WARN=0 silences this.",
grownMB,
frames,
projection);
}
m_leakWarning = buffer;
return m_leakWarning.c_str();
}
static const char* handleTagName(WasmScriptingVM::HandleTable::Tag tag)
{
using Tag = WasmScriptingVM::HandleTable::Tag;
switch (tag)
{
case Tag::path:
return "path";
case Tag::measure:
return "measure";
case Tag::paint:
return "paint";
case Tag::renderer:
return "renderer";
case Tag::shader:
return "shader";
case Tag::object:
return "object";
case Tag::viewModelInstance:
return "viewModelInstance";
case Tag::instanceValue:
return "instanceValue";
case Tag::image:
return "image";
case Tag::font:
return "font";
case Tag::buffer:
return "buffer";
case Tag::canvas:
return "canvas";
case Tag::gpuCanvas:
return "gpuCanvas";
case Tag::gpuPass:
return "gpuPass";
case Tag::gpuBuffer:
return "gpuBuffer";
case Tag::gpuTexture:
return "gpuTexture";
case Tag::gpuSampler:
return "gpuSampler";
case Tag::gpuTextureView:
return "gpuTextureView";
case Tag::gpuShaderModule:
return "gpuShaderModule";
case Tag::gpuBindGroupLayout:
return "gpuBindGroupLayout";
case Tag::gpuBindGroup:
return "gpuBindGroup";
case Tag::gpuPipeline:
return "gpuPipeline";
case Tag::dataContext:
return "dataContext";
case Tag::artboard:
return "artboard";
case Tag::animation:
return "animation";
case Tag::node:
return "node";
case Tag::audioSource:
return "audioSource";
case Tag::audioSound:
return "audioSound";
case Tag::drawable:
return "drawable";
case Tag::empty:
case Tag::count:
break;
}
return "unknown";
}
// Growing linear memory reallocs it, which in-flight AOT frames do not
// tolerate (fields root cause #3): on an artifact, take the module's whole
// declared ceiling up front, while no frames are live, so it never grows
// again. Modules without a declared ceiling keep growth-on-demand.
static void pregrowAotMemory(wasm_module_inst_t instance)
{
wasm_memory_inst_t memory = wasm_runtime_get_default_memory(instance);
if (memory == nullptr)
{
return;
}
uint32_t pages = (uint32_t)wasm_memory_get_cur_page_count(memory);
uint32_t maxPages = (uint32_t)wasm_memory_get_max_page_count(memory);
constexpr uint32_t kUnboundedPages = 65536;
if (maxPages >= kUnboundedPages)
{
fprintf(stderr,
"wasm aot: module declares no wasmMaxPages; memory cannot be "
"reserved up front, so growth during frames may trap\n");
return;
}
if (maxPages <= pages)
{
return;
}
if (!wasm_runtime_enlarge_memory(instance, maxPages - pages))
{
fprintf(stderr,
"wasm aot: pregrow to %u pages failed; growth during frames "
"may trap\n",
maxPages);
}
}
uint32_t WasmScriptingVM::memoryPages() const
{
if (m_state == nullptr || m_state->instance == nullptr)
{
return 0;
}
wasm_memory_inst_t memory =
wasm_runtime_get_default_memory(m_state->instance);
return memory == nullptr ? 0
: (uint32_t)wasm_memory_get_cur_page_count(memory);
}
uint32_t WasmScriptingVM::frameMajors()
{
return m_frameMajorsProbe ? callModule("__riveFrameMajors", 0, nullptr) : 0;
}
const char* WasmScriptingVM::handleLeakWarning()
{
if (!m_handleWatch || !m_advancedOnce)
{
return nullptr;
}
uint32_t live =
(uint32_t)(m_handles.slots.size() - m_handles.freeSlots.size());
if (m_handleBaselineLive == 0)
{
// Bias by one so a zero-handle module still records its baseline.
m_handleBaselineLive = live + 1;
return nullptr;
}
m_handleFrames++;
// Hundreds of handles past warmup over two seconds of frames is a
// per-frame mint with no release, not a working set.
constexpr uint32_t kHandleWarnCount = 512;
constexpr uint32_t kHandleWarnMinFrames = 120;
if (live + 1 < m_handleBaselineLive + kHandleWarnCount)
{
m_handleCollectPending = false;
return nullptr;
}
if (m_handleFrames < kHandleWarnMinFrames)
{
return nullptr;
}
// Wrappers that outlived a boundary die in the old region, which only
// a major sweeps, and its size trigger cannot see the host side of a
// handle. Ask for one and only warn if the growth survives it. A bake
// without the majors counter cannot say when that is, so it stays quiet.
if (m_collectedRuntime && !m_frameMajorsProbe)
{
return nullptr;
}
if (m_collectedRuntime && !m_handleCollectPending)
{
m_handleCollectPending = true;
m_handleCollectMajors = frameMajors();
callModule("__riveCollect", 0, nullptr);
return nullptr;
}
// A sliced major spans boundaries, so wait for the count to move.
if (m_handleCollectPending && frameMajors() == m_handleCollectMajors)
{
return nullptr;
}
m_handleCollectPending = false;
// Re-arm so a leaking session keeps warning every 512 handles.
uint32_t grown = live + 1 - m_handleBaselineLive;
uint32_t frames = m_handleFrames;
m_handleBaselineLive = live + 1;
m_handleFrames = 0;
uint32_t counts[(size_t)HandleTable::Tag::count] = {0};
for (const HandleTable::Slot& slot : m_handles.slots)
{
if (slot.tag != HandleTable::Tag::empty)
{
counts[(size_t)slot.tag]++;
}
}
size_t top = 0;
for (size_t i = 1; i < sizeof(counts) / sizeof(counts[0]); i++)
{
if (counts[i] > counts[top])
{
top = i;
}
}
char buffer[256];
snprintf(buffer,
sizeof(buffer),
"script leaked %u host handles over %u frames (most: %u %s); "
"resources created per frame need release() or finish(). "
"RIVE_WASM_LEAK_WARN=0 silences this.",
grown,
frames,
counts[top],
handleTagName((HandleTable::Tag)top));
m_leakWarning = buffer;
return m_leakWarning.c_str();
}
bool WasmScriptingVM::applyTierArtifact(Span<const uint8_t> artifactBytes,
ExecutionTier tier,
std::string& error,
bool hwBounds)
{
auto next = std::make_unique<WamrState>();
next->artifactBytes.assign(artifactBytes.begin(), artifactBytes.end());
char loadError[256] = {0};
next->module = wasm_runtime_load(next->artifactBytes.data(),
(uint32_t)next->artifactBytes.size(),
loadError,
sizeof(loadError));
if (next->module == nullptr)
{
error = std::string("artifact load failed: ") + loadError;
return false;
}
if (hwBounds)
{
wasm_runtime_set_module_hw_bounds(next->module, true);
}
next->instance = wasm_runtime_instantiate(next->module,
512 * 1024,
0,
loadError,
sizeof(loadError));
if (next->instance == nullptr)
{
error = std::string("artifact instantiate failed: ") + loadError;
return false;
}
// No ctors on the target: every byte of initialized state arrives from
// the live instance.
if (!wamrTransplantState(m_state->instance, next->instance, error))
{
return false;
}
next->execEnv = wasm_runtime_create_exec_env(next->instance, 512 * 1024);
if (next->execEnv == nullptr)
{
error = "artifact exec env creation failed";
return false;
}
wasm_runtime_set_user_data(next->execEnv, this);
if (!hwBounds)
{
// Guard-page memory never moves on growth; only the sw lane needs
// the ceiling reserved up front.
pregrowAotMemory(next->instance);
}
m_state = std::move(next);
m_tier = tier;
return true;
}
bool WasmScriptingVM::registerBytecode(const std::string& name,
Span<const uint8_t> bytecode)
{
uint32_t nameSizeArgs[1] = {(uint32_t)name.size() + 1};
uint32_t namePtr = callModule("malloc", 1, nameSizeArgs);
uint32_t bcSizeArgs[1] = {(uint32_t)bytecode.size()};
uint32_t bcPtr = callModule("malloc", 1, bcSizeArgs);
if (namePtr == 0 || bcPtr == 0)
{
m_lastError = "bytecode allocation failed";
return false;
}
memcpy(resolveModulePtr(namePtr, (uint32_t)name.size() + 1),
name.c_str(),
name.size() + 1);
memcpy(resolveModulePtr(bcPtr, (uint32_t)bytecode.size()),
bytecode.data(),
bytecode.size());
uint32_t args[4] = {m_L, namePtr, bcPtr, (uint32_t)bytecode.size()};
callModule("host_register_module", 4, args);
uint32_t freeName[1] = {namePtr};
callModule("free", 1, freeName);
// The registry keeps its own copy via lua_pushlstring; the staging
// buffer frees.
uint32_t freeBc[1] = {bcPtr};
callModule("free", 1, freeBc);
return true;
}
#ifdef WITH_RIVE_TOOLS
void WasmScriptingVM::registerShaderRstb(std::string name,
std::vector<uint8_t> bytes)
{
m_shaderRstbs[std::move(name)] = std::move(bytes);
}
const std::vector<uint8_t>* WasmScriptingVM::findShaderRstb(
const std::string& name) const
{
auto it = m_shaderRstbs.find(name);
if (it != m_shaderRstbs.end())
{
return &it->second;
}
// Registered names are library-mangled paths; scripts ask by short name.
for (const auto& entry : m_shaderRstbs)
{
size_t slash = entry.first.rfind('/');
if (slash != std::string::npos && entry.first.substr(slash + 1) == name)
{
return &entry.second;
}
}
return nullptr;
}
#endif
bool WasmScriptingVM::requireModule(const std::string& name, int* outResultRef)
{
uint32_t sizeArgs[1] = {(uint32_t)name.size() + 1};
uint32_t namePtr = callModule("malloc", 1, sizeArgs);
if (namePtr == 0)
{
m_lastError = "module name allocation failed";
return false;
}
memcpy(resolveModulePtr(namePtr, (uint32_t)name.size() + 1),
name.c_str(),
name.size() + 1);
uint32_t requireArgs[2] = {m_L, namePtr};
uint32_t status = 0;
CallOutcome outcome =
callModuleChecked("host_require", 2, requireArgs, &status);
uint32_t freeArgs[1] = {namePtr};
callModule("free", 1, freeArgs);
if (outcome != CallOutcome::ok)
{
// Folding a trap into "status 0" once read as a successful require
// with no generator; fail the require instead.
m_lastError = outcome == CallOutcome::trapped
? "module require trapped"
: "module has no host_require export";
uint32_t topArgs[2] = {m_L, 0};
callModule("host_settop", 2, topArgs);
return false;
}
if (status != 0)
{
uint32_t strArgs[2] = {m_L, (uint32_t)-1};
uint32_t messagePtr = callModule("host_tostring", 2, strArgs);
const char* message = messagePtr != 0
? (const char*)resolveModulePtr(messagePtr, 1)
: nullptr;
m_lastError = message != nullptr ? message : "module execution failed";
}
else if (outResultRef != nullptr)
{
uint32_t refArgs[1] = {m_L};
*outResultRef = (int)callModule("host_ref", 1, refArgs);
}
uint32_t topArgs[2] = {m_L, 0};
callModule("host_settop", 2, topArgs);
return status == 0;
}
// --- ScriptBackend over the module's host_obj_* exports ---------------------
bool WasmScriptingVM::valid() const
{
return m_state != nullptr && m_state->execEnv != nullptr && m_L != 0;
}
void WasmScriptingVM::releaseRef(int ref)
{
if (!valid() || ref == 0)
{
return;
}
auto it = m_contextObjects.find(ref);
if (it != m_contextObjects.end())
{
m_handles.release(it->second, HandleTable::Tag::object);
m_contextObjects.erase(it);
}
m_inputSlots.erase(ref);
uint32_t args[2] = {m_L, (uint32_t)ref};
// Listeners owned by this script instance stop immediately, mirroring the
// Luau backend's tracked property dispose.
callModule("host_obj_dispose", 2, args);
callModule("host_unref", 2, args);
}
int WasmScriptingVM::instantiate(int generatorRef,
ScriptedObject* object,
int* outContextRef,
ScriptedContext** outContextPtr)
{
uint32_t objectHandle = m_handles.mint(HandleTable::Tag::object, object);
uint32_t args[3] = {m_L, (uint32_t)generatorRef, objectHandle};
int selfRef = (int)callModule("host_obj_instantiate", 3, args);
if (selfRef == 0)
{
m_handles.release(objectHandle, HandleTable::Tag::object);
return 0;
}
uint32_t contextArgs[1] = {m_L};
*outContextRef = (int)callModule("host_obj_context", 1, contextArgs);
if (*outContextRef != 0)
{
m_contextObjects[*outContextRef] = objectHandle;
}
else
{
m_handles.release(objectHandle, HandleTable::Tag::object);
}
// The wasm context lives entirely in the module until the binding layer
// moves in; there is no host-side ScriptedContext.
*outContextPtr = nullptr;
return selfRef;
}
ScriptBackend::InitResult WasmScriptingVM::callUserInit(ScriptedObject* object,
int selfRef,
int contextRef)
{
// callRet folds a trap into 0, which here would read as notImplemented
// and mark a crashed init as done; call directly so failure is failure.
wasm_function_inst_t f =
wasm_runtime_lookup_function(m_state->instance, "host_obj_user_init");
if (f == nullptr)
{
return InitResult::notImplemented;
}
uint32_t buf[3] = {m_L, (uint32_t)selfRef, (uint32_t)contextRef};
uint32_t status = 2;
ScriptCallScope callScope(this);
if (!wasm_runtime_call_wasm(m_state->execEnv, f, 3, buf))
{
const char* exception =
fullTrapMessage(wasm_runtime_get_exception(m_state->instance));
m_lastError = exception != nullptr ? exception : "script init trapped";
fprintf(stderr, "script init trapped: %s\n", m_lastError.c_str());
#if WASM_ENABLE_DUMP_CALL_STACK
wasm_runtime_dump_call_stack(m_state->execEnv);
#endif
wasm_runtime_clear_exception(m_state->instance);
}
else
{
status = buf[0];
}
switch (status)
{
case 0:
return InitResult::notImplemented;
case 1:
return InitResult::succeeded;
default:
return InitResult::failed;
}
}
bool WasmScriptingVM::callAdvance(ScriptedObject* object,
int selfRef,
float elapsedSeconds)
{
wasm_function_inst_t f =
wasm_runtime_lookup_function(m_state->instance, "host_obj_advance");
if (f == nullptr)
{
return false;
}
wasm_val_t args[3];
args[0].kind = WASM_I32;
args[0].of.i32 = (int32_t)m_L;
args[1].kind = WASM_I32;
args[1].of.i32 = selfRef;
args[2].kind = WASM_F64;
args[2].of.f64 = elapsedSeconds;
wasm_val_t results[1];
results[0].kind = WASM_I32;
ScriptCallScope callScope(this);
if (!wasm_runtime_call_wasm_a(m_state->execEnv, f, 1, results, 3, args))
{
return false;
}
if (!m_advancedOnce)
{
m_advancedOnce = true;
}
return results[0].of.i32 != 0;
}
void WasmScriptingVM::callUpdate(ScriptedObject* object, int selfRef)
{
// Marks issued during update must not re-arm it, like the Luau backend.
object->setInUpdatePhase(true);
uint32_t args[2] = {m_L, (uint32_t)selfRef};
callModule("host_obj_update", 2, args);
object->setInUpdatePhase(false);
}
void WasmScriptingVM::callTrigger(ScriptedObject* object,
int selfRef,
const char* name)
{
uint32_t arg = 0;
uint32_t owned = 0;
if (!inputArg(selfRef, name, arg, owned))
{
return;
}
uint32_t args[3] = {m_L, (uint32_t)selfRef, arg};
callModule("host_obj_trigger", 3, args);
guestFree(owned);
}
bool WasmScriptingVM::callNumberMethod(ScriptedObject* object,
int selfRef,
const char* name,
const float* args,
size_t argCount,
float* outResult)
{
uint32_t namePtr = guestString(name);
if (namePtr == 0)
{
return false;
}
uint32_t scratchSize = (uint32_t)(argCount * sizeof(float)) + sizeof(float);
uint32_t sizeArgs[1] = {scratchSize};
uint32_t scratch = callModule("malloc", 1, sizeArgs);
if (scratch == 0)
{
guestFree(namePtr);
return false;
}
memcpy(resolveModulePtr(scratch, scratchSize),
args,
argCount * sizeof(float));
uint32_t outPtr = scratch + (uint32_t)(argCount * sizeof(float));
uint32_t callArgs[6] =
{m_L, (uint32_t)selfRef, namePtr, scratch, (uint32_t)argCount, outPtr};
uint32_t ok = callModule("host_obj_number_method", 6, callArgs);
if (ok != 0)
{
memcpy(outResult,
resolveModulePtr(outPtr, sizeof(float)),
sizeof(float));
}
guestFree(scratch);
guestFree(namePtr);
return ok != 0;
}
bool WasmScriptingVM::callBooleanMethod(ScriptedObject* object,
int selfRef,
const char* name)
{
uint32_t namePtr = guestString(name);
if (namePtr == 0)
{
return false;
}
uint32_t args[3] = {m_L, (uint32_t)selfRef, namePtr};
uint32_t result = callModule("host_obj_boolean_method", 3, args);
guestFree(namePtr);
return result != 0;
}
bool WasmScriptingVM::callPathEffectUpdate(ScriptedObject* object,
int selfRef,
const RawPath& sourcePath,
const ShapePaint* shapePaint,
RawPath* outPath)
{
if (!valid() || shapePaint == nullptr || outPath == nullptr)
{
return false;
}
// The exact subset the Luau lane's ScriptedPaintData(ShapePaint*)
// snapshot captures; gradients stay host side there too.
PathEffectPaintWire paint;
paint.style = (uint32_t)RenderPaintStyle::fill;
paint.blendMode = (uint32_t)BlendMode::srcOver;
paint.thickness = 1;
paint.color = 0xFF000000;
if (shapePaint->is<Stroke>())
{
auto stroke = shapePaint->as<Stroke>();
paint.style = (uint32_t)RenderPaintStyle::stroke;
paint.thickness = stroke->thickness();
paint.cap = stroke->cap();
paint.join = stroke->join();
}
for (auto& child : shapePaint->children())
{
if (child->is<SolidColor>())
{
paint.color = child->as<SolidColor>()->colorValue();
break;
}
}
if (shapePaint->feather() != nullptr)
{
paint.feather = shapePaint->feather()->strength();
}
paint.blendMode = (uint32_t)shapePaint->blendModeValue();
uint32_t verbCount = (uint32_t)sourcePath.verbs().size();
uint32_t floatCount = (uint32_t)sourcePath.points().size() * 2;
uint32_t pointBytes = floatCount * (uint32_t)sizeof(float);
// One staging block, float-aligned parts first: [paint][points][verbs].
uint32_t byteCount = (uint32_t)sizeof(paint) + pointBytes + verbCount;
uint32_t sizeArgs[1] = {byteCount};
uint32_t dataPtr = callModule("malloc", 1, sizeArgs);
if (dataPtr == 0)
{
return false;
}
uint8_t* out = (uint8_t*)resolveModulePtr(dataPtr, byteCount);
memcpy(out, &paint, sizeof(paint));
memcpy(out + sizeof(paint), sourcePath.points().data(), pointBytes);
memcpy(out + sizeof(paint) + pointBytes,
sourcePath.verbs().data(),
verbCount);
m_pathEffectOut = outPath;
uint32_t args[7] = {m_L,
(uint32_t)selfRef,
dataPtr + (uint32_t)sizeof(paint) + pointBytes,
verbCount,
dataPtr + (uint32_t)sizeof(paint),
floatCount,
dataPtr};
uint32_t ok = callModule("host_obj_path_effect", 7, args);
m_pathEffectOut = nullptr;
guestFree(dataPtr);
return ok != 0;
}
bool WasmScriptingVM::callDataConvert(ScriptedObject* object,
int selfRef,
const char* method,
DataValue* input,
ScriptDataResult* outResult)
{
if (!valid() || input == nullptr)
{
return false;
}
DataConvertWire wire;
const std::string* text = nullptr;
if (input->is<DataValueNumber>())
{
wire.kind = DataConvertWire::kindNumber;
wire.number = input->as<DataValueNumber>()->value();
}
else if (input->is<DataValueString>())
{
wire.kind = DataConvertWire::kindString;
text = &input->as<DataValueString>()->value();
wire.stringLength = (uint32_t)text->size();
}
else if (input->is<DataValueBoolean>())
{
wire.kind = DataConvertWire::kindBoolean;
wire.boolean = input->as<DataValueBoolean>()->value() ? 1 : 0;
}
else if (input->is<DataValueColor>())
{
wire.kind = DataConvertWire::kindColor;
wire.color = input->as<DataValueColor>()->value();
}
else
{
// Unsupported input kinds report handled with no result, so the
// converter yields an empty DataValue like the Luau backend.
return true;
}
uint32_t methodPtr = guestString(method);
if (methodPtr == 0)
{
return false;
}
uint32_t byteCount = (uint32_t)sizeof(wire) + wire.stringLength;
uint32_t sizeArgs[1] = {byteCount};
uint32_t wirePtr = callModule("malloc", 1, sizeArgs);
if (wirePtr == 0)
{
guestFree(methodPtr);
return false;
}
uint8_t* out = (uint8_t*)resolveModulePtr(wirePtr, byteCount);
memcpy(out, &wire, sizeof(wire));
if (text != nullptr)
{
memcpy(out + sizeof(wire), text->data(), wire.stringLength);
}
m_convertResultOut = outResult;
uint32_t args[4] = {m_L, (uint32_t)selfRef, methodPtr, wirePtr};
uint32_t ran = callModule("host_obj_data_convert", 4, args);
m_convertResultOut = nullptr;
guestFree(wirePtr);
guestFree(methodPtr);
return ran != 0;
}
bool WasmScriptingVM::callPointerEvent(ScriptedObject* object,
int selfRef,
const char* method,
int pointerId,
Vec2D localPosition,
HitResult* outResult)
{
if (!valid())
{
return false;
}
wasm_function_inst_t f =
wasm_runtime_lookup_function(m_state->instance,
"host_obj_pointer_event");
if (f == nullptr)
{
return false;
}
uint32_t methodPtr = guestString(method);
if (methodPtr == 0)
{
return false;
}
wasm_val_t args[6];
args[0].kind = WASM_I32;
args[0].of.i32 = (int32_t)m_L;
args[1].kind = WASM_I32;
args[1].of.i32 = selfRef;
args[2].kind = WASM_I32;
args[2].of.i32 = (int32_t)methodPtr;
args[3].kind = WASM_I32;
args[3].of.i32 = pointerId;
args[4].kind = WASM_F64;
args[4].of.f64 = localPosition.x;
args[5].kind = WASM_F64;
args[5].of.f64 = localPosition.y;
wasm_val_t results[1];
results[0].kind = WASM_I32;
ScriptCallScope callScope(this);
bool ok =
wasm_runtime_call_wasm_a(m_state->execEnv, f, 1, results, 6, args);
guestFree(methodPtr);
if (!ok || results[0].of.i32 == 0)
{
return false;
}
*outResult = (HitResult)(results[0].of.i32 - 1);
return true;
}
bool WasmScriptingVM::callKeyboardEvent(ScriptedObject* object,
int selfRef,
Key key,
KeyModifiers modifiers,
bool isPressed,
bool isRepeat)
{
if (!valid())
{
return false;
}
uint32_t args[6] = {m_L,
(uint32_t)selfRef,
(uint32_t)key,
(uint32_t)modifiers,
isPressed ? 1u : 0u,
isRepeat ? 1u : 0u};
return callModule("host_obj_keyboard_event", 6, args) != 0;
}
bool WasmScriptingVM::callTextEvent(ScriptedObject* object,
int selfRef,
const std::string& text)
{
if (!valid())
{
return false;
}
// Length crosses explicitly, so embedded nulls survive.
uint32_t sizeArgs[1] = {(uint32_t)text.size() + 1};
uint32_t textPtr = callModule("malloc", 1, sizeArgs);
if (textPtr == 0)
{
return false;
}
memcpy(resolveModulePtr(textPtr, (uint32_t)text.size() + 1),
text.data(),
text.size());
uint32_t args[4] = {m_L, (uint32_t)selfRef, textPtr, (uint32_t)text.size()};
uint32_t result = callModule("host_obj_text_event", 4, args);
guestFree(textPtr);
return result != 0;
}
// Flattens the gamepad alternatives into the wire layout the module rebuilds
// snapshots from; false for non-gamepad kinds. Shared by callGamepadEvent and
// callListenerPerform.
static bool packGamepadWire(const ListenerInvocation& invocation,
GamepadWire& wire,
const GamepadSnapshot*& snapshot)
{
if (const GamepadConnectedInvocation* c = invocation.asGamepadConnected())
{
wire.kind = GamepadWire::kindConnected;
snapshot = &c->snapshot;
}
else if (const GamepadEventInvocation* e = invocation.asGamepadEvent())
{
wire.kind = GamepadWire::kindEvent;
snapshot = &e->fullState;
wire.changeKind = (uint32_t)e->change.kind;
wire.changeIndex = e->change.index;
wire.changeValue = e->change.value;
wire.hasStandardButtonIntent = e->hasStandardButtonIntent ? 1 : 0;
wire.standardButton = (uint32_t)e->standardButton;
wire.hasStandardAxisIntent = e->hasStandardAxisIntent ? 1 : 0;
wire.standardAxis = (uint32_t)e->standardAxis;
}
else if (const GamepadDisconnectedInvocation* d =
invocation.asGamepadDisconnected())
{
wire.kind = GamepadWire::kindDisconnected;
wire.deviceId = d->deviceId;
}
else
{
return false;
}
if (snapshot != nullptr)
{
wire.deviceId = snapshot->deviceId;
wire.mapping = (uint32_t)snapshot->mapping;
wire.buttonMaskLo = (uint32_t)snapshot->buttonMask;
wire.buttonMaskHi = (uint32_t)(snapshot->buttonMask >> 32);
wire.buttonCount = (uint32_t)snapshot->buttonValues.size();
wire.axisCount = (uint32_t)snapshot->axes.size();
}
return true;
}
static void writeGamepadPayload(uint8_t* out,
const GamepadWire& wire,
const GamepadSnapshot* snapshot)
{
memcpy(out, &wire, sizeof(wire));
if (snapshot == nullptr)
{
return;
}
float* values = (float*)(out + sizeof(wire));
if (wire.buttonCount != 0)
{
memcpy(values,
snapshot->buttonValues.data(),
wire.buttonCount * sizeof(float));
}
if (wire.axisCount != 0)
{
memcpy(values + wire.buttonCount,
snapshot->axes.data(),
wire.axisCount * sizeof(float));
}
}
bool WasmScriptingVM::callGamepadEvent(ScriptedObject* object,
int selfRef,
const char* method,
const ListenerInvocation& invocation)
{
if (!valid())
{
return false;
}
GamepadWire wire;
const GamepadSnapshot* snapshot = nullptr;
if (!packGamepadWire(invocation, wire, snapshot))
{
return false;
}
uint32_t methodPtr = guestString(method);
if (methodPtr == 0)
{
return false;
}
uint32_t byteCount =
(uint32_t)(sizeof(wire) +
(wire.buttonCount + wire.axisCount) * sizeof(float));
uint32_t sizeArgs[1] = {byteCount};
uint32_t dataPtr = callModule("malloc", 1, sizeArgs);
if (dataPtr == 0)
{
guestFree(methodPtr);
return false;
}
writeGamepadPayload((uint8_t*)resolveModulePtr(dataPtr, byteCount),
wire,
snapshot);
uint32_t args[5] = {m_L, (uint32_t)selfRef, methodPtr, dataPtr, byteCount};
uint32_t result = callModule("host_obj_gamepad_event", 5, args);
guestFree(dataPtr);
guestFree(methodPtr);
return result != 0;
}
void WasmScriptingVM::callListenerPerform(ScriptedObject* object,
int selfRef,
const ListenerInvocation& invocation)
{
if (!valid())
{
return;
}
ListenerWire wire;
wire.kind = (uint32_t)invocation.kind();
const std::string* text = nullptr;
GamepadWire gamepad;
const GamepadSnapshot* snapshot = nullptr;
bool hasGamepad = false;
if (const PointerInvocation* p = invocation.asPointer())
{
wire.posX = p->position.x;
wire.posY = p->position.y;
wire.prevX = p->previousPosition.x;
wire.prevY = p->previousPosition.y;
wire.pointerId = p->pointerId;
wire.hitEvent = (uint32_t)p->hitEvent;
wire.timeStamp = p->timeStamp;
}
else if (const KeyboardInvocation* k = invocation.asKeyboard())
{
wire.key = (uint32_t)k->key;
wire.modifiers = (uint32_t)k->modifiers;
wire.isPressed = k->isPressed ? 1 : 0;
wire.isRepeat = k->isRepeat ? 1 : 0;
}
else if (const TextInputInvocation* t = invocation.asTextInput())
{
text = &t->text;
wire.textLength = (uint32_t)t->text.size();
}
else if (const FocusInvocation* f = invocation.asFocus())
{
wire.isFocus = f->isFocus ? 1 : 0;
}
else if (const ReportedEventInvocation* e = invocation.asReportedEvent())
{
wire.delaySeconds = e->delaySeconds;
}
else if (const SemanticInvocation* s = invocation.asSemantic())
{
wire.semanticAction = (uint32_t)s->actionType;
}
else
{
// viewModelChange and none carry only their kind; the gamepad kinds
// append their own payload.
hasGamepad = packGamepadWire(invocation, gamepad, snapshot);
}
uint32_t tailBytes =
text != nullptr
? wire.textLength
: (hasGamepad ? (uint32_t)(sizeof(gamepad) + (gamepad.buttonCount +
gamepad.axisCount) *
sizeof(float))
: 0);
uint32_t byteCount = (uint32_t)sizeof(wire) + tailBytes;
uint32_t sizeArgs[1] = {byteCount};
uint32_t dataPtr = callModule("malloc", 1, sizeArgs);
if (dataPtr == 0)
{
return;
}
uint8_t* out = (uint8_t*)resolveModulePtr(dataPtr, byteCount);
memcpy(out, &wire, sizeof(wire));
if (text != nullptr)
{
memcpy(out + sizeof(wire), text->data(), wire.textLength);
}
else if (hasGamepad)
{
writeGamepadPayload(out + sizeof(wire), gamepad, snapshot);
}
uint32_t args[4] = {m_L, (uint32_t)selfRef, dataPtr, byteCount};
callModule("host_obj_listener_perform", 4, args);
guestFree(dataPtr);
}
// The transition protocol has no module-lane wire type for TransitionChild
// yet, so these hold the host-side defaults: the container composites nothing.
// Because callTransitionDraw below is a no-op, we must NOT claim to manage the
// incoming child -- otherwise it would be hidden from the normal draw loop for
// the duration of every transition and nothing at all would render.
bool WasmScriptingVM::transitionManagesTo(int selfRef) { return false; }
void WasmScriptingVM::callTransitionChanged(ScriptedObject* object,
int selfRef,
const TransitionChildRef& from,
const TransitionChildRef& to,
int direction)
{}
void WasmScriptingVM::callTransitionDraw(ScriptedObject* object,
int selfRef,
Renderer* renderer,
const TransitionChildRef& from,
const TransitionChildRef& to)
{}
void WasmScriptingVM::callLayoutResize(ScriptedObject* object,
int selfRef,
Vec2D size)
{
if (!valid())
{
return;
}
// _v2 carries the surface scale; vm modules built before it export
// only the four-argument name, which must be called as such.
wasm_function_inst_t f =
wasm_runtime_lookup_function(m_state->instance,
"host_obj_layout_resize_v2");
bool legacy = f == nullptr;
if (legacy)
{
f = wasm_runtime_lookup_function(m_state->instance,
"host_obj_layout_resize");
}
if (f == nullptr)
{
return;
}
wasm_val_t args[5];
args[0].kind = WASM_I32;
args[0].of.i32 = (int32_t)m_L;
args[1].kind = WASM_I32;
args[1].of.i32 = selfRef;
args[2].kind = WASM_F64;
args[2].of.f64 = size.x;
args[3].kind = WASM_F64;
args[3].of.f64 = size.y;
args[4].kind = WASM_F64;
args[4].of.f64 = displayScale();
ScriptCallScope callScope(this);
wasm_runtime_call_wasm_a(m_state->execEnv,
f,
0,
nullptr,
legacy ? 4 : 5,
args);
}
bool WasmScriptingVM::callLayoutMeasure(ScriptedObject* object,
int selfRef,
Vec2D* outSize)
{
if (!valid())
{
return false;
}
uint32_t sizeArgs[1] = {2 * sizeof(float)};
uint32_t scratch = callModule("malloc", 1, sizeArgs);
if (scratch == 0)
{
return false;
}
// Prefill so the module's error paths leave outSize untouched, like the
// Luau backend.
float* out = (float*)resolveModulePtr(scratch, 2 * sizeof(float));
out[0] = outSize->x;
out[1] = outSize->y;
uint32_t args[3] = {m_L, (uint32_t)selfRef, scratch};
uint32_t measured = callModule("host_obj_layout_measure", 3, args);
if (measured != 0)
{
out = (float*)resolveModulePtr(scratch, 2 * sizeof(float));
outSize->x = out[0];
outSize->y = out[1];
}
guestFree(scratch);
return measured != 0;
}
int32_t WasmScriptingVM::inputSlot(int selfRef, const char* name)
{
auto& slots = m_inputSlots[selfRef];
auto it = slots.find(name);
if (it != slots.end())
{
return it->second;
}
int32_t slot = -1;
uint32_t namePtr = guestString(name);
if (namePtr != 0)
{
uint32_t args[3] = {m_L, (uint32_t)selfRef, namePtr};
slot = (int32_t)callModule("host_obj_input_slot", 3, args);
guestFree(namePtr);
}
slots.emplace(name, slot);
return slot;
}
bool WasmScriptingVM::legacyInputs() const
{
return wasm_runtime_lookup_function(m_state->instance,
"host_obj_input_slot") == nullptr;
}
bool WasmScriptingVM::inputArg(int selfRef,
const char* name,
uint32_t& arg,
uint32_t& owned)
{
owned = 0;
if (legacyInputs())
{
owned = arg = guestString(name);
return arg != 0;
}
int32_t slot = inputSlot(selfRef, name);
if (slot < 0)
{
return false;
}
arg = (uint32_t)slot;
return true;
}
void WasmScriptingVM::setInputBoolean(int selfRef, const char* name, bool value)
{
uint32_t arg = 0;
uint32_t owned = 0;
if (!inputArg(selfRef, name, arg, owned))
{
return;
}
uint32_t args[4] = {m_L, (uint32_t)selfRef, arg, value ? 1u : 0u};
callModule("host_obj_set_boolean", 4, args);
guestFree(owned);
}
void WasmScriptingVM::setInputNumber(int selfRef, const char* name, float value)
{
uint32_t arg = 0;
uint32_t owned = 0;
if (!inputArg(selfRef, name, arg, owned))
{
return;
}
wasm_function_inst_t f =
wasm_runtime_lookup_function(m_state->instance, "host_obj_set_number");
if (f != nullptr)
{
wasm_val_t args[4];
args[0].kind = WASM_I32;
args[0].of.i32 = (int32_t)m_L;
args[1].kind = WASM_I32;
args[1].of.i32 = selfRef;
args[2].kind = WASM_I32;
args[2].of.i32 = (int32_t)arg;
args[3].kind = WASM_F64;
args[3].of.f64 = value;
ScriptCallScope callScope(this);
wasm_runtime_call_wasm_a(m_state->execEnv, f, 0, nullptr, 4, args);
}
guestFree(owned);
}
void WasmScriptingVM::setInputUnsigned(int selfRef,
const char* name,
uint32_t value)
{
uint32_t arg = 0;
uint32_t owned = 0;
if (!inputArg(selfRef, name, arg, owned))
{
return;
}
uint32_t args[4] = {m_L, (uint32_t)selfRef, arg, value};
callModule(owned != 0 ? "host_obj_set_unsigned" : "host_obj_set_color",
4,
args);
guestFree(owned);
}
void WasmScriptingVM::setInputString(int selfRef,
const char* name,
const char* value)
{
uint32_t arg = 0;
uint32_t owned = 0;
if (!inputArg(selfRef, name, arg, owned))
{
return;
}
uint32_t valuePtr = guestString(value);
if (valuePtr != 0)
{
uint32_t args[4] = {m_L, (uint32_t)selfRef, arg, valuePtr};
callModule("host_obj_set_string", 4, args);
guestFree(valuePtr);
}
guestFree(owned);
}
void WasmScriptingVM::setInputViewModel(int selfRef,
const char* name,
ViewModelInstanceValue* value)
{
if (!valid() || value == nullptr)
{
return;
}
switch (value->coreType())
{
case ViewModelInstanceViewModelBase::typeKey:
{
auto viewModel = value->as<ViewModelInstanceViewModel>();
auto vmi = viewModel->referenceViewModelInstance();
if (vmi == nullptr)
{
fprintf(stderr,
"setInputViewModel - passed in a "
"ViewModelInstanceViewModel with no associated "
"ViewModelInstance.\n");
return;
}
uint32_t arg = 0;
uint32_t owned = 0;
if (!inputArg(selfRef, name, arg, owned))
{
return;
}
// The module's view model userdata owns the handle and releases
// it from its finalizer.
uint32_t handle =
m_handles.mint(HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{std::move(vmi)});
uint32_t args[4] = {m_L, (uint32_t)selfRef, arg, handle};
callModule("host_obj_set_view_model", 4, args);
guestFree(owned);
break;
}
default:
// Nothing assigned; leave self untouched, like the Luau backend.
break;
}
}
void WasmScriptingVM::setInputArtboard(int selfRef,
const char* name,
ScriptedObject* object,
Artboard* artboard)
{
if (!valid() || object == nullptr || object->scriptAsset() == nullptr ||
object->scriptAsset()->file() == nullptr || artboard == nullptr)
{
return;
}
uint32_t arg = 0;
uint32_t owned = 0;
if (!inputArg(selfRef, name, arg, owned))
{
return;
}
auto artboardInstance = artboard->instance();
artboardInstance->frameOrigin(false);
// The module's artboard userdata owns the handle and releases it from
// its finalizer.
uint32_t handle =
m_handles.mint(HandleTable::Tag::artboard,
new HostArtboard(object->scriptAsset()->file(),
std::move(artboardInstance),
nullptr,
object->dataContext()));
uint32_t args[4] = {m_L, (uint32_t)selfRef, arg, handle};
callModule("host_obj_set_artboard", 4, args);
guestFree(owned);
}
uint32_t WasmScriptingVM::guestString(const char* text)
{
if (text == nullptr)
{
return 0;
}
size_t size = strlen(text) + 1;
uint32_t sizeArgs[1] = {(uint32_t)size};
uint32_t ptr = callModule("malloc", 1, sizeArgs);
if (ptr != 0)
{
memcpy(resolveModulePtr(ptr, (uint32_t)size), text, size);
}
return ptr;
}
void WasmScriptingVM::guestFree(uint32_t ptr)
{
if (ptr == 0)
{
return;
}
uint32_t args[1] = {ptr};
callModule("free", 1, args);
}
// Browser-lane exports over the same impl cores; nothing off emscripten.
#include "wasm_natives_web_gen.hpp"
#endif