blob: e10c312877745f8bbd1e5f4bcb9dc36880a541bb [file] [edit]
#ifdef WITH_RIVE_SCRIPTING_WASM
#include "rive/wasm/wasm_scripting_vm.hpp"
#include "rive/async/compare_exchange.hpp"
#include "rive/async/timeouts.hpp"
#include "rive/wasm/wasm_jobs.hpp"
#include "rive/wasm/wasm_parallel.hpp"
#include "rive/wasm/wasm_lane.hpp"
#include "rive/wasm/wasm_shared.hpp"
#include "rive/wasm/wasm_workers.hpp"
#include "rive/wasm/wasm_processors.hpp"
#ifndef __EMSCRIPTEN__
#include "rive/wasm/aot_artifact.hpp"
#include "rive/wasm/module_tier_ladder.hpp"
#include "rive/wasm/prelinked_aot.hpp"
#else
#include <emscripten/emscripten.h>
#include <emscripten/threading.h>
#endif
#include <mutex>
#include <thread>
#include <unordered_map>
#ifdef WITH_RIVE_TEXT
#include "rive/text/raw_text.hpp"
#include "rive/text/cursor.hpp"
#include "rive/text/font_hb.hpp"
#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/input/focus_manager.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_reader.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/decoded_file.hpp"
#include "rive/scripted/scripted_object.hpp"
#include "rive/scripted/transition_child_ref.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"
#include "rive/wasm/net_wire.hpp"
#include "rive/bindable_artboard.hpp"
#include "rive/viewmodel/viewmodel_instance_artboard.hpp"
#ifdef WITH_RIVE_SCRIPTNET
#include "rive/scriptnet/net.hpp"
#endif
#ifdef __EMSCRIPTEN__
#include "rive/async/browser_image_decode.hpp"
#elif defined(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_deferred_bind_groups.hpp"
#include "rive/renderer/ore/ore_draw_bindings.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_shader_asset_modules.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 <vector>
#include <atomic>
#include <unordered_map>
#include <condition_variable>
#include <thread>
#include <mutex>
#include <cmath>
#include <cassert>
#endif
#ifndef __EMSCRIPTEN__
#include "wasm_export.h"
#endif
#include "wasm_utf16.hpp"
#include <chrono>
#include <cstdarg>
#include <cstddef>
#include <string.h>
#include <time.h>
using namespace rive;
// Streams only feed audio processors, so builds that refuse those leave them
// out, answering as a build without audio.
#if defined(WITH_RIVE_AUDIO) && RIVE_PROCESSOR_AUDIO()
#define RIVE_AUDIO_STREAMS() 1
#else
#define RIVE_AUDIO_STREAMS() 0
#endif
namespace
{
constexpr char kLongjmpTag[] = "rive-longjmp";
#ifndef __EMSCRIPTEN__
WasmScriptingVM* vmFromEnv(wasm_exec_env_t env)
{
return static_cast<WasmScriptingVM*>(wasm_runtime_get_user_data(env));
}
// A lane has no VM, so its lines go to stderr.
void reportScriptError(WasmScriptingVM* vm, const std::string& line)
{
if (vm != nullptr)
{
vm->reportError(line);
}
else
{
fprintf(stderr, "%s\n", line.c_str());
}
}
#endif
std::string budgetExceededLine(int ms)
{
return "script error: execution exceeded " + std::to_string(ms) +
" millisecond timeout";
}
// 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 natives its top level reaches.
thread_local WasmScriptingVM* s_booting = nullptr;
std::atomic<bool> s_requireBounds{false};
thread_local WasmScriptingVM::BootHook s_bootHook;
} // namespace
// The VMs one thread's polls reach. A VM may be destroyed on another thread,
// so it leaves its list under the lock a poll holds while it delivers.
struct rive::VMPollList
{
#if RIVE_ONE_THREAD()
// One thread runs every poll and VM, and its lists outlive them all.
using Ref = VMPollList*;
struct Lock
{
explicit Lock(const VMPollList&) {}
};
#else
using Ref = std::shared_ptr<VMPollList>;
struct Lock : std::lock_guard<std::recursive_mutex>
{
explicit Lock(const VMPollList& list) : lock_guard(*list.mutex) {}
};
// A thread's lists share it, as delivering either one can join the other.
std::shared_ptr<std::recursive_mutex> mutex;
explicit VMPollList(std::shared_ptr<std::recursive_mutex> threadMutex)
: mutex(std::move(threadMutex))
{}
#endif
std::vector<WasmScriptingVM*> vms;
// Joins list, set to the thread's own the first time.
static void join(Ref& list, const Ref& thread, WasmScriptingVM* vm)
{
if (list == nullptr)
{
list = thread;
}
Lock lock(*list);
if (std::find(list->vms.begin(), list->vms.end(), vm) == list->vms.end())
{
list->vms.push_back(vm);
}
}
static void leave(const Ref& list, WasmScriptingVM* vm)
{
if (list != nullptr)
{
Lock lock(*list);
list->vms.erase(
std::remove(list->vms.begin(), list->vms.end(), vm),
list->vms.end());
}
}
// A delivery runs script, which can destroy another VM on the list.
template <typename Deliver> void deliver(Deliver&& each)
{
Lock lock(*this);
for (WasmScriptingVM* vm : std::vector<WasmScriptingVM*>(vms))
{
if (std::find(vms.begin(), vms.end(), vm) != vms.end())
{
each(vm);
}
}
}
};
namespace
{
#if RIVE_ONE_THREAD()
// Never destroyed, so they need no static constructor or destructor.
[[clang::no_destroy]] VMPollList s_holdingList;
[[clang::no_destroy]] VMPollList s_laneList;
VMPollList* const t_holdingVMs = &s_holdingList;
VMPollList* const t_laneVMs = &s_laneList;
#else
thread_local std::shared_ptr<std::recursive_mutex>
t_pollMutex = std::make_shared<std::recursive_mutex>();
// The VMs holding outcomes, so a poll reaches them without walking every
// artboard that might own one.
thread_local std::shared_ptr<VMPollList>
t_holdingVMs = std::make_shared<VMPollList>(t_pollMutex);
// The VMs with lanes started on this thread, so a poll reaches their output.
thread_local std::shared_ptr<VMPollList>
t_laneVMs = std::make_shared<VMPollList>(t_pollMutex);
#endif
constexpr size_t kLaneResultBytesPerPoll = 8u << 20;
} // namespace
struct rive::WasmScriptingVMNatives
{
static void print(WasmScriptingVM* vm, const char* data, size_t size);
static int startFuelMs(const WasmScriptingVM* vm);
static const char* startRefusal(const WasmScriptingVM* vm);
#ifndef __EMSCRIPTEN__
static WasmScriptingVM* adoptBooting(wasm_exec_env_t env);
#else
static void booting(WasmScriptingVM* vm);
#endif
static std::unordered_map<std::string, int64_t>& consoleTimers(
WasmScriptingVM* vm);
};
#ifndef __EMSCRIPTEN__
// Loaded modules are immutable and shared: translating one costs about 11ms,
// instantiating it microseconds, and files of one content load the same bytes.
struct SharedWasmModule
{
// The buffer WAMR loads from and references until the module unloads.
std::vector<uint8_t> bytes;
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;
// No device run covers unloading prelinked code.
bool pinned = false;
// The wasm behind code that skips bounds checks, for a fallback.
std::vector<uint8_t> hardwareBoundsWasm;
// Orders the idle ones, so the least recently leased unloads first.
uint64_t lastLeased = 0;
~SharedWasmModule()
{
if (module != nullptr)
{
wasm_runtime_unload(module);
}
}
};
using SharedModuleCache = std::
unordered_map<uint64_t, std::shared_ptr<SharedWasmModule>>;
static SharedModuleCache& sharedModuleCache()
{
// Never destroyed, so nothing unloads at exit and threads still booting
// at exit never see it torn down.
static auto* cache = new SharedModuleCache();
return *cache;
}
// Hosts boot modules on more than one thread, such as a build thread and
// a render thread.
static std::mutex& sharedModuleCacheMutex()
{
static std::mutex mutex;
return mutex;
}
// Leases start only under the cache mutex, so under it an entry nothing else
// holds is idle and stays idle.
static std::shared_ptr<void> leaseLocked(
const std::shared_ptr<SharedWasmModule>& entry)
{
static uint64_t leases = 0;
entry->lastLeased = ++leases;
return std::shared_ptr<void>(entry, entry->module);
}
static std::shared_ptr<void> leaseCachedModule(
uint64_t key,
WasmScriptingVM::ExecutionTier& tier)
{
std::lock_guard<std::mutex> lock(sharedModuleCacheMutex());
auto cached = sharedModuleCache().find(key);
if (cached == sharedModuleCache().end())
{
return nullptr;
}
tier = cached->second->tier;
return leaseLocked(cached->second);
}
static void evictIdleModulesLocked(
std::vector<std::shared_ptr<SharedWasmModule>>& evicted)
{
SharedModuleCache& cache = sharedModuleCache();
std::vector<std::pair<uint64_t, uint64_t>> idle;
for (const auto& [key, entry] : cache)
{
if (entry.use_count() == 1 && !entry->pinned)
{
idle.emplace_back(entry->lastLeased, key);
}
}
if (idle.size() <= WasmScriptingVM::kIdleModules)
{
return;
}
std::sort(idle.begin(), idle.end());
idle.resize(idle.size() - WasmScriptingVM::kIdleModules);
for (const auto& [lastLeased, key] : idle)
{
auto found = cache.find(key);
evicted.push_back(std::move(found->second));
cache.erase(found);
}
}
// Compiled modules cache apart from interpreted ones of the same bytes, and
// each species apart from the others, since the module carries its tier.
static uint64_t compiledModuleKey(uint64_t key, TierSpecies species)
{
key ^= 0x9e3779b97f4a7c15ull;
if (species == TierSpecies::hw)
{
// The hw bounds flag lives on the module.
key ^= 0xc2b2ae3d27d4eb4full;
}
if (species == TierSpecies::o0)
{
key ^= 0x27d4eb2f165667c5ull;
}
return key;
}
struct WasmScriptingVM::WamrState
{
// Keeps module loaded until the destructor has ended the instance.
std::shared_ptr<void> lease;
wasm_module_t module = nullptr;
wasm_module_inst_t instance = nullptr;
wasm_exec_env_t execEnv = nullptr;
// 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];
// The module's __riveCall globals: state of the call in progress, which
// a trap unwinds without restoring.
static constexpr uint32_t kMaxCallState = 8;
uint32_t* callState[kMaxCallState] = {};
uint32_t callStateCount = 0;
// Puts the call state back when a call into the module ends.
class CallStateScope
{
public:
explicit CallStateScope(const WamrState& state) : m_state(state)
{
for (uint32_t i = 0; i < state.callStateCount; i++)
{
m_saved[i] = *state.callState[i];
}
}
~CallStateScope()
{
for (uint32_t i = 0; i < m_state.callStateCount; i++)
{
*m_state.callState[i] = m_saved[i];
}
}
private:
const WamrState& m_state;
uint32_t m_saved[kMaxCallState];
};
void findCallState()
{
int32_t count = wasm_runtime_get_export_count(module);
for (int32_t i = 0; i < count; i++)
{
wasm_export_t entry;
wasm_runtime_get_export_type(module, i, &entry);
wasm_global_inst_t global;
if (entry.kind != WASM_IMPORT_EXPORT_KIND_GLOBAL ||
strncmp(entry.name, "__riveCall", 10) != 0 ||
!wasm_runtime_get_export_global_inst(instance, entry.name, &global) ||
global.kind != WASM_I32 || !global.is_mutable)
{
continue;
}
if (callStateCount == kMaxCallState)
{
fprintf(
stderr,
"wasm module has more call state than the host "
"restores; a trap may leave %s stale\n",
entry.name);
continue;
}
callState[callStateCount++] = (uint32_t*)global.global_data;
}
}
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);
}
}
};
#endif
#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;
}
struct WasmScriptingVM::BudgetSlot
{
std::atomic<int64_t> deadline{0};
// Set when the watchdog interrupted the last budgeted call.
std::atomic<bool> fired{false};
#ifndef __EMSCRIPTEN__
wasm_exec_env_t env = nullptr;
#endif
};
namespace
{
int64_t steadyNanos()
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
} // namespace
// The watchdog interrupts WAMR; the browser's engine has no such hook, so
// there a call runs unbounded.
#ifndef __EMSCRIPTEN__
namespace
{
// One thread bounds every VM's calls, so modules need no fuel counters: it
// polls each VM's deadline and, on an overrun, sets the call's terminate
// flag so the interpreter or AOT code traps at its next loop back-edge.
// Calls only store and swap an atomic, so the script thread never waits.
constexpr int64_t kFiring = -1;
class ScriptWatchdog
{
public:
// Leaked, with a detached thread, so no static destructor joins it or
// frees the mutex a VM in another static still uses.
static ScriptWatchdog& instance()
{
static ScriptWatchdog* watchdog = new ScriptWatchdog();
return *watchdog;
}
// A polled slot is armed from any thread, the audio thread's included,
// so while one exists the watchdog ticks and arming makes no syscall.
void add(WasmScriptingVM::BudgetSlot* slot, bool polled = false)
{
m_polled.fetch_add(polled ? 1 : 0);
std::lock_guard<std::mutex> lock(m_mutex);
if (!m_started)
{
m_started = true;
std::thread([this] { run(); }).detach();
}
m_slots.push_back(slot);
// A watchdog waiting without a deadline starts ticking.
if (polled)
{
m_wake.wake();
}
}
void remove(WasmScriptingVM::BudgetSlot* slot, bool polled = false)
{
// An arm skipped its wake while slots were polled, so the last one
// going rescans before the watchdog may sleep without a deadline.
bool lastPolled = polled && m_polled.fetch_sub(1) == 1;
{
std::lock_guard<std::mutex> lock(m_mutex);
m_slots.erase(
std::remove(m_slots.begin(), m_slots.end(), slot),
m_slots.end());
}
if (lastPolled)
{
m_wake.wake();
}
}
// Calls arm without any lock, so the audio thread may: only an arm that
// finds the thread idle, or asleep until a later deadline, wakes it.
void armed(int64_t deadline)
{
if (m_polled.load() != 0)
{
return;
}
if (m_idle.load() || deadline < m_waitingUntil.load())
{
m_wake.wake();
}
}
private:
void run()
{
while (true)
{
// Read before the scan, so an arm racing it either shows up in
// the scan or moves the word and ends the wait.
uint32_t seen = m_wake.value();
m_idle.store(true);
int64_t earliest = 0;
{
std::lock_guard<std::mutex> lock(m_mutex);
int64_t now = steadyNanos();
for (WasmScriptingVM::BudgetSlot* slot : m_slots)
{
int64_t deadline = slot->deadline.load();
if (deadline > 0 && now >= deadline &&
slot->deadline.compare_exchange_strong(deadline, kFiring))
{
slot->fired.store(true);
wasm_runtime_rive_interrupt(slot->env);
slot->deadline.store(0);
}
else if (deadline > 0 && (earliest == 0 || deadline < earliest))
{
earliest = deadline;
}
}
}
if (m_polled.load() != 0)
{
int64_t tick = steadyNanos() + kPollNanos;
earliest = earliest == 0 ? tick : std::min(earliest, tick);
}
if (earliest == 0)
{
m_waitingUntil.store(INT64_MAX);
m_wake.wait(seen, std::chrono::steady_clock::time_point::max());
continue;
}
// Stays idle until the deadline is published, so an arm in
// between still wakes it.
m_waitingUntil.store(earliest);
m_idle.store(false);
m_wake.wait(
seen,
std::chrono::steady_clock::time_point(
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::nanoseconds(earliest))));
}
}
// How often the watchdog checks while a polled slot exists.
static constexpr int64_t kPollNanos = 2000000;
std::atomic<int32_t> m_polled{0};
std::mutex m_mutex;
WakeWord m_wake;
std::vector<WasmScriptingVM::BudgetSlot*> m_slots;
std::atomic<bool> m_idle{false};
std::atomic<int64_t> m_waitingUntil{INT64_MAX};
bool m_started = false;
};
// Swaps the slot's deadline out for none, waiting out a firing so its
// interrupt never lands on a later call; answers the deadline it took.
int64_t takeDeadline(WasmScriptingVM::BudgetSlot& slot)
{
int64_t deadline = slot.deadline.load();
while (deadline == kFiring ||
!slot.deadline.compare_exchange_weak(deadline, 0))
{
deadline = slot.deadline.load();
}
return deadline;
}
} // namespace
struct WasmCallBudget::State
{
WasmScriptingVM::BudgetSlot budget;
};
WasmCallBudget::WasmCallBudget(void* execEnv)
: m_state(std::make_unique<State>())
{
m_state->budget.env = (wasm_exec_env_t)execEnv;
ScriptWatchdog::instance().add(&m_state->budget, true);
}
WasmCallBudget::~WasmCallBudget()
{
disarm();
ScriptWatchdog::instance().remove(&m_state->budget, true);
}
#if defined(WITH_RIVE_TOOLS) || defined(TESTING)
static std::atomic<uint32_t> s_interruptDelayMs{0};
void WasmCallBudget::delayInterrupts(uint32_t ms) { s_interruptDelayMs = ms; }
#else
static constexpr uint32_t s_interruptDelayMs = 0;
#endif
void WasmCallBudget::arm(uint32_t timeoutMs)
{
m_state->budget.fired.store(false);
int64_t deadline = steadyNanos() +
((int64_t)timeoutMs + s_interruptDelayMs) * 1000000;
m_state->budget.deadline.store(deadline);
ScriptWatchdog::instance().armed(deadline);
}
bool WasmCallBudget::disarm()
{
WasmScriptingVM::BudgetSlot& slot = m_state->budget;
takeDeadline(slot);
bool fired = slot.fired.load();
wasm_runtime_rive_clear_interrupt(slot.env);
return fired;
}
#endif
namespace
{
// Module calls live on this thread, any VM's.
thread_local uint32_t t_moduleCalls = 0;
using Clock = std::chrono::steady_clock;
} // namespace
// What the module call on this thread has left of its budget, infinity
// when nothing bounds it.
static double callSecondsLeft()
{
Clock::time_point ends = callDeadline();
if (ends == Clock::time_point::max())
{
return INFINITY;
}
return std::max(
0.0,
std::chrono::duration<double>(ends - Clock::now()).count());
}
// What a wait may spend of its call's budget, keeping a little for the
// script to act once the wait ends.
static double joinableSeconds()
{
constexpr double kJoinerReserveSeconds = 0.02;
return callSecondsLeft() - kJoinerReserveSeconds;
}
WasmScriptingVM::WaitScope::WaitScope(WasmScriptingVM* vm)
: m_vm(vm)
, m_usable(joinableSeconds())
, m_start(Clock::now())
{
#ifndef __EMSCRIPTEN__
// A deadline already fired stays fired.
if (vm != nullptr && vm->m_budgetSlot != nullptr)
{
m_paused = takeDeadline(*vm->m_budgetSlot);
}
#endif
}
WasmScriptingVM::WaitScope::~WaitScope()
{
// A wait begun with nothing usable gives no time back, so retries cannot
// move the call's end.
double over = std::isinf(m_usable) || m_usable <= 0
? 0
: std::chrono::duration<double>(Clock::now() - m_start)
.count() -
m_usable;
auto shift = std::chrono::duration_cast<Clock::duration>(
std::chrono::duration<double>(std::max(0.0, over)));
#ifndef __EMSCRIPTEN__
if (m_paused > 0)
{
int64_t deadline = m_paused +
std::chrono::duration_cast<std::chrono::nanoseconds>(
shift)
.count();
m_vm->m_budgetSlot->deadline.store(deadline);
ScriptWatchdog::instance().armed(deadline);
}
#endif
if (over <= 0 || m_vm == nullptr)
{
return;
}
Clock::time_point deadline = callDeadline();
if (deadline != Clock::time_point::max())
{
restoreCallDeadline(deadline + shift);
}
// A metered module times its own budget, so it moves its deadline on,
// rounded up so it never falls behind the host's.
if (m_vm->m_fuelShift && m_vm->m_fuelBudget)
{
uint32_t args[1] = {(uint32_t)std::ceil(over * 1000)};
m_vm->callModule("__riveFuelShift", 1, args);
}
}
bool WasmScriptingVM::inModuleCallOnThread() { return t_moduleCalls != 0; }
WasmScriptingVM::ScriptCallScope::ScriptCallScope(
WasmScriptingVM* vm,
bool budgeted)
: m_vm(vm)
{
t_moduleCalls++;
if (vm->m_debugHooks != nullptr)
{
vm->m_debugHooks->onCallBegin(*vm);
}
// The outermost scope sweeps what any nested call left open.
bool outermost = vm->m_callDepth++ == 0;
if (outermost)
{
// Another VM's call on this thread may hold a region of its own.
m_outerRegion = stashHeldRegion(&m_outerRegionId);
// Paths that consume their own traps never reach reportTrap to clear
// it.
vm->m_budgetRaised = false;
vm->m_thrownRaised = false;
}
#ifndef __EMSCRIPTEN__
// Script a runtime service calls back into arms its own.
if (budgeted && vm->m_hostBudget && !vm->m_budgetArmed && vm->boundsCalls())
{
vm->armWatchdog(vm->m_state->execEnv);
m_budgeted = true;
}
#endif
m_deadlineArmed = budgeted && vm->armDeadline();
if (!outermost)
{
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()
{
t_moduleCalls--;
if (m_deadlineArmed)
{
m_vm->disarmDeadline();
}
#ifndef __EMSCRIPTEN__
if (m_budgeted)
{
m_vm->disarmWatchdog();
}
#endif
if (--m_vm->m_callDepth == 0)
{
// A trap can unwind out of an exclusive section with its lock held.
restoreHeldRegion(m_outerRegionId, m_outerRegion);
#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 = nullptr;
#ifndef __EMSCRIPTEN__
trap = m_vm->m_state != nullptr && m_vm->m_state->instance != nullptr
? wasm_runtime_get_exception(m_vm->m_state->instance)
: nullptr;
#endif
m_vm->m_debugHooks->onCallEnd(*m_vm, trap);
}
}
// A debugger pauses mid call, so its sessions run unbudgeted. A metered
// module ends its own overrun, but waits must still give up in time.
bool WasmScriptingVM::boundsCalls() const
{
return (m_hostBudget || m_fuelBudget) && m_timeoutMs > 0 &&
m_debugHooks == nullptr;
}
uint32_t WasmScriptingVM::pageThreadBudgetMs() const
{
return boundsCalls() ? (uint32_t)m_timeoutMs : 0;
}
uint32_t WasmScriptingVM::parallelSize() const
{
#ifdef RIVE_WASM_PARALLEL
// A debugger steps one thread, so every part runs on the one it stops,
// and a top level runs before its instance can take members.
return m_limits.parallelSafeMode || m_debugHooks != nullptr ||
s_booting == this
? 1
: WasmParallel::size();
#elif defined(__EMSCRIPTEN_PTHREADS__)
// The page runs parts on its own team of threads, as many as its runner
// picks up to a native call's 64.
return m_limits.parallelSafeMode || m_debugHooks != nullptr ||
s_booting == this
? 1
: 64;
#else
return 1;
#endif
}
// Only joins, region locks and signal waits read the call deadline.
bool WasmScriptingVM::armDeadline()
{
if (m_deadlineArmed || !hasLanes() || !boundsCalls())
{
return false;
}
m_outerDeadline = armCallDeadline(
Clock::now() + std::chrono::milliseconds(m_timeoutMs));
m_deadlineArmed = true;
return true;
}
void WasmScriptingVM::disarmDeadline()
{
restoreCallDeadline(m_outerDeadline);
m_deadlineArmed = false;
}
#ifndef __EMSCRIPTEN__
void WasmScriptingVM::armWatchdog(void* env)
{
if (m_budgetSlot == nullptr)
{
m_budgetSlot = std::make_unique<BudgetSlot>();
ScriptWatchdog::instance().add(m_budgetSlot.get());
}
BudgetSlot* slot = m_budgetSlot.get();
slot->env = (wasm_exec_env_t)env;
slot->fired.store(false);
int64_t deadline = steadyNanos() + (int64_t)watchdogBudgetMs() * 1000000;
slot->deadline.store(deadline);
ScriptWatchdog::instance().armed(deadline);
m_budgetArmed = true;
}
void WasmScriptingVM::disarmWatchdog()
{
BudgetSlot* slot = m_budgetSlot.get();
takeDeadline(*slot);
if (slot->fired.load())
{
reportError(budgetExceededLine(watchdogBudgetMs()));
// The interrupt can land inside the module's allocator or
// collector, so its heap is no longer trusted.
poison();
reportError(
"script stopped: its module may have been interrupted mid-update");
}
wasm_runtime_rive_clear_interrupt(slot->env);
m_budgetArmed = false;
}
WasmScriptingVM::CallOutcome WasmScriptingVM::callModuleChecked(
const char* name,
uint32_t argc,
uint32_t* argv,
uint32_t* result)
{
if (m_poisoned)
{
return CallOutcome::trapped;
}
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;
}
// WAMR writes results into the argument cells, so both must fit buf.
if (argc > 8 || wasmCallCells(m_state->instance, f, 8) != argc)
{
recordTrap(name, "export signature mismatch");
return CallOutcome::trapped;
}
uint32_t buf[8] = {0};
for (uint32_t i = 0; i < argc; i++)
{
buf[i] = argv[i];
}
bool ok;
{
ensureWasmThreadEnv();
WamrState::CallStateScope callState(*m_state);
// Runtime services, the collector among them, must never be cut off.
ScriptCallScope callScope(this, strncmp(name, "__rive", 6) != 0);
ok = m_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)
{
reportTrap(name);
return CallOutcome::trapped;
}
*result = buf[0];
return CallOutcome::ok;
}
WasmScriptingVM::CallOutcome WasmScriptingVM::callF64(
const char* name,
const double* args,
uint32_t argc,
double* result)
{
if (m_poisoned)
{
return CallOutcome::trapped;
}
wasm_function_inst_t f = m_state->lookupExport(name);
if (f == nullptr)
{
return CallOutcome::missing;
}
wasm_module_inst_t inst = m_state->instance;
uint32_t retCount = wasm_func_get_result_count(f, inst);
// Modules load unsigned, so the export may not match.
// host_obj_scroll_event takes the most.
constexpr uint32_t kMaxArgs = 10;
if (argc > kMaxArgs || wasm_func_get_param_count(f, inst) != argc ||
retCount > 1)
{
recordTrap(name, "export signature mismatch");
return CallOutcome::trapped;
}
wasm_valkind_t kinds[kMaxArgs];
wasm_func_get_param_types(f, inst, kinds);
wasm_val_t values[kMaxArgs];
for (uint32_t i = 0; i < argc; i++)
{
values[i].kind = kinds[i];
switch (kinds[i])
{
case WASM_F64:
values[i].of.f64 = args[i];
break;
case WASM_F32:
values[i].of.f32 = (float)args[i];
break;
case WASM_I64:
values[i].of.i64 = (int64_t)args[i];
break;
default:
values[i].of.i32 = (int32_t)(uint32_t)(int64_t)args[i];
break;
}
}
wasm_val_t ret[1];
bool ok;
{
ensureWasmThreadEnv();
WamrState::CallStateScope callState(*m_state);
ScriptCallScope callScope(this);
ok = wasm_runtime_call_wasm_a(
m_state->execEnv,
f,
retCount,
ret,
argc,
values);
}
if (!ok)
{
reportTrap(name);
return CallOutcome::trapped;
}
*result = 0;
if (retCount != 0)
{
switch (ret[0].kind)
{
case WASM_F64:
*result = ret[0].of.f64;
break;
case WASM_F32:
*result = ret[0].of.f32;
break;
case WASM_I64:
*result = (double)ret[0].of.i64;
break;
default:
*result = ret[0].of.i32;
break;
}
}
return CallOutcome::ok;
}
bool WasmScriptingVM::hasExport(const char* name)
{
return m_state->lookupExport(name) != nullptr;
}
void WasmScriptingVM::reportTrap(const char* where)
{
wasm_module_inst_t inst = m_state->instance;
recordTrap(where, wasm_runtime_get_exception(inst));
#if WASM_ENABLE_DUMP_CALL_STACK != 0
if (!m_quietTraps)
{
wasm_runtime_dump_call_stack(m_state->execEnv);
}
#endif
// A pending exception makes WAMR refuse the next call, the collector's
// included.
wasm_runtime_clear_exception(inst);
}
#endif
void WasmScriptingVM::recordTrap(const char* where, const char* exception)
{
// A silent fold hides real traps; name them so a script that dies
// mid-call is diagnosable instead of a mystery no-op.
exception = fullTrapMessage(exception);
// AOT code traps as unreachable; both tiers name the timeout.
m_lastTrap.budget = m_budgetRaised ||
(m_budgetSlot != nullptr && m_budgetSlot->fired.load());
m_lastTrap.thrown = m_thrownRaised && !m_lastTrap.budget;
m_budgetRaised = false;
m_thrownRaised = false;
if (m_lastTrap.budget)
{
exception = "execution exceeded timeout";
}
m_lastTrap.message = exception != nullptr ? exception : "";
if (exception == nullptr || m_quietTraps)
{
return;
}
reportError(std::string("wasm call trapped in ") + where + ": " + exception);
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;
reportError(
"wasm call trapped after " + std::to_string(m_leakWarningCount) +
" script heap leak warnings; the module likely hit its "
"wasmMaxPages ceiling");
}
}
void WasmScriptingVM::reportError(const std::string& line)
{
if (m_error)
{
m_error(line);
}
else
{
fprintf(stderr, "%s\n", line.c_str());
}
}
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;
}
#ifndef __EMSCRIPTEN__
void* WasmScriptingVM::resolveModulePtr(uint32_t appAddr, uint32_t size)
{
return wasmAppBytes(m_state->instance, appAddr, size);
}
const char* WasmScriptingVM::resolveModuleString(uint32_t appAddr)
{
// The terminator has to lie inside linear memory for strlen to stop.
wasm_module_inst_t inst = m_state->instance;
uint64_t end = 0;
if (!wasm_runtime_get_app_addr_range(inst, appAddr, nullptr, &end))
{
return nullptr;
}
auto str = (const char*)wasm_runtime_addr_app_to_native(inst, appAddr);
return memchr(str, '\0', end - appAddr) != nullptr ? str : nullptr;
}
bool WasmScriptingVM::copyToModule(
uint32_t appAddr,
const void* src,
uint32_t size)
{
void* dst = resolveModulePtr(appAddr, size);
if (dst == nullptr)
{
return false;
}
memcpy(dst, src, size);
return true;
}
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);
}
#else
// The browser backend overrides every seam; these complete the vtable.
WasmScriptingVM::CallOutcome WasmScriptingVM::callModuleChecked(
const char*,
uint32_t,
uint32_t*,
uint32_t*)
{
return CallOutcome::missing;
}
WasmScriptingVM::CallOutcome WasmScriptingVM::callF64(
const char*,
const double*,
uint32_t,
double*)
{
return CallOutcome::missing;
}
bool WasmScriptingVM::hasExport(const char*) { return false; }
void* WasmScriptingVM::resolveModulePtr(uint32_t, uint32_t) { return nullptr; }
bool WasmScriptingVM::copyToModule(uint32_t, const void*, uint32_t)
{
return false;
}
const char* WasmScriptingVM::resolveModuleString(uint32_t) { return nullptr; }
void WasmScriptingVM::raiseModuleError(const char*) {}
bool WasmScriptingVM::memoryLimits(uint32_t*, uint32_t*) const { return false; }
bool WasmScriptingVM::valid() const { return false; }
#endif
void WasmScriptingVM::raiseBudgetExceeded()
{
m_budgetRaised = true;
raiseModuleError("execution exceeded timeout");
}
void WasmScriptingVM::raiseThrown(const char* message)
{
m_thrownRaised = true;
raiseModuleError(message);
}
const char* WasmScriptingVM::fullTrapMessage(const char* exception) const
{
const char* text = trapText(exception);
if (text == nullptr)
{
return nullptr;
}
// 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.empty() &&
m_moduleErrorDetail.compare(0, strlen(text), text) == 0)
{
return m_moduleErrorDetail.c_str();
}
return text;
}
#ifndef __EMSCRIPTEN__
WasmScriptingVM* WasmScriptingVMNatives::adoptBooting(wasm_exec_env_t env)
{
// Instantiate assigns the same instance on success and null on failure.
if (s_booting != nullptr)
{
s_booting->m_state->instance = wasm_runtime_get_module_inst(env);
}
return s_booting;
}
#else
void WasmScriptingVMNatives::booting(WasmScriptingVM* vm)
{
// A top level's waits give up in time, as a call's do, and it neither
// sees a region the call it nests in holds nor leaves one held.
if (vm != nullptr)
{
vm->armDeadline();
vm->m_outerBooting = s_booting;
vm->m_outerRegion = stashHeldRegion(&vm->m_outerRegionId);
s_booting = vm;
}
else if (WasmScriptingVM* ending = s_booting)
{
if (ending->m_deadlineArmed)
{
ending->disarmDeadline();
}
restoreHeldRegion(ending->m_outerRegionId, ending->m_outerRegion);
s_booting = ending->m_outerBooting;
}
s_bootPrint = s_booting != nullptr ? &s_booting->m_print : nullptr;
}
#endif
std::unordered_map<std::string, int64_t>& WasmScriptingVMNatives::consoleTimers(
WasmScriptingVM* vm)
{
return vm->m_consoleTimers;
}
// The budget a metered module arms its own fuel with around its top level,
// as its calls do, or 0. A debugger pauses mid call, so it runs unbudgeted.
int WasmScriptingVMNatives::startFuelMs(const WasmScriptingVM* vm)
{
return vm->m_fuelBudget && vm->m_debugHooks == nullptr && vm->m_timeoutMs > 0
? vm->m_timeoutMs
: 0;
}
// Why a host that requires bounds will not start this module, or null.
const char* WasmScriptingVMNatives::startRefusal(const WasmScriptingVM* vm)
{
if (!s_requireBounds.load() || vm->m_fuelBudget)
{
return nullptr;
}
#ifdef __EMSCRIPTEN__
return "this host runs only scripts it can bound, and a browser bounds "
"only a module with fuel; bake it with wasmFuel";
#else
return vm->m_hostBudget
? nullptr
: "this host runs only scripts it can bound, AnimaScript with "
"fuel or the watchdog, and dangerouslyFast bakes and Luau "
"wasm modules have neither";
#endif
}
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);
// A unit is three bytes at most, a surrogate pair four for its two.
size_t most = (size_t)(byteCount / 2) * 3;
char* out = m_inline;
if (most > sizeof(m_inline))
{
m_heap.resize(most);
out = &m_heap[0];
}
m_data = out;
for (uint32_t i = 0; i + 1 < byteCount;)
{
out += utf8Of(nextUtf16(in, byteCount, i), (uint8_t*)out);
}
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;
};
// Transcodes UTF-8 the way the module's own decoder would, writing only the
// units that fit; returns the full UTF-16 byte length.
size_t encodeUtf16(const char* data, size_t size, uint8_t* out, size_t capacity)
{
auto in = reinterpret_cast<const uint8_t*>(data);
size_t written = 0;
auto unit = [&](uint32_t value) {
if (written + 2 <= capacity)
{
out[written] = (uint8_t)value;
out[written + 1] = (uint8_t)(value >> 8);
}
written += 2;
};
size_t i = 0;
while (i < size)
{
uint32_t code = in[i];
if (code < 0x80)
{
unit(code);
i++;
continue;
}
size_t tail = (code & 0xE0) == 0xC0 ? 1 : (code & 0xF0) == 0xE0 ? 2 : 3;
if (i + tail >= size)
{
break;
}
code &= 0x3F >> tail;
for (size_t k = 1; k <= tail; k++)
{
code = code << 6 | (in[i + k] & 0x3F);
}
i += tail + 1;
if (code < 0x10000)
{
unit(code);
}
else
{
code -= 0x10000;
unit(0xD800 | code >> 10);
unit(0xDC00 | (code & 0x3FF));
}
}
return written;
}
bool moduleReadsUtf16(WasmScriptingVM* vm)
{
WasmScriptingVM* owner = vm != nullptr ? vm : s_booting;
return owner != nullptr && owner->utf16HostStrings();
}
// A host string in the encoding the module reads.
std::string moduleStringBytes(
WasmScriptingVM* vm,
const char* data,
size_t size)
{
if (!moduleReadsUtf16(vm))
{
return std::string(data, size);
}
std::string out(encodeUtf16(data, size, nullptr, 0), '\0');
encodeUtf16(data, size, reinterpret_cast<uint8_t*>(&out[0]), out.size());
return out;
}
// The retrying length contract in the module's encoding: copies what fits,
// returns the full byte length.
uint32_t fillModuleString(
WasmScriptingVM* vm,
const std::string& value,
char* buffer,
uint32_t capacity)
{
if (!moduleReadsUtf16(vm))
{
memcpy(buffer, value.data(), std::min<size_t>(value.size(), capacity));
return (uint32_t)value.size();
}
return (uint32_t)encodeUtf16(
value.data(),
value.size(),
reinterpret_cast<uint8_t*>(buffer),
capacity);
}
#ifndef __EMSCRIPTEN__
// Module start runs inside wasm_runtime_instantiate, before the exec env
// carries the vm, so a native its top level reaches adopts the booting one;
// a lane that top level joined runs on this thread without one.
WasmScriptingVM* bootVmFromEnv(wasm_exec_env_t env)
{
WasmScriptingVM* vm = vmFromEnv(env);
return vm != nullptr || WasmLane::current() != nullptr
? vm
: WasmScriptingVMNatives::adoptBooting(env);
}
#else
// As on WAMR, module start resolves no vm for the ops that do not trap.
WasmScriptingVM* vmFromHandle(uint32_t handle)
{
auto vm = (WasmScriptingVM*)(uintptr_t)handle;
return vm == s_booting ? nullptr : vm;
}
// Lanes pass 1 and reach only the shared ops, which all boot.
WasmScriptingVM* bootVmFromHandle(uint32_t handle)
{
return handle == 1 ? nullptr : (WasmScriptingVM*)(uintptr_t)handle;
}
#endif
#ifndef __EMSCRIPTEN__
// A trap from a module call that has no VM: a lane's, or a parallel call
// member's, whose throw reaches the call that ran it.
void vmlessTrap(
wasm_exec_env_t env,
const char* text,
const char* message = nullptr,
uint32_t length = 0)
{
WasmParallel::noteMemberTrap(text, message, length);
wasm_runtime_set_exception(wasm_runtime_get_module_inst(env), text);
}
#endif
// 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"
void consoleWrite(WasmScriptingVM* vm, const std::string& message)
{
rtLogImpl(vm, 0, message.data(), (uint32_t)message.size());
}
void consoleTimeStart(WasmScriptingVM* vm, const std::string& label)
{
if (vm != nullptr)
{
startConsoleTimer(WasmScriptingVMNatives::consoleTimers(vm), label);
}
}
void consoleTimeReport(WasmScriptingVM* vm, const std::string& name, bool end)
{
if (vm != nullptr)
{
consoleWrite(
vm,
reportConsoleTimer(
WasmScriptingVMNatives::consoleTimers(vm),
name,
end));
}
}
std::string traceNumber(double value)
{
if (std::isnan(value))
{
return "NaN";
}
if (std::isinf(value))
{
return value < 0 ? "-Infinity" : "Infinity";
}
char text[32];
if (value == std::trunc(value) && std::fabs(value) < 1e21)
{
snprintf(text, sizeof(text), "%.0f", value);
return text;
}
for (int digits = 1; digits <= 17; digits++)
{
snprintf(text, sizeof(text), "%.*g", digits, value);
if (strtod(text, nullptr) == value)
{
break;
}
}
return text;
}
void traceWrite(
WasmScriptingVM* vm,
const std::string& message,
int32_t count,
const double* args)
{
std::string line = "trace: " + message;
for (int32_t i = 0; i < count && i < 5; i++)
{
line += ' ' + traceNumber(args[i]);
}
consoleWrite(vm, line);
}
#ifndef __EMSCRIPTEN__
// --- 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);
// Module start has no exec env yet to restore the stack through.
if (vm == nullptr)
{
wasm_runtime_set_exception(inst, "script module invoke at start");
return;
}
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 ---------------------------------------
// Module start reaches these too, before the exec env carries the vm, so
// they resolve memory through the booting one and trap when there is none.
WasmScriptingVM* envVm(wasm_exec_env_t env)
{
WasmScriptingVM* vm = bootVmFromEnv(env);
if (vm == nullptr)
{
wasm_runtime_set_exception(
wasm_runtime_get_module_inst(env),
"script module import without a vm");
}
return vm;
}
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 = envVm(env);
if (vm == nullptr)
{
return;
}
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 rtNowImpl(WasmScriptingVM* vm)
{
// Same dev hook as dateNow: clock() feeds Luau's default RNG seed. A
// pinned clock also parks the module's execution budget.
static const bool fixed = getenv("RIVE_WASM_FIXED_DATE") != nullptr;
if (fixed)
{
return 0;
}
auto now = std::chrono::steady_clock::now().time_since_epoch();
return std::chrono::duration<double, std::milli>(now).count();
}
double rtDateNowImpl(WasmScriptingVM* vm)
{
// 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();
}
// The Luau host module reads the same clocks under the names emscripten
// gives them.
double getNow(wasm_exec_env_t env) { return rtNowImpl(nullptr); }
double dateNow(wasm_exec_env_t env) { return rtDateNowImpl(nullptr); }
// 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;
}
#endif
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
}
#ifndef __EMSCRIPTEN__
// --- 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)
{
WasmScriptingVM* vm = envVm(env);
int32_t* fields = vm != nullptr ? (int32_t*)vm->resolveModulePtr(tmPtr, 44)
: nullptr;
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 = envVm(env);
if (vm == nullptr)
{
return;
}
#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 = envVm(env);
if (vm == nullptr)
{
return 0;
}
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 = bootVmFromEnv(env);
auto text = [vm](uint32_t ptr) {
const char* str = ptr != 0 && vm != nullptr ? vm->resolveModuleString(ptr)
: nullptr;
return str != nullptr ? str : "?";
};
reportScriptError(
vm,
std::string("module assertion failed: ") + text(message) + " (" +
text(file) + ":" + std::to_string(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 = envVm(env);
if (vm == nullptr)
{
return 28; // EINVAL
}
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);
}
}
reportScriptError(
bootVmFromEnv(env),
std::string("[wasm] C++ exception thrown: ") + typeName);
wasm_runtime_set_exception(inst, "c++ exception");
}
// Math.random's seed. A deterministic run's seed comes first, so --seed
// picks the same sequence here as in Luau; the fixed date env that pins
// script time pins it too, to 1, the deterministic default.
double envSeed(wasm_exec_env_t)
{
if (File::deterministicMode)
{
return (double)File::deterministicSeed;
}
if (getenv("RIVE_WASM_FIXED_DATE") != nullptr)
{
return 1.0;
}
return (double)std::chrono::steady_clock::now().time_since_epoch().count();
}
// The std's console passes a managed string: UTF-16 whose byte length is the
// word ahead of it.
std::string consoleString(wasm_exec_env_t env, uint32_t text)
{
uint32_t length = 0;
auto data = (const char*)
wasmConsoleText(wasm_runtime_get_module_inst(env), text, length);
if (data == nullptr)
{
return std::string();
}
WasmStringArg utf8(vmFromEnv(env), data, length);
return std::string(utf8.data(), utf8.size());
}
void consoleLog(wasm_exec_env_t env, uint32_t text)
{
if (WasmLane* lane = WasmLane::current())
{
lane->logConsole(env, text);
return;
}
consoleWrite(vmFromEnv(env), consoleString(env, text));
}
void consoleTime(wasm_exec_env_t env, uint32_t label)
{
if (WasmLane* lane = WasmLane::current())
{
lane->consoleTimer(env, label, false, false);
return;
}
consoleTimeStart(bootVmFromEnv(env), consoleString(env, label));
}
void consoleTimeLog(wasm_exec_env_t env, uint32_t label)
{
if (WasmLane* lane = WasmLane::current())
{
lane->consoleTimer(env, label, true, false);
return;
}
consoleTimeReport(bootVmFromEnv(env), consoleString(env, label), false);
}
void consoleTimeEnd(wasm_exec_env_t env, uint32_t label)
{
if (WasmLane* lane = WasmLane::current())
{
lane->consoleTimer(env, label, true, true);
return;
}
consoleTimeReport(bootVmFromEnv(env), consoleString(env, label), true);
}
void envTrace(
wasm_exec_env_t env,
uint32_t message,
int32_t count,
double a0,
double a1,
double a2,
double a3,
double a4)
{
double args[5] = {a0, a1, a2, a3, a4};
traceWrite(vmFromEnv(env), consoleString(env, message), count, args);
}
NativeSymbol kEnvNatives[] = {
{"console.log", (void*)consoleLog, "(i)", nullptr},
{"console.debug", (void*)consoleLog, "(i)", nullptr},
{"console.info", (void*)consoleLog, "(i)", nullptr},
{"console.warn", (void*)consoleLog, "(i)", nullptr},
{"console.error", (void*)consoleLog, "(i)", nullptr},
{"console.time", (void*)consoleTime, "(i)", nullptr},
{"console.timeLog", (void*)consoleTimeLog, "(i)", nullptr},
{"console.timeEnd", (void*)consoleTimeEnd, "(i)", nullptr},
{"trace", (void*)envTrace, "(iiFFFFF)", nullptr},
{"invoke_vii", (void*)invokeViiNative, "(iii)", nullptr},
{"seed", (void*)envSeed, "()F", 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
}
// The blob's libc asks for the environment at start and closes its streams
// at exit; there is no environment and the streams are the log.
uint32_t environSizesGet(wasm_exec_env_t env, uint32_t* count, uint32_t* size)
{
wasm_module_inst_t inst = wasm_runtime_get_module_inst(env);
if (!wasm_runtime_validate_native_addr(inst, count, 4) ||
!wasm_runtime_validate_native_addr(inst, size, 4))
{
return 21; // WASI EFAULT
}
*count = 0;
*size = 0;
return 0;
}
uint32_t environGet(wasm_exec_env_t env, uint32_t* environ, char* buffer)
{
return 0;
}
uint32_t fdClose(wasm_exec_env_t env, uint32_t fd) { return 0; }
NativeSymbol kWasiNatives[] = {
{"fd_write", (void*)fdWrite, "(i*i*)i", nullptr},
{"fd_seek", (void*)fdSeek, "(iiiii)i", nullptr},
{"environ_sizes_get", (void*)environSizesGet, "(**)i", nullptr},
{"environ_get", (void*)environGet, "(**)i", nullptr},
{"fd_close", (void*)fdClose, "(i)i", nullptr},
};
#endif
void deleteHostObject(
WasmScriptingVM* vm,
WasmScriptingVM::HandleTable::Tag tag,
void* object);
void releaseHostHandle(
WasmScriptingVM* vm,
uint32_t handle,
WasmScriptingVM::HandleTable::Tag tag)
{
void* object = vm != nullptr ? vm->handles().resolve(handle, tag) : nullptr;
if (object == nullptr)
{
return;
}
deleteHostObject(vm, tag, object);
vm->handles().release(handle, tag);
}
// --- rive_path/paint/renderer_v1: handle-backed render objects --------------
// The module's ModuleRenderPath mirror; geometry arrives through update and
// rebuilds the real render path.
// Size then fill: copies when it fits, always answers the full count.
template <typename T>
uint32_t fillOut(Span<const T> words, T* out, uint32_t outCount)
{
if (out != nullptr && words.size() <= outCount)
{
memcpy(out, words.data(), words.size() * sizeof(T));
}
return (uint32_t)words.size();
}
template <typename T>
uint32_t fillOut(const std::vector<T>& words, T* out, uint32_t outCount)
{
return fillOut(Span<const T>(words.data(), words.size()), out, outCount);
}
// A RawPath's points as the flat float stream the modules read.
Span<const float> pathFloats(const RawPath& path)
{
return Span<const float>(
(const float*)path.points().data(),
path.points().size() * 2);
}
Span<const uint8_t> pathVerbBytes(const RawPath& path)
{
return Span<const uint8_t>(
(const uint8_t*)path.verbs().data(),
path.verbs().size());
}
// The geometry stays host side so paths composed from other paths never
// cross back; the render path rebuilds lazily after a host side append.
struct HostPath
{
RawPath raw;
FillRule fillRule = FillRule::clockwise;
rcp<RenderPath> path;
bool renderDirty = false;
};
HostPath* resolvePath(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostPath*>(
vm->handles().resolve(handle, WasmScriptingVM::HandleTable::Tag::path));
}
void rebuildRenderPath(WasmScriptingVM* vm, HostPath* hostPath)
{
// makeRenderPath may take the arrays it is handed, and raw stays the
// geometry add and the readbacks use.
RawPath copy(hostPath->raw);
hostPath->path = vm->factory()->makeRenderPath(copy, hostPath->fillRule);
hostPath->renderDirty = false;
}
RenderPath* hostRenderPath(WasmScriptingVM* vm, HostPath* hostPath)
{
if (hostPath == nullptr)
{
return nullptr;
}
if (hostPath->renderDirty)
{
rebuildRenderPath(vm, hostPath);
}
return hostPath->path.get();
}
struct HostPaint
{
rcp<RenderPaint> paint;
};
// Enums cross as ordinals; one out of range raises as a bad name does in the
// Luau lane, since the renderer's switches treat other values as unreachable.
bool validOrdinal(
WasmScriptingVM* vm,
uint32_t value,
bool valid,
const char* type)
{
if (!valid)
{
char message[64];
snprintf(message, sizeof(message), "%u is not a valid %s", value, type);
vm->raiseModuleError(message);
}
return valid;
}
bool validBlendMode(WasmScriptingVM* vm, uint32_t value)
{
return validOrdinal(
vm,
value,
value == (uint32_t)BlendMode::srcOver ||
value == (uint32_t)BlendMode::additive ||
(value >= (uint32_t)BlendMode::screen &&
value <= (uint32_t)BlendMode::luminosity),
"BlendMode");
}
// Backends index a sampler table with the key.
bool validSamplerKey(WasmScriptingVM* vm, uint32_t key)
{
return validOrdinal(
vm,
key,
key < ImageSampler::MAX_SAMPLER_PERMUTATIONS,
"ImageSampler key");
}
// RawPath walkers trust a stream to open with a move and its verbs to take
// exactly its points, so guest geometry is checked before it becomes one.
bool guestRawPath(
WasmScriptingVM* vm,
const uint8_t* verbs,
uint32_t verbCount,
const float* points,
uint32_t floatCount,
RawPath* out)
{
uint64_t floats = 0;
bool valid = verbCount == 0 || verbs[0] == (uint8_t)PathVerb::move;
for (uint32_t i = 0; valid && i < verbCount; i++)
{
switch ((PathVerb)verbs[i])
{
case PathVerb::move:
case PathVerb::line:
floats += 2;
continue;
case PathVerb::quad:
floats += 4;
continue;
case PathVerb::cubic:
floats += 6;
continue;
case PathVerb::close:
continue;
}
valid = false;
}
if (!valid || floats != floatCount)
{
vm->raiseModuleError("Path: verbs and points do not match");
return false;
}
*out = RawPath(
Span<const PathVerb>((const PathVerb*)verbs, verbCount),
Span<const Vec2D>((const Vec2D*)points, floatCount / 2));
return true;
}
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 ||
!validOrdinal(
vm,
fillRule,
fillRule <= (uint32_t)FillRule::clockwise,
"FillRule") ||
!guestRawPath(vm, verbs, verbCount, points, floatCount, &hostPath->raw))
{
return;
}
hostPath->fillRule = (FillRule)fillRule;
rebuildRenderPath(vm, hostPath);
}
void pathAddImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t otherHandle,
float xx,
float xy,
float yx,
float yy,
float tx,
float ty)
{
auto hostPath = resolvePath(vm, handle);
auto other = resolvePath(vm, otherHandle);
if (hostPath == nullptr || other == nullptr)
{
return;
}
Mat2D transform(xx, xy, yx, yy, tx, ty);
hostPath->raw.addPath(other->raw, &transform);
hostPath->renderDirty = true;
}
uint32_t pathVerbsImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint8_t* out,
uint32_t outCount)
{
auto hostPath = resolvePath(vm, handle);
if (hostPath == nullptr)
{
return 0;
}
return fillOut(pathVerbBytes(hostPath->raw), out, outCount);
}
uint32_t pathPointsImpl(
WasmScriptingVM* vm,
uint32_t handle,
float* out,
uint32_t outCount)
{
auto hostPath = resolvePath(vm, handle);
if (hostPath == nullptr)
{
return 0;
}
return fillOut(pathFloats(hostPath->raw), out, outCount);
}
void pathEffectResultImpl(
WasmScriptingVM* vm,
const uint8_t* verbs,
uint32_t verbCount,
const float* points,
uint32_t floatCount)
{
RawPath rawPath;
if (vm == nullptr || vm->pathEffectOut() == nullptr ||
!guestRawPath(vm, verbs, verbCount, points, floatCount, &rawPath))
{
return;
}
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(
WasmScriptingVM* vm,
const uint8_t* verbs,
uint32_t verbCount,
const float* points,
uint32_t floatCount)
{
auto path = std::make_unique<RawPath>();
if (vm == nullptr ||
!guestRawPath(vm, verbs, verbCount, points, floatCount, path.get()))
{
return nullptr;
}
return path;
}
uint32_t measurePathNewImpl(
WasmScriptingVM* vm,
const uint8_t* verbs,
uint32_t verbCount,
const float* points,
uint32_t floatCount)
{
auto source = copyGeometry(vm, verbs, verbCount, points, floatCount);
if (source == nullptr)
{
return 0;
}
auto measure = new HostMeasure();
measure->source = std::move(source);
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)
{
auto source = copyGeometry(vm, verbs, verbCount, points, floatCount);
if (source == nullptr)
{
return 0;
}
auto measure = new HostMeasure();
measure->source = std::move(source);
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)
{
releaseHostHandle(vm, handle, WasmScriptingVM::HandleTable::Tag::measure);
}
void pathReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
releaseHostHandle(vm, 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)
{
releaseHostHandle(vm, 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)
{
auto paint = resolvePaint(vm, handle);
if (paint != nullptr && validOrdinal(
vm,
value,
value <= (uint32_t)StrokeJoin::bevel,
"StrokeJoin"))
{
paint->join((StrokeJoin)value);
}
}
void paintCapImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value)
{
auto paint = resolvePaint(vm, handle);
if (paint != nullptr && validOrdinal(
vm,
value,
value <= (uint32_t)StrokeCap::square,
"StrokeCap"))
{
paint->cap((StrokeCap)value);
}
}
void paintBlendModeImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value)
{
auto paint = resolvePaint(vm, handle);
if (paint != nullptr && validBlendMode(vm, value))
{
// As ScriptedPaint::blendMode in the Luau lane: scripts have no amount.
paint->blendMode((BlendMode)value);
paint->additiveness(additivenessFor((BlendMode)value, 255));
}
}
void paintFeatherImpl(WasmScriptingVM* vm, uint32_t handle, float value)
{
if (auto paint = resolvePaint(vm, handle))
{
paint->feather(value);
}
}
struct HostShader
{
rcp<RenderShader> shader;
};
// Both gradients read count colors and count stops from the module.
bool resolveGradientStops(
WasmScriptingVM* vm,
uint32_t colorsPtr,
uint32_t stopsPtr,
uint32_t count,
const ColorInt*& colors,
const float*& stops)
{
// A larger count wraps the byte size, which then passes the bounds check.
if (count > UINT32_MAX / 4)
{
return false;
}
colors = (const ColorInt*)vm->resolveModulePtr(colorsPtr, count * 4);
stops = (const float*)vm->resolveModulePtr(stopsPtr, count * 4);
return colors != nullptr && stops != nullptr;
}
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;
}
const ColorInt* colors = nullptr;
const float* stops = nullptr;
if (!resolveGradientStops(vm, colorsPtr, stopsPtr, count, colors, stops))
{
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;
}
const ColorInt* colors = nullptr;
const float* stops = nullptr;
if (!resolveGradientStops(vm, colorsPtr, stopsPtr, count, colors, stops))
{
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)
{
releaseHostHandle(vm, 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);
}
void paintShaderTransformImpl(
WasmScriptingVM* vm,
uint32_t paintHandle,
const float* values,
uint32_t floatCount)
{
if (values == nullptr || floatCount < 6)
{
return;
}
if (auto paint = resolvePaint(vm, paintHandle))
{
paint->shaderTransform(Mat2D(
values[0],
values[1],
values[2],
values[3],
values[4],
values[5]));
}
}
// 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.
FileAsset* findFileAsset(
WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t length,
uint16_t typeKey,
bool (*accept)(FileAsset*))
{
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->isTypeOf(typeKey) || asset->name() != key)
{
continue;
}
if (accept == nullptr || accept(asset.get()))
{
return asset.get();
}
}
return nullptr;
}
template <typename T>
T* findFileAsset(
WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t length,
bool (*accept)(FileAsset*) = nullptr)
{
return static_cast<T*>(
findFileAsset(vm, objectHandle, name, length, T::typeKey, accept));
}
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)
{
releaseHostHandle(vm, 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;
// setBindGroup calls made before the first setPipeline.
ore::DeferredBindGroups deferredBindGroups;
ore::DrawBindings drawBindings;
bool pipelineSet = false;
};
struct HostGpuBuffer
{
rcp<ore::Buffer> buffer;
// Ore allows no update to an immutable buffer.
bool immutable;
};
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;
}
template <typename E>
bool validOre(WasmScriptingVM* vm, uint32_t value, E last, const char* type)
{
return validOrdinal(vm, value, value <= (uint32_t)last, type);
}
bool guardPassed(WasmScriptingVM* vm, bool passed, const std::string& error)
{
if (!passed)
{
vm->raiseModuleError(error.c_str());
}
return passed;
}
uint32_t gpuCanvasNewImpl(WasmScriptingVM* vm, uint32_t width, uint32_t height)
{
if (vm == nullptr || vm->factory() == nullptr)
{
return 0;
}
// Like the Luau lane, a zero size gives a canvas without backing that
// resize allocates once the layout size is known.
if (width == 0 || height == 0)
{
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::gpuCanvas,
new HostGpuCanvas{});
}
auto* renderContext = static_cast<gpu::RenderContext*>(
vm->factory()->renderContext());
ore::Context* oreContext = gpuOreContext(vm);
if (renderContext == nullptr || oreContext == nullptr)
{
return 0;
}
auto* deferredHost = vm->factory()->deferredCanvasHost();
auto canvas = deferredHost != nullptr
? deferredHost->makeContentCanvas(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)
{
releaseHostHandle(vm, 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[2] = (uint32_t)oreContext->canvasTargetFormat();
props[3] = 1;
if (host->canvas == nullptr)
{
props[0] = 0;
props[1] = 0;
return 0;
}
props[0] = host->canvas->width();
props[1] = host->canvas->height();
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* deferredHost = vm->factory()->deferredCanvasHost();
auto canvas = deferredHost != nullptr
? deferredHost->makeContentCanvas(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 gpuTargetViewImpl(
WasmScriptingVM* vm,
uint32_t current,
uint32_t* props,
uint32_t propCount)
{
ore::Context* oreContext = gpuOreContext(vm);
// Props stay unwritten where no host could ever expose a target.
if (vm == nullptr || oreContext == nullptr || !oreContext->isRecording() ||
!oreContext->exposesTarget() || propCount < 4)
{
return 0;
}
rcp<ore::TextureView> view = oreContext->targetView();
if (view == nullptr)
{
props[0] = 0;
props[1] = 0;
props[2] = (uint32_t)ore::TextureFormat::rgba8unorm;
props[3] = 1;
return 0;
}
props[0] = view->width();
props[1] = view->height();
props[2] = (uint32_t)view->texture()->format();
props[3] = view->texture()->sampleCount();
auto host = static_cast<HostGpuTextureView*>(vm->handles().resolve(
current,
WasmScriptingVM::HandleTable::Tag::gpuTextureView));
if (host != nullptr && host->view == view)
{
return current;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::gpuTextureView,
new HostGpuTextureView{std::move(view)});
}
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;
// Guests built before the alignment field ask for 19.
if (outCount < 20)
{
return 19;
}
out[19] = f.minUniformBufferOffsetAlignment;
return 20;
}
uint32_t gpuPassBeginImpl(
WasmScriptingVM* vm,
const rive_gpu_pass_desc_v1* podDesc,
uint32_t descByteCount,
const uint8_t* blob,
uint32_t blobCount)
{
ore::Context* oreContext = gpuOreContext(vm);
// The label is optional, so a descriptor from before it still passes.
if (vm == nullptr || oreContext == nullptr ||
descByteCount < offsetof(rive_gpu_pass_desc_v1, labelSize) ||
podDesc->colorCount > 4)
{
return 0;
}
const uint32_t labelSize = descByteCount >= sizeof(*podDesc)
? podDesc->labelSize
: 0;
const uint64_t colorBytes = uint64_t(podDesc->colorCount) *
sizeof(rive_gpu_pass_color_attachment_v1);
if (colorBytes + labelSize != blobCount)
{
return 0;
}
auto colors = reinterpret_cast<const rive_gpu_pass_color_attachment_v1*>(
blob);
std::string label;
if (labelSize != 0)
{
label.assign(reinterpret_cast<const char*>(blob + colorBytes), labelSize);
}
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;
if (!validOre(vm, colors[i].loadOp, ore::LoadOp::dontCare, "LoadOp") ||
!validOre(vm, colors[i].storeOp, ore::StoreOp::discard, "StoreOp"))
{
return 0;
}
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;
}
}
if (!validOre(vm, podDesc->depthLoadOp, ore::LoadOp::dontCare, "LoadOp") ||
!validOre(vm, podDesc->depthStoreOp, ore::StoreOp::discard, "StoreOp") ||
!validOre(vm, podDesc->stencilLoadOp, ore::LoadOp::dontCare, "LoadOp") ||
!validOre(vm, podDesc->stencilStoreOp, ore::StoreOp::discard, "StoreOp"))
{
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;
desc.label = label.empty() ? nullptr : label.c_str();
oreContext->clearLastError();
auto pass = ore::cmd::beginRecordedRenderPass(*oreContext, desc);
if (pass == nullptr)
{
// A pass refused without an error drops, like one with no target.
return oreContext->lastError().empty()
? 0
: gpuRejected(vm, oreContext, "beginRenderPass");
}
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 (host->sourceCanvas != nullptr)
{
view = oreContext->recordWrapCanvasImage(
host->sourceCanvas.get(),
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
{
// A foreign image the recorder was handed rather than created.
view = oreContext->recordWrapForeignImageView(
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;
}
if (std::string err; !host->drawBindings.validate(&err))
{
vm->raiseModuleError(err.c_str());
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)
{
// The pass resolves first: it is null with no VM, as during module start.
auto* host = resolveHostPass(vm, passHandle);
if (host == nullptr)
{
return;
}
auto pipeline = static_cast<HostGpuPipeline*>(vm->handles().resolve(
pipelineHandle,
WasmScriptingVM::HandleTable::Tag::gpuPipeline));
if (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());
host->deferredBindGroups.flush(host->pass.get());
if (oreContext != nullptr && !oreContext->lastError().empty())
{
gpuRejected(vm, oreContext, "setPipeline");
return;
}
host->drawBindings.setPipeline(pipeline->pipeline.get());
host->pipelineSet = true;
}
void gpuPassSetVertexBufferImpl(
WasmScriptingVM* vm,
uint32_t passHandle,
uint32_t slot,
uint32_t bufferHandle,
uint32_t offset)
{
auto* pass = resolvePass(vm, passHandle);
if (pass == nullptr)
{
return;
}
auto buffer = static_cast<HostGpuBuffer*>(vm->handles().resolve(
bufferHandle,
WasmScriptingVM::HandleTable::Tag::gpuBuffer));
if (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);
if (pass == nullptr)
{
return;
}
auto buffer = static_cast<HostGpuBuffer*>(vm->handles().resolve(
bufferHandle,
WasmScriptingVM::HandleTable::Tag::gpuBuffer));
if (buffer == nullptr ||
!validOrdinal(
vm,
indexFormat,
indexFormat == (uint32_t)ore::IndexFormat::uint16 ||
indexFormat == (uint32_t)ore::IndexFormat::uint32,
"IndexFormat"))
{
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* host = resolveHostPass(vm, passHandle);
if (host == nullptr || host->pass == nullptr)
{
return;
}
auto bindGroup = static_cast<HostGpuBindGroup*>(vm->handles().resolve(
bindGroupHandle,
WasmScriptingVM::HandleTable::Tag::gpuBindGroup));
if (bindGroup == nullptr || dynamicOffsetByteCount % sizeof(uint32_t) != 0)
{
return;
}
uint32_t count = dynamicOffsetByteCount / (uint32_t)sizeof(uint32_t);
// The checks the Luau binding makes, so a misuse raises on the call
// instead of reaching a backend assert or a silent misbind.
ore::Context* oreContext = gpuOreContext(vm);
if (std::string err; !ore::validateSetBindGroup(
groupIndex,
bindGroup->bindGroup.get(),
dynamicOffsets,
count,
ore::scriptDynamicOffsetAlignment(oreContext),
&err))
{
vm->raiseModuleError(err.c_str());
return;
}
if (!host->pipelineSet)
{
host->deferredBindGroups
.defer(groupIndex, bindGroup->bindGroup.get(), dynamicOffsets, count);
host->drawBindings.setBindGroup(groupIndex, bindGroup->bindGroup.get());
return;
}
if (oreContext != nullptr)
{
oreContext->clearLastError();
}
host->pass->setBindGroup(
groupIndex,
bindGroup->bindGroup.get(),
count != 0 ? dynamicOffsets : nullptr,
count);
if (oreContext != nullptr && !oreContext->lastError().empty())
{
gpuRejected(vm, oreContext, "setBindGroup");
return;
}
host->drawBindings.setBindGroup(groupIndex, bindGroup->bindGroup.get());
}
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();
}
host->drawBindings = {};
}
void gpuPassReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
releaseHostHandle(vm, 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) ||
!validOre(vm, usage, ore::BufferUsage::uniform, "BufferUsage"))
{
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), desc.immutable});
}
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 || host->immutable || dataCount == 0 ||
uint64_t(dstOffset) + dataCount > host->buffer->size())
{
return;
}
host->buffer->update(data, dataCount, dstOffset);
}
void gpuBufferUpdateBlobImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t dstOffset,
uint32_t blob,
uint32_t srcOffset,
uint32_t byteCount)
{
if (vm == nullptr)
{
return;
}
// The lookup holds the blob through the update, which copies the bytes
// into the buffer or the recording before it returns.
auto found = SharedRegistry::find<SharedBlob>(blob, vm->sharedOwner());
if (found == nullptr || uint64_t(srcOffset) + byteCount > found->size())
{
return;
}
gpuBufferUpdateImpl(
vm,
handle,
dstOffset,
found->bytes() + srcOffset,
byteCount);
}
uint32_t gpuBufferNewBlobImpl(
WasmScriptingVM* vm,
uint32_t usage,
uint32_t immutable,
uint32_t blob,
uint32_t srcOffset,
uint32_t byteCount)
{
if (vm == nullptr)
{
return 0;
}
// The lookup holds the blob while the create copies its bytes.
auto found = SharedRegistry::find<SharedBlob>(blob, vm->sharedOwner());
if (found == nullptr || uint64_t(srcOffset) + byteCount > found->size())
{
return 0;
}
return gpuBufferNewImpl(
vm,
usage,
byteCount,
immutable,
found->bytes() + srcOffset,
byteCount);
}
void gpuBufferReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
releaseHostHandle(vm, 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);
std::string error;
if (oreContext == nullptr || descByteCount < sizeof(*podDesc) ||
!validOre(
vm,
podDesc->format,
ore::TextureFormat::astc8x8,
"TextureFormat") ||
!validOre(
vm,
podDesc->textureType,
ore::TextureType::array2D,
"TextureType") ||
!guardPassed(
vm,
ore::validateSampleCount(oreContext, podDesc->sampleCount, &error),
error) ||
!guardPassed(
vm,
ore::validateTextureFormat(
oreContext,
(ore::TextureFormat)podDesc->format,
&error),
error))
{
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)
{
releaseHostHandle(vm, 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) ||
!validOre(vm, podDesc->minFilter, ore::Filter::linear, "Filter") ||
!validOre(vm, podDesc->magFilter, ore::Filter::linear, "Filter") ||
!validOre(vm, podDesc->mipmapFilter, ore::Filter::linear, "Filter") ||
!validOre(vm, podDesc->wrapU, ore::WrapMode::clampToEdge, "WrapMode") ||
!validOre(vm, podDesc->wrapV, ore::WrapMode::clampToEdge, "WrapMode") ||
!validOre(vm, podDesc->wrapW, ore::WrapMode::clampToEdge, "WrapMode") ||
(podDesc->compare != 0xFFFFFFFFu && !validOre(
vm,
podDesc->compare,
ore::CompareFunction::always,
"CompareFunction")))
{
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;
std::string error;
if (!guardPassed(
vm,
ore::validateSamplerDesc(oreContext, desc, &error),
error))
{
return 0;
}
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)
{
releaseHostHandle(vm, 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;
}
if (!validOre(
vm,
podDesc->dimension,
ore::TextureViewDimension::cubeArray,
"TextureViewDimension") ||
!validOre(
vm,
podDesc->aspect,
ore::TextureAspect::stencilOnly,
"TextureAspect"))
{
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)
{
releaseHostHandle(
vm,
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 mintShaderModule(
WasmScriptingVM* vm,
ore::Context* oreContext,
const ore::ShaderModuleDesc& desc)
{
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)});
}
// The module serving entry, built as every lane builds an asset's modules.
static uint32_t mintAssetEntryModule(
WasmScriptingVM* vm,
ore::Context* oreContext,
const ShaderAsset& asset,
uint32_t entry)
{
uint32_t handle = 0;
ore::visitShaderAssetModules(
oreContext->shaderTarget(),
asset,
[&](const ore::ShaderModuleDesc& desc,
uint32_t first,
uint32_t count,
const std::vector<ore::RstbEntryView>&) {
if (entry < first || entry - first >= count)
{
return true;
}
handle = mintShaderModule(vm, oreContext, desc);
return false;
});
return handle;
}
#ifdef WITH_RIVE_TOOLS
// The same from a bare RSTB, which only edit time hosts take.
static uint32_t mintRstbEntryModule(
WasmScriptingVM* vm,
ore::Context* oreContext,
const uint8_t* rstb,
uint32_t rstbCount,
uint32_t assetId,
uint32_t entry)
{
ShaderAsset asset;
if (!ore::decodeBareRstb(asset, rstb, rstbCount))
{
return 0;
}
asset.assetId(assetId);
return mintAssetEntryModule(vm, oreContext, asset, entry);
}
#endif
uint32_t gpuShaderModuleFromAssetImpl(
WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t nameLength,
uint32_t entry)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr)
{
return 0;
}
const ShaderAsset* fileAsset = findFileAsset<ShaderAsset>(
vm,
objectHandle,
name,
nameLength);
#ifdef WITH_RIVE_TOOLS
// Editor path first, matching shader_asset_bytes.
if (const std::vector<uint8_t>* rstb = vm->findShaderRstb(
std::string(name, nameLength)))
{
return mintRstbEntryModule(
vm,
oreContext,
rstb->data(),
(uint32_t)rstb->size(),
fileAsset != nullptr ? fileAsset->assetId() : 0,
entry);
}
#endif
if (fileAsset == nullptr)
{
return 0;
}
return mintAssetEntryModule(vm, oreContext, *fileAsset, entry);
}
#ifdef WITH_RIVE_TOOLS
uint32_t gpuShaderModuleFromRstbImpl(
WasmScriptingVM* vm,
const uint8_t* rstb,
uint32_t rstbCount,
uint32_t assetId,
uint32_t entry)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr)
{
return 0;
}
return mintRstbEntryModule(vm, oreContext, rstb, rstbCount, assetId, entry);
}
#endif
void gpuShaderModuleReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
releaseHostHandle(
vm,
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];
if (!validOre(
vm,
in.kind,
ore::BindingKind::comparisonSampler,
"BindingKind") ||
!validOrdinal(
vm,
in.visibility,
in.visibility <= (ore::StageVisibility::kVertex |
ore::StageVisibility::kFragment |
ore::StageVisibility::kCompute),
"StageVisibility") ||
!validOre(
vm,
in.textureViewDim,
ore::TextureViewDimension::cubeArray,
"TextureViewDimension") ||
!validOre(
vm,
in.textureSampleType,
ore::BindGroupLayoutEntry::SampleType::uint,
"SampleType"))
{
return 0;
}
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)
{
releaseHostHandle(
vm,
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 gpuBindGroupLayoutFromShadersImpl(
WasmScriptingVM* vm,
uint32_t vertexModule,
uint32_t fragmentModule,
uint32_t groupIndex,
const uint32_t* dynamicUBOs,
uint32_t dynamicUBOCount)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr)
{
return 0;
}
auto resolveModule = [vm](uint32_t handle) -> ore::ShaderModule* {
auto host = static_cast<HostGpuShaderModule*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::gpuShaderModule));
return host != nullptr ? host->shaderModule.get() : nullptr;
};
ore::ShaderModule* vertex = resolveModule(vertexModule);
ore::ShaderModule* fragment = resolveModule(fragmentModule);
if (vertex == nullptr || (fragmentModule != 0 && fragment == nullptr))
{
return 0;
}
auto layout = ore::makeBindGroupLayoutFromBindingMap(
*oreContext,
ore::bindingMapForStages(vertex, fragment),
groupIndex,
dynamicUBOs,
dynamicUBOCount,
vertex,
fragment);
if (layout == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::gpuBindGroupLayout,
new HostGpuBindGroupLayout{std::move(layout)});
}
uint32_t gpuBindGroupLayoutFromEntriesImpl(
WasmScriptingVM* vm,
uint32_t vertexModule,
const char* vertexEntry,
uint32_t vertexEntryLength,
uint32_t fragmentModule,
const char* fragmentEntry,
uint32_t fragmentEntryLength,
uint32_t groupIndex)
{
ore::Context* oreContext = gpuOreContext(vm);
if (oreContext == nullptr)
{
return 0;
}
auto resolveModule = [vm](uint32_t handle) -> ore::ShaderModule* {
auto host = static_cast<HostGpuShaderModule*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::gpuShaderModule));
return host != nullptr ? host->shaderModule.get() : nullptr;
};
ore::ShaderModule* vertex = resolveModule(vertexModule);
ore::ShaderModule* fragment = resolveModule(fragmentModule);
if (vertex == nullptr || (fragmentModule != 0 && fragment == nullptr))
{
return 0;
}
const std::string vertexName(vertexEntry, vertexEntryLength);
const std::string fragmentName(fragmentEntry, fragmentEntryLength);
const ore::BindingMap map = ore::bindingMapForEntries(
vertex,
vertexName.c_str(),
fragment,
fragmentName.c_str());
bool bound = false;
for (size_t i = 0; i < map.size() && !bound; ++i)
{
bound = map.at(i).group == groupIndex;
}
if (!bound)
{
return 0;
}
auto layout = ore::makeBindGroupLayoutFromBindingMap(
*oreContext,
map,
groupIndex,
nullptr,
0,
vertex,
fragment,
vertexName.c_str(),
fragmentName.c_str());
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)
{
releaseHostHandle(
vm,
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));
if (vertexModule == nullptr)
{
vm->raiseModuleError("GPUPipeline.new: 'vertex' is required");
return 0;
}
desc.vertexModule = vertexModule->shaderModule.get();
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++)
{
const auto& in = colorTargets[i];
if (!validOre(
vm,
in.format,
ore::TextureFormat::astc8x8,
"TextureFormat") ||
!validOre(
vm,
in.srcColor,
ore::BlendFactor::oneMinusBlendColor,
"BlendFactor") ||
!validOre(
vm,
in.dstColor,
ore::BlendFactor::oneMinusBlendColor,
"BlendFactor") ||
!validOre(vm, in.colorOp, ore::BlendOp::max, "BlendOp") ||
!validOre(
vm,
in.srcAlpha,
ore::BlendFactor::oneMinusBlendColor,
"BlendFactor") ||
!validOre(
vm,
in.dstAlpha,
ore::BlendFactor::oneMinusBlendColor,
"BlendFactor") ||
!validOre(vm, in.alphaOp, ore::BlendOp::max, "BlendOp") ||
!validOre(vm, in.writeMask, ore::ColorWriteMask::all, "ColorWriteMask"))
{
return 0;
}
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++)
{
if (!validOre(
vm,
attributePods[i].format,
ore::VertexFormat::uint32,
"VertexFormat"))
{
return 0;
}
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 ||
!validOre(
vm,
bufferPods[i].stepMode,
ore::VertexStepMode::instance,
"VertexStepMode"))
{
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;
std::string error;
if (!validOre(
vm,
podDesc->topology,
ore::PrimitiveTopology::triangleStrip,
"PrimitiveTopology") ||
!validOre(
vm,
podDesc->indexFormat,
ore::IndexFormat::uint32,
"IndexFormat") ||
!validOre(vm, podDesc->cullMode, ore::CullMode::back, "CullMode") ||
!validOre(
vm,
podDesc->winding,
ore::FaceWinding::counterClockwise,
"FaceWinding") ||
!validOre(
vm,
podDesc->depthFormat,
ore::TextureFormat::astc8x8,
"TextureFormat") ||
!validOre(
vm,
podDesc->depthCompare,
ore::CompareFunction::always,
"CompareFunction") ||
!validOre(
vm,
podDesc->stencilFrontCompare,
ore::CompareFunction::always,
"CompareFunction") ||
!validOre(
vm,
podDesc->stencilFrontFailOp,
ore::StencilOp::decrementWrap,
"StencilOp") ||
!validOre(
vm,
podDesc->stencilFrontDepthFailOp,
ore::StencilOp::decrementWrap,
"StencilOp") ||
!validOre(
vm,
podDesc->stencilFrontPassOp,
ore::StencilOp::decrementWrap,
"StencilOp") ||
!validOre(
vm,
podDesc->stencilBackCompare,
ore::CompareFunction::always,
"CompareFunction") ||
!validOre(
vm,
podDesc->stencilBackFailOp,
ore::StencilOp::decrementWrap,
"StencilOp") ||
!validOre(
vm,
podDesc->stencilBackDepthFailOp,
ore::StencilOp::decrementWrap,
"StencilOp") ||
!validOre(
vm,
podDesc->stencilBackPassOp,
ore::StencilOp::decrementWrap,
"StencilOp") ||
!guardPassed(
vm,
ore::validateSampleCount(oreContext, podDesc->sampleCount, &error),
error) ||
!guardPassed(
vm,
ore::validateVertexLayout(oreContext, desc, &error),
error))
{
return 0;
}
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)
{
releaseHostHandle(vm, 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 gpuTargetViewImpl(WasmScriptingVM*, 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 uint8_t*,
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 gpuBufferUpdateBlobImpl(
WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
uint32_t,
uint32_t)
{}
uint32_t gpuBufferNewBlobImpl(
WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
uint32_t,
uint32_t)
{
return 0;
}
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 gpuShaderModuleFromAssetImpl(
WasmScriptingVM*,
uint32_t,
const char*,
uint32_t,
uint32_t)
{
return 0;
}
#ifdef WITH_RIVE_TOOLS
uint32_t gpuShaderModuleFromRstbImpl(
WasmScriptingVM*,
const uint8_t*,
uint32_t,
uint32_t,
uint32_t)
{
return 0;
}
#endif
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 gpuBindGroupLayoutFromShadersImpl(
WasmScriptingVM*,
uint32_t,
uint32_t,
uint32_t,
const uint32_t*,
uint32_t)
{
return 0;
}
uint32_t gpuBindGroupLayoutFromEntriesImpl(
WasmScriptingVM*,
uint32_t,
const char*,
uint32_t,
uint32_t,
const char*,
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
// A renderer the module draws through, with the saves it opened in the scope
// in flight; it may restore only those, never the host's.
struct HostRenderer
{
Renderer* renderer = nullptr;
int saves = 0;
// A trapped call returns with its saves open, and hosts reuse renderers
// frame to frame.
void restoreSaves()
{
for (; saves > 0; saves--)
{
renderer->restore();
}
}
};
HostRenderer* resolveHostRenderer(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostRenderer*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::renderer));
}
#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;
HostRenderer frameRenderer;
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.
auto* deferredHost = vm->factory()->deferredCanvasHost();
rcp<gpu::RenderCanvas> canvas = deferredHost != nullptr
? deferredHost->makeContentCanvas(
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->frameRenderer = {renderer};
host->rendererHandle = vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::renderer,
&host->frameRenderer);
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->frameRenderer.restoreSaves();
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;
bool spent = false;
};
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 ||
!validOrdinal(
vm,
bufferType,
bufferType <= (uint32_t)RenderBufferType::vertex,
"RenderBufferType") ||
!validOrdinal(
vm,
flags,
flags <= (uint32_t)RenderBufferFlags::mappedOnceAtInitialization,
"RenderBufferFlags"))
{
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;
}
if (host->spent)
{
vm->raiseModuleError(
"RenderBuffer: a buffer mapped once at initialization takes one "
"update");
return;
}
host->spent = enums::is_flag_set(
host->buffer->flags(),
RenderBufferFlags::mappedOnceAtInitialization);
void* data = host->buffer->map();
if (data != nullptr)
{
memcpy(data, bytes, byteCount);
host->buffer->unmap();
}
}
void bufferReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
releaseHostHandle(vm, handle, WasmScriptingVM::HandleTable::Tag::buffer);
}
struct HostMeshInstances
{
rcp<ImageMeshInstances> instances;
};
HostMeshInstances* resolveMeshInstances(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostMeshInstances*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::meshInstances));
}
uint32_t meshInstancesNewImpl(WasmScriptingVM* vm, uint32_t count)
{
if (vm == nullptr)
{
return 0;
}
auto instances = vm->factory()->makeImageMeshInstances(count);
if (instances == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::meshInstances,
new HostMeshInstances{std::move(instances)});
}
void meshInstancesResizeImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t count)
{
auto host = resolveMeshInstances(vm, handle);
if (host == nullptr)
{
return;
}
host->instances->edit(count);
host->instances->endEdit();
}
void meshInstancesUpdateImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t first,
const uint8_t* bytes,
uint32_t byteCount)
{
auto host = resolveMeshInstances(vm, handle);
if (host == nullptr || byteCount % sizeof(ImageMeshInstanceData) != 0)
{
return;
}
size_t count = byteCount / sizeof(ImageMeshInstanceData);
Span<ImageMeshInstanceData> data = host->instances->edit();
if (first <= data.size() && count <= data.size() - first)
{
memcpy(data.data() + first, bytes, byteCount);
}
host->instances->endEdit();
}
void meshInstancesReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
releaseHostHandle(
vm,
handle,
WasmScriptingVM::HandleTable::Tag::meshInstances);
}
Renderer* resolveRenderer(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveHostRenderer(vm, handle);
return host != nullptr ? host->renderer : nullptr;
}
uint32_t testBlobImpl(
WasmScriptingVM* vm,
const char* name,
uint32_t nameLength,
uint8_t* out,
uint32_t outCount)
{
if (vm == nullptr)
{
return 0;
}
return fillOut(vm->testBlob(std::string(name, nameLength)), out, outCount);
}
// A transition child handle points at the caller's TransitionChildRef and
// is released when the call returns.
const ScriptBackend::TransitionChildRef* resolveTransitionChild(
WasmScriptingVM* vm,
uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<const ScriptBackend::TransitionChildRef*>(
vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::transitionChild));
}
void transitionChildDrawImpl(
WasmScriptingVM* vm,
uint32_t child,
uint32_t rendererHandle)
{
auto ref = resolveTransitionChild(vm, child);
Renderer* renderer = resolveRenderer(vm, rendererHandle);
if (ref != nullptr && renderer != nullptr)
{
ref->draw(renderer);
}
}
float transitionChildWidthImpl(WasmScriptingVM* vm, uint32_t child)
{
auto ref = resolveTransitionChild(vm, child);
return ref != nullptr ? ref->width() : 0.0f;
}
float transitionChildHeightImpl(WasmScriptingVM* vm, uint32_t child)
{
auto ref = resolveTransitionChild(vm, child);
return ref != nullptr ? ref->height() : 0.0f;
}
void rendererSaveImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (auto host = resolveHostRenderer(vm, handle))
{
host->renderer->save();
host->saves++;
}
}
void rendererRestoreImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveHostRenderer(vm, handle);
if (host == nullptr)
{
return;
}
if (host->saves == 0)
{
vm->raiseModuleError("Renderer save/restore stack was unbalanced by "
"trying to restore more times than saved.");
return;
}
host->renderer->restore();
host->saves--;
}
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;
}
RenderPath* path = hostRenderPath(vm, resolvePath(vm, pathHandle));
if (path == nullptr)
{
return;
}
renderer->drawPath(path, 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 ||
!validSamplerKey(vm, samplerKey) || !validBlendMode(vm, blend))
{
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 || !validSamplerKey(vm, samplerKey) ||
!validBlendMode(vm, blend))
{
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 rendererDrawImageMeshInstancedImpl(
WasmScriptingVM* vm,
uint32_t rendererHandle,
uint32_t imageHandle,
uint32_t samplerKey,
uint32_t vertexHandle,
uint32_t uvHandle,
uint32_t indexHandle,
uint32_t instancesHandle)
{
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);
auto instances = resolveMeshInstances(vm, instancesHandle);
if (renderer == nullptr || image == nullptr || vertex == nullptr ||
uv == nullptr || index == nullptr || instances == nullptr ||
!validSamplerKey(vm, samplerKey))
{
return;
}
renderer->drawImageMeshInstanced(
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)),
instances->instances);
}
void rendererClipPathImpl(
WasmScriptingVM* vm,
uint32_t rendererHandle,
uint32_t pathHandle)
{
auto renderer = resolveRenderer(vm, rendererHandle);
if (vm == nullptr || renderer == nullptr)
{
return;
}
RenderPath* path = hostRenderPath(vm, resolvePath(vm, pathHandle));
if (path == nullptr)
{
return;
}
renderer->clipPath(path);
}
void rendererModulateOpacityImpl(
WasmScriptingVM* vm,
uint32_t rendererHandle,
float opacity)
{
auto renderer = resolveRenderer(vm, rendererHandle);
if (renderer != nullptr)
{
renderer->modulateOpacity(opacity);
}
}
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)
{
vm->reportError(budgetExceededLine((int)ms));
// Terminates like a trap when the native returns, so the caller's
// failed-op handling engages.
vm->raiseBudgetExceeded();
}
void rtErrorImpl(WasmScriptingVM* vm, const char* message, uint32_t length)
{
vm->raiseThrown(std::string(message, length).c_str());
}
// Unfused multiply-add keeps these bit identical to the module's own f32
// math, on every tier and platform.
#pragma clang fp contract(off)
void mat4MultiplyImpl(
WasmScriptingVM*,
uint8_t* outBytes,
uint32_t outByteCount,
const uint8_t* aBytes,
uint32_t aByteCount,
const uint8_t* bBytes,
uint32_t bByteCount)
{
if (outByteCount < 64 || aByteCount < 64 || bByteCount < 64)
{
return;
}
float a[16];
float b[16];
memcpy(a, aBytes, sizeof(a));
memcpy(b, bBytes, sizeof(b));
float result[16];
for (int col = 0; col < 4; col++)
{
for (int row = 0; row < 4; row++)
{
result[col * 4 + row] = a[row] * b[col * 4] +
a[4 + row] * b[col * 4 + 1] +
a[8 + row] * b[col * 4 + 2] +
a[12 + row] * b[col * 4 + 3];
}
}
memcpy(outBytes, result, sizeof(result));
}
uint32_t mat4InvertImpl(
WasmScriptingVM*,
uint8_t* outBytes,
uint32_t outByteCount,
const uint8_t* srcBytes,
uint32_t srcByteCount)
{
if (outByteCount < 64 || srcByteCount < 64)
{
return 0;
}
float m[16];
memcpy(m, srcBytes, sizeof(m));
float inv[16];
inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] +
m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10];
inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] -
m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10];
inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] +
m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9];
inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] -
m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9];
inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] -
m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10];
inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] +
m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10];
inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] -
m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9];
inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] +
m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9];
inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] +
m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6];
inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] -
m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6];
inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] +
m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5];
inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] -
m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5];
inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] -
m[5] * m[3] * m[10] - m[9] * m[2] * m[7] + m[9] * m[3] * m[6];
inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] +
m[4] * m[3] * m[10] + m[8] * m[2] * m[7] - m[8] * m[3] * m[6];
inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] -
m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5];
inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] +
m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5];
float det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];
if (det == 0)
{
return 0;
}
float detInv = 1.0f / det;
for (int i = 0; i < 16; i++)
{
inv[i] *= detInv;
}
memcpy(outBytes, inv, sizeof(inv));
return 1;
}
#pragma clang fp contract(on)
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);
return fillModuleString(vm, std::string(name, length), buffer, capacity);
}
// 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 final : public ViewModelInstanceValueDelegate
{
WasmScriptingVM* vm = nullptr;
uint32_t token = 0;
void valueChanged() override { vm->notifyDataValueChanged(token); }
};
struct HostInstanceValue
{
// Values and list items point back at their instance raw, so the handle
// keeps it alive.
rcp<ViewModelInstance> owner;
rcp<ViewModelInstanceValue> value;
HostValueDelegate* delegate = nullptr;
// What string_get last handed the module, for string_changed.
std::string sent;
bool hasSent = false;
explicit HostInstanceValue(ViewModelInstanceValue* instanceValue)
: owner(ref_rcp(instanceValue->viewModelInstance()))
, value(ref_rcp(instanceValue))
{}
~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));
auto instance = object != nullptr && object->dataContext() != nullptr
? object->dataContext()->mainViewModelInstance()
: nullptr;
if (instance == nullptr)
{
vm->noteMissingRequestedData();
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{std::move(instance)});
}
void dataVmiReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
releaseHostHandle(
vm,
handle,
WasmScriptingVM::HandleTable::Tag::viewModelInstance);
}
uint32_t mintInstanceValue(
WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length,
uint16_t typeKey)
{
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->isTypeOf(typeKey))
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::instanceValue,
new HostInstanceValue(value));
}
template <typename T>
uint32_t mintInstanceValue(
WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
return mintInstanceValue(vm, vmiHandle, name, length, T::typeKey);
}
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)
{
releaseHostHandle(
vm,
handle,
WasmScriptingVM::HandleTable::Tag::instanceValue);
}
// A ref, so the value outlives a native whose change notifies listeners that
// release its last handle.
rcp<ViewModelInstanceValue> resolveInstanceValue(
WasmScriptingVM* vm,
uint32_t handle,
uint16_t typeKey)
{
if (vm == nullptr)
{
return nullptr;
}
auto host = static_cast<HostInstanceValue*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::instanceValue));
if (host == nullptr || !host->value->isTypeOf(typeKey))
{
return nullptr;
}
return host->value;
}
template <typename T>
rcp<T> resolveInstanceValue(WasmScriptingVM* vm, uint32_t handle)
{
return static_rcp_cast<T>(resolveInstanceValue(vm, handle, T::typeKey));
}
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;
}
uint32_t length = fillModuleString(
vm,
string->propertyValue(),
buffer,
capacity);
if (length <= capacity)
{
auto host = static_cast<HostInstanceValue*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::instanceValue));
host->sent = string->propertyValue();
host->hasSent = true;
}
return length;
}
uint32_t dataStringChangedImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto string = resolveInstanceValue<ViewModelInstanceString>(vm, handle);
if (string == nullptr)
{
return 0;
}
auto host = static_cast<HostInstanceValue*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::instanceValue));
if (host->hasSent && host->sent == string->propertyValue())
{
return ~0u;
}
return (uint32_t)fillModuleString(vm, string->propertyValue(), nullptr, 0);
}
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;
}
return fillModuleString(vm, values[index]->key(), buffer, capacity);
}
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;
}
// The swap notifies, and a listener can release the parent's last handle.
auto parent = ref_rcp(value->parentViewModelInstance());
parent->replaceViewModelByProperty(value.get(), 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(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();
}
return fillModuleString(vm, joined, buffer, capacity);
}
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));
auto instance = object != nullptr && object->dataContext() != nullptr
? object->dataContext()->rootViewModelInstance()
: nullptr;
if (instance == nullptr)
{
vm->noteMissingRequestedData();
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;
}
return fillModuleString(vm, joined, buffer, capacity);
}
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)
{
vm->noteMissingRequestedData();
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)
{
releaseHostHandle(vm, 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);
}
};
// A ref, so the artboard outlives a native whose script callbacks release it.
rcp<HostArtboard> resolveArtboard(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return ref_rcp(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);
auto renderer = resolveRenderer(vm, rendererHandle);
if (host != nullptr && renderer != nullptr)
{
#ifdef WITH_RIVE_TOOLS
host->artboard->settleIfImagesRevealed();
#endif
host->artboard->drawInternal(renderer);
}
}
namespace
{
struct WasmDrawVisit
{
WasmScriptingVM* vm;
// Resolved around each visit, which may end the canvas frame behind it.
uint32_t renderer;
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);
// The host has the artboard's own saves open here, so each visit gets
// a scope of its own and leaves the renderer as it found it.
HostRenderer* host = resolveHostRenderer(visit->vm, visit->renderer);
int outer = host != nullptr ? host->saves : 0;
if (host != nullptr)
{
host->saves = 0;
}
visit->failed = !visit->vm->notifyDrawVisit(handle);
host = resolveHostRenderer(visit->vm, visit->renderer);
if (host != nullptr)
{
host->restoreSaves();
host->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 = resolveHostRenderer(vm, rendererHandle);
if (host == nullptr || renderer == nullptr)
{
return;
}
WasmDrawVisit visit = {vm, rendererHandle};
#ifdef WITH_RIVE_TOOLS
host->artboard->settleIfImagesRevealed();
#endif
host->artboard
->drawInternal(renderer->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)
{
#ifdef WITH_RIVE_TOOLS
host->artboard->settleIfImagesRevealed();
#endif
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;
}
return fillModuleString(
vm,
property->as<CustomPropertyString>()->propertyValue(),
out,
outCount);
}
#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;
}
uint32_t artboardScrollEventImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t pointerId,
float x,
float y,
float dx,
float dy,
uint32_t phase,
uint32_t precise,
float timeStamp)
{
auto host = resolveArtboard(vm, handle);
if (host == nullptr || host->stateMachine == nullptr ||
phase > (uint32_t)ScrollPhase::inertiaCancel)
{
return 0;
}
ScrollEvent event{Vec2D(dx, dy), (ScrollPhase)phase, precise != 0};
return (uint32_t)(int)host->stateMachine
->pointerScroll(Vec2D(x, y), event, timeStamp, (int)pointerId);
}
// 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{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.
// Without an owner the node borrows a path effect's shape for that update.
struct HostNode
{
rcp<HostArtboard> owner;
TransformComponent* component;
};
uint32_t mintNode(
WasmScriptingVM* vm,
rcp<HostArtboard> owner,
TransformComponent* component)
{
bool borrowed = owner == nullptr;
uint32_t handle = vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::node,
new HostNode{std::move(owner), component});
if (borrowed)
{
vm->scopedNodes().push_back(handle);
}
return handle;
}
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 mintNode(vm, host, component);
}
void artboardAddToPathImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t pathHandle,
float xx,
float xy,
float yx,
float yy,
float tx,
float ty)
{
auto host = resolveArtboard(vm, handle);
auto hostPath = resolvePath(vm, pathHandle);
if (host == nullptr || hostPath == nullptr)
{
return;
}
Mat2D transform(xx, xy, yx, yy, tx, ty);
host->artboard->addToRawPath(hostPath->raw, &transform);
hostPath->renderDirty = true;
}
uint32_t artboardGamepadEventImpl(
WasmScriptingVM* vm,
uint32_t handle,
const uint8_t* payload,
uint32_t byteCount)
{
auto host = resolveArtboard(vm, handle);
GamepadWire wire;
if (host == nullptr || host->stateMachine == nullptr ||
!decodeGamepadWire(payload, byteCount, wire))
{
return 0;
}
auto invocation = decodeGamepadInvocation(wire, payload);
if (!invocation)
{
return 0;
}
// The Luau artboard:gamepad* path: focus first, then every scripted
// drawable that did not already take it.
ScriptedDrawable* dispatched = nullptr;
if (auto* focus = host->stateMachine->focusManager())
{
(void)focus->gamepadDispatch(*invocation, &dispatched);
}
return (uint32_t)host->stateMachine->broadcastGamepadToScriptedDrawables(
*invocation,
dispatched);
}
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);
// A borrowed node is released when its path effect update returns.
if (host == nullptr || host->owner == nullptr)
{
return;
}
vm->handles().release(handle, WasmScriptingVM::HandleTable::Tag::node);
delete host;
}
void artboardNodeTransformImpl(
WasmScriptingVM* vm,
uint32_t handle,
float* out,
uint32_t outCount)
{
if (outCount < 5)
{
return;
}
auto host = resolveNode(vm, handle);
// A stale node reads as the identity, the same on every lane.
if (host == nullptr)
{
const float identity[5] = {0, 0, 0, 1, 1};
memcpy(out, identity, sizeof(identity));
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)
{
if (outCount < 6)
{
return;
}
auto host = resolveNode(vm, handle);
Mat2D world = host != nullptr ? host->component->worldTransform() : Mat2D();
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;
}
return fillOut(
pathVerbBytes(host->component->as<Path>()->rawPath()),
out,
outCount);
}
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;
}
return fillOut(
pathFloats(host->component->as<Path>()->rawPath()),
out,
outCount);
}
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++] = mintNode(vm, 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 mintNode(vm, host->owner, parent->as<TransformComponent>());
}
// --- rive_data_v1 asset properties (image/font/blob) ------------------------
struct HostFont
{
rcp<Font> font;
#ifdef WITH_RIVE_TEXT
RawPath glyphPath;
uint32_t glyphPathId = 0;
bool hasGlyphPath = false;
#endif
};
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);
}
HostFont* resolveFont(WasmScriptingVM* vm, uint32_t handle);
void dataFontReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
// A font the teardown sweep already freed resolves to nothing here.
releaseHostHandle(vm, handle, WasmScriptingVM::HandleTable::Tag::font);
}
// --- rive_font_v1 / rive_text_v1
// ----------------------------------------------
HostFont* resolveFont(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostFont*>(
vm->handles().resolve(handle, WasmScriptingVM::HandleTable::Tag::font));
}
uint32_t mintFont(WasmScriptingVM* vm, rcp<Font> font)
{
if (font == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::font,
new HostFont{std::move(font)});
}
uint32_t floatBits(float value)
{
uint32_t bits;
memcpy(&bits, &value, sizeof(bits));
return bits;
}
float bitsFloat(uint32_t bits)
{
float value;
memcpy(&value, &bits, sizeof(value));
return value;
}
uint32_t fontFromAssetImpl(
WasmScriptingVM* vm,
uint32_t objectHandle,
const char* name,
uint32_t length)
{
auto asset = findFileAsset<FontAsset>(vm, objectHandle, name, length);
return asset == nullptr ? 0 : mintFont(vm, asset->font());
}
void fontReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
dataFontReleaseImpl(vm, handle);
}
void fontMetricsImpl(
WasmScriptingVM* vm,
uint32_t handle,
float* out,
uint32_t outCount)
{
auto host = resolveFont(vm, handle);
if (host == nullptr || out == nullptr || outCount < 4)
{
return;
}
const Font::LineMetrics& metrics = host->font->lineMetrics();
out[0] = metrics.ascent;
out[1] = metrics.descent;
out[2] = metrics.capHeight;
out[3] = metrics.xHeight;
}
uint32_t fontWeightImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveFont(vm, handle);
return host == nullptr ? 0 : host->font->getWeight();
}
uint32_t fontIsItalicImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveFont(vm, handle);
return host != nullptr && host->font->isItalic() ? 1 : 0;
}
uint32_t fontAxisCountImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveFont(vm, handle);
return host == nullptr ? 0 : host->font->getAxisCount();
}
void fontAxisImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t index,
float* out,
uint32_t outCount)
{
auto host = resolveFont(vm, handle);
if (host == nullptr || out == nullptr || outCount < 4 ||
index >= host->font->getAxisCount())
{
return;
}
Font::Axis axis = host->font->getAxis((uint16_t)index);
out[0] = bitsFloat(axis.tag);
out[1] = axis.min;
out[2] = axis.def;
out[3] = axis.max;
}
float fontAxisValueImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t tag)
{
auto host = resolveFont(vm, handle);
return host == nullptr ? 0.0f : host->font->getAxisValue(tag);
}
uint32_t fontFeaturesImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t* out,
uint32_t outCount)
{
auto host = resolveFont(vm, handle);
if (host == nullptr)
{
return 0;
}
SimpleArray<uint32_t> features = host->font->features();
return fillOut(
Span<const uint32_t>(features.data(), features.size()),
out,
outCount);
}
uint32_t fontHasGlyphImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t codepoint)
{
auto host = resolveFont(vm, handle);
return host != nullptr && host->font->hasGlyph(codepoint) ? 1 : 0;
}
uint32_t fontWithOptionsImpl(
WasmScriptingVM* vm,
uint32_t handle,
const uint32_t* coords,
uint32_t coordCount,
const uint32_t* features,
uint32_t featureCount)
{
auto host = resolveFont(vm, handle);
if (host == nullptr)
{
return 0;
}
std::vector<Font::Coord> axes;
for (uint32_t i = 0; i + 1 < coordCount; i += 2)
{
axes.push_back({coords[i], bitsFloat(coords[i + 1])});
}
std::vector<Font::Feature> settings;
for (uint32_t i = 0; i + 1 < featureCount; i += 2)
{
settings.push_back({features[i], features[i + 1]});
}
return mintFont(vm, host->font->withOptions(axes, settings));
}
#ifdef WITH_RIVE_TEXT
uint32_t fontDecodeImpl(
WasmScriptingVM* vm,
const uint8_t* bytes,
uint32_t byteCount)
{
if (vm == nullptr || bytes == nullptr)
{
return 0;
}
return mintFont(vm, HBFont::Decode(Span<const uint8_t>(bytes, byteCount)));
}
// The outline crosses in two calls, so the last one is kept for the second.
const RawPath& fontGlyph(HostFont* host, uint32_t glyph)
{
if (!host->hasGlyphPath || host->glyphPathId != glyph)
{
host->glyphPath = RawText::glyphPath(*host->font, (GlyphID)glyph);
host->glyphPathId = glyph;
host->hasGlyphPath = true;
}
return host->glyphPath;
}
uint32_t fontGlyphVerbsImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t glyph,
uint8_t* out,
uint32_t outCount)
{
auto host = resolveFont(vm, handle);
if (host == nullptr)
{
return 0;
}
return fillOut(pathVerbBytes(fontGlyph(host, glyph)), out, outCount);
}
uint32_t fontGlyphPointsImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t glyph,
float* out,
uint32_t outCount)
{
auto host = resolveFont(vm, handle);
if (host == nullptr)
{
return 0;
}
return fillOut(pathFloats(fontGlyph(host, glyph)), out, outCount);
}
// A Text builtin: the layout plus the flat records the module reads it
// through, rebuilt after any change.
struct HostText
{
HostText(Factory* factory) : text(factory) {}
RawText text;
std::vector<const GlyphRun*> runs;
std::vector<uint32_t> lineWords;
std::vector<uint32_t> runWords;
std::vector<uint32_t> glyphWords;
// Fonts the layout produced, fallbacks included, handed to the module as
// handles this text owns.
std::unordered_map<const Font*, uint32_t> fontHandles;
// RawText revision the records were built from; ~0u before the first.
uint32_t snapshotRevision = ~0u;
};
HostText* resolveText(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostText*>(
vm->handles().resolve(handle, WasmScriptingVM::HandleTable::Tag::text));
}
void deleteHostText(WasmScriptingVM* vm, HostText* host)
{
if (host == nullptr)
{
return;
}
for (auto& pair : host->fontHandles)
{
dataFontReleaseImpl(vm, pair.second);
}
delete host;
}
void buildTextSnapshot(WasmScriptingVM* vm, HostText* host)
{
if (host->snapshotRevision == host->text.revision())
{
return;
}
host->snapshotRevision = host->text.revision();
host->runs.clear();
host->lineWords.clear();
host->runWords.clear();
host->glyphWords.clear();
RawText& text = host->text;
const GlyphLookup& lookup = text.layoutView().glyphLookup();
std::unordered_map<const GlyphRun*, uint32_t> runIndices;
uint32_t glyphCount = 0;
for (uint32_t line = 0; line < text.lineCount(); line++)
{
const OrderedLine& orderedLine = text.orderedLines()[line];
uint32_t glyphStart = glyphCount;
text.forEachGlyph(
line,
[&](const GlyphRun& run, uint32_t glyphIndex, Vec2D position) {
auto indexed = runIndices.emplace(&run, (uint32_t)host->runs.size());
if (indexed.second)
{
host->runs.push_back(&run);
}
glyphCount++;
GlyphID glyphId = run.glyphs[glyphIndex];
uint32_t flags = (run.level & 1) |
(run.font->isColorGlyph(glyphId) ? 2 : 0);
host->glyphWords.insert(
host->glyphWords.end(),
{glyphId,
run.textIndices[glyphIndex],
indexed.first->second,
flags,
floatBits(position.x),
floatBits(position.y),
floatBits(run.advances[glyphIndex])});
return true;
});
host->lineWords.insert(
host->lineWords.end(),
{glyphStart,
glyphCount - glyphStart,
(uint32_t)text.lineDirection(line),
orderedLine.firstCodePointIndex(lookup),
orderedLine.lastCodePointIndex(lookup),
floatBits(orderedLine.startX()),
floatBits(text.lineTop(line)),
floatBits(orderedLine.y()),
floatBits(orderedLine.bottom())});
}
// A font no run uses any more gives its handle back, or a text relaid
// out with a fresh font variant every frame would keep one per frame.
std::unordered_map<const Font*, uint32_t> fontHandles;
for (const GlyphRun* run : host->runs)
{
auto found = fontHandles.find(run->font.get());
if (found == fontHandles.end())
{
auto kept = host->fontHandles.find(run->font.get());
uint32_t handle;
if (kept != host->fontHandles.end())
{
handle = kept->second;
host->fontHandles.erase(kept);
}
else
{
handle = mintFont(vm, run->font);
}
found = fontHandles.emplace(run->font.get(), handle).first;
}
host->runWords.insert(
host->runWords.end(),
{found->second,
(uint32_t)(run->level & 1),
floatBits(run->size),
floatBits(run->lineHeight),
floatBits(run->letterSpacing)});
}
for (auto& pair : host->fontHandles)
{
dataFontReleaseImpl(vm, pair.second);
}
host->fontHandles = std::move(fontHandles);
}
uint32_t textNewImpl(WasmScriptingVM* vm)
{
if (vm == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::text,
new HostText(vm->factory()));
}
void textReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
releaseHostHandle(vm, handle, WasmScriptingVM::HandleTable::Tag::text);
}
void textAppendImpl(
WasmScriptingVM* vm,
uint32_t handle,
const char* chars,
uint32_t length,
uint32_t fontHandle,
uint32_t paintHandle,
float size,
float lineHeight,
float letterSpacing,
uint32_t foreground)
{
auto host = resolveText(vm, handle);
auto font = resolveFont(vm, fontHandle);
if (host == nullptr || font == nullptr)
{
return;
}
RenderPaint* paint = resolvePaint(vm, paintHandle);
host->text.append(
std::string(chars, length),
paint != nullptr ? ref_rcp(paint) : nullptr,
font->font,
size,
lineHeight,
letterSpacing,
foreground);
}
void textClearImpl(WasmScriptingVM* vm, uint32_t handle)
{
if (auto host = resolveText(vm, handle))
{
host->text.clear();
}
}
void textLayoutImpl(
WasmScriptingVM* vm,
uint32_t handle,
const rive_text_layout_desc_v1* desc,
uint32_t descByteCount)
{
auto host = resolveText(vm, handle);
if (host == nullptr || desc == nullptr ||
descByteCount < sizeof(rive_text_layout_desc_v1))
{
return;
}
RawText& text = host->text;
text.sizingIndex((int)desc->sizing);
text.overflowIndex((int)desc->overflow);
text.alignIndex((int)desc->align);
text.wrapIndex((int)desc->wrap);
text.wordBreakIndex((int)desc->wordBreak);
text.originIndex((int)desc->origin);
text.directionIndex((int)desc->direction);
text.maxWidth(desc->maxWidth);
text.maxHeight(desc->maxHeight);
text.paragraphSpacing(desc->paragraphSpacing);
}
void textDrawImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t rendererHandle,
uint32_t paintHandle)
{
auto host = resolveText(vm, handle);
auto renderer = resolveRenderer(vm, rendererHandle);
if (host == nullptr || renderer == nullptr)
{
return;
}
RenderPaint* paint = resolvePaint(vm, paintHandle);
host->text.render(renderer, paint != nullptr ? ref_rcp(paint) : nullptr);
}
void textBoundsImpl(
WasmScriptingVM* vm,
uint32_t handle,
float* out,
uint32_t outCount)
{
auto host = resolveText(vm, handle);
if (host == nullptr || out == nullptr || outCount < 4)
{
return;
}
AABB bounds = host->text.bounds();
out[0] = bounds.minX;
out[1] = bounds.minY;
out[2] = bounds.maxX;
out[3] = bounds.maxY;
}
uint32_t textLengthImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto host = resolveText(vm, handle);
return host == nullptr ? 0 : (uint32_t)host->text.length();
}
uint32_t textLinesImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t* out,
uint32_t outCount)
{
auto host = resolveText(vm, handle);
if (host == nullptr)
{
return 0;
}
buildTextSnapshot(vm, host);
return fillOut(host->lineWords, out, outCount);
}
uint32_t textRunsImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t* out,
uint32_t outCount)
{
auto host = resolveText(vm, handle);
if (host == nullptr)
{
return 0;
}
buildTextSnapshot(vm, host);
return fillOut(host->runWords, out, outCount);
}
uint32_t textGlyphsImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t* out,
uint32_t outCount)
{
auto host = resolveText(vm, handle);
if (host == nullptr)
{
return 0;
}
buildTextSnapshot(vm, host);
return fillOut(host->glyphWords, out, outCount);
}
uint32_t textHitTestImpl(WasmScriptingVM* vm, uint32_t handle, float x, float y)
{
auto host = resolveText(vm, handle);
if (host == nullptr)
{
return 0;
}
TextLayoutView view = host->text.layoutView();
return CursorPosition::fromTranslation(Vec2D(x, y), view).codePointIndex();
}
uint32_t textCaretImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t index,
float* out,
uint32_t outCount)
{
auto host = resolveText(vm, handle);
if (host == nullptr || out == nullptr || outCount < 3)
{
return 0;
}
TextLayoutView view = host->text.layoutView();
auto visual = CursorPosition::atIndex(index, view).visualPosition(view);
if (!visual.found())
{
return 0;
}
out[0] = visual.x();
out[1] = visual.top();
out[2] = visual.bottom();
return 1;
}
uint32_t textSelectionRectsImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t from,
uint32_t to,
float* out,
uint32_t outCount)
{
auto host = resolveText(vm, handle);
if (host == nullptr)
{
return 0;
}
TextLayoutView view = host->text.layoutView();
Cursor cursor(
CursorPosition::atIndex(from, view),
CursorPosition::atIndex(to, view));
std::vector<AABB> rects;
cursor.selectionRects(rects, view);
std::vector<float> words;
for (const AABB& rect : rects)
{
words.insert(words.end(), {rect.minX, rect.minY, rect.maxX, rect.maxY});
}
return fillOut(words, out, outCount);
}
#else
struct HostText
{};
void deleteHostText(WasmScriptingVM*, HostText*) {}
#endif
// 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,
[](FileAsset* blob) { return !blob->as<BlobAsset>()->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_file_v1 and artboard properties: context:decodeFile ---------------
//
// The Luau lane's decodeFile, RiveFile, BindableArtboard and
// Property<BindableArtboard> (lua_rive_file.cpp, ScriptedPropertyArtboard),
// with the file and its bindables held host side. Like the Luau lane they
// exist only where fetch() does; elsewhere every op answers none.
struct HostRiveFile
{
rcp<File> file;
};
using HostBindableArtboard = DecodedBindable;
HostRiveFile* resolveRiveFile(WasmScriptingVM* vm, uint32_t handle)
{
return vm == nullptr ? nullptr
: static_cast<HostRiveFile*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::riveFile));
}
HostBindableArtboard* resolveBindableArtboard(
WasmScriptingVM* vm,
uint32_t handle)
{
return vm == nullptr
? nullptr
: static_cast<HostBindableArtboard*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::bindableArtboard));
}
// The VM decoding a file, which the file's own VMs collect with.
thread_local WasmScriptingVM* s_decodingParent = nullptr;
uint32_t fileDecodeImpl(
WasmScriptingVM* vm,
const uint8_t* bytes,
uint32_t byteCount,
uint32_t* statusOut,
uint32_t statusCount)
{
auto status = [&](uint32_t value) {
if (statusCount > 0)
{
statusOut[0] = value;
}
};
#ifdef WITH_RIVE_SCRIPTNET
if (vm == nullptr || vm->factory() == nullptr)
{
status(FileDecodeWire::statusNoFactory);
return 0;
}
ImportResult result = ImportResult::malformed;
WasmScriptingVM* outer = s_decodingParent;
s_decodingParent = vm;
auto file = byteCount == 0 ? nullptr
: importDecodedFile(
Span<const uint8_t>(bytes, byteCount),
vm->factory(),
&result);
s_decodingParent = outer;
if (file == nullptr)
{
status(
result == ImportResult::unsupportedVersion
? FileDecodeWire::statusUnsupportedVersion
: FileDecodeWire::statusMalformed);
return 0;
}
status(FileDecodeWire::statusOk);
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::riveFile,
new HostRiveFile{std::move(file)});
#else
(void)vm;
(void)bytes;
(void)byteCount;
status(FileDecodeWire::statusUnavailable);
return 0;
#endif
}
void fileReleaseImpl(WasmScriptingVM* vm, uint32_t file)
{
releaseHostHandle(vm, file, WasmScriptingVM::HandleTable::Tag::riveFile);
}
uint32_t fileArtboardCountImpl(WasmScriptingVM* vm, uint32_t file)
{
auto host = resolveRiveFile(vm, file);
return host == nullptr ? 0 : (uint32_t)host->file->artboardCount();
}
// One name at a time: names are file data, so no separator is safe.
uint32_t fileArtboardNameImpl(
WasmScriptingVM* vm,
uint32_t file,
uint32_t index,
char* buffer,
uint32_t capacity)
{
auto host = resolveRiveFile(vm, file);
if (host == nullptr || index >= host->file->artboardCount())
{
return 0;
}
return fillModuleString(
vm,
host->file->artboard(index)->name(),
buffer,
capacity);
}
uint32_t fileBindableImpl(
WasmScriptingVM* vm,
uint32_t file,
const char* name,
uint32_t nameLength,
uint32_t useDefault)
{
auto host = resolveRiveFile(vm, file);
if (host == nullptr)
{
return 0;
}
std::string named(name, nameLength);
DecodedBindable decoded = decodedBindable(
*host->file,
useDefault != 0 ? nullptr : named.c_str());
if (decoded.artboard == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::bindableArtboard,
new HostBindableArtboard(std::move(decoded)));
}
void fileBindableReleaseImpl(WasmScriptingVM* vm, uint32_t bindable)
{
releaseHostHandle(
vm,
bindable,
WasmScriptingVM::HandleTable::Tag::bindableArtboard);
}
uint32_t fileBindableNameImpl(
WasmScriptingVM* vm,
uint32_t bindable,
char* buffer,
uint32_t capacity)
{
auto host = resolveBindableArtboard(vm, bindable);
if (host == nullptr)
{
return 0;
}
return fillModuleString(
vm,
host->artboard->artboard()->name(),
buffer,
capacity);
}
uint32_t fileBindableDataImpl(WasmScriptingVM* vm, uint32_t bindable)
{
auto host = resolveBindableArtboard(vm, bindable);
if (host == nullptr || host->viewModel == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::viewModelInstance,
new HostViewModelInstance{host->viewModel});
}
uint32_t fileBindableEqualImpl(WasmScriptingVM* vm, uint32_t a, uint32_t b)
{
auto first = resolveBindableArtboard(vm, a);
auto second = resolveBindableArtboard(vm, b);
return first != nullptr && second != nullptr &&
first->artboard == second->artboard
? 1
: 0;
}
uint32_t dataVmiArtboardImpl(
WasmScriptingVM* vm,
uint32_t vmiHandle,
const char* name,
uint32_t length)
{
#ifdef WITH_RIVE_SCRIPTNET
return mintInstanceValue<ViewModelInstanceArtboard>(
vm,
vmiHandle,
name,
length);
#else
(void)vm;
(void)vmiHandle;
(void)name;
(void)length;
return 0;
#endif
}
// Mirrors ScriptedPropertyArtboard::pushValue: nil while the property names
// one of the host file's own artboards.
uint32_t dataArtboardGetImpl(WasmScriptingVM* vm, uint32_t handle)
{
auto value = resolveInstanceValue<ViewModelInstanceArtboard>(vm, handle);
if (value == nullptr || value->asset() == nullptr)
{
return 0;
}
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::bindableArtboard,
new HostBindableArtboard{
value->asset(),
value->boundViewModelInstance()});
}
// Mirrors ScriptedPropertyArtboard::setValue: the artboard's own view model
// goes in first, since asset() notifies and whatever mounts the artboard
// binds it to that instance.
void dataArtboardSetImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t bindable)
{
auto value = resolveInstanceValue<ViewModelInstanceArtboard>(vm, handle);
if (value == nullptr)
{
return;
}
auto host = resolveBindableArtboard(vm, bindable);
value->boundViewModelInstance(host != nullptr ? host->viewModel : nullptr);
value->asset(host != nullptr ? host->artboard : nullptr);
}
// --- rive_net_v1: fetch() --------------------------------------------------
uint32_t netFetchImpl(
WasmScriptingVM* vm,
const uint8_t* request,
uint32_t requestCount,
uint32_t token)
{
if (vm == nullptr)
{
return 0;
}
return vm->startFetch(Span<const uint8_t>(request, requestCount), token) ? 1
: 0;
}
void netFetchCancelImpl(WasmScriptingVM* vm, uint32_t token)
{
if (vm != nullptr)
{
vm->cancelFetch(token);
}
}
// --- 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,
[](FileAsset* audio) {
return audio->as<AudioAsset>()->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
}
// Settles through the main lane's waits with the blob a job decoded.
uint32_t audioSourceToBlobImpl(
WasmScriptingVM* vm,
uint32_t handle,
uint32_t sampleRate,
uint32_t channels)
{
#ifdef WITH_RIVE_AUDIO
auto host = resolveAudioSource(vm, handle);
if (host == nullptr)
{
return 0;
}
return vm->startAudioDecode(host->source, sampleRate, channels);
#else
return 0;
#endif
}
#if RIVE_AUDIO_STREAMS()
bool isSharedId(uint32_t id);
// The fewest chunks a poll decodes per stream, well ahead of a frame's worth.
static constexpr uint32_t kStreamChunksPerPump = 16;
// Leads each stream record. Our std's AudioStreamReader reads it in place,
// and published files carry that reader, so it never changes.
struct StreamRecordHeader
{
// The seeks before it, which the processor drops records older than.
uint32_t seek;
uint32_t flags;
// Its first frame's place in the source, at the stream's rate.
double frame;
};
static_assert(sizeof(StreamRecordHeader) == 16, "the std reads 16 bytes");
static constexpr size_t kRecordHeaderFloats = sizeof(StreamRecordHeader) /
sizeof(float);
static constexpr uint32_t kRecordLast = 1;
// A seek dropped the frames before it; a loop's wrap is seamless, so is not.
static constexpr uint32_t kRecordDiscontinuity = 2;
} // namespace
// Keeps a ring a processor drains topped up with records of decoded frames.
struct rive::HostAudioStream
{
rcp<AudioReader> reader;
rcp<SharedRing> ring;
uint32_t ringId = 0;
rcp<SharedSignal> seeks;
uint32_t seeksId = 0;
uint64_t owner = 0;
// 0 once released, after which it decodes on while a reader holds the ring.
uint32_t handle = 0;
uint32_t chunkFrames = 0;
bool discontinuity = false;
double position = 0;
// The record in chunk that has not fit the ring yet, 0 when none.
uint32_t queuedBytes = 0;
// The ring's bytes after the last top up, so a poll sees what was played.
uint32_t usedAfterFill = 0;
uint32_t fullRecordBytes = 0;
bool queuedLast = false;
bool loop = false;
bool ended = false;
// A header, then chunkFrames frames.
std::vector<float> chunk;
~HostAudioStream()
{
SharedRegistry::release(ringId, owner);
SharedRegistry::release(seeksId, owner);
}
void fill(uint32_t maxChunks)
{
for (uint32_t chunks = 0; !ended && chunks < maxChunks; chunks++)
{
if (queuedBytes == 0)
{
decodeRecord();
}
if (!ring->write(
Span<const uint8_t>((const uint8_t*)chunk.data(), queuedBytes)))
{
break;
}
queuedBytes = 0;
ended = queuedLast;
}
usedAfterFill = ring->used();
}
// Twice what was played since the last poll, so a processor reading
// faster than real time keeps up.
void pump()
{
uint32_t used = ring->used();
uint32_t played = usedAfterFill > used ? usedAfterFill - used : 0;
fill(std::max(kStreamChunksPerPump, 2 * (played / fullRecordBytes) + 1));
}
void decodeRecord()
{
float* samples = chunk.data() + kRecordHeaderFloats;
uint64_t read = 0;
bool decoded = reader->readInto(samples, chunkFrames, read);
// Rewound once a pass, so a source with no frames ends rather than spins.
if (decoded && read == 0 && loop && reader->rewind())
{
position = 0;
decoded = reader->readInto(samples, chunkFrames, read);
}
queuedLast = !decoded || read == 0;
// The signal's own count, so a notify the host did not make drops nothing.
StreamRecordHeader header = {
(uint32_t)seeks->seq(),
(discontinuity ? kRecordDiscontinuity : 0) |
(queuedLast ? kRecordLast : 0),
position};
memcpy(chunk.data(), &header, sizeof(header));
discontinuity = false;
position += (double)read;
queuedBytes = (uint32_t)(sizeof(header) +
read * reader->channels() * sizeof(float));
}
// Drops what the processor has not played, so it hears frame next.
bool restart(uint64_t frame)
{
uint64_t landed = 0;
if (!reader->seek(frame, &landed))
{
return false;
}
// Before the drain, so the processor drops what it already took too.
seeks->notify();
uint32_t capacity = (uint32_t)(chunk.size() * sizeof(float));
while (ring->read((uint8_t*)chunk.data(), capacity) != 0)
{
}
queuedBytes = 0;
ended = false;
position = (double)landed;
discontinuity = true;
fill(kStreamChunksPerPump);
return true;
}
// Answers whether its first records are queued.
bool ready() { return ended || ring->used() != 0; }
};
namespace
{
HostAudioStream* resolveAudioStream(WasmScriptingVM* vm, uint32_t handle)
{
if (vm == nullptr)
{
return nullptr;
}
return static_cast<HostAudioStream*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::audioStream));
}
void pumpAudioStreams(WasmScriptingVM* vm)
{
auto& streams = vm->audioStreams();
for (auto it = streams.begin(); it != streams.end();)
{
HostAudioStream* stream = *it;
if (stream->handle == 0 &&
(stream->ended ||
SharedRegistry::holds(stream->ringId, stream->owner) <= 1))
{
delete stream;
it = streams.erase(it);
continue;
}
stream->pump();
++it;
}
}
// Another hold on id for the caller, which answers it; else 0.
uint32_t retainedId(uint32_t id, uint64_t owner)
{
return SharedRegistry::retain(id, owner) ? id : 0;
}
#endif
uint32_t audioStreamImpl(
WasmScriptingVM* vm,
uint32_t source,
uint32_t sampleRate,
uint32_t channels,
uint32_t chunkFrames,
uint32_t bufferFrames)
{
#if RIVE_AUDIO_STREAMS()
auto host = resolveAudioSource(vm, source);
if (host == nullptr || chunkFrames == 0)
{
return 0;
}
auto reader = host->source->makeReader(channels, sampleRate);
if (reader == nullptr)
{
return 0;
}
uint64_t chunkBytes = sizeof(StreamRecordHeader) +
(uint64_t)chunkFrames * reader->channels() * 4;
if (chunkBytes > shared_layout::kSharedMaxBytes)
{
return 0;
}
uint64_t records = std::max<uint64_t>(
1,
((uint64_t)bufferFrames + chunkFrames - 1) / chunkFrames);
uint64_t recordBytes = SharedRing::recordBytes((size_t)chunkBytes, 0);
uint64_t capacity = records * recordBytes;
if (capacity > shared_layout::kSharedMaxBytes)
{
return 0;
}
uint32_t ringId = sharedRingCreateImpl(vm, (uint32_t)capacity);
if (!isSharedId(ringId))
{
return 0;
}
uint64_t owner = vm->sharedOwner();
uint32_t seeksId = sharedSignalCreateImpl(vm);
if (!isSharedId(seeksId))
{
SharedRegistry::release(ringId, owner);
return 0;
}
auto stream = new HostAudioStream();
stream->owner = owner;
stream->ring = SharedRegistry::find<SharedRing>(ringId, owner);
stream->ringId = ringId;
stream->seeks = SharedRegistry::find<SharedSignal>(seeksId, owner);
stream->seeksId = seeksId;
stream->chunkFrames = chunkFrames;
stream->fullRecordBytes = (uint32_t)recordBytes;
stream->chunk.resize(
kRecordHeaderFloats + (size_t)chunkFrames * reader->channels());
stream->reader = std::move(reader);
stream->fill(kStreamChunksPerPump);
stream->handle = vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::audioStream,
stream);
vm->audioStreams().push_back(stream);
vm->watchLanes();
return vm->watchStream(stream->handle);
#else
return 0;
#endif
}
uint32_t audioStreamRingImpl(WasmScriptingVM* vm, uint32_t handle)
{
#if RIVE_AUDIO_STREAMS()
auto stream = resolveAudioStream(vm, handle);
return stream != nullptr ? retainedId(stream->ringId, stream->owner) : 0;
#else
return 0;
#endif
}
uint32_t audioStreamSeeksImpl(WasmScriptingVM* vm, uint32_t handle)
{
#if RIVE_AUDIO_STREAMS()
auto stream = resolveAudioStream(vm, handle);
return stream != nullptr ? retainedId(stream->seeksId, stream->owner) : 0;
#else
return 0;
#endif
}
uint32_t audioStreamChannelsImpl(WasmScriptingVM* vm, uint32_t handle)
{
#if RIVE_AUDIO_STREAMS()
auto stream = resolveAudioStream(vm, handle);
return stream != nullptr ? stream->reader->channels() : 0;
#else
return 0;
#endif
}
uint32_t audioStreamSampleRateImpl(WasmScriptingVM* vm, uint32_t handle)
{
#if RIVE_AUDIO_STREAMS()
auto stream = resolveAudioStream(vm, handle);
return stream != nullptr ? stream->reader->sampleRate() : 0;
#else
return 0;
#endif
}
uint32_t audioStreamSeekFrameImpl(
WasmScriptingVM* vm,
uint32_t handle,
double frame)
{
#if RIVE_AUDIO_STREAMS()
auto stream = resolveAudioStream(vm, handle);
return stream != nullptr &&
stream->restart(
frame > 0 ? (uint64_t)std::min(frame, 1.8e19) : 0)
? 1
: 0;
#else
return 0;
#endif
}
void audioStreamSetLoopImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t loop)
{
#if RIVE_AUDIO_STREAMS()
auto stream = resolveAudioStream(vm, handle);
if (stream == nullptr)
{
return;
}
stream->loop = loop != 0;
if (!stream->loop)
{
return;
}
// A stream that ran out starts over, after what is queued.
if (stream->ended && stream->reader->rewind())
{
stream->ended = false;
stream->position = 0;
stream->fill(kStreamChunksPerPump);
}
// Its last record waits for room, so the next decode loops instead.
else if (
stream->queuedLast && stream->queuedBytes == sizeof(StreamRecordHeader))
{
stream->queuedBytes = 0;
}
#endif
}
uint32_t audioStreamEndedImpl(WasmScriptingVM* vm, uint32_t handle)
{
#if RIVE_AUDIO_STREAMS()
auto stream = resolveAudioStream(vm, handle);
return stream != nullptr && stream->ended ? 1 : 0;
#else
return 0;
#endif
}
void audioStreamReleaseImpl(WasmScriptingVM* vm, uint32_t handle)
{
#if RIVE_AUDIO_STREAMS()
releaseHostHandle(vm, handle, WasmScriptingVM::HandleTable::Tag::audioStream);
#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)
{
uint64_t frame;
uint32_t rate;
if (WasmProcessors::offlineClock(frame, rate))
{
return (float)((double)frame / rate);
}
#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)
{
uint64_t frame;
uint32_t rate;
if (WasmProcessors::offlineClock(frame, rate))
{
return (double)frame;
}
#ifdef WITH_RIVE_AUDIO
auto engine = AudioEngine::RuntimeEngine(true);
return engine != nullptr ? (double)engine->timeInFrames() : 0.0;
#else
return 0.0;
#endif
}
uint32_t audioProcessorSampleRateImpl(WasmScriptingVM* vm)
{
return WasmProcessors::format().sampleRate;
}
uint32_t audioProcessorChannelsImpl(WasmScriptingVM* vm)
{
return WasmProcessors::format().channels;
}
uint32_t audioSampleRateImpl(WasmScriptingVM* vm)
{
uint64_t frame;
uint32_t rate;
if (WasmProcessors::offlineClock(frame, rate))
{
return rate;
}
#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;
}
return fillModuleString(vm, asset->name(), buffer, capacity);
}
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;
}
#ifndef __EMSCRIPTEN__
bool initRuntime()
{
RuntimeInitArgs args;
memset(&args, 0, sizeof(args));
args.mem_alloc_type = Alloc_With_System_Allocator;
if (!wasm_runtime_full_init(&args))
{
return false;
}
// Memory grows on demand, so a failed grow is the only sign of a
// ceiling or device limit before the module traps.
wasm_runtime_set_enlarge_mem_error_callback(
[](uint32_t incPages,
uint64_t currentBytes,
uint32_t,
enlarge_memory_error_reason_t reason,
wasm_module_inst_t,
wasm_exec_env_t env,
void*) {
const char* why = reason == MAX_SIZE_REACHED
? "the module hit its wasmMaxPages ceiling"
: "the device could not provide the memory";
#ifdef RIVE_WASM_PARALLEL
if (WasmParallel::runningParts())
{
why = "the memory cannot grow during a parallel call";
}
#endif
reportScriptError(
env != nullptr ? bootVmFromEnv(env) : nullptr,
"wasm memory: growing " + std::to_string(currentBytes >> 20) +
" MB by " + std::to_string(incPages) + " pages failed: " + why);
},
nullptr);
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;
}
registerLaneNatives();
registerJobNatives();
registerWorkerNatives();
return true;
}
bool ensureRuntime()
{
// Hosts boot on more than one thread, so a boot waits out another's init.
static bool ok = initRuntime();
return ok;
}
#endif
// --- rive_shared_v1: memory every lane shares -----------------------------
using shared_layout::kSharedMaxBytes;
// The lane a native runs on, null on the main lane.
WasmLane* callingLane(WasmScriptingVM* vm)
{
return vm == nullptr ? WasmLane::current() : nullptr;
}
WasmLane* callingProcessor(WasmScriptingVM* vm)
{
#if RIVE_PROCESSOR_AUDIO()
WasmLane* lane = callingLane(vm);
return lane != nullptr && lane->kind() == LaneKind::processor ? lane
: nullptr;
#else
return nullptr;
#endif
}
// Whose holds a call takes and gives back: the VM's, or its lane's.
uint64_t sharedHolder(WasmScriptingVM* vm)
{
WasmLane* lane = callingLane(vm);
return vm != nullptr ? vm->sharedOwner()
: lane != nullptr ? lane->sharedOwner()
: 0;
}
template <typename T> rcp<T> sharedFind(WasmScriptingVM* vm, uint32_t id)
{
return SharedRegistry::find<T>(id, sharedHolder(vm));
}
bool isSharedId(uint32_t id) { return (id & shared_layout::kIdBit) != 0; }
// The creator, charged size; else 0 with the create's answer in refusal, as
// a processor never may create and a job makes only blobs.
uint64_t sharedCreator(
WasmScriptingVM* vm,
uint64_t size,
bool allowEmpty,
bool blob,
uint32_t& refusal)
{
WasmLane* lane = callingLane(vm);
bool may = lane == nullptr || lane->kind() == LaneKind::worker ||
(blob && lane->kind() == LaneKind::job);
uint64_t owner = may ? sharedHolder(vm) : 0;
refusal = shared_layout::kCreateRefused;
if (owner != 0 && ((size == 0 && !allowEmpty) || size > kSharedMaxBytes))
{
refusal = shared_layout::kCreateBadSize;
owner = 0;
}
if (owner != 0 && !SharedRegistry::charge(owner, size))
{
refusal = shared_layout::kCreateNoMemory;
owner = 0;
}
return owner;
}
// What a create answers for object, made for owner with size charged: its
// id once in the registry, else why not, its charge refunded.
uint32_t sharedCreated(
WasmScriptingVM* vm,
uint64_t owner,
uint64_t size,
rcp<SharedObject> object)
{
if (!object->made())
{
SharedRegistry::refund(owner, size);
return shared_layout::kCreateNoMemory;
}
object->charge = size;
uint32_t id = SharedRegistry::add(std::move(object), owner);
if (!isSharedId(id))
{
SharedRegistry::refund(owner, size);
return id;
}
if (vm == nullptr)
{
WasmLane::current()->addCreated(id);
}
return id;
}
// Whether a misuse warns: once per object, never on a processor, which may
// be on the audio thread, so the text is built only for a warning.
bool sharedWarns(WasmScriptingVM* vm, SharedObject& object)
{
WasmLane* lane = callingLane(vm);
return (vm != nullptr ||
(lane != nullptr && lane->kind() != LaneKind::processor)) &&
object.firstWarning();
}
#if defined(__GNUC__) || defined(__clang__)
__attribute__((format(printf, 2, 3)))
#endif
void sharedWarn(WasmScriptingVM* vm, const char* format, ...)
{
char text[256];
va_list args;
va_start(args, format);
vsnprintf(text, sizeof(text), format, args);
va_end(args);
if (vm != nullptr)
{
rtLogImpl(vm, 0, text, (uint32_t)strlen(text));
}
else
{
callingLane(vm)->log(text);
}
}
// A processor may be on the audio thread, where taking or giving back a hold
// could lock or free, so a value naming shared objects fails it instead.
using lane_layout::kProcessorHolds;
uint32_t processorRead(WasmLane* processor, uint32_t got)
{
if (got != SharedObject::kCarriesHolds)
{
return got;
}
processor->fail(kProcessorHolds);
return 0;
}
uint32_t sharedRegionCreateImpl(WasmScriptingVM* vm, uint32_t size)
{
uint32_t refusal;
uint64_t owner = sharedCreator(vm, size, false, false, refusal);
if (owner == 0)
{
return refusal;
}
return sharedCreated(vm, owner, size, make_rcp<SharedRegion>(size));
}
uint32_t sharedRegionSizeImpl(WasmScriptingVM* vm, uint32_t region)
{
auto found = sharedFind<SharedRegion>(vm, region);
return found != nullptr ? found->size() : 0;
}
uint32_t sharedRegionLockImpl(
WasmScriptingVM* vm,
uint32_t region,
double timeoutSeconds)
{
auto found = sharedFind<SharedRegion>(vm, region);
if (found == nullptr)
{
return shared_layout::kRegionLockGone;
}
// One region at a time keeps lock order acyclic.
if (sharedHeldRegion() != 0)
{
return shared_layout::kRegionLockNested;
}
bool locked = false;
{
WasmScriptingVM::WaitScope wait(vm);
// A lock begun in the time kept for the script gets nothing back, so
// it spins for half of what is left, as a timed wait may oversleep.
if (wait.usable() <= 0)
{
Clock::time_point until = deadlineAfter(
std::min(timeoutSeconds, callSecondsLeft() / 2));
spinUntil([&]() {
locked = found->lock(0);
return locked || Clock::now() >= until;
});
}
else
{
locked = found->lock(timeoutSeconds);
}
}
if (!locked)
{
return shared_layout::kRegionLockTimedOut;
}
setSharedHeldRegion(region, std::move(found));
return shared_layout::kRegionLockAcquired;
}
void sharedRegionUnlockImpl(WasmScriptingVM* vm, uint32_t region)
{
if (SharedRegion* held = sharedHeld(region))
{
held->unlock();
setSharedHeldRegion(0, nullptr);
}
}
uint32_t sharedRegionReadImpl(
WasmScriptingVM* vm,
uint32_t region,
uint32_t offset,
uint8_t* out,
uint32_t outByteCount)
{
SharedRegion* found = sharedHeld(region);
if (found == nullptr || (uint64_t)offset + outByteCount > found->size())
{
return 0;
}
if (outByteCount != 0)
{
memcpy(out, found->bytes() + offset, outByteCount);
}
return 1;
}
uint32_t sharedRegionWriteImpl(
WasmScriptingVM* vm,
uint32_t region,
uint32_t offset,
const uint8_t* data,
uint32_t dataByteCount)
{
SharedRegion* found = sharedHeld(region);
if (found == nullptr || (uint64_t)offset + dataByteCount > found->size())
{
return 0;
}
if (dataByteCount != 0)
{
memcpy(found->bytes() + offset, data, dataByteCount);
}
return 1;
}
uint32_t sharedRegionPoisonedImpl(WasmScriptingVM* vm, uint32_t region)
{
auto found = sharedFind<SharedRegion>(vm, region);
return found != nullptr && found->poisoned() ? 1 : 0;
}
// -1 for a word outside the region or a region gone, which the std throws
// for.
template <typename Use>
double sharedAtomic(
WasmScriptingVM* vm,
uint32_t region,
uint32_t offset,
Use use)
{
SharedRegion* found = sharedRecentRegion(region, sharedHolder(vm));
std::atomic<uint32_t>* word = found != nullptr ? found->word(offset)
: nullptr;
return word != nullptr ? (double)use(*word) : -1.0;
}
double sharedAtomicLoadImpl(
WasmScriptingVM* vm,
uint32_t region,
uint32_t offset)
{
return sharedAtomic(vm, region, offset, [](std::atomic<uint32_t>& word) {
return word.load();
});
}
double sharedAtomicStoreImpl(
WasmScriptingVM* vm,
uint32_t region,
uint32_t offset,
uint32_t value)
{
return sharedAtomic(vm, region, offset, [value](std::atomic<uint32_t>& word) {
word.store(value);
return 0u;
});
}
double sharedAtomicAddImpl(
WasmScriptingVM* vm,
uint32_t region,
uint32_t offset,
uint32_t value)
{
return sharedAtomic(vm, region, offset, [value](std::atomic<uint32_t>& word) {
return word.fetch_add(value);
});
}
double sharedAtomicCasImpl(
WasmScriptingVM* vm,
uint32_t region,
uint32_t offset,
uint32_t expected,
uint32_t desired)
{
return sharedAtomic(
vm,
region,
offset,
[expected, desired](std::atomic<uint32_t>& word) {
uint32_t held = expected;
compareExchangeStrong(word, held, desired);
return held;
});
}
// The holds a value in flight carries, retained here for it; an id already
// gone is left out.
#if RIVE_ONE_THREAD()
[[clang::no_destroy]] std::vector<uint32_t> t_carried;
#else
thread_local std::vector<uint32_t> t_carried;
#endif
Span<const uint32_t> retainCarried(uint64_t owner, Span<const uint32_t> ids)
{
t_carried.clear();
for (uint32_t id : ids)
{
if (SharedRegistry::retain(id, owner))
{
t_carried.push_back(id);
}
}
return t_carried;
}
void releaseAll(uint64_t owner, Span<const uint32_t> ids)
{
for (uint32_t id : ids)
{
SharedRegistry::release(id, owner);
}
}
// The main lane keeps a read's holds while its std decodes; a lane keeps
// each until it ends or releases it.
void handHolds(WasmScriptingVM* vm, Span<const uint32_t> ids)
{
if (ids.empty())
{
return;
}
if (vm != nullptr)
{
vm->keepReadHolds(ids);
}
else if (WasmLane* lane = callingLane(vm))
{
lane->adoptShared(ids);
}
}
// What a read takes holds into before handing them over.
#if RIVE_ONE_THREAD()
[[clang::no_destroy]] std::vector<uint32_t> t_taken;
#else
thread_local std::vector<uint32_t> t_taken;
#endif
uint32_t sharedRingCreateImpl(WasmScriptingVM* vm, uint32_t capacity)
{
uint32_t refusal;
uint64_t owner = sharedCreator(vm, capacity, false, false, refusal);
if (owner == 0)
{
return refusal;
}
auto ring = make_rcp<SharedRing>(capacity);
if (!ring->made())
{
return sharedCreated(vm, owner, capacity, ring);
}
uint32_t data = SharedRegistry::add(ring->dataSignal(), owner);
uint32_t space = SharedRegistry::add(ring->spaceSignal(), owner);
ring->setSignalIds(data, space);
// A signal that failed answers why.
uint32_t id = isSharedId(data) ? space : data;
if (isSharedId(id))
{
id = sharedCreated(vm, owner, capacity, ring);
}
else
{
SharedRegistry::refund(owner, capacity);
}
if (!isSharedId(id))
{
SharedRegistry::release(data, owner);
SharedRegistry::release(space, owner);
}
return id;
}
uint32_t sharedRingWriteImpl(
WasmScriptingVM* vm,
uint32_t ring,
const uint8_t* data,
uint32_t dataByteCount,
const uint32_t* holds,
uint32_t holdCount)
{
auto found = sharedFind<SharedRing>(vm, ring);
if (found == nullptr)
{
return shared_layout::kRingWriteFull;
}
if (WasmLane* processor = callingProcessor(vm);
processor != nullptr && holdCount != 0)
{
processor->fail(kProcessorHolds);
return shared_layout::kRingWriteFull;
}
uint64_t owner = sharedHolder(vm);
auto carried = retainCarried(owner, Span<const uint32_t>(holds, holdCount));
if (found->write(Span<const uint8_t>(data, dataByteCount), carried))
{
return shared_layout::kRingWriteWritten;
}
releaseAll(owner, carried);
uint64_t bytes = SharedRing::recordBytes(dataByteCount, holdCount);
uint32_t capacity = found->block()->capacity;
if (bytes > capacity)
{
if (sharedWarns(vm, *found))
{
sharedWarn(
vm,
"Ring.send, Ring.writeF32 or Port.post: a %u byte record and its "
"%llu %s never fit this %u byte ring; create it larger, or send "
"large data as a Blob, which crosses by id",
dataByteCount,
(unsigned long long)(bytes - dataByteCount),
holdCount == 0 ? "byte length" : "bytes of length and holds",
capacity);
}
return shared_layout::kRingWriteNeverFits;
}
return shared_layout::kRingWriteFull;
}
uint32_t sharedRingReadImpl(
WasmScriptingVM* vm,
uint32_t ring,
uint8_t* out,
uint32_t outByteCount)
{
auto found = sharedFind<SharedRing>(vm, ring);
if (found == nullptr)
{
return 0;
}
if (WasmLane* processor = callingProcessor(vm))
{
return processorRead(processor, found->read(out, outByteCount, nullptr));
}
t_taken.clear();
uint32_t got = found->read(out, outByteCount, &t_taken);
handHolds(vm, t_taken);
return got;
}
uint32_t sharedRingDropImpl(
WasmScriptingVM* vm,
uint32_t ring,
uint32_t capacity)
{
auto found = sharedFind<SharedRing>(vm, ring);
if (found == nullptr)
{
return 0;
}
if (WasmLane* processor = callingProcessor(vm))
{
return processorRead(processor, found->drop(capacity));
}
t_taken.clear();
uint32_t dropped = found->drop(capacity, &t_taken);
releaseAll(sharedHolder(vm), t_taken);
return dropped;
}
uint32_t sharedRingUsedImpl(WasmScriptingVM* vm, uint32_t ring)
{
auto found = sharedFind<SharedRing>(vm, ring);
return found != nullptr ? found->used() : 0;
}
uint32_t sharedRingSignalImpl(
WasmScriptingVM* vm,
uint32_t ring,
uint32_t space)
{
auto found = sharedFind<SharedRing>(vm, ring);
if (found == nullptr)
{
return 0;
}
return space != 0 ? found->spaceSignalId() : found->dataSignalId();
}
uint32_t sharedSnapshotCreateImpl(WasmScriptingVM* vm, uint32_t capacity)
{
uint32_t refusal;
uint64_t owner = sharedCreator(
vm,
(uint64_t)capacity * SharedSnapshot::kCopies,
true,
false,
refusal);
if (owner == 0)
{
return refusal;
}
return sharedCreated(
vm,
owner,
(uint64_t)capacity * SharedSnapshot::kCopies,
make_rcp<SharedSnapshot>(capacity));
}
uint32_t sharedSnapshotPublishImpl(
WasmScriptingVM* vm,
uint32_t snapshot,
const uint8_t* data,
uint32_t dataByteCount,
const uint32_t* holds,
uint32_t holdCount)
{
auto found = sharedFind<SharedSnapshot>(vm, snapshot);
if (found == nullptr)
{
return 0;
}
Span<const uint8_t> value(data, dataByteCount);
if (WasmLane* processor = callingProcessor(vm))
{
if (holdCount == 0 && found->publish(value))
{
return 1;
}
if (holdCount != 0 || dataByteCount <= found->block()->capacity)
{
processor->fail(kProcessorHolds);
}
return 0;
}
uint64_t owner = sharedHolder(vm);
auto carried = retainCarried(owner, Span<const uint32_t>(holds, holdCount));
t_taken.clear();
if (found->publish(value, carried, &t_taken))
{
releaseAll(owner, t_taken);
return 1;
}
releaseAll(owner, carried);
if (sharedWarns(vm, *found))
{
sharedWarn(
vm,
"Snapshot.publish: a %u byte value does not fit this %u byte "
"snapshot, so readers keep the last value that fit; create the "
"snapshot larger, or send large data as a Blob, which crosses by id",
dataByteCount,
found->block()->capacity);
}
return 0;
}
uint32_t sharedSnapshotReadImpl(
WasmScriptingVM* vm,
uint32_t snapshot,
uint8_t* out,
uint32_t outByteCount)
{
auto found = sharedFind<SharedSnapshot>(vm, snapshot);
if (found == nullptr)
{
return 0;
}
if (WasmLane* processor = callingProcessor(vm))
{
return processorRead(processor, found->read(out, outByteCount, nullptr));
}
uint64_t owner = sharedHolder(vm);
// A publish gives back the holds of the value it replaced once seq moves,
// so holds taken before seq moved are on live objects.
for (;;)
{
uint64_t seq = found->seq();
t_taken.clear();
uint32_t got = found->read(out, outByteCount, &t_taken);
auto taken = retainCarried(owner, t_taken);
if (t_taken.empty() || found->seq() == seq)
{
handHolds(vm, taken);
return got;
}
releaseAll(owner, taken);
}
}
double sharedSnapshotSeqImpl(WasmScriptingVM* vm, uint32_t snapshot)
{
auto found = sharedFind<SharedSnapshot>(vm, snapshot);
return found != nullptr ? (double)found->seq() : 0.0;
}
uint32_t sharedSignalCreateImpl(WasmScriptingVM* vm)
{
uint32_t refusal;
uint64_t owner = sharedCreator(vm, 0, true, false, refusal);
if (owner == 0)
{
return refusal;
}
return sharedCreated(vm, owner, 0, make_rcp<SharedSignal>());
}
void sharedSignalNotifyImpl(WasmScriptingVM* vm, uint32_t signal)
{
if (auto found = sharedFind<SharedSignal>(vm, signal))
{
found->notify();
}
}
double sharedSignalSeqImpl(WasmScriptingVM* vm, uint32_t signal)
{
auto found = sharedFind<SharedSignal>(vm, signal);
return found != nullptr ? (double)found->seq() : 0.0;
}
uint32_t sharedBlobCreateImpl(
WasmScriptingVM* vm,
const uint8_t* data,
uint32_t dataByteCount)
{
uint32_t refusal;
uint64_t owner = sharedCreator(vm, dataByteCount, true, true, refusal);
if (owner == 0)
{
return refusal;
}
return sharedCreated(
vm,
owner,
dataByteCount,
make_rcp<SharedBlob>(Span<const uint8_t>(data, dataByteCount)));
}
uint32_t sharedBlobSizeImpl(WasmScriptingVM* vm, uint32_t blob)
{
auto found = sharedFind<SharedBlob>(vm, blob);
return found != nullptr ? found->size() : 0;
}
uint32_t sharedBlobReadImpl(
WasmScriptingVM* vm,
uint32_t blob,
uint32_t offset,
uint8_t* out,
uint32_t outByteCount)
{
auto found = sharedFind<SharedBlob>(vm, blob);
return found != nullptr ? found->read(offset, out, outByteCount) : 0;
}
void sharedReadDoneImpl(WasmScriptingVM* vm)
{
if (vm != nullptr)
{
vm->releaseReadHolds();
}
}
uint32_t sharedRetainImpl(WasmScriptingVM* vm, uint32_t object)
{
return vm != nullptr && SharedRegistry::retain(object, vm->sharedOwner()) ? 1
: 0;
}
// The main lane gives back a handle's hold; a lane only what it created or
// holds, and a processor nothing, as its holds go with it.
void sharedReleaseImpl(WasmScriptingVM* vm, uint32_t object)
{
if (vm != nullptr)
{
SharedRegistry::release(object, vm->sharedOwner());
return;
}
WasmLane* lane = WasmLane::current();
if (lane != nullptr && lane->kind() != LaneKind::processor &&
lane->dropShared(object))
{
SharedRegistry::release(object, lane->sharedOwner());
}
}
// --- rive_lanes_v1: jobs, workers and audio processors -----------------------
struct HostLane
{
virtual ~HostLane() = default;
WasmLane* lane = nullptr;
};
template <class T> struct HostLaneOf : HostLane
{
explicit HostLaneOf(rcp<T> held) : owned(std::move(held))
{
lane = owned.get();
}
rcp<T> owned;
};
WasmLane* resolveLane(WasmScriptingVM* vm, uint32_t handle)
{
auto* host = vm != nullptr ? static_cast<HostLane*>(vm->handles().resolve(
handle,
WasmScriptingVM::HandleTable::Tag::lane))
: nullptr;
return host != nullptr ? host->lane : nullptr;
}
WasmJob* resolveJob(WasmScriptingVM* vm, uint32_t handle)
{
WasmLane* lane = resolveLane(vm, handle);
return lane != nullptr && lane->kind() == LaneKind::job
? static_cast<WasmJob*>(lane)
: nullptr;
}
// The encoded value a done job or worker hands its main lane.
const std::vector<uint8_t>& laneResult(WasmLane* lane)
{
return lane->kind() == LaneKind::job
? static_cast<WasmJob*>(lane)->output()
: static_cast<WasmWorker*>(lane)->result();
}
template <class T>
uint32_t mintLane(WasmScriptingVM* vm, rcp<T> lane, Span<const uint32_t> holds)
{
if (lane == nullptr)
{
return 0;
}
// The script's handles outlive this call, so a lane already running
// sees them alive until it holds them.
lane->holdShared(holds);
vm->watchLanes();
return vm->handles().mint(
WasmScriptingVM::HandleTable::Tag::lane,
new HostLaneOf<T>(std::move(lane)));
}
uint32_t lanesStartImpl(
WasmScriptingVM* vm,
uint32_t kind,
uint32_t entry,
const uint8_t* input,
uint32_t inputByteCount,
const uint32_t* holds,
uint32_t holdCount)
{
if (vm == nullptr)
{
return 0;
}
if (kind < 3 && !vm->laneLoadError((LaneKind)kind).empty())
{
vm->refusedStart = (LaneKind)kind;
return lane_layout::kStartFailed;
}
std::vector<uint8_t> bytes(input, input + inputByteCount);
Span<const uint32_t> held(holds, holdCount);
uint32_t pageBudgetMs = vm->pageThreadBudgetMs();
switch ((LaneKind)kind)
{
case LaneKind::job:
return vm->jobs() != nullptr
? mintLane(
vm,
vm->jobs()->run(entry, std::move(bytes), pageBudgetMs),
held)
: 0;
case LaneKind::worker:
return vm->workers() != nullptr
? mintLane(
vm,
vm->workers()
->spawn(entry, std::move(bytes), pageBudgetMs),
held)
: 0;
case LaneKind::processor:
return vm->processors() != nullptr
? mintLane(
vm,
vm->processors()->start(
entry,
std::move(bytes),
vm->limits().audioHardBlocks),
held)
: 0;
}
return 0;
}
uint32_t lanesStateImpl(WasmScriptingVM* vm, uint32_t lane)
{
WasmLane* found = resolveLane(vm, lane);
if (found == nullptr)
{
return (uint32_t)LaneState::stopped;
}
LaneState state = found->state();
// What it printed lands ahead of whatever its ending makes the caller
// print.
if (state >= LaneState::done)
{
vm->deliverLaneOutput(found);
}
return (uint32_t)state;
}
uint32_t lanesFailedImpl(
WasmScriptingVM* vm,
uint32_t lane,
char* buffer,
uint32_t capacity)
{
WasmLane* found = resolveLane(vm, lane);
std::string failure;
if (vm != nullptr && lane == lane_layout::kStartFailed)
{
failure = vm->laneLoadError(vm->refusedStart);
}
else if (found == nullptr || found->state() != LaneState::failed)
{
return 0;
}
else
{
failure = found->failure();
if (failure.empty())
{
failure = found->name() + " failed";
}
}
if (buffer != nullptr && failure.size() <= capacity)
{
memcpy(buffer, failure.data(), failure.size());
}
return (uint32_t)failure.size();
}
void lanesStopImpl(WasmScriptingVM* vm, uint32_t lane)
{
if (WasmLane* found = resolveLane(vm, lane))
{
found->stop();
}
}
void lanesReleaseImpl(WasmScriptingVM* vm, uint32_t lane)
{
WasmLane* found = resolveLane(vm, lane);
if (found == nullptr)
{
return;
}
// Now rather than at the next poll, so a call releasing many lanes keeps
// none it is done with.
vm->deliverLaneOutput(found);
releaseHostHandle(vm, lane, WasmScriptingVM::HandleTable::Tag::lane);
switch (found->kind())
{
case LaneKind::job:
vm->jobs()->prune(found);
break;
case LaneKind::worker:
vm->workers()->prune(found);
break;
case LaneKind::processor:
vm->processors()->prune(found);
break;
}
}
uint32_t lanesResultImpl(
WasmScriptingVM* vm,
uint32_t lane,
uint32_t out,
uint32_t outCount)
{
WasmLane* found = resolveLane(vm, lane);
if (found == nullptr || found->state() != LaneState::done)
{
return 0;
}
const std::vector<uint8_t>& result = laneResult(found);
if (result.size() <= outCount && !result.empty() &&
!vm->copyToModule(out, result.data(), (uint32_t)result.size()))
{
vm->raiseModuleError("lane result buffer is outside module memory");
}
return (uint32_t)result.size();
}
uint32_t lanesWatchImpl(WasmScriptingVM* vm, uint32_t lane)
{
return resolveLane(vm, lane) != nullptr ? vm->watchLane(lane) : 0;
}
uint32_t lanesWaitImpl(
WasmScriptingVM* vm,
uint32_t signal,
double after,
double timeoutSeconds)
{
return vm != nullptr ? vm->waitOnMain(signal, after, timeoutSeconds) : 0;
}
void lanesCancelWaitImpl(WasmScriptingVM* vm, uint32_t token)
{
if (vm != nullptr)
{
vm->cancelMainWait(token);
}
}
double lanesWaitWithinImpl(
WasmScriptingVM* vm,
uint32_t signal,
double after,
double timeoutSeconds)
{
if (vm == nullptr)
{
return lane_layout::kWaitGone;
}
// Waiting while holding a region could wait on a lane that waits for it.
if (sharedHeldRegion() != 0)
{
return lane_layout::kWaitRefused;
}
// Without lanes nothing else notifies, and no call deadline is armed.
double limit = vm->hasLanes()
? std::chrono::duration<double>(waitLimit()).count()
: 0;
uint64_t owner = vm->sharedOwner();
WasmScriptingVM::WaitScope wait(vm);
return SignalWait(
0,
owner,
signal,
SignalWait::find(signal, owner).get(),
after,
std::min({timeoutSeconds, wait.usable(), limit}))
.block();
}
// --- rive_parallel_v1: parallel calls -------------------------------------
uint32_t parallelSizeImpl(WasmScriptingVM* vm)
{
return vm != nullptr ? vm->parallelSize() : 1;
}
#ifdef RIVE_WASM_PARALLEL
uint32_t parallelRunImpl(WasmScriptingVM* vm, uint32_t control)
{
if (vm == nullptr)
{
WasmParallel::refuseMemberCall();
return 0;
}
return vm->runParallel(control);
}
#endif
// Only a caller that cannot park calls it, and natively every caller parks.
void parallelWaitingImpl(WasmScriptingVM* vm, uint32_t control) {}
// --- rive_jobs_v1: script jobs ---------------------------------------------
uint32_t jobsThreadsImpl(WasmScriptingVM* vm)
{
return WasmJobs::pool().threadCount();
}
uint32_t jobsJoinImpl(WasmScriptingVM* vm, uint32_t job, double timeoutSeconds)
{
WasmJob* found = resolveJob(vm, job);
if (found == nullptr)
{
return (uint32_t)LaneState::stopped;
}
// Waiting while holding a region could wait on a job that waits for it.
if (sharedHeldRegion() != 0)
{
return WasmJob::kJoinRefused;
}
{
WasmScriptingVM::WaitScope wait(vm);
vm->jobs()->join(found, timeoutSeconds, wait.usable());
}
// Its lines land ahead of whatever the joiner prints next.
vm->deliverLaneOutput(found);
return (uint32_t)found->state();
}
uint32_t jobsJoinAnyImpl(
WasmScriptingVM* vm,
const uint32_t* jobs,
uint32_t count,
double timeoutSeconds)
{
if (sharedHeldRegion() != 0)
{
return lane_layout::kJoinAnyRefused;
}
std::vector<WasmJob*> live;
std::vector<uint32_t> indices;
for (uint32_t i = 0; i < count; i++)
{
if (WasmJob* found = resolveJob(vm, jobs[i]))
{
live.push_back(found);
indices.push_back(i);
}
}
if (live.empty())
{
return 0;
}
int found;
{
WasmScriptingVM::WaitScope wait(vm);
found = vm->jobs()->joinAny(live, timeoutSeconds, wait.usable());
}
if (found < 0)
{
return 0;
}
vm->deliverLaneOutput(live[found]);
return indices[found] + 1;
}
void deleteHostObject(
WasmScriptingVM* vm,
WasmScriptingVM::HandleTable::Tag tag,
void* object)
{
using Tag = WasmScriptingVM::HandleTable::Tag;
switch (tag)
{
case Tag::instanceValue:
delete static_cast<HostInstanceValue*>(object);
break;
case Tag::viewModelInstance:
delete static_cast<HostViewModelInstance*>(object);
break;
case Tag::path:
delete static_cast<HostPath*>(object);
break;
case Tag::measure:
delete static_cast<HostMeasure*>(object);
break;
case Tag::paint:
delete static_cast<HostPaint*>(object);
break;
case Tag::shader:
delete static_cast<HostShader*>(object);
break;
case Tag::image:
delete static_cast<HostImage*>(object);
break;
case Tag::font:
delete static_cast<HostFont*>(object);
break;
case Tag::text:
deleteHostText(vm, static_cast<HostText*>(object));
break;
case Tag::buffer:
delete static_cast<HostBuffer*>(object);
break;
case Tag::meshInstances:
delete static_cast<HostMeshInstances*>(object);
break;
case Tag::dataContext:
delete static_cast<HostDataContext*>(object);
break;
case Tag::artboard:
// Refcounted: animations and nodes may still hold it.
static_cast<HostArtboard*>(object)->unref();
break;
case Tag::animation:
delete static_cast<HostAnimation*>(object);
break;
case Tag::node:
delete static_cast<HostNode*>(object);
break;
case Tag::riveFile:
delete static_cast<HostRiveFile*>(object);
break;
case Tag::bindableArtboard:
delete static_cast<HostBindableArtboard*>(object);
break;
#ifdef WITH_RIVE_AUDIO
case Tag::audioSource:
delete static_cast<HostAudioSource*>(object);
break;
case Tag::audioSound:
delete static_cast<HostAudioSound*>(object);
break;
case Tag::audioStream:
#if RIVE_AUDIO_STREAMS()
// Its poll frees it, once no reader holds the ring.
static_cast<HostAudioStream*>(object)->handle = 0;
#endif
break;
#endif
#ifdef RIVE_CANVAS
case Tag::canvas:
delete static_cast<HostCanvas*>(object);
break;
#endif
#if defined(RIVE_CANVAS) && defined(RIVE_ORE)
case Tag::gpuPass:
delete static_cast<HostGpuPass*>(object);
break;
case Tag::gpuCanvas:
delete static_cast<HostGpuCanvas*>(object);
break;
case Tag::gpuBuffer:
delete static_cast<HostGpuBuffer*>(object);
break;
case Tag::gpuTexture:
delete static_cast<HostGpuTexture*>(object);
break;
case Tag::gpuSampler:
delete static_cast<HostGpuSampler*>(object);
break;
case Tag::gpuTextureView:
delete static_cast<HostGpuTextureView*>(object);
break;
case Tag::gpuShaderModule:
delete static_cast<HostGpuShaderModule*>(object);
break;
case Tag::gpuBindGroupLayout:
delete static_cast<HostGpuBindGroupLayout*>(object);
break;
case Tag::gpuBindGroup:
delete static_cast<HostGpuBindGroup*>(object);
break;
case Tag::gpuPipeline:
delete static_cast<HostGpuPipeline*>(object);
break;
#endif
case Tag::lane:
{
// A lane whose wrapper died is not waited for.
auto* host = static_cast<HostLane*>(object);
host->lane->stop();
delete host;
break;
}
default:
break;
}
}
} // namespace
uint32_t WasmScriptingVM::HandleTable::mint(Tag tag, void* object)
{
uint32_t slot;
if (!freeSlots.empty())
{
slot = freeSlots.back();
freeSlots.pop_back();
}
else if (slots.size() < slotLimit)
{
slot = (uint32_t)slots.size();
slots.push_back({});
}
else if (!retiredSlots.empty())
{
// Callers allocate before minting, so a full table reissues old
// handles rather than fail.
slot = retiredSlots.front();
retiredSlots.pop_front();
}
else
{
return 0;
}
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;
// Seven bits, so no handle sets bit 31, which marks shared ids.
entry.generation = (entry.generation + 1) & 0x7f;
uint32_t index = slot - 1;
if (entry.generation == 0)
{
retiredSlots.push_back(index);
if (retiredSlots.size() <= retiredLimit)
{
return;
}
index = retiredSlots.front();
retiredSlots.pop_front();
}
freeSlots.push_back(index);
}
WasmScriptingVM::WasmScriptingVM() : m_decodedBy(s_decodingParent)
{
if (m_decodedBy != nullptr)
{
m_decodedBy->m_decodedVMs.push_back(this);
}
}
#ifndef __EMSCRIPTEN__
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))
{
if (vm->m_error)
{
vm->m_error(kModuleStartFailed + vm->m_lastError);
}
else
{
outError = vm->m_lastError;
}
return nullptr;
}
return vm;
}
#endif
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.
HostRenderer host{renderer};
uint32_t handle = m_handles.mint(HandleTable::Tag::renderer, &host);
uint32_t args[3] = {m_L, (uint32_t)selfRef, handle};
callModule("host_obj_draw", 3, args);
host.restoreSaves();
m_handles.release(handle, HandleTable::Tag::renderer);
}
#ifdef RIVE_WASM_PARALLEL
uint32_t WasmScriptingVM::runParallel(uint32_t control)
{
if (m_parallel == nullptr)
{
m_parallel = std::make_unique<WasmParallel>(m_state->instance);
}
WasmParallel::Outcome outcome = m_parallel->run(
m_state->execEnv,
control,
parallelSize() == 1);
using Kind = WasmParallel::Outcome::Kind;
switch (outcome.kind)
{
case Kind::ok:
case Kind::callerFailed:
break;
case Kind::trapped:
raiseModuleError(outcome.message.c_str());
break;
case Kind::threw:
{
WasmStringArg message(
this,
outcome.message.data(),
(uint32_t)outcome.message.size());
raiseThrown(std::string(message.data(), message.size()).c_str());
break;
}
case Kind::budget:
reportError(budgetExceededLine(m_timeoutMs));
raiseBudgetExceeded();
break;
case Kind::interrupted:
// The watchdog's own report names the timeout.
raiseModuleError("execution exceeded timeout");
break;
}
return outcome.threads;
}
#endif
WasmScriptingVM::~WasmScriptingVM()
{
// First, so no poll reaches it while it goes.
VMPollList::leave(m_laneList, this);
VMPollList::leave(m_holdingList, this);
for (const MainWait& wait : m_mainWaits)
{
abandonWait(wait);
}
// Workers and jobs may hold this VM's shared memory; they stop first.
m_processors.reset();
m_workers.reset();
m_jobs.reset();
#ifndef __EMSCRIPTEN__
// Members share the instance's memory, so they go before it.
m_parallel.reset();
#endif
if (m_sharedOwner != 0)
{
SharedRegistry::releaseOwner(m_sharedOwner);
}
if (m_decodedBy != nullptr)
{
auto& siblings = m_decodedBy->m_decodedVMs;
siblings.erase(
std::remove(siblings.begin(), siblings.end(), this),
siblings.end());
}
for (WasmScriptingVM* decoded : m_decodedVMs)
{
decoded->m_decodedBy = nullptr;
}
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.
#ifdef __EMSCRIPTEN__
for (const auto& pending : m_pendingDecodes)
{
cancelBrowserImageDecode(pending.second);
}
#else
if (m_decodeOwnerId != 0)
{
auto& pool = getGlobalWorkPoolIfExists();
if (pool != nullptr)
{
pool->cancelAllForOwner(m_decodeOwnerId);
}
}
#endif
#ifdef WITH_RIVE_SCRIPTNET
// Same for fetches: their listeners hold this VM.
if (m_netOwnerId != 0)
{
scriptnet::cancelAllForOwner(m_netOwnerId);
}
#endif
#ifndef __EMSCRIPTEN__
// 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);
}
#endif
// No script runs from here: disposing the swept artboards' scripts would
// mint handles into the table the sweep walks.
m_poisoned = true;
// 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.
// Indexed, and each slot cleared before its object goes, because host
// teardown may still mint and grow the table.
for (size_t i = 0; i < m_handles.slots.size(); ++i)
{
HandleTable::Slot& slot = m_handles.slots[i];
HandleTable::Tag tag = slot.tag;
void* object = slot.object;
slot.tag = HandleTable::Tag::empty;
slot.object = nullptr;
deleteHostObject(this, tag, object);
}
#if RIVE_AUDIO_STREAMS()
for (HostAudioStream* stream : m_audioStreams)
{
delete stream;
}
#endif
#ifndef __EMSCRIPTEN__
// Last, as the stats call above can still arm and register a slot.
if (m_budgetSlot != nullptr)
{
ScriptWatchdog::instance().remove(m_budgetSlot.get());
}
#endif
}
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 ---------------------------------
#if !defined(__EMSCRIPTEN__) && defined(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)
{
#ifdef __EMSCRIPTEN__
// Like the Luau lane on the web, the browser decodes.
m_pendingDecodes[token] = startBrowserImageDecode(
bytes,
byteCount,
[this, token](
uint32_t,
uint32_t width,
uint32_t height,
Span<const uint8_t> pixels,
const char* error) {
if (error != nullptr)
{
rejectImageDecode(token, error);
return;
}
resolveImageDecode(
token,
width,
height,
pixels.data(),
(uint32_t)pixels.size());
});
return true;
#elif !defined(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())
{
#ifdef __EMSCRIPTEN__
cancelBrowserImageDecode(it->second);
#else
it->second->cancel();
#endif
m_pendingDecodes.erase(it);
}
}
void WasmScriptingVM::deliverDecodeResult(const DecodeResult& result)
{
if (result.ok)
{
uint32_t byteCount = (uint32_t)result.pixels.size();
// The listener's own buffer, where the module hands one out, saves
// a second copy as big as the image.
uint32_t bufferArgs[2] = {m_L, byteCount};
uint32_t pixelsPtr = 0;
bool staged = callModuleChecked(
"host_image_buffer",
2,
bufferArgs,
&pixelsPtr) != CallOutcome::ok;
if (staged)
{
uint32_t sizeArgs[1] = {byteCount};
pixelsPtr = callModule("malloc", 1, sizeArgs);
}
if (pixelsPtr == 0)
{
DecodeResult failure;
failure.token = result.token;
failure.error = "failed to allocate decoded pixels";
deliverDecodeResult(failure);
return;
}
copyToModule(pixelsPtr, result.pixels.data(), byteCount);
uint32_t args[6] =
{m_L, result.token, result.width, result.height, pixelsPtr, byteCount};
callModule("host_image_decoded", 6, args);
if (staged)
{
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)
{
if (inModuleCall())
{
// The decoder frees its pixels once this returns.
std::vector<uint8_t> owned(pixels, pixels + byteCount);
holdOutcome([this, token, width, height, owned = std::move(owned)] {
resolveImageDecode(
token,
width,
height,
owned.data(),
(uint32_t)owned.size());
});
return;
}
if (m_pendingDecodes.erase(token) == 0 || !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)
{
if (inModuleCall())
{
holdOutcome([this, token, error = std::string(message)] {
rejectImageDecode(token, error.c_str());
});
return;
}
if (m_pendingDecodes.erase(token) == 0 || !valid())
{
return;
}
DecodeResult result;
result.token = token;
result.error = message;
deliverDecodeResult(result);
}
// --- fetch() over the module ABI --------------------------------------------
#ifdef WITH_RIVE_SCRIPTNET
namespace
{
// The Luau lane's rejection value: "<code>: <message>".
std::string fetchRejection(
scriptnet::NetErrorCode code,
const std::string& message)
{
return std::string(scriptnet::netErrorCodeName(code)) + ": " + message;
}
// Carries one request's outcome back to the VM that started it. scriptnet
// delivers on this VM's thread from its poll; teardown cancels every
// outstanding request first, which clears the VM pointer here.
class WasmFetchListener : public scriptnet::FetchListener
{
public:
WasmFetchListener(WasmScriptingVM* vm, uint32_t token)
: m_vm(vm)
, m_token(token)
{}
void onResponse(scriptnet::HttpResponse&& response) override
{
if (m_vm != nullptr)
{
m_vm->resolveFetch(m_token, std::move(response));
}
}
void onError(const scriptnet::NetError& error) override
{
if (m_vm != nullptr)
{
m_vm->rejectFetch(
m_token,
(uint32_t)error.code,
fetchRejection(error.code, error.message));
}
}
void onCancel() override { m_vm = nullptr; }
private:
WasmScriptingVM* m_vm;
uint32_t m_token;
};
} // namespace
#endif
bool WasmScriptingVM::startFetch(Span<const uint8_t> wire, uint32_t token)
{
#ifdef WITH_RIVE_SCRIPTNET
// Reusing a live token would leave its first request uncancellable.
if (m_pendingFetches.count(token) != 0)
{
return false;
}
scriptnet::HttpRequest request;
// The module's fetch() writes the stream from arguments it validated, so
// a bad one is a bug; it goes out empty and the policy refuses it,
// settling like any other refusal.
if (!NetWire::decode(wire, request))
{
request = scriptnet::HttpRequest();
}
if (m_netOwnerId == 0)
{
m_netOwnerId = WorkPool::nextOwnerId();
}
m_pendingFetches[token] = scriptnet::fetch(
m_netOwnerId,
std::move(request),
make_rcp<WasmFetchListener>(this, token));
return true;
#else
(void)wire;
(void)token;
return false;
#endif
}
void WasmScriptingVM::cancelFetch(uint32_t token)
{
auto it = m_pendingFetches.find(token);
if (it == m_pendingFetches.end())
{
return;
}
uint32_t id = it->second;
m_pendingFetches.erase(it);
#ifdef WITH_RIVE_SCRIPTNET
scriptnet::cancel(id);
#else
(void)id;
#endif
}
void WasmScriptingVM::resolveFetch(
uint32_t token,
scriptnet::HttpResponse&& response)
{
if (inModuleCall())
{
holdOutcome([this, token, response = std::move(response)]() mutable {
resolveFetch(token, std::move(response));
});
return;
}
// Cancelled while held.
if (m_pendingFetches.erase(token) == 0 || !valid())
{
return;
}
auto tooLarge = [&](const char* message) {
#ifdef WITH_RIVE_SCRIPTNET
deliverFetchFailure(
token,
(uint32_t)scriptnet::NetErrorCode::TooLarge,
fetchRejection(scriptnet::NetErrorCode::TooLarge, message));
#else
(void)message;
#endif
};
// The body stays in the host, so it reaches a lane without ever entering
// module memory.
uint32_t body = 0;
if (m_fetchBlobBody)
{
uint64_t size = response.body.size();
uint64_t owner = sharedCreator(this, size, true, true, body);
if (owner != 0)
{
body = sharedCreated(
this,
owner,
size,
make_rcp<SharedBlob>(
Span<const uint8_t>(response.body.data(), response.body.size())));
}
if (!isSharedId(body))
{
tooLarge("the response body does not fit in shared memory");
return;
}
}
NetWire::Response wire{response, body};
uint32_t byteCount = (uint32_t)NetWire::encodedSize(wire);
uint32_t sizeArgs[1] = {byteCount};
uint32_t responsePtr = callModule("malloc", 1, sizeArgs);
if (responsePtr == 0)
{
if (body != 0)
{
SharedRegistry::release(body, sharedOwner());
}
tooLarge("the response does not fit in script memory");
return;
}
NetWire::encode(
wire,
(uint8_t*)resolveModuleWritePtr(responsePtr, byteCount));
uint32_t args[4] = {m_L, token, responsePtr, byteCount};
callModule("host_fetch_resolved", 4, args);
guestFree(responsePtr);
}
void WasmScriptingVM::rejectFetch(
uint32_t token,
uint32_t code,
const std::string& message)
{
if (inModuleCall())
{
holdOutcome(
[this, token, code, message] { rejectFetch(token, code, message); });
return;
}
if (m_pendingFetches.erase(token) != 0 && valid())
{
deliverFetchFailure(token, code, message);
}
}
void WasmScriptingVM::deliverFetchFailure(
uint32_t token,
uint32_t code,
const std::string& message)
{
uint32_t messagePtr = guestString(message.c_str());
if (messagePtr == 0)
{
return;
}
uint32_t args[4] = {m_L, token, code, messagePtr};
callModule("host_fetch_failed", 4, args);
guestFree(messagePtr);
}
bool WasmScriptingVM::inModuleCall() const { return m_callDepth != 0; }
void WasmScriptingVM::holdOutcome(std::function<void()> deliver)
{
if (m_heldOutcomes.empty())
{
VMPollList::join(m_holdingList, t_holdingVMs, this);
}
m_heldOutcomes.push_back(std::move(deliver));
}
void WasmScriptingVM::attachLanes(Span<const uint8_t> module)
{
// Not generic, so the three kinds share one copy.
auto load = [&](LaneKind kind) {
std::string& error = m_laneLoadErrors[(int)kind];
rcp<WasmLaneModule> lanes = loadLaneModule(module, kind, error);
if (!error.empty())
{
consoleWrite(
this,
std::string("script ") + laneNoun(kind) + "s unavailable: " + error);
}
return lanes;
};
auto attach = [&](auto& set, LaneKind kind) {
rcp<WasmLaneModule> lanes = load(kind);
if (lanes != nullptr)
{
set.reset(new std::remove_reference_t<decltype(*set)>(std::move(lanes)));
set->setSharedOwner(sharedOwner());
}
};
attach(m_jobs, LaneKind::job);
attach(m_workers, LaneKind::worker);
attach(m_processors, LaneKind::processor);
}
void WasmScriptingVM::laneTierLines(std::vector<std::string>& lines) const
{
#ifndef __EMSCRIPTEN__
auto add = [&](const auto& lanes, LaneKind kind) {
if (lanes != nullptr)
{
lines.push_back(
std::string(laneSpec(kind).module) + " tier: " + lanes->tier());
}
};
add(m_jobs, LaneKind::job);
add(m_workers, LaneKind::worker);
add(m_processors, LaneKind::processor);
#endif
}
void WasmScriptingVM::poison()
{
m_poisoned = true;
auto stopAll = [](auto* set) {
if (set == nullptr)
{
return;
}
for (const auto& lane : set->lanes())
{
lane->stop();
}
};
stopAll(m_jobs.get());
stopAll(m_workers.get());
stopAll(m_processors.get());
}
void WasmScriptingVM::deliverAllLaneOutput()
{
t_laneVMs->deliver([](WasmScriptingVM* vm) {
// At a poll no module call is live, so workers without threads may
// run here.
if (vm->m_workers != nullptr)
{
vm->m_workers->pump();
}
vm->deliverLaneOutput();
});
}
void WasmScriptingVM::watchLanes()
{
// Its lanes start only from one thread, whose polls reach their output.
assert(m_laneList == nullptr || m_laneList == t_laneVMs);
VMPollList::join(m_laneList, t_laneVMs, this);
}
// Decodes on a job thread into a blob the main lane registers as it settles.
class rive::WasmAudioDecode : public JobTask
{
public:
#ifdef WITH_RIVE_AUDIO
WasmAudioDecode(rcp<AudioReader> reader, uint64_t maxFrames)
: m_reader(std::move(reader))
, m_maxFrames(maxFrames)
{}
#endif
// Bytes 0 when it did not decode; blob null when they cannot be one.
uint64_t bytes = 0;
rcp<SharedBlob> blob;
// What the main lane charged its owner up front, refunded as it settles.
uint64_t charged = 0;
uint64_t owner = 0;
// The main lane decodes it a slice per poll instead, as a pool with no
// threads runs a job to its end.
bool sliced = false;
// True once it is done, else it stopped at until.
bool decodeUntil(Clock::time_point until)
{
#ifdef WITH_RIVE_AUDIO
bool done = false;
do
{
if (m_stop.load(std::memory_order_relaxed) ||
!m_reader->readBlock(m_maxFrames, m_samples, done))
{
m_samples = {};
return true;
}
} while (!done && Clock::now() < until);
if (!done)
{
return false;
}
bytes = m_samples.size() * sizeof(float);
if (bytes != 0 && bytes <= kSharedMaxBytes)
{
blob = make_rcp<SharedBlob>(
Span<const uint8_t>((const uint8_t*)m_samples.data(), bytes));
}
m_samples = {};
#endif
return true;
}
protected:
void run() override { decodeUntil(Clock::time_point::max()); }
void interrupt() override { m_stop.store(true, std::memory_order_relaxed); }
private:
std::atomic<bool> m_stop{false};
#ifdef WITH_RIVE_AUDIO
rcp<AudioReader> m_reader;
uint64_t m_maxFrames;
std::vector<float> m_samples;
#endif
};
uint32_t WasmScriptingVM::startAudioDecode(
rcp<AudioSource> source,
uint32_t sampleRate,
uint32_t channels)
{
#ifdef WITH_RIVE_AUDIO
// Made here, as the source caches its format lazily and unguarded.
auto reader = source->makeReader(channels, sampleRate);
if (reader == nullptr)
{
return 0;
}
watchLanes();
uint64_t frameBytes = sizeof(float) * (uint64_t)reader->channels();
uint64_t maxFrames = kSharedMaxBytes / frameBytes;
// Charged now, so decodes running at once cannot outgrow the budget.
uint64_t estimate = std::min(reader->lengthInFrames(), maxFrames) *
frameBytes;
uint64_t owner = sharedOwner();
uint32_t token = m_nextMainWait++;
if (!SharedRegistry::charge(owner, estimate))
{
wakeMain(token, shared_layout::kCreateNoMemory);
return token;
}
auto decode = make_rcp<WasmAudioDecode>(std::move(reader), maxFrames);
decode->charged = estimate;
decode->owner = owner;
decode->sliced = WasmJobs::pool().threadCount() == 0;
if (!decode->sliced)
{
WasmJobs::pool().submit(decode);
}
m_mainWaits.push_back({SignalWait(token), 0, std::move(decode)});
return token;
#else
return 0;
#endif
}
uint32_t WasmScriptingVM::watchLane(uint32_t lane)
{
m_mainWaits.push_back({SignalWait(m_nextMainWait), lane});
return m_nextMainWait++;
}
uint32_t WasmScriptingVM::watchStream(uint32_t stream)
{
m_mainWaits.push_back({SignalWait(m_nextMainWait), 0, nullptr, stream});
return m_nextMainWait++;
}
uint32_t WasmScriptingVM::waitOnMain(
uint32_t signal,
double after,
double timeoutSeconds)
{
watchLanes();
uint64_t owner = sharedOwner();
rcp<SharedSignal> found = SignalWait::find(signal, owner);
m_mainWaits.push_back(
{SignalWait(
m_nextMainWait,
owner,
signal,
found.get(),
after,
timeoutSeconds),
0});
return m_nextMainWait++;
}
void WasmScriptingVM::abandonWait(const MainWait& wait)
{
if (wait.decode != nullptr)
{
WasmJobs::pool().cancel(wait.decode.get());
SharedRegistry::refund(wait.decode->owner, wait.decode->charged);
}
#if RIVE_AUDIO_STREAMS()
releaseHostHandle(this, wait.stream, HandleTable::Tag::audioStream);
#endif
}
void WasmScriptingVM::cancelMainWait(uint32_t token)
{
for (const MainWait& wait : m_mainWaits)
{
if (wait.wait.id == token)
{
abandonWait(wait);
// A promise that parked it still takes one wake, which says it went.
wakeMain(token, 0);
}
}
m_mainWaits.erase(
std::remove_if(
m_mainWaits.begin(),
m_mainWaits.end(),
[token](const MainWait& wait) { return wait.wait.id == token; }),
m_mainWaits.end());
}
static constexpr double kOfflineDecodeSeconds = 10;
// The blob's id or why not, -1 when nothing decoded.
static double settleAudioDecode(WasmScriptingVM* vm, WasmAudioDecode& decode)
{
SharedRegistry::refund(decode.owner, decode.charged);
if (decode.bytes == 0)
{
return -1;
}
uint32_t answer = 0;
uint64_t owner = sharedCreator(vm, decode.bytes, false, true, answer);
if (owner == 0)
{
return answer;
}
return sharedCreated(vm, owner, decode.bytes, std::move(decode.blob));
}
void WasmScriptingVM::settleMainWaits()
{
auto now = std::chrono::steady_clock::now();
auto sliceEnd = now +
std::chrono::milliseconds(lane_timing::kCooperativeSliceMs);
size_t resultBytes = 0;
for (auto it = m_mainWaits.begin(); it != m_mainWaits.end();)
{
double value = 0;
bool fire = false;
if (it->decode != nullptr)
{
// Offline renders outrun real time, so one waits on what it decodes.
#if RIVE_PROCESSOR_AUDIO()
bool offline = WasmProcessors::renderOffline();
#else
bool offline = false;
#endif
if (it->decode->sliced)
{
fire = it->decode->decodeUntil(
offline ? Clock::time_point::max() : sliceEnd);
}
else
{
if (offline)
{
WasmJobs::pool().join(it->decode.get(), kOfflineDecodeSeconds);
}
fire = it->decode->finished();
}
if (fire)
{
value = settleAudioDecode(this, *it->decode);
}
}
#if RIVE_AUDIO_STREAMS()
else if (it->stream != 0)
{
HostAudioStream* stream = resolveAudioStream(this, it->stream);
fire = stream == nullptr || stream->ready();
value = stream != nullptr ? it->stream : 0;
}
#endif
else if (it->lane != 0)
{
WasmLane* lane = resolveLane(this, it->lane);
LaneState state = lane != nullptr ? lane->state() : LaneState::stopped;
fire = state >= LaneState::done;
value = (double)state;
// Each result is decoded into the main heap as it wakes, so a
// burst of large ones spreads over polls.
if (fire && state == LaneState::done)
{
size_t bytes = laneResult(lane).size();
if (resultBytes != 0 && resultBytes + bytes > kLaneResultBytesPerPoll)
{
++it;
continue;
}
resultBytes += bytes;
}
}
else
{
fire = it->wait.fired(now, value);
}
if (!fire)
{
++it;
continue;
}
uint32_t token = it->wait.id;
it = m_mainWaits.erase(it);
wakeMain(token, value);
}
}
void WasmScriptingVM::wakeMain(uint32_t token, double value)
{
// Held even outside a call, so an await always resumes at a poll.
holdOutcome([this, token, value] {
double args[3] = {(double)m_L, (double)token, value};
double unused = 0;
callF64("host_lane_wake", args, 3, &unused);
});
}
void WasmScriptingVM::deliverLaneOutput(WasmLane* lane)
{
lane->takeLogs(
[](void* vm, std::string_view line) {
rtLogImpl((WasmScriptingVM*)vm, 0, line.data(), (uint32_t)line.size());
},
this);
// A job's failure is its joiner's result, so only workers and processors
// announce theirs.
if (lane->kind() == LaneKind::job || lane->reportedFailure ||
lane->state() != LaneState::failed)
{
return;
}
lane->reportedFailure = true;
std::string line = lane->name() + " failed: " + lane->failure();
if (m_error)
{
m_error(line);
}
else
{
consoleWrite(this, line);
}
// A failed processor renders only silence, so it leaves the mix.
lane->stop();
}
void WasmScriptingVM::deliverLaneOutput()
{
settleMainWaits();
#if RIVE_AUDIO_STREAMS()
pumpAudioStreams(this);
#endif
auto deliver = [this](auto* set) {
if (set == nullptr)
{
return;
}
for (const auto& lane : set->lanes())
{
deliverLaneOutput(lane.get());
}
// Their output is out, and an idle slot keeps the watchdog polling.
set->prune();
};
deliver(m_jobs.get());
deliver(m_workers.get());
deliver(m_processors.get());
}
bool WasmScriptingVM::hasPendingAsyncWork() const
{
// A started job keeps hosts ticking until the script can see it finish,
// and a worker only while it needs a step, never for being alive.
return !m_pendingDecodes.empty() || !m_pendingFetches.empty() ||
!m_heldOutcomes.empty() || !m_mainWaits.empty() ||
(m_jobs != nullptr && m_jobs->hasUnfinished()) ||
(m_workers != nullptr && m_workers->due()) ||
(m_processors != nullptr && m_processors->starting()) || streaming();
}
bool WasmScriptingVM::streaming() const
{
#if RIVE_AUDIO_STREAMS()
// Only a poll tops a stream up, so one still decoding keeps hosts ticking.
for (const HostAudioStream* stream : m_audioStreams)
{
if (!stream->ended)
{
return true;
}
}
#endif
return false;
}
uint64_t WasmScriptingVM::sharedOwner()
{
if (m_sharedOwner == 0)
{
m_sharedOwner = SharedRegistry::nextOwner(m_limits.sharedBytesPerFile);
}
return m_sharedOwner;
}
void WasmScriptingVM::keepReadHolds(Span<const uint32_t> ids)
{
m_readHolds.insert(m_readHolds.end(), ids.begin(), ids.end());
}
void WasmScriptingVM::releaseReadHolds()
{
for (uint32_t id : m_readHolds)
{
SharedRegistry::release(id, m_sharedOwner);
}
m_readHolds.clear();
}
void WasmScriptingVM::deliverHeldOutcomes()
{
if (inModuleCall() || m_heldOutcomes.empty())
{
return;
}
VMPollList::leave(m_holdingList, this);
// Deliveries can hold more, which wait for the next pass.
std::vector<std::function<void()>> held;
held.swap(m_heldOutcomes);
for (auto& deliver : held)
{
deliver();
}
}
void WasmScriptingVM::deliverAllHeldOutcomes()
{
t_holdingVMs->deliver([](WasmScriptingVM* vm) { vm->deliverHeldOutcomes(); });
}
void WasmScriptingVM::setBootHook(BootHook hook)
{
s_bootHook = std::move(hook);
}
void WasmScriptingVM::requireBounds(bool on) { s_requireBounds.store(on); }
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);
}
}
std::string WasmScriptingVM::tierName(
ExecutionTier tier,
const std::string& interpReason)
{
switch (tier)
{
case ExecutionTier::interp:
return interpReason.empty() ? "interp" : "interp, " + interpReason;
case ExecutionTier::aotO0:
return "aot -O0";
case ExecutionTier::aotO3:
return "aot -O3";
}
return "unknown";
}
#ifndef __EMSCRIPTEN__
std::shared_ptr<void> WasmScriptingVM::loadSharedModule(
Span<const uint8_t> bytes,
ExecutionTier& tier,
std::string& interpReason,
std::string& outError,
bool checkedBounds)
{
if (!ensureRuntime())
{
outError = "wamr runtime init failed";
return nullptr;
}
// One content hash serves the AOT artifact lookups and the shared module
// cache key; the key folds in which artifact actually loads so interp
// and AOT modules never share an entry.
const uint64_t plainKey = wasmModuleKey(bytes);
uint64_t moduleKey = plainKey;
tier = ExecutionTier::interp;
// wasm_runtime_load keeps referencing the buffer, and may rewrite it.
std::vector<uint8_t> moduleBytes;
wasm_module_t loaded = nullptr;
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;
auto setCompiledSpecies = [&](TierSpecies species) {
haveHwAot = species == TierSpecies::hw;
haveO0Aot = species == TierSpecies::o0;
haveAot = !haveHwAot && !haveO0Aot;
};
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/%s",
cacheDir,
aotArtifactName(plainKey, ".hw").c_str());
struct stat hwAotStat;
haveHwAot = !checkedBounds && stat(aotPath, &hwAotStat) == 0;
#endif
if (!haveHwAot)
{
snprintf(
aotPath,
sizeof(aotPath),
"%s/%s",
cacheDir,
aotArtifactName(plainKey).c_str());
struct stat aotStat;
haveAot = stat(aotPath, &aotStat) == 0;
}
if (!haveAot && !haveHwAot)
{
char dumpPath[1024];
snprintf(
dumpPath,
sizeof(dumpPath),
"%s/%s",
cacheDir,
wasmModuleName(plainKey).c_str());
struct stat dumpStat;
if (stat(dumpPath, &dumpStat) != 0)
{
if (FILE* dump = fopen(dumpPath, "wb"))
{
fwrite(bytes.data(), 1, bytes.size(), dump);
fclose(dump);
fprintf(stderr, "wasm aot: dumped %s for wamrc\n", dumpPath);
}
}
}
}
// Tools builds boot compiled code: a ladder cache hit loads its
// artifact and a miss compiles one on this thread, blocking the load.
// RIVE_WASM_AOT_SYNC=o0 asks for the faster debug compile and =interp
// for no compile at all.
const char* tierEnv = getenv("RIVE_WASM_AOT_SYNC");
bool wantInterp = tierEnv != nullptr && strcmp(tierEnv, "interp") == 0;
TierSpecies species = bootTierSpecies();
if (checkedBounds && species == TierSpecies::hw)
{
species = TierSpecies::o3;
}
bool ladderArtifact = false;
if (!haveAot && !haveHwAot)
{
if (wantInterp)
{
interpReason = "RIVE_WASM_AOT_SYNC=interp";
}
else if (
auto cached = leaseCachedModule(
compiledModuleKey(plainKey, species),
tier))
{
interpReason.clear();
return cached;
}
else
{
TierCompile compile = ModuleTierLadder::instance().bootCompile(
plainKey,
bytes,
species);
if (!compile.path.empty() && compile.path.size() < sizeof(aotPath))
{
memcpy(aotPath, compile.path.c_str(), compile.path.size() + 1);
setCompiledSpecies(species);
ladderArtifact = true;
}
else
{
interpReason = compile.reason;
}
}
}
#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(
plainKey,
bytes.size());
if (prelinked != nullptr)
{
moduleKey ^= 0x94d049bb133111ebull;
}
#endif
if (haveAot || haveHwAot || haveO0Aot)
{
moduleKey = compiledModuleKey(
moduleKey,
haveHwAot ? TierSpecies::hw
: haveO0Aot ? TierSpecies::o0
: TierSpecies::o3);
}
bool pinned = false;
std::shared_ptr<void> lease = leaseCachedModule(moduleKey, tier);
if (lease == nullptr)
{
if (haveAot || haveHwAot || haveO0Aot)
{
if (FILE* aot = fopen(aotPath, "rb"))
{
fseek(aot, 0, SEEK_END);
long size = ftell(aot);
fseek(aot, 0, SEEK_SET);
moduleBytes.resize(size);
size_t read = fread(moduleBytes.data(), 1, size, aot);
fclose(aot);
fprintf(stderr, "wasm aot: loaded %s (%zu bytes)\n", aotPath, read);
tier = haveO0Aot ? ExecutionTier::aotO0 : ExecutionTier::aotO3;
}
}
else
{
moduleBytes.assign(bytes.begin(), bytes.end());
}
#if WASM_ENABLE_PRELINKED_AOT != 0
if (prelinked != nullptr)
{
loaded = (wasm_module_t)
loadPrelinkedAotModule(*prelinked, error, sizeof(error));
if (loaded != nullptr)
{
tier = ExecutionTier::aotO3;
pinned = true;
fprintf(
stderr,
"wasm aot: prelinked module %016llx (%zu byte text)\n",
(unsigned long long)plainKey,
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);
moduleKey = plainKey;
lease = leaseCachedModule(moduleKey, tier);
}
}
if (loaded == nullptr && lease == nullptr)
#endif
{
auto loadBytes = [&]() {
loaded = wasm_runtime_load(
moduleBytes.data(),
(uint32_t)moduleBytes.size(),
error,
sizeof(error));
};
loadBytes();
if (loaded == nullptr && (haveAot || haveHwAot || haveO0Aot))
{
// A stale artifact, say from another WAMR, must not fail the
// file; drop to the wasm bytes under the plain key.
fprintf(
stderr,
"wasm aot: %s rejected (%s); falling back\n",
aotPath,
error);
haveAot = haveHwAot = haveO0Aot = false;
tier = ExecutionTier::interp;
interpReason = std::string("aot artifact rejected, ") + error;
// Every later load would read and reject it again.
if (ladderArtifact)
{
ModuleTierLadder::instance().rejectArtifact(
plainKey,
species,
interpReason);
}
else
{
remove(aotPath);
}
moduleKey = plainKey;
lease = leaseCachedModule(moduleKey, tier);
if (lease == nullptr)
{
moduleBytes.assign(bytes.begin(), bytes.end());
loadBytes();
}
}
}
if (loaded != nullptr)
{
auto entry = std::make_shared<SharedWasmModule>();
if (wasm_runtime_rive_module_hw_bounds(loaded))
{
entry->hardwareBoundsWasm.assign(bytes.begin(), bytes.end());
}
entry->bytes = std::move(moduleBytes);
entry->module = loaded;
entry->tier = tier;
entry->pinned = pinned;
// Like entry, declared before the lock so modules unload once it
// is released, where they stall no other boot.
std::vector<std::shared_ptr<SharedWasmModule>> evicted;
std::lock_guard<std::mutex> lock(sharedModuleCacheMutex());
auto inserted = sharedModuleCache().try_emplace(moduleKey, entry);
if (inserted.second)
{
lease = leaseLocked(entry);
evictIdleModulesLocked(evicted);
}
else
{
// Another thread loaded the same bytes first, so entry
// unloads this copy.
tier = inserted.first->second->tier;
lease = leaseLocked(inserted.first->second);
}
}
}
if (lease == nullptr)
{
outError = error;
return nullptr;
}
// Prelinked code runs compiled whatever the ladder said.
if (tier != ExecutionTier::interp)
{
interpReason.clear();
}
return lease;
}
std::shared_ptr<void> WasmScriptingVM::loadFallbackModule(
Span<const uint8_t> bytes,
const char* what,
ExecutionTier& tier,
std::string& interpReason)
{
std::string error;
auto lease = loadSharedModule(bytes, tier, interpReason, error, true);
if (lease != nullptr)
{
fprintf(
stderr,
"wasm aot: no hardware bounds reservation left, %s falls back to "
"software bounds on %s\n",
what,
tierName(tier, interpReason).c_str());
}
else
{
fprintf(
stderr,
"wasm aot: no hardware bounds reservation left, and %s did not load "
"on software bounds: %s\n",
what,
error.c_str());
}
return lease;
}
Span<const uint8_t> WasmScriptingVM::hardwareBoundsWasm(
const std::shared_ptr<void>& lease)
{
std::lock_guard<std::mutex> lock(sharedModuleCacheMutex());
for (const auto& cached : sharedModuleCache())
{
const std::vector<uint8_t>& wasm = cached.second->hardwareBoundsWasm;
if (cached.second->module == lease.get())
{
return Span<const uint8_t>(wasm.data(), wasm.size());
}
}
return {};
}
bool WasmScriptingVM::hardwareBounds() const
{
return m_state != nullptr && m_state->module != nullptr &&
wasm_runtime_rive_module_hw_bounds(m_state->module);
}
bool WasmScriptingVM::init(Span<const uint8_t> module)
{
// Unsigned modules are safe only as bytecode WAMR compiles itself; AOT
// comes solely from our own artifacts, found by this bytecode's hash.
if (module.size() < 4 || memcmp(module.data(), "\0asm", 4) != 0)
{
m_lastError = "script module is not wasm bytecode";
return false;
}
m_state = std::make_unique<WamrState>();
m_state
->lease = loadSharedModule(module, m_tier, m_interpReason, m_lastError);
m_state->module = (wasm_module_t)m_state->lease.get();
if (m_state->module == nullptr)
{
m_lastError = "module load failed: " + m_lastError;
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);
}
}
int32_t exportCount = wasm_runtime_get_export_count(m_state->module);
for (int32_t i = 0; i < exportCount; i++)
{
wasm_export_t moduleExport;
wasm_runtime_get_export_type(m_state->module, i, &moduleExport);
noteModuleExport(moduleExport.name);
}
if (const char* refusal = WasmScriptingVMNatives::startRefusal(this))
{
m_lastError = std::string("module instantiate failed: ") + refusal;
return false;
}
// The top level may start lanes.
attachLanes(module);
// A top level that decodes a file starts another module inside this one.
const std::function<void(const char*, size_t)>* outerPrint = s_bootPrint;
WasmScriptingVM* outerBooting = s_booting;
s_bootPrint = &m_print;
s_booting = this;
char error[256] = {0};
// Module start is a call into the module like any other: the top
// level's probes need the bracket, its budget bounds it, and its trap
// is the instantiate error.
if (m_debugHooks != nullptr)
{
m_debugHooks->onCallBegin(*this);
}
// Set once: WAMR keeps it in a plain global, as ARMv7 Android loads no
// ELF TLS, and the hook finds its VM through s_booting.
static std::once_flag startHookOnce;
std::call_once(startHookOnce, [] {
wasm_runtime_rive_set_start_hook(
[](wasm_exec_env_t env, bool begin, void*) {
WasmScriptingVM* vm = s_booting;
// A lane the top level joined starts on this thread too.
if (vm == nullptr || WasmLane::current() != nullptr)
{
return;
}
if (begin)
{
// Trusted modules run their top level unbounded, like their
// calls.
if (vm->m_hostBudget && !vm->m_budgetArmed && vm->boundsCalls())
{
vm->armWatchdog(env);
}
vm->armDeadline();
// A wait moves a metered module's fuel on with a call.
vm->m_state->execEnv = env;
}
else
{
vm->m_state->execEnv = nullptr;
if (vm->m_deadlineArmed)
{
vm->disarmDeadline();
}
if (vm->m_budgetArmed && vm->m_budgetSlot->env == env)
{
vm->disarmWatchdog();
}
}
uint32_t fuelMs = (uint32_t)WasmScriptingVMNatives::startFuelMs(vm);
wasm_module_inst_t inst = wasm_runtime_get_module_inst(env);
wasm_function_inst_t arm = wasm_runtime_lookup_function(
inst,
"__riveFuelArm");
wasm_function_inst_t end = wasm_runtime_lookup_function(
inst,
"__riveFuelEnd");
// A start that trapped leaves no fuel to put back.
if (fuelMs != 0 && arm != nullptr && end != nullptr &&
wasm_runtime_get_exception(inst) == nullptr)
{
wasm_runtime_call_wasm(
env,
begin ? arm : end,
begin ? 1 : 0,
&fuelMs);
}
},
nullptr);
});
// A start nested in a call must not see the region it holds, and a top
// level that trapped inside exclusive must not leave its region held.
uint32_t outerRegionId;
SharedRegion* outerRegion = stashHeldRegion(&outerRegionId);
ensureWasmThreadEnv();
auto instantiate = [&]() {
m_state->instance = wasm_runtime_instantiate(
m_state->module,
512 * 1024,
0,
error,
sizeof(error));
};
instantiate();
if (m_state->instance == nullptr &&
strstr(error, WASM_RIVE_HW_RESERVE_FAILED) != nullptr)
{
if (auto checked = loadFallbackModule(
module,
"main module",
m_tier,
m_interpReason))
{
m_state->lease = std::move(checked);
m_state->module = (wasm_module_t)m_state->lease.get();
instantiate();
}
}
restoreHeldRegion(outerRegionId, outerRegion);
if (m_debugHooks != nullptr)
{
m_debugHooks->onCallEnd(
*this,
m_state->instance == nullptr ? error : nullptr);
}
s_booting = outerBooting;
s_bootPrint = outerPrint;
if (m_state->instance == nullptr || m_poisoned)
{
// The runtime's exception buffer truncates what start threw, and AOT
// code traps on a timeout as unreachable.
std::string detail = m_poisoned ? "execution exceeded timeout"
: m_moduleErrorDetail.empty() ? std::string(error)
: m_moduleErrorDetail;
m_lastError = "module instantiate failed: " + detail;
return false;
}
m_state->findCallState();
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);
return bootModule();
}
#endif
void WasmScriptingVM::noteModuleExport(const char* name)
{
m_utf16Strings |= strcmp(name, "__riveUtf16Strings") == 0;
m_utf16HostStrings |= strcmp(name, "__riveUtf16HostStrings") == 0;
m_hostBudget |= strcmp(name, "__riveHostBudget") == 0;
m_fuelBudget |= strcmp(name, "__riveFuelCheck") == 0;
m_fuelShift |= strcmp(name, "__riveFuelShift") == 0;
m_fetchBlobBody |= strcmp(name, "__riveFetchBlobBody") == 0;
}
bool WasmScriptingVM::bootModule()
{
// 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 = hasExport("riveRegister") &&
(leakEnv == nullptr || strcmp(leakEnv, "0") != 0);
m_collectedRuntime = hasExport("__riveCollected");
m_handleWatch = m_leakWatch;
// Frame collector modules scavenge at every boundary instead of
// rewinding or finalizing.
m_frameMinor = hasExport("__riveFrameMinor");
m_frameRelease = m_frameMinor && hasExport("__riveFrameRelease");
m_frameMajorsProbe = hasExport("__riveFrameMajors");
// Stub modules report their bump position, finer than page counts.
m_heapUsedProbe = hasExport("__riveHeapUsed");
m_rawAllocExport = hasExport("__riveRawAlloc");
m_inputSlotExport = hasExport("host_obj_input_slot");
m_pathEffectV2 = hasExport("host_obj_path_effect_v2");
m_pointerEventV2 = hasExport("host_obj_pointer_event_v2");
m_layoutResizeV2 = hasExport("host_obj_layout_resize_v2");
m_transitionManagesToExport = hasExport("host_obj_transition_manages_to");
callModule("__wasm_call_ctors", 0, nullptr);
m_L = callModule("host_newstate", 0, nullptr);
if (m_L == 0)
{
m_lastError = "host_newstate failed";
return false;
}
// A deterministic run pins math.random, as ScriptingVM::init does for
// native Luau. Older blobs without the export stay clock-seeded.
if (File::deterministicMode && hasExport("host_randomseed"))
{
uint32_t seedArgs[2] = {m_L, (uint32_t)File::deterministicSeed};
callModule("host_randomseed", 2, seedArgs);
}
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;
}
const char* WasmScriptingVM::frameBoundary()
{
const char* warning = collectFrame();
if (m_decodedVMs.empty())
{
return warning;
}
m_frameReport = warning != nullptr ? warning : "";
// A file a script decoded has no host to give it boundaries.
for (WasmScriptingVM* decoded : std::vector<WasmScriptingVM*>(m_decodedVMs))
{
// Releasing one file can end another.
if (std::find(m_decodedVMs.begin(), m_decodedVMs.end(), decoded) ==
m_decodedVMs.end())
{
continue;
}
if (const char* notice = decoded->frameBoundary())
{
joinFrameReport(m_frameReport, notice);
}
}
return m_frameReport.empty() ? nullptr : m_frameReport.c_str();
}
const char* WasmScriptingVM::frameBoundaryWarnings()
{
const char* report = frameBoundary();
std::string warnings;
for (std::string_view rest = report != nullptr ? report : ""; !rest.empty();)
{
std::string_view line = rest.substr(0, rest.find('\n'));
rest.remove_prefix(std::min(rest.size(), line.size() + 1));
if (line != kFrameGcNotice)
{
joinFrameReport(warnings, line);
}
}
m_frameReport = std::move(warnings);
return m_frameReport.empty() ? nullptr : m_frameReport.c_str();
}
void WasmScriptingVM::joinFrameReport(
std::string& joined,
std::string_view report)
{
if (!joined.empty())
{
joined += '\n';
}
joined += report;
}
const char* WasmScriptingVM::collectFrame()
{
if (!valid())
{
return nullptr;
}
releaseReadHolds();
if (m_frameMinor)
{
uint32_t promoted = 0;
if (callModuleChecked("__riveFrameMinor", 0, nullptr, &promoted) ==
CallOutcome::trapped)
{
// Objects may be half moved, so nothing may run on the heap.
poison();
return "script stopped: its collector trapped mid collection";
}
if (getenv("RIVE_FRAME_GC_DEBUG") != nullptr)
{
fprintf(stderr, "frameMinor promoted=%u\n", promoted);
callModule("__riveFrameVerify", 0, nullptr);
}
if (m_frameRelease)
{
// The handles the collection found dead. Releasing one can
// destroy an artboard instance whose scripted objects call
// back into the module (host_obj_dispose, host_unref), so this
// runs after the collection, never inside it. (A host that
// skips it gets them released at the next boundary.)
callModule("__riveFrameRelease", 0, nullptr);
}
if (!m_frameMinorAnnounced)
{
m_frameMinorAnnounced = true;
return kFrameGcNotice;
}
if (const char* warning = handleLeakWarning())
{
return warning;
}
return heapGrowthWarning();
}
if (const char* warning = handleLeakWarning())
{
return warning;
}
return heapGrowthWarning();
}
#ifdef __EMSCRIPTEN__
// A page cannot set the environment.
static constexpr const char* kLeakWarnOffHint = "";
#else
static constexpr const char*
kLeakWarnOffHint = " RIVE_WASM_LEAK_WARN=0 silences this.";
#endif
const char* WasmScriptingVM::heapGrowthWarning()
{
if (!m_leakWatch || !m_advancedOnce)
{
return nullptr;
}
uint32_t pages = 0;
uint32_t maxPages = 0;
if (!memoryLimits(&pages, &maxPages))
{
return nullptr;
}
// The stub bump position beats page counts when available.
if (m_heapUsedProbe)
{
pages = std::max(1u, callModule("__riveHeapUsed", 0, nullptr) >> 16);
}
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};
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).%s",
grownMB,
frames,
projection,
kLeakWarnOffHint);
}
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.%s",
grownMB,
frames,
projection,
kLeakWarnOffHint);
}
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::text:
return "text";
case Tag::buffer:
return "buffer";
case Tag::meshInstances:
return "meshInstances";
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::transitionChild:
return "transitionChild";
case Tag::audioSource:
return "audioSource";
case Tag::audioSound:
return "audioSound";
case Tag::audioStream:
return "audioStream";
case Tag::drawable:
return "drawable";
case Tag::riveFile:
return "riveFile";
case Tag::bindableArtboard:
return "bindableArtboard";
case Tag::lane:
return "lane";
case Tag::empty:
case Tag::count:
break;
}
return "unknown";
}
#ifndef __EMSCRIPTEN__
bool WasmScriptingVM::memoryLimits(uint32_t* pages, uint32_t* maxPages) const
{
if (m_state == nullptr || m_state->instance == nullptr)
{
return false;
}
wasm_memory_inst_t memory = wasm_runtime_get_default_memory(
m_state->instance);
if (memory == nullptr)
{
return false;
}
*pages = (uint32_t)wasm_memory_get_cur_page_count(memory);
*maxPages = (uint32_t)wasm_memory_get_max_page_count(memory);
return true;
}
#endif
uint32_t WasmScriptingVM::memoryPages() const
{
uint32_t pages = 0;
uint32_t maxPages = 0;
return memoryLimits(&pages, &maxPages) ? pages : 0;
}
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.live();
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_handleCollects = 0;
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;
}
// A major already in flight keeps wrappers that died after it began,
// so growth is judged by one requested once the first finishes.
constexpr uint8_t kHandleJudgeCollects = 2;
if (m_collectedRuntime)
{
uint32_t majors = frameMajors();
if (m_handleCollects != 0 && majors == m_handleCollectMajors)
{
return nullptr;
}
if (m_handleCollects < kHandleJudgeCollects)
{
m_handleCollects++;
m_handleCollectMajors = majors;
callModule("__riveCollect", 0, nullptr);
return nullptr;
}
}
m_handleCollects = 0;
// 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().%s",
grown,
frames,
counts[top],
handleTagName((HandleTable::Tag)top),
kLeakWarnOffHint);
m_leakWarning = buffer;
return m_leakWarning.c_str();
}
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;
}
copyToModule(namePtr, name.c_str(), (uint32_t)name.size() + 1);
copyToModule(bcPtr, bytecode.data(), (uint32_t)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 namePtr = guestString(name.c_str());
if (namePtr == 0)
{
m_lastError = "module name allocation failed";
return false;
}
uint32_t requireArgs[2] = {m_L, namePtr};
uint32_t status = 0;
CallOutcome outcome = callModuleChecked(
"host_require",
2,
requireArgs,
&status);
guestFree(namePtr);
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 ? resolveModuleString(messagePtr)
: 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 ---------------------
#ifndef __EMSCRIPTEN__
bool WasmScriptingVM::valid() const
{
return m_state != nullptr && m_state->execEnv != nullptr && m_L != 0 &&
!m_poisoned;
}
#endif
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)
{
double args[3] = {(double)m_L, (double)selfRef, (double)contextRef};
double status = 0;
// An init can run a nested one; each reads only its own misses.
bool outerMissing = m_missingRequestedData;
m_missingRequestedData = false;
CallOutcome outcome = callF64("host_obj_user_init", args, 3, &status);
bool missing = m_missingRequestedData;
m_missingRequestedData = outerMissing;
switch (outcome)
{
case CallOutcome::missing:
return InitResult::notImplemented;
case CallOutcome::trapped:
m_lastError = lastTrap().message;
return InitResult::failed;
case CallOutcome::ok:
break;
}
if (status == 0)
{
return InitResult::notImplemented;
}
return status == 1 && !missing ? InitResult::succeeded : InitResult::failed;
}
bool WasmScriptingVM::callAdvance(
ScriptedObject* object,
int selfRef,
float elapsedSeconds)
{
double args[3] = {(double)m_L, (double)selfRef, elapsedSeconds};
double keepGoing = 0;
if (callF64("host_obj_advance", args, 3, &keepGoing) != CallOutcome::ok)
{
return false;
}
m_advancedOnce = true;
return keepGoing != 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)
{
GuestName nameCopy(this, name);
uint32_t namePtr = nameCopy.ptr;
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)
{
return false;
}
copyToModule(scratch, args, (uint32_t)(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);
return ok != 0;
}
bool WasmScriptingVM::callBooleanMethod(
ScriptedObject* object,
int selfRef,
const char* name)
{
GuestName nameCopy(this, name);
uint32_t namePtr = nameCopy.ptr;
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);
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();
return callPathEffectUpdate(
object,
selfRef,
sourcePath,
paint,
shapePaint->parentTransformComponent(),
outPath);
}
bool WasmScriptingVM::callPathEffectUpdate(
ScriptedObject* object,
int selfRef,
const RawPath& sourcePath,
const PathEffectPaintWire& paint,
TransformComponent* shape,
RawPath* outPath)
{
if (!valid() || outPath == nullptr)
{
return false;
}
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;
// _v2 also gets a node over the shape, the Luau lane's NodeData; modules
// built before it get the paint alone.
bool withNode = m_pathEffectV2;
size_t scopeStart = m_scopedNodes.size();
uint32_t nodeHandle = 0;
if (withNode && shape != nullptr)
{
nodeHandle = mintNode(this, nullptr, shape);
}
uint32_t args[8] = {
m_L,
(uint32_t)selfRef,
dataPtr + (uint32_t)sizeof(paint) + pointBytes,
verbCount,
dataPtr + (uint32_t)sizeof(paint),
floatCount,
dataPtr,
nodeHandle};
uint32_t ok = withNode ? callModule("host_obj_path_effect_v2", 8, args)
: callModule("host_obj_path_effect", 7, args);
releaseScopedNodes(scopeStart);
m_pathEffectOut = nullptr;
guestFree(dataPtr);
return ok != 0;
}
void WasmScriptingVM::releaseScopedNodes(size_t from)
{
for (size_t i = from; i < m_scopedNodes.size(); i++)
{
uint32_t handle = m_scopedNodes[i];
if (auto node = static_cast<HostNode*>(
m_handles.resolve(handle, HandleTable::Tag::node)))
{
m_handles.release(handle, HandleTable::Tag::node);
delete node;
}
}
m_scopedNodes.resize(from);
}
bool WasmScriptingVM::callDataConvert(
ScriptedObject* object,
int selfRef,
const char* method,
DataValue* input,
ScriptDataResult* outResult)
{
if (!valid() || input == nullptr)
{
return false;
}
DataConvertWire wire;
std::string text;
if (input->is<DataValueNumber>())
{
wire.kind = DataConvertWire::kindNumber;
wire.number = input->as<DataValueNumber>()->value();
}
else if (input->is<DataValueString>())
{
wire.kind = DataConvertWire::kindString;
const std::string& value = input->as<DataValueString>()->value();
text = moduleStringBytes(this, value.data(), value.size());
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;
}
GuestName methodCopy(this, method);
uint32_t methodPtr = methodCopy.ptr;
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)
{
return false;
}
uint8_t* out = (uint8_t*)resolveModulePtr(wirePtr, byteCount);
memcpy(out, &wire, sizeof(wire));
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);
return ran != 0;
}
bool WasmScriptingVM::callHitExport(
const char* name,
const double* args,
uint32_t argc,
HitResult* outResult)
{
double hit = 0;
if (callF64(name, args, argc, &hit) != CallOutcome::ok || hit == 0)
{
return false;
}
*outResult = (HitResult)((int)hit - 1);
return true;
}
bool WasmScriptingVM::callPointerEvent(
ScriptedObject* object,
int selfRef,
const char* method,
int pointerId,
Vec2D localPosition,
ListenerType hitType,
float timeStamp,
HitResult* outResult)
{
if (!valid())
{
return false;
}
GuestName methodCopy(this, method);
uint32_t methodPtr = methodCopy.ptr;
if (methodPtr == 0)
{
return false;
}
double args[8] = {
(double)m_L,
(double)selfRef,
(double)methodPtr,
(double)pointerId,
localPosition.x,
localPosition.y,
(double)hitType,
timeStamp};
// Modules built before _v2 take no listener type or time stamp.
return m_pointerEventV2
? callHitExport("host_obj_pointer_event_v2", args, 8, outResult)
: callHitExport("host_obj_pointer_event", args, 6, outResult);
}
bool WasmScriptingVM::callScrollEvent(
ScriptedObject* object,
int selfRef,
int pointerId,
Vec2D localPosition,
const ScrollEvent& event,
float timeStamp,
HitResult* outResult)
{
if (!valid())
{
return false;
}
double args[10] = {
(double)m_L,
(double)selfRef,
(double)pointerId,
localPosition.x,
localPosition.y,
event.delta.x,
event.delta.y,
(double)event.phase,
event.precise ? 1.0 : 0.0,
timeStamp};
// Modules built before scroll input lack the export and never scroll.
return callHitExport("host_obj_scroll_event", args, 10, outResult);
}
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.
std::string bytes = moduleStringBytes(this, text.data(), text.size());
uint32_t sizeArgs[1] = {(uint32_t)bytes.size() + 1};
uint32_t textPtr = callModule("malloc", 1, sizeArgs);
if (textPtr == 0)
{
return false;
}
copyToModule(textPtr, bytes.data(), (uint32_t)bytes.size());
uint32_t args[4] = {m_L, (uint32_t)selfRef, textPtr, (uint32_t)bytes.size()};
uint32_t result = callModule("host_obj_text_event", 4, args);
guestFree(textPtr);
return result != 0;
}
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;
}
GuestName methodCopy(this, method);
uint32_t methodPtr = methodCopy.ptr;
if (methodPtr == 0)
{
return false;
}
uint32_t byteCount = gamepadPayloadSize(wire);
uint32_t sizeArgs[1] = {byteCount};
uint32_t dataPtr = callModule("malloc", 1, sizeArgs);
if (dataPtr == 0)
{
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);
return result != 0;
}
void WasmScriptingVM::callListenerPerform(
ScriptedObject* object,
int selfRef,
const ListenerInvocation& invocation)
{
if (!valid())
{
return;
}
ListenerWire wire;
wire.kind = (uint32_t)invocation.kind();
std::string textBytes;
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())
{
textBytes = moduleStringBytes(this, t->text.data(), t->text.size());
text = &textBytes;
wire.textLength = (uint32_t)textBytes.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 ? gamepadPayloadSize(gamepad) : 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);
}
// A module predating Transition cannot draw the incoming child, so the
// runtime keeps drawing it.
bool WasmScriptingVM::transitionManagesTo(int selfRef)
{
if (!valid() || !m_transitionManagesToExport)
{
return false;
}
uint32_t args[2] = {m_L, (uint32_t)selfRef};
return callModule("host_obj_transition_manages_to", 2, args) != 0;
}
uint32_t WasmScriptingVM::mintTransitionChild(const TransitionChildRef& ref)
{
return ref.artboard == nullptr ? 0
: m_handles.mint(
HandleTable::Tag::transitionChild,
const_cast<TransitionChildRef*>(&ref));
}
void WasmScriptingVM::releaseTransitionChild(uint32_t handle)
{
if (handle != 0)
{
m_handles.release(handle, HandleTable::Tag::transitionChild);
}
}
void WasmScriptingVM::callTransitionChanged(
ScriptedObject* object,
int selfRef,
const TransitionChildRef& from,
const TransitionChildRef& to,
int direction)
{
if (!valid())
{
return;
}
uint32_t fromHandle = mintTransitionChild(from);
uint32_t toHandle = mintTransitionChild(to);
uint32_t args[5] =
{m_L, (uint32_t)selfRef, fromHandle, toHandle, (uint32_t)direction};
callModule("host_obj_transition_changed", 5, args);
releaseTransitionChild(fromHandle);
releaseTransitionChild(toHandle);
}
void WasmScriptingVM::callTransitionDraw(
ScriptedObject* object,
int selfRef,
Renderer* renderer,
const TransitionChildRef& from,
const TransitionChildRef& to)
{
if (!valid())
{
return;
}
HostRenderer host{renderer};
uint32_t rendererHandle = m_handles.mint(HandleTable::Tag::renderer, &host);
uint32_t fromHandle = mintTransitionChild(from);
uint32_t toHandle = mintTransitionChild(to);
uint32_t args[5] =
{m_L, (uint32_t)selfRef, rendererHandle, fromHandle, toHandle};
callModule("host_obj_transition_draw", 5, args);
host.restoreSaves();
releaseTransitionChild(fromHandle);
releaseTransitionChild(toHandle);
m_handles.release(rendererHandle, HandleTable::Tag::renderer);
}
void WasmScriptingVM::callLayoutResize(
ScriptedObject* object,
int selfRef,
Vec2D size)
{
if (!valid())
{
return;
}
double args[5] =
{(double)m_L, (double)selfRef, size.x, size.y, displayScale()};
double unused = 0;
// Modules built before _v2 take no surface scale.
if (m_layoutResizeV2)
{
callF64("host_obj_layout_resize_v2", args, 5, &unused);
}
else
{
callF64("host_obj_layout_resize", args, 4, &unused);
}
}
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;
}
uint32_t namePtr = guestString(name);
if (namePtr == 0)
{
return -1;
}
uint32_t args[3] = {m_L, (uint32_t)selfRef, namePtr};
uint32_t slot = 0;
CallOutcome outcome = callModuleChecked(
"host_obj_input_slot",
3,
args,
&slot);
guestFree(namePtr);
// A trap answers nothing, so the next lookup asks again.
if (outcome != CallOutcome::ok)
{
return -1;
}
slots.emplace(name, (int32_t)slot);
return (int32_t)slot;
}
bool WasmScriptingVM::legacyInputs() { return !m_inputSlotExport; }
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;
}
double args[4] = {(double)m_L, (double)selfRef, (double)arg, value};
double unused = 0;
callF64("host_obj_set_number", args, 4, &unused);
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};
uint32_t unused = 0;
if (callModuleChecked("host_obj_set_view_model", 4, args, &unused) !=
CallOutcome::ok)
{
releaseHostHandle(this, handle, HandleTable::Tag::viewModelInstance);
}
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};
uint32_t unused = 0;
if (callModuleChecked("host_obj_set_artboard", 4, args, &unused) !=
CallOutcome::ok)
{
releaseHostHandle(this, handle, HandleTable::Tag::artboard);
}
guestFree(owned);
}
uint32_t WasmScriptingVM::guestString(const char* text, const char* allocator)
{
if (text == nullptr)
{
return 0;
}
std::string bytes = moduleStringBytes(this, text, strlen(text));
// The terminator is one unit wide.
bytes.append(m_utf16HostStrings ? 2 : 1, '\0');
uint32_t sizeArgs[1] = {(uint32_t)bytes.size()};
uint32_t ptr = callModule(allocator, 1, sizeArgs);
if (ptr != 0)
{
copyToModule(ptr, bytes.data(), (uint32_t)bytes.size());
}
return ptr;
}
WasmScriptingVM::GuestName::GuestName(WasmScriptingVM* vm, const char* name)
: m_vm(vm)
{
if (!vm->m_rawAllocExport)
{
ptr = vm->guestString(name);
m_owned = true;
return;
}
auto& names = vm->m_guestNames;
auto it = names.find(name);
if (it != names.end())
{
ptr = it->second;
return;
}
ptr = vm->guestString(name, "__riveRawAlloc");
if (ptr != 0)
{
names.emplace(name, ptr);
}
}
WasmScriptingVM::GuestName::~GuestName()
{
if (m_owned)
{
m_vm->guestFree(ptr);
}
}
void WasmScriptingVM::guestFree(uint32_t ptr)
{
if (ptr == 0)
{
return;
}
uint32_t args[1] = {ptr};
callModule("free", 1, args);
}
#ifdef __EMSCRIPTEN__
// The runner sets the booting VM around a module's start.
extern "C" EMSCRIPTEN_KEEPALIVE void rive_web_vm_booting(uint32_t handle)
{
WasmScriptingVMNatives::booting((WasmScriptingVM*)(uintptr_t)handle);
}
extern "C" EMSCRIPTEN_KEEPALIVE int rive_web_vm_start_fuel(uint32_t handle)
{
return WasmScriptingVMNatives::startFuelMs(
(WasmScriptingVM*)(uintptr_t)handle);
}
extern "C" EMSCRIPTEN_KEEPALIVE const char* rive_web_vm_start_refusal(
uint32_t handle)
{
return WasmScriptingVMNatives::startRefusal(
(WasmScriptingVM*)(uintptr_t)handle);
}
// The std's console imports, by the method codes the page's env block uses.
extern "C" void rive_web_console(
uint32_t handle,
uint32_t method,
const char* text,
uint32_t length)
{
WasmScriptingVM* vm = bootVmFromHandle(handle);
WasmStringArg utf8(vm, text, length);
std::string value(utf8.data(), utf8.size());
switch (method)
{
case 0:
consoleWrite(vmFromHandle(handle), value);
break;
case 1:
consoleTimeStart(vm, value);
break;
default:
consoleTimeReport(vm, value, method == 3);
break;
}
}
extern "C" void rive_web_trace(
uint32_t handle,
const char* text,
uint32_t length,
int32_t count,
double a0,
double a1,
double a2,
double a3,
double a4)
{
WasmStringArg utf8(bootVmFromHandle(handle), text, length);
double args[5] = {a0, a1, a2, a3, a4};
traceWrite(
vmFromHandle(handle),
std::string(utf8.data(), utf8.size()),
count,
args);
}
#endif
// Browser-lane exports over the same impl cores; nothing off emscripten.
#include "wasm_natives_web_gen.hpp"
#endif