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