| #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/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/bones/root_bone.hpp" | |
| #include "rive/constraints/constraint.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/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 | |
| #if defined(RIVE_CANVAS) && defined(RIVE_ORE) | |
| #include "rive/renderer/render_context.hpp" | |
| #include "rive/renderer/render_canvas.hpp" | |
| #include "rive/renderer/ore/ore_context.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> | |
| #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; | |
| } // 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 | |
| { | |
| std::vector<uint8_t> bytes; | |
| wasm_module_t module = nullptr; | |
| // Artifact-backed entries hand every later VM their real tier; without | |
| // this a cache hit reports interp while running compiled code. | |
| WasmScriptingVM::ExecutionTier tier = | |
| WasmScriptingVM::ExecutionTier::interp; | |
| }; | |
| 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; | |
| ~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); | |
| } | |
| } | |
| }; | |
| 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::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 = wasm_runtime_lookup_function(inst, 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]; | |
| } | |
| if (!wasm_runtime_call_wasm(m_state->execEnv, f, argc, buf)) | |
| { | |
| // 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 = wasm_runtime_get_exception(inst); | |
| if (exception != nullptr) | |
| { | |
| fprintf(stderr, "wasm call trapped in %s: %s\n", name, exception); | |
| if (m_leakWarningCount > 0 && !m_leakTrapContextPrinted) | |
| { | |
| // A bare trap after leak warnings is almost always the | |
| // memory ceiling; say so once for hosts that dropped the | |
| // warning strings. | |
| m_leakTrapContextPrinted = true; | |
| 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::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) | |
| { | |
| wasm_runtime_set_exception(m_state->instance, message); | |
| } | |
| 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 | |
| { | |
| // 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; | |
| } | |
| uint32_t strftimeNative(wasm_exec_env_t env, | |
| uint32_t a, | |
| uint32_t b, | |
| uint32_t c, | |
| uint32_t d) | |
| { | |
| return 0; | |
| } | |
| void tzsetJs(wasm_exec_env_t env, | |
| uint32_t a, | |
| uint32_t b, | |
| uint32_t c, | |
| uint32_t d) | |
| {} | |
| void localtimeJs(wasm_exec_env_t env, uint32_t a, uint32_t b, uint32_t c) {} | |
| void gmtimeJs(wasm_exec_env_t env, uint32_t a, uint32_t b, uint32_t c) {} | |
| // 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; | |
| } | |
| NativeSymbol kEnvNatives[] = { | |
| {"invoke_vii", (void*)invokeViiNative, "(iii)", 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}, | |
| {"_tzset_js", (void*)tzsetJs, "(iiii)", nullptr}, | |
| {"_localtime_js", (void*)localtimeJs, "(iii)", nullptr}, | |
| {"_gmtime_js", (void*)gmtimeJs, "(iii)", nullptr}, | |
| {"__assert_fail", (void*)assertFail, "(iiii)", nullptr}, | |
| }; | |
| // Module streams are not seekable. The blob imports the emscripten-legalized | |
| // form, offset i64 split across two i32s. | |
| uint32_t fdSeek(wasm_exec_env_t env, | |
| uint32_t fd, | |
| uint32_t offsetLow, | |
| uint32_t offsetHigh, | |
| uint32_t whence, | |
| uint32_t newOffsetPtr) | |
| { | |
| return 70; // WASI ESPIPE | |
| } | |
| NativeSymbol kWasiNatives[] = { | |
| {"fd_write", (void*)fdWrite, "(i*i*)i", nullptr}, | |
| {"fd_seek", (void*)fdSeek, "(iiiii)i", nullptr}, | |
| }; | |
| // --- rive_path/paint/renderer_v1: handle-backed render objects -------------- | |
| // The module's ModuleRenderPath mirror; geometry arrives through update and | |
| // rebuilds the real render path. | |
| struct HostPath | |
| { | |
| rcp<RenderPath> path; | |
| }; | |
| struct HostPaint | |
| { | |
| rcp<RenderPaint> paint; | |
| }; | |
| uint32_t pathNewImpl(WasmScriptingVM* vm) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return 0; | |
| } | |
| return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::path, | |
| new HostPath()); | |
| } | |
| void pathUpdateImpl(WasmScriptingVM* vm, | |
| uint32_t handle, | |
| const uint8_t* verbs, | |
| uint32_t verbCount, | |
| const float* points, | |
| uint32_t floatCount, | |
| uint32_t fillRule) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return; | |
| } | |
| auto hostPath = static_cast<HostPath*>( | |
| vm->handles().resolve(handle, WasmScriptingVM::HandleTable::Tag::path)); | |
| if (hostPath == nullptr) | |
| { | |
| return; | |
| } | |
| RawPath rawPath(Span<const PathVerb>((const PathVerb*)verbs, verbCount), | |
| Span<const Vec2D>((const Vec2D*)points, floatCount / 2)); | |
| hostPath->path = vm->factory()->makeRenderPath(rawPath, (FillRule)fillRule); | |
| } | |
| void pathEffectResultImpl(WasmScriptingVM* vm, | |
| const uint8_t* verbs, | |
| uint32_t verbCount, | |
| const float* points, | |
| uint32_t floatCount) | |
| { | |
| if (vm == nullptr || vm->pathEffectOut() == nullptr) | |
| { | |
| return; | |
| } | |
| RawPath rawPath(Span<const PathVerb>((const PathVerb*)verbs, verbCount), | |
| Span<const Vec2D>((const Vec2D*)points, floatCount / 2)); | |
| vm->pathEffectOut()->addPath(rawPath); | |
| } | |
| 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); | |
| } | |
| struct HostImage | |
| { | |
| rcp<RenderImage> image; | |
| }; | |
| 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) | |
| { | |
| 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 key(name, length); | |
| for (const auto& asset : object->scriptAsset()->file()->assets()) | |
| { | |
| if (asset->is<ImageAsset>() && asset->name() == key) | |
| { | |
| RenderImage* renderImage = asset->as<ImageAsset>()->renderImage(); | |
| if (renderImage == nullptr) | |
| { | |
| return 0; | |
| } | |
| return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::image, | |
| new HostImage{ref_rcp(renderImage)}); | |
| } | |
| } | |
| return 0; | |
| } | |
| 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; | |
| ore::Context* context; | |
| ~HostGpuPass() | |
| { | |
| // Mirror the Luau wrapper: never leave the active-pass slot dangling. | |
| if (context != nullptr && pass != nullptr && | |
| context->activeRenderPass() == pass.get()) | |
| { | |
| context->setActiveRenderPass(nullptr); | |
| } | |
| } | |
| }; | |
| 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; | |
| } | |
| 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()))}); | |
| } | |
| 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; | |
| // One active encoder per command buffer; finish a stale pass first, | |
| // matching the Luau binding. | |
| if (oreContext->activeRenderPass() != nullptr && | |
| !oreContext->activeRenderPass()->isFinished()) | |
| { | |
| oreContext->activeRenderPass()->finish(); | |
| oreContext->setActiveRenderPass(nullptr); | |
| } | |
| auto pass = | |
| ore::cmd::beginRenderPassRecordingOrImmediate(*oreContext, desc); | |
| if (pass == nullptr) | |
| { | |
| return 0; | |
| } | |
| oreContext->setActiveRenderPass(pass.get()); | |
| return vm->handles().mint(WasmScriptingVM::HandleTable::Tag::gpuPass, | |
| new HostGpuPass{std::move(pass), oreContext}); | |
| } | |
| 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: deferred images | |
| // record by resource id, everything else wraps as a canvas image. | |
| rcp<ore::TextureView> view; | |
| 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)}); | |
| } | |
| ore::RenderPass* resolvePass(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| auto host = static_cast<HostGpuPass*>( | |
| vm->handles().resolve(handle, | |
| WasmScriptingVM::HandleTable::Tag::gpuPass)); | |
| return host != nullptr ? host->pass.get() : nullptr; | |
| } | |
| void gpuPassSetPipelineImpl(WasmScriptingVM* vm, | |
| uint32_t passHandle, | |
| uint32_t pipelineHandle) | |
| { | |
| auto* pass = resolvePass(vm, passHandle); | |
| auto pipeline = static_cast<HostGpuPipeline*>( | |
| vm->handles().resolve(pipelineHandle, | |
| WasmScriptingVM::HandleTable::Tag::gpuPipeline)); | |
| if (pass == nullptr || pipeline == nullptr) | |
| { | |
| return; | |
| } | |
| pass->setPipeline(pipeline->pipeline.get()); | |
| } | |
| 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; | |
| } | |
| 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) | |
| { | |
| if (auto* pass = resolvePass(vm, passHandle)) | |
| { | |
| 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) | |
| { | |
| if (auto* pass = resolvePass(vm, passHandle)) | |
| { | |
| 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(); | |
| } | |
| if (host->context->activeRenderPass() == host->pass.get()) | |
| { | |
| host->context->setActiveRenderPass(nullptr); | |
| } | |
| } | |
| 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; | |
| auto buffer = oreContext->makeBuffer(desc); | |
| if (buffer == nullptr) | |
| { | |
| return 0; | |
| } | |
| 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; | |
| auto texture = oreContext->makeTexture(desc); | |
| if (texture == nullptr) | |
| { | |
| return 0; | |
| } | |
| 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; | |
| } | |
| // The module validated the region against the texture; recheck the one | |
| // invariant guest memory enforces nothing about. | |
| if (uint64_t(region->bytesPerRow) * region->rowsPerImage * | |
| std::max(1u, region->depth) > | |
| dataCount) | |
| { | |
| return; | |
| } | |
| ore::TextureDataDesc upload; | |
| upload.data = data; | |
| 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; | |
| host->texture->upload(upload); | |
| } | |
| 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; | |
| auto sampler = oreContext->makeSampler(desc); | |
| if (sampler == nullptr) | |
| { | |
| return 0; | |
| } | |
| 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; | |
| auto view = oreContext->makeTextureView(desc); | |
| if (view == nullptr) | |
| { | |
| return 0; | |
| } | |
| 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 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 key(name, nameLength); | |
| for (const auto& asset : object->scriptAsset()->file()->assets()) | |
| { | |
| if (!asset->is<ShaderAsset>() || asset->name() != key) | |
| { | |
| continue; | |
| } | |
| auto rstb = asset->as<ShaderAsset>()->rstb(); | |
| if (rstb.size() <= outCount) | |
| { | |
| memcpy(out, rstb.data(), rstb.size()); | |
| } | |
| return (uint32_t)rstb.size(); | |
| } | |
| return 0; | |
| } | |
| uint32_t gpuShaderAssetIdImpl(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) | |
| { | |
| return 0; | |
| } | |
| std::string key(name, nameLength); | |
| for (const auto& asset : object->scriptAsset()->file()->assets()) | |
| { | |
| if (asset->is<ShaderAsset>() && asset->name() == key) | |
| { | |
| return asset->assetId(); | |
| } | |
| } | |
| return 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(); | |
| auto shaderModule = oreContext->makeShaderModule(desc); | |
| if (shaderModule == nullptr) | |
| { | |
| return 0; | |
| } | |
| 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; | |
| } | |
| ore::BindGroupLayoutDesc desc; | |
| desc.groupIndex = groupIndex; | |
| desc.entries = resolved.data(); | |
| desc.entryCount = count; | |
| auto layout = oreContext->makeBindGroupLayout(desc); | |
| if (layout == nullptr) | |
| { | |
| return 0; | |
| } | |
| 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; | |
| auto bindGroup = oreContext->makeBindGroup(desc); | |
| if (bindGroup == nullptr) | |
| { | |
| return 0; | |
| } | |
| 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; | |
| auto pipeline = oreContext->makePipeline(desc); | |
| if (pipeline == nullptr) | |
| { | |
| return 0; | |
| } | |
| 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 | |
| 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(); | |
| } | |
| } | |
| void rendererRestoreImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| if (auto renderer = resolveRenderer(vm, handle)) | |
| { | |
| renderer->restore(); | |
| } | |
| } | |
| 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 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"); | |
| } | |
| 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); | |
| } | |
| } | |
| 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; | |
| } | |
| // --- 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) | |
| { | |
| 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 key(name, nameLength); | |
| for (const auto& asset : object->scriptAsset()->file()->assets()) | |
| { | |
| if (!asset->is<BlobAsset>() || asset->name() != key || | |
| asset->as<BlobAsset>()->bytes().empty()) | |
| { | |
| continue; | |
| } | |
| auto bytes = asset->as<BlobAsset>()->bytes(); | |
| if (bytes.size() <= outCount) | |
| { | |
| memcpy(out, bytes.data(), bytes.size()); | |
| } | |
| return (uint32_t)bytes.size(); | |
| } | |
| return 0; | |
| } | |
| 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 (!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() | |
| { | |
| // 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::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; | |
| #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; | |
| } | |
| } | |
| 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::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); | |
| } | |
| } | |
| } | |
| 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()); | |
| // 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; | |
| 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. | |
| if (!haveAot && !haveHwAot) | |
| { | |
| auto& ladder = ModuleTierLadder::instance(); | |
| if (ladder.enabled()) | |
| { | |
| #ifdef RIVE_WASM_HW_BOUNDS | |
| std::string hwPath = | |
| 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) | |
| { | |
| 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; | |
| } | |
| } | |
| } | |
| } | |
| if (haveAot || haveHwAot) | |
| { | |
| 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; | |
| } | |
| auto& cache = sharedModuleCache(); | |
| auto cached = cache.find(moduleKey); | |
| if (cached != cache.end()) | |
| { | |
| m_state->module = cached->second.module; | |
| m_state->ownsModule = false; | |
| m_tier = cached->second.tier; | |
| // The VM's own copy is redundant against the cache entry, but the | |
| // tier ladder still needs the bytes; entries live for the process. | |
| m_scheduleBytes = Span<const uint8_t>(cached->second.bytes.data(), | |
| cached->second.bytes.size()); | |
| m_moduleBytes.clear(); | |
| } | |
| else | |
| { | |
| if (haveAot || haveHwAot) | |
| { | |
| 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 = ExecutionTier::aotO3; | |
| } | |
| } | |
| if (!haveAot && !haveHwAot) | |
| { | |
| // 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) | |
| { | |
| if (haveHwAot) | |
| { | |
| wasm_runtime_set_module_hw_bounds(m_state->module, true); | |
| } | |
| SharedWasmModule entry; | |
| entry.bytes = std::move(m_moduleBytes); | |
| 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.bytes.data(), | |
| inserted.first->second.bytes.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); | |
| } | |
| } | |
| s_bootPrint = &m_print; | |
| m_state->instance = wasm_runtime_instantiate(m_state->module, | |
| 512 * 1024, | |
| 0, | |
| error, | |
| sizeof(error)); | |
| 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; | |
| // 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) | |
| { | |
| 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) | |
| { | |
| 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::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::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::empty: | |
| 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); | |
| } | |
| 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 (m_handleFrames < kHandleWarnMinFrames || | |
| live + 1 < m_handleBaselineLive + kHandleWarnCount) | |
| { | |
| return nullptr; | |
| } | |
| // 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::node + 1] = {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); | |
| } | |
| 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; | |
| if (!wasm_runtime_call_wasm(m_state->execEnv, f, 3, buf)) | |
| { | |
| const char* exception = wasm_runtime_get_exception(m_state->instance); | |
| fprintf(stderr, | |
| "script init trapped: %s\n", | |
| exception != nullptr ? exception : "unknown"); | |
| #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; | |
| 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 namePtr = guestString(name); | |
| if (namePtr == 0) | |
| { | |
| return; | |
| } | |
| uint32_t args[3] = {m_L, (uint32_t)selfRef, namePtr}; | |
| callModule("host_obj_trigger", 3, args); | |
| guestFree(namePtr); | |
| } | |
| 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; | |
| 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); | |
| } | |
| 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(); | |
| 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; | |
| } | |
| void WasmScriptingVM::setInputBoolean(int selfRef, const char* name, bool value) | |
| { | |
| uint32_t namePtr = guestString(name); | |
| if (namePtr == 0) | |
| { | |
| return; | |
| } | |
| uint32_t args[4] = {m_L, (uint32_t)selfRef, namePtr, value ? 1u : 0u}; | |
| callModule("host_obj_set_boolean", 4, args); | |
| guestFree(namePtr); | |
| } | |
| void WasmScriptingVM::setInputNumber(int selfRef, const char* name, float value) | |
| { | |
| wasm_function_inst_t f = | |
| wasm_runtime_lookup_function(m_state->instance, "host_obj_set_number"); | |
| uint32_t namePtr = guestString(name); | |
| if (f == nullptr || namePtr == 0) | |
| { | |
| return; | |
| } | |
| 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)namePtr; | |
| args[3].kind = WASM_F64; | |
| args[3].of.f64 = value; | |
| wasm_runtime_call_wasm_a(m_state->execEnv, f, 0, nullptr, 4, args); | |
| guestFree(namePtr); | |
| } | |
| void WasmScriptingVM::setInputUnsigned(int selfRef, | |
| const char* name, | |
| uint32_t value) | |
| { | |
| uint32_t namePtr = guestString(name); | |
| if (namePtr == 0) | |
| { | |
| return; | |
| } | |
| uint32_t args[4] = {m_L, (uint32_t)selfRef, namePtr, value}; | |
| callModule("host_obj_set_unsigned", 4, args); | |
| guestFree(namePtr); | |
| } | |
| void WasmScriptingVM::setInputString(int selfRef, | |
| const char* name, | |
| const char* value) | |
| { | |
| uint32_t namePtr = guestString(name); | |
| uint32_t valuePtr = guestString(value); | |
| if (namePtr == 0 || valuePtr == 0) | |
| { | |
| guestFree(namePtr); | |
| guestFree(valuePtr); | |
| return; | |
| } | |
| uint32_t args[4] = {m_L, (uint32_t)selfRef, namePtr, valuePtr}; | |
| callModule("host_obj_set_string", 4, args); | |
| guestFree(valuePtr); | |
| guestFree(namePtr); | |
| } | |
| 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 namePtr = guestString(name); | |
| if (namePtr == 0) | |
| { | |
| 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, namePtr, handle}; | |
| callModule("host_obj_set_view_model", 4, args); | |
| guestFree(namePtr); | |
| 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 namePtr = guestString(name); | |
| if (namePtr == 0) | |
| { | |
| 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, namePtr, handle}; | |
| callModule("host_obj_set_artboard", 4, args); | |
| guestFree(namePtr); | |
| } | |
| 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 |