| #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 |