blob: 6ef7b54c2a4ec469de909034add24c85d39d568a [file]
#ifndef _RIVE_WASM_SCRIPTING_VM_HPP_
#define _RIVE_WASM_SCRIPTING_VM_HPP_
#ifdef WITH_RIVE_SCRIPTING_WASM
#include "rive/refcnt.hpp"
#include "rive/scripted/script_backend.hpp"
#include "rive/span.hpp"
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace rive
{
/// Executes a .riv's self contained wasm script module (VM + bindings +
/// compiled scripts) under WAMR. One instance per file instance. The module
/// needs no instantiation time wiring beyond emscripten's setjmp/longjmp glue,
/// provided here as native symbols per the luau_wasm EH spike. Implements the
/// ScriptBackend seam over the module's host_obj_* exports, whose bodies
/// mirror the Luau backend.
class Factory;
class File;
class RenderPaint;
class RenderPath;
class ViewModel;
class WorkTask;
class WasmScriptingVM : public ScriptBackend
{
public:
/// factory makes the real render objects the module's handles resolve to;
/// it must outlive the VM. print is installed before instantiation, the
/// only way to see output from module start: rasc runs every script's top
/// level there, riveRegister included.
static std::unique_ptr<WasmScriptingVM> make(
Span<const uint8_t> module,
Factory* factory,
std::string& outError,
std::function<void(const char*, size_t)> print = {});
~WasmScriptingVM();
/// One live host object per module handle: 24-bit slot, 8-bit generation,
/// type tag checked on resolve. Stale or foreign handles resolve null.
struct HandleTable
{
enum class Tag : uint8_t
{
empty,
path,
paint,
renderer,
shader,
object,
viewModelInstance,
instanceValue,
image,
font,
buffer,
gpuCanvas,
gpuPass,
gpuBuffer,
gpuTexture,
gpuSampler,
gpuTextureView,
gpuShaderModule,
gpuBindGroupLayout,
gpuBindGroup,
gpuPipeline,
dataContext,
artboard,
animation,
node,
};
struct Slot
{
Tag tag = Tag::empty;
uint8_t generation = 0;
void* object = nullptr;
};
std::vector<Slot> slots;
std::vector<uint32_t> freeSlots;
uint32_t mint(Tag tag, void* object);
void* resolve(uint32_t handle, Tag tag) const;
/// Bumps the generation so outstanding copies of the handle go stale.
void release(uint32_t handle, Tag tag);
};
HandleTable& handles() { return m_handles; }
Factory* factory() const { return m_factory; }
/// The file's view models, backing the module's Data constructors; the
/// file outlives the VM.
void viewModels(std::vector<ViewModel*>* value) { m_viewModels = value; }
std::vector<ViewModel*>* viewModels() const { return m_viewModels; }
/// The owning file, for id-bound asset property resolution through its
/// registry; the file outlives the VM.
void file(File* value) { m_file = value; }
File* file() const { return m_file; }
/// Loads and runs a module registered in the embedded registry; returns
/// false with the error in lastError(). outResultRef, when set, receives
/// a ref to the module's result (the generator for protocol scripts).
bool requireModule(const std::string& name, int* outResultRef = nullptr);
/// Registers loose Luau bytecode under `name` for require, the dynamic
/// twin of the linker's embedded registry; lets bytecode-only files run
/// on a stock vm module.
bool registerBytecode(const std::string& name,
Span<const uint8_t> bytecode);
/// Delivers a watched value change to the module's listener registry.
void notifyDataValueChanged(uint32_t token);
/// context:decodeImage plumbing. The module issues the token; the decode
/// runs as a WorkTask on the global WorkPool, so completion lands through
/// the same per-advance rive_pollAsyncWork poll as the Luau backend and
/// resolves the module's promise via its host_image_decoded /
/// host_image_decode_failed exports. Returns false when this build has
/// no decoders.
bool startImageDecode(const uint8_t* bytes,
uint32_t byteCount,
uint32_t token);
/// Flags the in-flight decode cancelled so the poll skips its callbacks;
/// fired by the module promise's onCancel hook.
void cancelImageDecode(uint32_t token);
/// WorkTask completion callbacks, delivered from rive_pollAsyncWork.
void resolveImageDecode(uint32_t token,
uint32_t width,
uint32_t height,
const uint8_t* pixels,
uint32_t byteCount);
void rejectImageDecode(uint32_t token, const char* message);
/// Advances detached view model instances the module holds handles to,
/// mirroring the Luau context's tracked instance advance.
void advanceDetachedViewModels();
// ScriptBackend.
bool valid() const override;
void releaseRef(int ref) override;
int instantiate(int generatorRef,
ScriptedObject* object,
int* outContextRef,
ScriptedContext** outContextPtr) override;
InitResult callUserInit(ScriptedObject* object,
int selfRef,
int contextRef) override;
bool callAdvance(ScriptedObject* object,
int selfRef,
float elapsedSeconds) override;
void callUpdate(ScriptedObject* object, int selfRef) override;
void callTrigger(ScriptedObject* object,
int selfRef,
const char* name) override;
bool callNumberMethod(ScriptedObject* object,
int selfRef,
const char* name,
const float* args,
size_t argCount,
float* outResult) override;
bool callBooleanMethod(ScriptedObject* object,
int selfRef,
const char* name) override;
/// The node argument exposes the Luau lane's PaintData snapshot of the
/// ShapePaint (paint / asPaint()); its live transform surface stays host
/// side.
bool callPathEffectUpdate(ScriptedObject* object,
int selfRef,
const RawPath& sourcePath,
const ShapePaint* shapePaint,
RawPath* outPath) override;
bool callDataConvert(ScriptedObject* object,
int selfRef,
const char* method,
DataValue* input,
ScriptDataResult* outResult) override;
/// Every invocation kind crosses; the host-entangled pointers (focus
/// group, reported Event, view model source, semantic group) are dropped
/// because no script surface reads them.
void callListenerPerform(ScriptedObject* object,
int selfRef,
const ListenerInvocation& invocation) override;
void callDraw(ScriptedObject* object,
int selfRef,
Renderer* renderer) override;
bool callGamepadEvent(ScriptedObject* object,
int selfRef,
const char* method,
const ListenerInvocation& invocation) override;
bool callPointerEvent(ScriptedObject* object,
int selfRef,
const char* method,
int pointerId,
Vec2D localPosition,
HitResult* outResult) override;
bool callKeyboardEvent(ScriptedObject* object,
int selfRef,
Key key,
KeyModifiers modifiers,
bool isPressed,
bool isRepeat) override;
bool callTextEvent(ScriptedObject* object,
int selfRef,
const std::string& text) override;
void callLayoutResize(ScriptedObject* object,
int selfRef,
Vec2D size) override;
bool callLayoutMeasure(ScriptedObject* object,
int selfRef,
Vec2D* outSize) override;
void setInputBoolean(int selfRef, const char* name, bool value) override;
void setInputNumber(int selfRef, const char* name, float value) override;
void setInputUnsigned(int selfRef,
const char* name,
uint32_t value) override;
void setInputString(int selfRef,
const char* name,
const char* value) override;
/// self[name] becomes a module Artboard userdata over a host-owned
/// instance handle, mirroring the Luau lane's ScriptedArtboard.
void setInputArtboard(int selfRef,
const char* name,
ScriptedObject* object,
Artboard* artboard) override;
void setInputViewModel(int selfRef,
const char* name,
ViewModelInstanceValue* value) override;
/// Result sinks the module's rive_path_v1.effect_result and
/// rive_data_v1.convert_result imports write through; non-null only
/// while the corresponding seam call runs.
RawPath* pathEffectOut() const { return m_pathEffectOut; }
ScriptDataResult* convertResultOut() const { return m_convertResultOut; }
/// Modules baked into the embedded registry, from host_register_embedded.
int embeddedModuleCount() const { return m_embeddedModules; }
#ifdef WITH_RIVE_TOOLS
/// Edit-time shader side-band, the wasm twin of ScriptingContext's RSTB
/// registry: requestWasmVM registers freshly compiled WGSL here and the
/// module's shader natives consult it before file assets.
void registerShaderRstb(std::string name, std::vector<uint8_t> bytes);
const std::vector<uint8_t>* findShaderRstb(const std::string& name) const;
#endif
const std::string& lastError() const { return m_lastError; }
/// The representation currently executing; interp is tier 0, artifacts
/// arrive by transplant.
enum class ExecutionTier : uint8_t
{
interp = 0,
aotO0 = 1,
aotO3 = 2,
};
ExecutionTier executionTier() const { return m_tier; }
/// Queue this module on the process tier ladder; arrivals are picked up
/// by maybeUpgradeTier. laneId identifies the logical script so a newer
/// module content supersedes in-flight compiles.
void scheduleTierCompiles(const std::string& laneId);
/// Frame-boundary poll: when a better artifact than the current tier is
/// in the ladder cache, swap onto it, carrying all live state. Call with
/// no wasm frames on the stack. Returns true when a swap happened.
bool maybeUpgradeTier();
/// The per-frame collection point: frame collector modules scavenge
/// here (call once during load after init so init promotion lands in
/// load time). All rasc modules also get the leak watch: once the heap
/// grows well past its post-first-advance baseline the call returns a
/// warning for the host's log channel, re-armed per 8MB of further
/// growth and carrying a time-to-trap estimate (RIVE_WASM_LEAK_WARN=0
/// silences it, e.g. for benches).
///
/// Call once per host frame with no wasm frames live; returns null when
/// there is nothing to report.
const char* frameBoundary();
const char* handleLeakWarning();
const char* heapGrowthWarning();
/// Current size of the module's linear memory in 64KB wasm pages.
uint32_t memoryPages() const;
/// Swap execution onto a compiled artifact of this module, carrying
/// memory, globals, and tables. No wasm frames may be live. On failure
/// the current instance keeps running.
bool applyTierArtifact(Span<const uint8_t> artifactBytes,
ExecutionTier tier,
std::string& error,
bool hwBounds = false);
/// Backend seams: host code reaches module memory and module functions
/// only through these, so a browser backend can substitute staged
/// copies and JS dispatch for WAMR's direct access.
/// Resolves size bytes of module memory to a host pointer, null when
/// out of range. The pointer and writes through it stay coherent until
/// the next callModule, when a copying backend flushes staged ranges;
/// WAMR returns the linear memory address directly.
virtual void* resolveModulePtr(uint32_t appAddr, uint32_t size);
/// Calls a module export with i32 args; returns the first result, or 0
/// when the export is missing or the call traps.
virtual uint32_t callModule(const char* name,
uint32_t argc,
uint32_t* argv);
/// The honest variant: distinguishes a missing export from a trap from
/// a real zero result, for callers where the difference is a failure.
enum class CallOutcome
{
ok,
missing,
trapped,
};
CallOutcome callModuleChecked(const char* name,
uint32_t argc,
uint32_t* argv,
uint32_t* result);
/// Ends module execution like a trap once the current native returns.
virtual void raiseModuleError(const char* message);
/// Function imports the module declares that no host native resolves.
/// They trap only when first called, so surface them at load instead.
const std::vector<std::string>& unresolvedImports() const
{
return m_unresolvedImports;
}
/// Script execution budget, matching the Luau backend: 0 disables the
/// metering, which is the host's trust decision for validated content
/// (armed metering costs about 5 percent on loop heavy scripts).
void setTimeoutMs(int ms);
int timeoutMs() const { return m_timeoutMs; }
/// Budget for VMs created after the call; the host's trust decision for
/// files whose modules it baked itself (e.g. dangerouslyFast content).
static void defaultTimeoutMs(int ms) { sm_defaultTimeoutMs = ms; }
/// Receives script print output; defaults to stdout.
void onPrint(std::function<void(const char*, size_t)> handler)
{
m_print = std::move(handler);
}
protected:
// The browser backend subclasses over the seams and boots without the
// WAMR init, so it shares construction, error, factory, and lua state.
// Out of line so subclass TUs need no complete member types.
WasmScriptingVM();
std::string m_lastError;
Factory* m_factory = nullptr;
uint32_t m_L = 0;
private:
bool init(Span<const uint8_t> module);
uint32_t guestString(const char* text);
void guestFree(uint32_t ptr);
struct WamrState;
std::unique_ptr<WamrState> m_state;
std::vector<uint8_t> m_moduleBytes;
// Stable view of the module bytes once the shared cache owns them.
Span<const uint8_t> m_scheduleBytes;
uint64_t m_moduleKey = 0;
ExecutionTier m_tier = ExecutionTier::interp;
std::function<void(const char*, size_t)> m_print;
std::vector<std::string> m_unresolvedImports;
static int sm_defaultTimeoutMs;
int m_timeoutMs = sm_defaultTimeoutMs;
HandleTable m_handles;
/// Object handles minted for the init scoped context, released once init
/// completes or the context ref is released.
std::unordered_map<int, uint32_t> m_contextObjects;
/// In-flight decodes by module token, for cancellation; the pool owns
/// execution, completed or cancelled entries erase themselves.
std::unordered_map<uint32_t, rcp<WorkTask>> m_pendingDecodes;
uint64_t m_decodeOwnerId = 0;
// Delivery is synchronous, so the pixels borrow the decoder's buffer.
struct DecodeResult
{
bool ok = false;
uint32_t token = 0;
uint32_t width = 0;
uint32_t height = 0;
Span<const uint8_t> pixels;
std::string error;
};
void deliverDecodeResult(const DecodeResult& result);
bool m_advancedOnce = false;
bool m_frameMinor = false;
bool m_frameMinorAnnounced = false;
/// Module exports __riveHeapUsed; the leak watch reads bump bytes
/// instead of page counts, which pregrown aot memory freezes.
bool m_heapUsedProbe = false;
/// Collected runtimes warn only on a second consecutive growth window.
bool m_leakArmedCollected = false;
bool m_leakWatch = false;
std::string m_leakWarning;
uint32_t m_leakBaselinePages = 0;
uint32_t m_leakFirstBaselinePages = 0;
uint32_t m_leakFrames = 0;
uint32_t m_leakTotalFrames = 0;
uint32_t m_leakWarningCount = 0;
bool m_leakTrapContextPrinted = false;
bool m_collectedRuntime = false;
bool m_handleWatch = false;
uint32_t m_handleBaselineLive = 0;
uint32_t m_handleFrames = 0;
RawPath* m_pathEffectOut = nullptr;
ScriptDataResult* m_convertResultOut = nullptr;
std::vector<ViewModel*>* m_viewModels = nullptr;
File* m_file = nullptr;
int m_embeddedModules = 0;
#ifdef WITH_RIVE_TOOLS
std::unordered_map<std::string, std::vector<uint8_t>> m_shaderRstbs;
#endif
friend struct WasmScriptingVMNatives;
};
} // namespace rive
#endif
#endif