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