| #ifdef WITH_RIVE_SCRIPTING_WASM | |
| #include "rive/wasm/wasm_scripting_vm.hpp" | |
| #include "rive/async/compare_exchange.hpp" | |
| #include "rive/async/timeouts.hpp" | |
| #include "rive/wasm/wasm_jobs.hpp" | |
| #include "rive/wasm/wasm_parallel.hpp" | |
| #include "rive/wasm/wasm_lane.hpp" | |
| #include "rive/wasm/wasm_shared.hpp" | |
| #include "rive/wasm/wasm_workers.hpp" | |
| #include "rive/wasm/wasm_processors.hpp" | |
| #ifndef __EMSCRIPTEN__ | |
| #include "rive/wasm/aot_artifact.hpp" | |
| #include "rive/wasm/module_tier_ladder.hpp" | |
| #include "rive/wasm/prelinked_aot.hpp" | |
| #else | |
| #include <emscripten/emscripten.h> | |
| #include <emscripten/threading.h> | |
| #endif | |
| #include <mutex> | |
| #include <thread> | |
| #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/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_reader.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/decoded_file.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" | |
| #include "rive/wasm/net_wire.hpp" | |
| #include "rive/bindable_artboard.hpp" | |
| #include "rive/viewmodel/viewmodel_instance_artboard.hpp" | |
| #ifdef WITH_RIVE_SCRIPTNET | |
| #include "rive/scriptnet/net.hpp" | |
| #endif | |
| #ifdef __EMSCRIPTEN__ | |
| #include "rive/async/browser_image_decode.hpp" | |
| #elif defined(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_deferred_bind_groups.hpp" | |
| #include "rive/renderer/ore/ore_draw_bindings.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_shader_asset_modules.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 | |
| #ifndef __EMSCRIPTEN__ | |
| #include "wasm_export.h" | |
| #endif | |
| #include "wasm_utf16.hpp" | |
| #include <chrono> | |
| #include <cstdarg> | |
| #include <cstddef> | |
| #include <string.h> | |
| #include <time.h> | |
| using namespace rive; | |
| // Streams only feed audio processors, so builds that refuse those leave them | |
| // out, answering as a build without audio. | |
| #if defined(WITH_RIVE_AUDIO) && RIVE_PROCESSOR_AUDIO() | |
| #define RIVE_AUDIO_STREAMS() 1 | |
| #else | |
| #define RIVE_AUDIO_STREAMS() 0 | |
| #endif | |
| namespace | |
| { | |
| constexpr char kLongjmpTag[] = "rive-longjmp"; | |
| #ifndef __EMSCRIPTEN__ | |
| WasmScriptingVM* vmFromEnv(wasm_exec_env_t env) | |
| { | |
| return static_cast<WasmScriptingVM*>(wasm_runtime_get_user_data(env)); | |
| } | |
| // A lane has no VM, so its lines go to stderr. | |
| void reportScriptError(WasmScriptingVM* vm, const std::string& line) | |
| { | |
| if (vm != nullptr) | |
| { | |
| vm->reportError(line); | |
| } | |
| else | |
| { | |
| fprintf(stderr, "%s\n", line.c_str()); | |
| } | |
| } | |
| #endif | |
| std::string budgetExceededLine(int ms) | |
| { | |
| return "script error: execution exceeded " + std::to_string(ms) + | |
| " millisecond timeout"; | |
| } | |
| // 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; | |
| std::atomic<bool> s_requireBounds{false}; | |
| thread_local WasmScriptingVM::BootHook s_bootHook; | |
| } // namespace | |
| // The VMs one thread's polls reach. A VM may be destroyed on another thread, | |
| // so it leaves its list under the lock a poll holds while it delivers. | |
| struct rive::VMPollList | |
| { | |
| #if RIVE_ONE_THREAD() | |
| // One thread runs every poll and VM, and its lists outlive them all. | |
| using Ref = VMPollList*; | |
| struct Lock | |
| { | |
| explicit Lock(const VMPollList&) {} | |
| }; | |
| #else | |
| using Ref = std::shared_ptr<VMPollList>; | |
| struct Lock : std::lock_guard<std::recursive_mutex> | |
| { | |
| explicit Lock(const VMPollList& list) : lock_guard(*list.mutex) {} | |
| }; | |
| // A thread's lists share it, as delivering either one can join the other. | |
| std::shared_ptr<std::recursive_mutex> mutex; | |
| explicit VMPollList(std::shared_ptr<std::recursive_mutex> threadMutex) | |
| : mutex(std::move(threadMutex)) | |
| {} | |
| #endif | |
| std::vector<WasmScriptingVM*> vms; | |
| // Joins list, set to the thread's own the first time. | |
| static void join(Ref& list, const Ref& thread, WasmScriptingVM* vm) | |
| { | |
| if (list == nullptr) | |
| { | |
| list = thread; | |
| } | |
| Lock lock(*list); | |
| if (std::find(list->vms.begin(), list->vms.end(), vm) == list->vms.end()) | |
| { | |
| list->vms.push_back(vm); | |
| } | |
| } | |
| static void leave(const Ref& list, WasmScriptingVM* vm) | |
| { | |
| if (list != nullptr) | |
| { | |
| Lock lock(*list); | |
| list->vms.erase( | |
| std::remove(list->vms.begin(), list->vms.end(), vm), | |
| list->vms.end()); | |
| } | |
| } | |
| // A delivery runs script, which can destroy another VM on the list. | |
| template <typename Deliver> void deliver(Deliver&& each) | |
| { | |
| Lock lock(*this); | |
| for (WasmScriptingVM* vm : std::vector<WasmScriptingVM*>(vms)) | |
| { | |
| if (std::find(vms.begin(), vms.end(), vm) != vms.end()) | |
| { | |
| each(vm); | |
| } | |
| } | |
| } | |
| }; | |
| namespace | |
| { | |
| #if RIVE_ONE_THREAD() | |
| // Never destroyed, so they need no static constructor or destructor. | |
| [[clang::no_destroy]] VMPollList s_holdingList; | |
| [[clang::no_destroy]] VMPollList s_laneList; | |
| VMPollList* const t_holdingVMs = &s_holdingList; | |
| VMPollList* const t_laneVMs = &s_laneList; | |
| #else | |
| thread_local std::shared_ptr<std::recursive_mutex> | |
| t_pollMutex = std::make_shared<std::recursive_mutex>(); | |
| // The VMs holding outcomes, so a poll reaches them without walking every | |
| // artboard that might own one. | |
| thread_local std::shared_ptr<VMPollList> | |
| t_holdingVMs = std::make_shared<VMPollList>(t_pollMutex); | |
| // The VMs with lanes started on this thread, so a poll reaches their output. | |
| thread_local std::shared_ptr<VMPollList> | |
| t_laneVMs = std::make_shared<VMPollList>(t_pollMutex); | |
| #endif | |
| constexpr size_t kLaneResultBytesPerPoll = 8u << 20; | |
| } // namespace | |
| struct rive::WasmScriptingVMNatives | |
| { | |
| static void print(WasmScriptingVM* vm, const char* data, size_t size); | |
| static int startFuelMs(const WasmScriptingVM* vm); | |
| static const char* startRefusal(const WasmScriptingVM* vm); | |
| #ifndef __EMSCRIPTEN__ | |
| static WasmScriptingVM* adoptBooting(wasm_exec_env_t env); | |
| #else | |
| static void booting(WasmScriptingVM* vm); | |
| #endif | |
| static std::unordered_map<std::string, int64_t>& consoleTimers( | |
| WasmScriptingVM* vm); | |
| }; | |
| #ifndef __EMSCRIPTEN__ | |
| // Loaded modules are immutable and shared: translating one costs about 11ms, | |
| // instantiating it microseconds, and files of one content load the same bytes. | |
| struct SharedWasmModule | |
| { | |
| // The buffer WAMR loads from and references until the module unloads. | |
| std::vector<uint8_t> bytes; | |
| wasm_module_t module = nullptr; | |
| // Artifact-backed entries hand every later VM their real tier; without | |
| // this a cache hit reports interp while running compiled code. | |
| WasmScriptingVM::ExecutionTier tier = WasmScriptingVM::ExecutionTier::interp; | |
| // No device run covers unloading prelinked code. | |
| bool pinned = false; | |
| // The wasm behind code that skips bounds checks, for a fallback. | |
| std::vector<uint8_t> hardwareBoundsWasm; | |
| // Orders the idle ones, so the least recently leased unloads first. | |
| uint64_t lastLeased = 0; | |
| ~SharedWasmModule() | |
| { | |
| if (module != nullptr) | |
| { | |
| wasm_runtime_unload(module); | |
| } | |
| } | |
| }; | |
| using SharedModuleCache = std:: | |
| unordered_map<uint64_t, std::shared_ptr<SharedWasmModule>>; | |
| static SharedModuleCache& sharedModuleCache() | |
| { | |
| // Never destroyed, so nothing unloads at exit and threads still booting | |
| // at exit never see it torn down. | |
| static auto* cache = new SharedModuleCache(); | |
| return *cache; | |
| } | |
| // Hosts boot modules on more than one thread, such as a build thread and | |
| // a render thread. | |
| static std::mutex& sharedModuleCacheMutex() | |
| { | |
| static std::mutex mutex; | |
| return mutex; | |
| } | |
| // Leases start only under the cache mutex, so under it an entry nothing else | |
| // holds is idle and stays idle. | |
| static std::shared_ptr<void> leaseLocked( | |
| const std::shared_ptr<SharedWasmModule>& entry) | |
| { | |
| static uint64_t leases = 0; | |
| entry->lastLeased = ++leases; | |
| return std::shared_ptr<void>(entry, entry->module); | |
| } | |
| static std::shared_ptr<void> leaseCachedModule( | |
| uint64_t key, | |
| WasmScriptingVM::ExecutionTier& tier) | |
| { | |
| std::lock_guard<std::mutex> lock(sharedModuleCacheMutex()); | |
| auto cached = sharedModuleCache().find(key); | |
| if (cached == sharedModuleCache().end()) | |
| { | |
| return nullptr; | |
| } | |
| tier = cached->second->tier; | |
| return leaseLocked(cached->second); | |
| } | |
| static void evictIdleModulesLocked( | |
| std::vector<std::shared_ptr<SharedWasmModule>>& evicted) | |
| { | |
| SharedModuleCache& cache = sharedModuleCache(); | |
| std::vector<std::pair<uint64_t, uint64_t>> idle; | |
| for (const auto& [key, entry] : cache) | |
| { | |
| if (entry.use_count() == 1 && !entry->pinned) | |
| { | |
| idle.emplace_back(entry->lastLeased, key); | |
| } | |
| } | |
| if (idle.size() <= WasmScriptingVM::kIdleModules) | |
| { | |
| return; | |
| } | |
| std::sort(idle.begin(), idle.end()); | |
| idle.resize(idle.size() - WasmScriptingVM::kIdleModules); | |
| for (const auto& [lastLeased, key] : idle) | |
| { | |
| auto found = cache.find(key); | |
| evicted.push_back(std::move(found->second)); | |
| cache.erase(found); | |
| } | |
| } | |
| // Compiled modules cache apart from interpreted ones of the same bytes, and | |
| // each species apart from the others, since the module carries its tier. | |
| static uint64_t compiledModuleKey(uint64_t key, TierSpecies species) | |
| { | |
| key ^= 0x9e3779b97f4a7c15ull; | |
| if (species == TierSpecies::hw) | |
| { | |
| // The hw bounds flag lives on the module. | |
| key ^= 0xc2b2ae3d27d4eb4full; | |
| } | |
| if (species == TierSpecies::o0) | |
| { | |
| key ^= 0x27d4eb2f165667c5ull; | |
| } | |
| return key; | |
| } | |
| struct WasmScriptingVM::WamrState | |
| { | |
| // Keeps module loaded until the destructor has ended the instance. | |
| std::shared_ptr<void> lease; | |
| wasm_module_t module = nullptr; | |
| wasm_module_inst_t instance = nullptr; | |
| wasm_exec_env_t execEnv = nullptr; | |
| // 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]; | |
| // The module's __riveCall globals: state of the call in progress, which | |
| // a trap unwinds without restoring. | |
| static constexpr uint32_t kMaxCallState = 8; | |
| uint32_t* callState[kMaxCallState] = {}; | |
| uint32_t callStateCount = 0; | |
| // Puts the call state back when a call into the module ends. | |
| class CallStateScope | |
| { | |
| public: | |
| explicit CallStateScope(const WamrState& state) : m_state(state) | |
| { | |
| for (uint32_t i = 0; i < state.callStateCount; i++) | |
| { | |
| m_saved[i] = *state.callState[i]; | |
| } | |
| } | |
| ~CallStateScope() | |
| { | |
| for (uint32_t i = 0; i < m_state.callStateCount; i++) | |
| { | |
| *m_state.callState[i] = m_saved[i]; | |
| } | |
| } | |
| private: | |
| const WamrState& m_state; | |
| uint32_t m_saved[kMaxCallState]; | |
| }; | |
| void findCallState() | |
| { | |
| int32_t count = wasm_runtime_get_export_count(module); | |
| for (int32_t i = 0; i < count; i++) | |
| { | |
| wasm_export_t entry; | |
| wasm_runtime_get_export_type(module, i, &entry); | |
| wasm_global_inst_t global; | |
| if (entry.kind != WASM_IMPORT_EXPORT_KIND_GLOBAL || | |
| strncmp(entry.name, "__riveCall", 10) != 0 || | |
| !wasm_runtime_get_export_global_inst(instance, entry.name, &global) || | |
| global.kind != WASM_I32 || !global.is_mutable) | |
| { | |
| continue; | |
| } | |
| if (callStateCount == kMaxCallState) | |
| { | |
| fprintf( | |
| stderr, | |
| "wasm module has more call state than the host " | |
| "restores; a trap may leave %s stale\n", | |
| entry.name); | |
| continue; | |
| } | |
| callState[callStateCount++] = (uint32_t*)global.global_data; | |
| } | |
| } | |
| 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); | |
| } | |
| } | |
| }; | |
| #endif | |
| #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}; | |
| #ifndef __EMSCRIPTEN__ | |
| wasm_exec_env_t env = nullptr; | |
| #endif | |
| }; | |
| namespace | |
| { | |
| int64_t steadyNanos() | |
| { | |
| return std::chrono::duration_cast<std::chrono::nanoseconds>( | |
| std::chrono::steady_clock::now().time_since_epoch()) | |
| .count(); | |
| } | |
| } // namespace | |
| // The watchdog interrupts WAMR; the browser's engine has no such hook, so | |
| // there a call runs unbounded. | |
| #ifndef __EMSCRIPTEN__ | |
| 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; | |
| 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; | |
| } | |
| // A polled slot is armed from any thread, the audio thread's included, | |
| // so while one exists the watchdog ticks and arming makes no syscall. | |
| void add(WasmScriptingVM::BudgetSlot* slot, bool polled = false) | |
| { | |
| m_polled.fetch_add(polled ? 1 : 0); | |
| std::lock_guard<std::mutex> lock(m_mutex); | |
| if (!m_started) | |
| { | |
| m_started = true; | |
| std::thread([this] { run(); }).detach(); | |
| } | |
| m_slots.push_back(slot); | |
| // A watchdog waiting without a deadline starts ticking. | |
| if (polled) | |
| { | |
| m_wake.wake(); | |
| } | |
| } | |
| void remove(WasmScriptingVM::BudgetSlot* slot, bool polled = false) | |
| { | |
| // An arm skipped its wake while slots were polled, so the last one | |
| // going rescans before the watchdog may sleep without a deadline. | |
| bool lastPolled = polled && m_polled.fetch_sub(1) == 1; | |
| { | |
| std::lock_guard<std::mutex> lock(m_mutex); | |
| m_slots.erase( | |
| std::remove(m_slots.begin(), m_slots.end(), slot), | |
| m_slots.end()); | |
| } | |
| if (lastPolled) | |
| { | |
| m_wake.wake(); | |
| } | |
| } | |
| // Calls arm without any lock, so the audio thread may: only an arm that | |
| // finds the thread idle, or asleep until a later deadline, wakes it. | |
| void armed(int64_t deadline) | |
| { | |
| if (m_polled.load() != 0) | |
| { | |
| return; | |
| } | |
| if (m_idle.load() || deadline < m_waitingUntil.load()) | |
| { | |
| m_wake.wake(); | |
| } | |
| } | |
| private: | |
| void run() | |
| { | |
| while (true) | |
| { | |
| // Read before the scan, so an arm racing it either shows up in | |
| // the scan or moves the word and ends the wait. | |
| uint32_t seen = m_wake.value(); | |
| m_idle.store(true); | |
| int64_t earliest = 0; | |
| { | |
| std::lock_guard<std::mutex> lock(m_mutex); | |
| int64_t now = steadyNanos(); | |
| 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 (m_polled.load() != 0) | |
| { | |
| int64_t tick = steadyNanos() + kPollNanos; | |
| earliest = earliest == 0 ? tick : std::min(earliest, tick); | |
| } | |
| if (earliest == 0) | |
| { | |
| m_waitingUntil.store(INT64_MAX); | |
| m_wake.wait(seen, std::chrono::steady_clock::time_point::max()); | |
| continue; | |
| } | |
| // Stays idle until the deadline is published, so an arm in | |
| // between still wakes it. | |
| m_waitingUntil.store(earliest); | |
| m_idle.store(false); | |
| m_wake.wait( | |
| seen, | |
| std::chrono::steady_clock::time_point( | |
| std::chrono::duration_cast<std::chrono::steady_clock::duration>( | |
| std::chrono::nanoseconds(earliest)))); | |
| } | |
| } | |
| // How often the watchdog checks while a polled slot exists. | |
| static constexpr int64_t kPollNanos = 2000000; | |
| std::atomic<int32_t> m_polled{0}; | |
| std::mutex m_mutex; | |
| WakeWord m_wake; | |
| std::vector<WasmScriptingVM::BudgetSlot*> m_slots; | |
| std::atomic<bool> m_idle{false}; | |
| std::atomic<int64_t> m_waitingUntil{INT64_MAX}; | |
| bool m_started = false; | |
| }; | |
| // Swaps the slot's deadline out for none, waiting out a firing so its | |
| // interrupt never lands on a later call; answers the deadline it took. | |
| int64_t takeDeadline(WasmScriptingVM::BudgetSlot& slot) | |
| { | |
| int64_t deadline = slot.deadline.load(); | |
| while (deadline == kFiring || | |
| !slot.deadline.compare_exchange_weak(deadline, 0)) | |
| { | |
| deadline = slot.deadline.load(); | |
| } | |
| return deadline; | |
| } | |
| } // namespace | |
| struct WasmCallBudget::State | |
| { | |
| WasmScriptingVM::BudgetSlot budget; | |
| }; | |
| WasmCallBudget::WasmCallBudget(void* execEnv) | |
| : m_state(std::make_unique<State>()) | |
| { | |
| m_state->budget.env = (wasm_exec_env_t)execEnv; | |
| ScriptWatchdog::instance().add(&m_state->budget, true); | |
| } | |
| WasmCallBudget::~WasmCallBudget() | |
| { | |
| disarm(); | |
| ScriptWatchdog::instance().remove(&m_state->budget, true); | |
| } | |
| #if defined(WITH_RIVE_TOOLS) || defined(TESTING) | |
| static std::atomic<uint32_t> s_interruptDelayMs{0}; | |
| void WasmCallBudget::delayInterrupts(uint32_t ms) { s_interruptDelayMs = ms; } | |
| #else | |
| static constexpr uint32_t s_interruptDelayMs = 0; | |
| #endif | |
| void WasmCallBudget::arm(uint32_t timeoutMs) | |
| { | |
| m_state->budget.fired.store(false); | |
| int64_t deadline = steadyNanos() + | |
| ((int64_t)timeoutMs + s_interruptDelayMs) * 1000000; | |
| m_state->budget.deadline.store(deadline); | |
| ScriptWatchdog::instance().armed(deadline); | |
| } | |
| bool WasmCallBudget::disarm() | |
| { | |
| WasmScriptingVM::BudgetSlot& slot = m_state->budget; | |
| takeDeadline(slot); | |
| bool fired = slot.fired.load(); | |
| wasm_runtime_rive_clear_interrupt(slot.env); | |
| return fired; | |
| } | |
| #endif | |
| namespace | |
| { | |
| // Module calls live on this thread, any VM's. | |
| thread_local uint32_t t_moduleCalls = 0; | |
| using Clock = std::chrono::steady_clock; | |
| } // namespace | |
| // What the module call on this thread has left of its budget, infinity | |
| // when nothing bounds it. | |
| static double callSecondsLeft() | |
| { | |
| Clock::time_point ends = callDeadline(); | |
| if (ends == Clock::time_point::max()) | |
| { | |
| return INFINITY; | |
| } | |
| return std::max( | |
| 0.0, | |
| std::chrono::duration<double>(ends - Clock::now()).count()); | |
| } | |
| // What a wait may spend of its call's budget, keeping a little for the | |
| // script to act once the wait ends. | |
| static double joinableSeconds() | |
| { | |
| constexpr double kJoinerReserveSeconds = 0.02; | |
| return callSecondsLeft() - kJoinerReserveSeconds; | |
| } | |
| WasmScriptingVM::WaitScope::WaitScope(WasmScriptingVM* vm) | |
| : m_vm(vm) | |
| , m_usable(joinableSeconds()) | |
| , m_start(Clock::now()) | |
| { | |
| #ifndef __EMSCRIPTEN__ | |
| // A deadline already fired stays fired. | |
| if (vm != nullptr && vm->m_budgetSlot != nullptr) | |
| { | |
| m_paused = takeDeadline(*vm->m_budgetSlot); | |
| } | |
| #endif | |
| } | |
| WasmScriptingVM::WaitScope::~WaitScope() | |
| { | |
| // A wait begun with nothing usable gives no time back, so retries cannot | |
| // move the call's end. | |
| double over = std::isinf(m_usable) || m_usable <= 0 | |
| ? 0 | |
| : std::chrono::duration<double>(Clock::now() - m_start) | |
| .count() - | |
| m_usable; | |
| auto shift = std::chrono::duration_cast<Clock::duration>( | |
| std::chrono::duration<double>(std::max(0.0, over))); | |
| #ifndef __EMSCRIPTEN__ | |
| if (m_paused > 0) | |
| { | |
| int64_t deadline = m_paused + | |
| std::chrono::duration_cast<std::chrono::nanoseconds>( | |
| shift) | |
| .count(); | |
| m_vm->m_budgetSlot->deadline.store(deadline); | |
| ScriptWatchdog::instance().armed(deadline); | |
| } | |
| #endif | |
| if (over <= 0 || m_vm == nullptr) | |
| { | |
| return; | |
| } | |
| Clock::time_point deadline = callDeadline(); | |
| if (deadline != Clock::time_point::max()) | |
| { | |
| restoreCallDeadline(deadline + shift); | |
| } | |
| // A metered module times its own budget, so it moves its deadline on, | |
| // rounded up so it never falls behind the host's. | |
| if (m_vm->m_fuelShift && m_vm->m_fuelBudget) | |
| { | |
| uint32_t args[1] = {(uint32_t)std::ceil(over * 1000)}; | |
| m_vm->callModule("__riveFuelShift", 1, args); | |
| } | |
| } | |
| bool WasmScriptingVM::inModuleCallOnThread() { return t_moduleCalls != 0; } | |
| WasmScriptingVM::ScriptCallScope::ScriptCallScope( | |
| WasmScriptingVM* vm, | |
| bool budgeted) | |
| : m_vm(vm) | |
| { | |
| t_moduleCalls++; | |
| if (vm->m_debugHooks != nullptr) | |
| { | |
| vm->m_debugHooks->onCallBegin(*vm); | |
| } | |
| // The outermost scope sweeps what any nested call left open. | |
| bool outermost = vm->m_callDepth++ == 0; | |
| if (outermost) | |
| { | |
| // Another VM's call on this thread may hold a region of its own. | |
| m_outerRegion = stashHeldRegion(&m_outerRegionId); | |
| // Paths that consume their own traps never reach reportTrap to clear | |
| // it. | |
| vm->m_budgetRaised = false; | |
| vm->m_thrownRaised = false; | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| // Script a runtime service calls back into arms its own. | |
| if (budgeted && vm->m_hostBudget && !vm->m_budgetArmed && vm->boundsCalls()) | |
| { | |
| vm->armWatchdog(vm->m_state->execEnv); | |
| m_budgeted = true; | |
| } | |
| #endif | |
| m_deadlineArmed = budgeted && vm->armDeadline(); | |
| if (!outermost) | |
| { | |
| return; | |
| } | |
| #if defined(RIVE_CANVAS) && defined(RIVE_ORE) | |
| ore::Context* oreContext = gpuOreContext(vm); | |
| m_passToken = oreContext != nullptr ? oreContext->nextRenderPassToken() : 0; | |
| #endif | |
| #ifdef RIVE_CANVAS | |
| m_frameToken = vm->m_nextCanvasFrameToken; | |
| #endif | |
| } | |
| WasmScriptingVM::ScriptCallScope::~ScriptCallScope() | |
| { | |
| t_moduleCalls--; | |
| if (m_deadlineArmed) | |
| { | |
| m_vm->disarmDeadline(); | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| if (m_budgeted) | |
| { | |
| m_vm->disarmWatchdog(); | |
| } | |
| #endif | |
| if (--m_vm->m_callDepth == 0) | |
| { | |
| // A trap can unwind out of an exclusive section with its lock held. | |
| restoreHeldRegion(m_outerRegionId, m_outerRegion); | |
| #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 = nullptr; | |
| #ifndef __EMSCRIPTEN__ | |
| trap = m_vm->m_state != nullptr && m_vm->m_state->instance != nullptr | |
| ? wasm_runtime_get_exception(m_vm->m_state->instance) | |
| : nullptr; | |
| #endif | |
| m_vm->m_debugHooks->onCallEnd(*m_vm, trap); | |
| } | |
| } | |
| // A debugger pauses mid call, so its sessions run unbudgeted. A metered | |
| // module ends its own overrun, but waits must still give up in time. | |
| bool WasmScriptingVM::boundsCalls() const | |
| { | |
| return (m_hostBudget || m_fuelBudget) && m_timeoutMs > 0 && | |
| m_debugHooks == nullptr; | |
| } | |
| uint32_t WasmScriptingVM::pageThreadBudgetMs() const | |
| { | |
| return boundsCalls() ? (uint32_t)m_timeoutMs : 0; | |
| } | |
| uint32_t WasmScriptingVM::parallelSize() const | |
| { | |
| #ifdef RIVE_WASM_PARALLEL | |
| // A debugger steps one thread, so every part runs on the one it stops, | |
| // and a top level runs before its instance can take members. | |
| return m_limits.parallelSafeMode || m_debugHooks != nullptr || | |
| s_booting == this | |
| ? 1 | |
| : WasmParallel::size(); | |
| #elif defined(__EMSCRIPTEN_PTHREADS__) | |
| // The page runs parts on its own team of threads, as many as its runner | |
| // picks up to a native call's 64. | |
| return m_limits.parallelSafeMode || m_debugHooks != nullptr || | |
| s_booting == this | |
| ? 1 | |
| : 64; | |
| #else | |
| return 1; | |
| #endif | |
| } | |
| // Only joins, region locks and signal waits read the call deadline. | |
| bool WasmScriptingVM::armDeadline() | |
| { | |
| if (m_deadlineArmed || !hasLanes() || !boundsCalls()) | |
| { | |
| return false; | |
| } | |
| m_outerDeadline = armCallDeadline( | |
| Clock::now() + std::chrono::milliseconds(m_timeoutMs)); | |
| m_deadlineArmed = true; | |
| return true; | |
| } | |
| void WasmScriptingVM::disarmDeadline() | |
| { | |
| restoreCallDeadline(m_outerDeadline); | |
| m_deadlineArmed = false; | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| void WasmScriptingVM::armWatchdog(void* env) | |
| { | |
| if (m_budgetSlot == nullptr) | |
| { | |
| m_budgetSlot = std::make_unique<BudgetSlot>(); | |
| ScriptWatchdog::instance().add(m_budgetSlot.get()); | |
| } | |
| BudgetSlot* slot = m_budgetSlot.get(); | |
| slot->env = (wasm_exec_env_t)env; | |
| slot->fired.store(false); | |
| int64_t deadline = steadyNanos() + (int64_t)watchdogBudgetMs() * 1000000; | |
| slot->deadline.store(deadline); | |
| ScriptWatchdog::instance().armed(deadline); | |
| m_budgetArmed = true; | |
| } | |
| void WasmScriptingVM::disarmWatchdog() | |
| { | |
| BudgetSlot* slot = m_budgetSlot.get(); | |
| takeDeadline(*slot); | |
| if (slot->fired.load()) | |
| { | |
| reportError(budgetExceededLine(watchdogBudgetMs())); | |
| // The interrupt can land inside the module's allocator or | |
| // collector, so its heap is no longer trusted. | |
| poison(); | |
| reportError( | |
| "script stopped: its module may have been interrupted mid-update"); | |
| } | |
| wasm_runtime_rive_clear_interrupt(slot->env); | |
| m_budgetArmed = false; | |
| } | |
| 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; | |
| } | |
| // WAMR writes results into the argument cells, so both must fit buf. | |
| if (argc > 8 || wasmCallCells(m_state->instance, f, 8) != argc) | |
| { | |
| recordTrap(name, "export signature mismatch"); | |
| return CallOutcome::trapped; | |
| } | |
| uint32_t buf[8] = {0}; | |
| for (uint32_t i = 0; i < argc; i++) | |
| { | |
| buf[i] = argv[i]; | |
| } | |
| bool ok; | |
| { | |
| ensureWasmThreadEnv(); | |
| WamrState::CallStateScope callState(*m_state); | |
| // Runtime services, the collector among them, must never be cut off. | |
| ScriptCallScope callScope(this, strncmp(name, "__rive", 6) != 0); | |
| ok = m_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; | |
| } | |
| WasmScriptingVM::CallOutcome WasmScriptingVM::callF64( | |
| const char* name, | |
| const double* args, | |
| uint32_t argc, | |
| double* result) | |
| { | |
| if (m_poisoned) | |
| { | |
| return CallOutcome::trapped; | |
| } | |
| wasm_function_inst_t f = m_state->lookupExport(name); | |
| if (f == nullptr) | |
| { | |
| return CallOutcome::missing; | |
| } | |
| wasm_module_inst_t inst = m_state->instance; | |
| uint32_t retCount = wasm_func_get_result_count(f, inst); | |
| // Modules load unsigned, so the export may not match. | |
| // host_obj_scroll_event takes the most. | |
| constexpr uint32_t kMaxArgs = 10; | |
| if (argc > kMaxArgs || wasm_func_get_param_count(f, inst) != argc || | |
| retCount > 1) | |
| { | |
| recordTrap(name, "export signature mismatch"); | |
| return CallOutcome::trapped; | |
| } | |
| wasm_valkind_t kinds[kMaxArgs]; | |
| wasm_func_get_param_types(f, inst, kinds); | |
| wasm_val_t values[kMaxArgs]; | |
| for (uint32_t i = 0; i < argc; i++) | |
| { | |
| values[i].kind = kinds[i]; | |
| switch (kinds[i]) | |
| { | |
| case WASM_F64: | |
| values[i].of.f64 = args[i]; | |
| break; | |
| case WASM_F32: | |
| values[i].of.f32 = (float)args[i]; | |
| break; | |
| case WASM_I64: | |
| values[i].of.i64 = (int64_t)args[i]; | |
| break; | |
| default: | |
| values[i].of.i32 = (int32_t)(uint32_t)(int64_t)args[i]; | |
| break; | |
| } | |
| } | |
| wasm_val_t ret[1]; | |
| bool ok; | |
| { | |
| ensureWasmThreadEnv(); | |
| WamrState::CallStateScope callState(*m_state); | |
| ScriptCallScope callScope(this); | |
| ok = wasm_runtime_call_wasm_a( | |
| m_state->execEnv, | |
| f, | |
| retCount, | |
| ret, | |
| argc, | |
| values); | |
| } | |
| if (!ok) | |
| { | |
| reportTrap(name); | |
| return CallOutcome::trapped; | |
| } | |
| *result = 0; | |
| if (retCount != 0) | |
| { | |
| switch (ret[0].kind) | |
| { | |
| case WASM_F64: | |
| *result = ret[0].of.f64; | |
| break; | |
| case WASM_F32: | |
| *result = ret[0].of.f32; | |
| break; | |
| case WASM_I64: | |
| *result = (double)ret[0].of.i64; | |
| break; | |
| default: | |
| *result = ret[0].of.i32; | |
| break; | |
| } | |
| } | |
| return CallOutcome::ok; | |
| } | |
| bool WasmScriptingVM::hasExport(const char* name) | |
| { | |
| return m_state->lookupExport(name) != nullptr; | |
| } | |
| void WasmScriptingVM::reportTrap(const char* where) | |
| { | |
| wasm_module_inst_t inst = m_state->instance; | |
| recordTrap(where, wasm_runtime_get_exception(inst)); | |
| #if WASM_ENABLE_DUMP_CALL_STACK != 0 | |
| if (!m_quietTraps) | |
| { | |
| wasm_runtime_dump_call_stack(m_state->execEnv); | |
| } | |
| #endif | |
| // A pending exception makes WAMR refuse the next call, the collector's | |
| // included. | |
| wasm_runtime_clear_exception(inst); | |
| } | |
| #endif | |
| void WasmScriptingVM::recordTrap(const char* where, const char* exception) | |
| { | |
| // A silent fold hides real traps; name them so a script that dies | |
| // mid-call is diagnosable instead of a mystery no-op. | |
| exception = fullTrapMessage(exception); | |
| // 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 || m_quietTraps) | |
| { | |
| return; | |
| } | |
| reportError(std::string("wasm call trapped in ") + where + ": " + exception); | |
| if (m_leakWarningCount > 0 && !m_leakTrapContextPrinted) | |
| { | |
| // A bare trap after leak warnings is almost always the memory | |
| // ceiling; say so once for hosts that dropped the warning strings. | |
| m_leakTrapContextPrinted = true; | |
| reportError( | |
| "wasm call trapped after " + std::to_string(m_leakWarningCount) + | |
| " script heap leak warnings; the module likely hit its " | |
| "wasmMaxPages ceiling"); | |
| } | |
| } | |
| void WasmScriptingVM::reportError(const std::string& line) | |
| { | |
| if (m_error) | |
| { | |
| m_error(line); | |
| } | |
| else | |
| { | |
| fprintf(stderr, "%s\n", line.c_str()); | |
| } | |
| } | |
| 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; | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| void* WasmScriptingVM::resolveModulePtr(uint32_t appAddr, uint32_t size) | |
| { | |
| return wasmAppBytes(m_state->instance, appAddr, size); | |
| } | |
| const char* WasmScriptingVM::resolveModuleString(uint32_t appAddr) | |
| { | |
| // The terminator has to lie inside linear memory for strlen to stop. | |
| wasm_module_inst_t inst = m_state->instance; | |
| uint64_t end = 0; | |
| if (!wasm_runtime_get_app_addr_range(inst, appAddr, nullptr, &end)) | |
| { | |
| return nullptr; | |
| } | |
| auto str = (const char*)wasm_runtime_addr_app_to_native(inst, appAddr); | |
| return memchr(str, '\0', end - appAddr) != nullptr ? str : nullptr; | |
| } | |
| bool WasmScriptingVM::copyToModule( | |
| uint32_t appAddr, | |
| const void* src, | |
| uint32_t size) | |
| { | |
| void* dst = resolveModulePtr(appAddr, size); | |
| if (dst == nullptr) | |
| { | |
| return false; | |
| } | |
| memcpy(dst, src, size); | |
| return true; | |
| } | |
| 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); | |
| } | |
| #else | |
| // The browser backend overrides every seam; these complete the vtable. | |
| WasmScriptingVM::CallOutcome WasmScriptingVM::callModuleChecked( | |
| const char*, | |
| uint32_t, | |
| uint32_t*, | |
| uint32_t*) | |
| { | |
| return CallOutcome::missing; | |
| } | |
| WasmScriptingVM::CallOutcome WasmScriptingVM::callF64( | |
| const char*, | |
| const double*, | |
| uint32_t, | |
| double*) | |
| { | |
| return CallOutcome::missing; | |
| } | |
| bool WasmScriptingVM::hasExport(const char*) { return false; } | |
| void* WasmScriptingVM::resolveModulePtr(uint32_t, uint32_t) { return nullptr; } | |
| bool WasmScriptingVM::copyToModule(uint32_t, const void*, uint32_t) | |
| { | |
| return false; | |
| } | |
| const char* WasmScriptingVM::resolveModuleString(uint32_t) { return nullptr; } | |
| void WasmScriptingVM::raiseModuleError(const char*) {} | |
| bool WasmScriptingVM::memoryLimits(uint32_t*, uint32_t*) const { return false; } | |
| bool WasmScriptingVM::valid() const { return false; } | |
| #endif | |
| void WasmScriptingVM::raiseBudgetExceeded() | |
| { | |
| m_budgetRaised = true; | |
| raiseModuleError("execution exceeded timeout"); | |
| } | |
| void WasmScriptingVM::raiseThrown(const char* message) | |
| { | |
| m_thrownRaised = true; | |
| raiseModuleError(message); | |
| } | |
| const char* WasmScriptingVM::fullTrapMessage(const char* exception) const | |
| { | |
| const char* text = trapText(exception); | |
| if (text == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| // 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; | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| 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; | |
| } | |
| #else | |
| void WasmScriptingVMNatives::booting(WasmScriptingVM* vm) | |
| { | |
| // A top level's waits give up in time, as a call's do, and it neither | |
| // sees a region the call it nests in holds nor leaves one held. | |
| if (vm != nullptr) | |
| { | |
| vm->armDeadline(); | |
| vm->m_outerBooting = s_booting; | |
| vm->m_outerRegion = stashHeldRegion(&vm->m_outerRegionId); | |
| s_booting = vm; | |
| } | |
| else if (WasmScriptingVM* ending = s_booting) | |
| { | |
| if (ending->m_deadlineArmed) | |
| { | |
| ending->disarmDeadline(); | |
| } | |
| restoreHeldRegion(ending->m_outerRegionId, ending->m_outerRegion); | |
| s_booting = ending->m_outerBooting; | |
| } | |
| s_bootPrint = s_booting != nullptr ? &s_booting->m_print : nullptr; | |
| } | |
| #endif | |
| std::unordered_map<std::string, int64_t>& WasmScriptingVMNatives::consoleTimers( | |
| WasmScriptingVM* vm) | |
| { | |
| return vm->m_consoleTimers; | |
| } | |
| // The budget a metered module arms its own fuel with around its top level, | |
| // as its calls do, or 0. A debugger pauses mid call, so it runs unbudgeted. | |
| int WasmScriptingVMNatives::startFuelMs(const WasmScriptingVM* vm) | |
| { | |
| return vm->m_fuelBudget && vm->m_debugHooks == nullptr && vm->m_timeoutMs > 0 | |
| ? vm->m_timeoutMs | |
| : 0; | |
| } | |
| // Why a host that requires bounds will not start this module, or null. | |
| const char* WasmScriptingVMNatives::startRefusal(const WasmScriptingVM* vm) | |
| { | |
| if (!s_requireBounds.load() || vm->m_fuelBudget) | |
| { | |
| return nullptr; | |
| } | |
| #ifdef __EMSCRIPTEN__ | |
| return "this host runs only scripts it can bound, and a browser bounds " | |
| "only a module with fuel; bake it with wasmFuel"; | |
| #else | |
| return vm->m_hostBudget | |
| ? nullptr | |
| : "this host runs only scripts it can bound, AnimaScript with " | |
| "fuel or the watchdog, and dangerouslyFast bakes and Luau " | |
| "wasm modules have neither"; | |
| #endif | |
| } | |
| 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); | |
| // A unit is three bytes at most, a surrogate pair four for its two. | |
| size_t most = (size_t)(byteCount / 2) * 3; | |
| char* out = m_inline; | |
| if (most > sizeof(m_inline)) | |
| { | |
| m_heap.resize(most); | |
| out = &m_heap[0]; | |
| } | |
| m_data = out; | |
| for (uint32_t i = 0; i + 1 < byteCount;) | |
| { | |
| out += utf8Of(nextUtf16(in, byteCount, i), (uint8_t*)out); | |
| } | |
| 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); | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| // Module start runs inside wasm_runtime_instantiate, before the exec env | |
| // carries the vm, so a native its top level reaches adopts the booting one; | |
| // a lane that top level joined runs on this thread without one. | |
| WasmScriptingVM* bootVmFromEnv(wasm_exec_env_t env) | |
| { | |
| WasmScriptingVM* vm = vmFromEnv(env); | |
| return vm != nullptr || WasmLane::current() != nullptr | |
| ? vm | |
| : WasmScriptingVMNatives::adoptBooting(env); | |
| } | |
| #else | |
| // As on WAMR, module start resolves no vm for the ops that do not trap. | |
| WasmScriptingVM* vmFromHandle(uint32_t handle) | |
| { | |
| auto vm = (WasmScriptingVM*)(uintptr_t)handle; | |
| return vm == s_booting ? nullptr : vm; | |
| } | |
| // Lanes pass 1 and reach only the shared ops, which all boot. | |
| WasmScriptingVM* bootVmFromHandle(uint32_t handle) | |
| { | |
| return handle == 1 ? nullptr : (WasmScriptingVM*)(uintptr_t)handle; | |
| } | |
| #endif | |
| #ifndef __EMSCRIPTEN__ | |
| // A trap from a module call that has no VM: a lane's, or a parallel call | |
| // member's, whose throw reaches the call that ran it. | |
| void vmlessTrap( | |
| wasm_exec_env_t env, | |
| const char* text, | |
| const char* message = nullptr, | |
| uint32_t length = 0) | |
| { | |
| WasmParallel::noteMemberTrap(text, message, length); | |
| wasm_runtime_set_exception(wasm_runtime_get_module_inst(env), text); | |
| } | |
| #endif | |
| // 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" | |
| void consoleWrite(WasmScriptingVM* vm, const std::string& message) | |
| { | |
| rtLogImpl(vm, 0, message.data(), (uint32_t)message.size()); | |
| } | |
| void consoleTimeStart(WasmScriptingVM* vm, const std::string& label) | |
| { | |
| if (vm != nullptr) | |
| { | |
| startConsoleTimer(WasmScriptingVMNatives::consoleTimers(vm), label); | |
| } | |
| } | |
| void consoleTimeReport(WasmScriptingVM* vm, const std::string& name, bool end) | |
| { | |
| if (vm != nullptr) | |
| { | |
| consoleWrite( | |
| vm, | |
| reportConsoleTimer( | |
| WasmScriptingVMNatives::consoleTimers(vm), | |
| name, | |
| end)); | |
| } | |
| } | |
| std::string traceNumber(double value) | |
| { | |
| if (std::isnan(value)) | |
| { | |
| return "NaN"; | |
| } | |
| if (std::isinf(value)) | |
| { | |
| return value < 0 ? "-Infinity" : "Infinity"; | |
| } | |
| char text[32]; | |
| if (value == std::trunc(value) && std::fabs(value) < 1e21) | |
| { | |
| snprintf(text, sizeof(text), "%.0f", value); | |
| return text; | |
| } | |
| for (int digits = 1; digits <= 17; digits++) | |
| { | |
| snprintf(text, sizeof(text), "%.*g", digits, value); | |
| if (strtod(text, nullptr) == value) | |
| { | |
| break; | |
| } | |
| } | |
| return text; | |
| } | |
| void traceWrite( | |
| WasmScriptingVM* vm, | |
| const std::string& message, | |
| int32_t count, | |
| const double* args) | |
| { | |
| std::string line = "trace: " + message; | |
| for (int32_t i = 0; i < count && i < 5; i++) | |
| { | |
| line += ' ' + traceNumber(args[i]); | |
| } | |
| consoleWrite(vm, line); | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| // --- 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); | |
| // Module start has no exec env yet to restore the stack through. | |
| if (vm == nullptr) | |
| { | |
| wasm_runtime_set_exception(inst, "script module invoke at start"); | |
| return; | |
| } | |
| 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 --------------------------------------- | |
| // Module start reaches these too, before the exec env carries the vm, so | |
| // they resolve memory through the booting one and trap when there is none. | |
| WasmScriptingVM* envVm(wasm_exec_env_t env) | |
| { | |
| WasmScriptingVM* vm = bootVmFromEnv(env); | |
| if (vm == nullptr) | |
| { | |
| wasm_runtime_set_exception( | |
| wasm_runtime_get_module_inst(env), | |
| "script module import without a vm"); | |
| } | |
| return vm; | |
| } | |
| 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 = envVm(env); | |
| if (vm == nullptr) | |
| { | |
| return; | |
| } | |
| 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 rtNowImpl(WasmScriptingVM* vm) | |
| { | |
| // Same dev hook as dateNow: clock() feeds Luau's default RNG seed. A | |
| // pinned clock also parks the module's execution budget. | |
| static const bool fixed = getenv("RIVE_WASM_FIXED_DATE") != nullptr; | |
| if (fixed) | |
| { | |
| return 0; | |
| } | |
| auto now = std::chrono::steady_clock::now().time_since_epoch(); | |
| return std::chrono::duration<double, std::milli>(now).count(); | |
| } | |
| double rtDateNowImpl(WasmScriptingVM* vm) | |
| { | |
| // 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(); | |
| } | |
| // The Luau host module reads the same clocks under the names emscripten | |
| // gives them. | |
| double getNow(wasm_exec_env_t env) { return rtNowImpl(nullptr); } | |
| double dateNow(wasm_exec_env_t env) { return rtDateNowImpl(nullptr); } | |
| // 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; | |
| } | |
| #endif | |
| 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 | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| // --- 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) | |
| { | |
| WasmScriptingVM* vm = envVm(env); | |
| int32_t* fields = vm != nullptr ? (int32_t*)vm->resolveModulePtr(tmPtr, 44) | |
| : nullptr; | |
| 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 = envVm(env); | |
| if (vm == nullptr) | |
| { | |
| return; | |
| } | |
| #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 = envVm(env); | |
| if (vm == nullptr) | |
| { | |
| return 0; | |
| } | |
| 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 = bootVmFromEnv(env); | |
| auto text = [vm](uint32_t ptr) { | |
| const char* str = ptr != 0 && vm != nullptr ? vm->resolveModuleString(ptr) | |
| : nullptr; | |
| return str != nullptr ? str : "?"; | |
| }; | |
| reportScriptError( | |
| vm, | |
| std::string("module assertion failed: ") + text(message) + " (" + | |
| text(file) + ":" + std::to_string(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 = envVm(env); | |
| if (vm == nullptr) | |
| { | |
| return 28; // EINVAL | |
| } | |
| 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); | |
| } | |
| } | |
| reportScriptError( | |
| bootVmFromEnv(env), | |
| std::string("[wasm] C++ exception thrown: ") + typeName); | |
| wasm_runtime_set_exception(inst, "c++ exception"); | |
| } | |
| // Math.random's seed. A deterministic run's seed comes first, so --seed | |
| // picks the same sequence here as in Luau; the fixed date env that pins | |
| // script time pins it too, to 1, the deterministic default. | |
| double envSeed(wasm_exec_env_t) | |
| { | |
| if (File::deterministicMode) | |
| { | |
| return (double)File::deterministicSeed; | |
| } | |
| if (getenv("RIVE_WASM_FIXED_DATE") != nullptr) | |
| { | |
| return 1.0; | |
| } | |
| return (double)std::chrono::steady_clock::now().time_since_epoch().count(); | |
| } | |
| // The std's console passes a managed string: UTF-16 whose byte length is the | |
| // word ahead of it. | |
| std::string consoleString(wasm_exec_env_t env, uint32_t text) | |
| { | |
| uint32_t length = 0; | |
| auto data = (const char*) | |
| wasmConsoleText(wasm_runtime_get_module_inst(env), text, length); | |
| if (data == nullptr) | |
| { | |
| return std::string(); | |
| } | |
| WasmStringArg utf8(vmFromEnv(env), data, length); | |
| return std::string(utf8.data(), utf8.size()); | |
| } | |
| void consoleLog(wasm_exec_env_t env, uint32_t text) | |
| { | |
| if (WasmLane* lane = WasmLane::current()) | |
| { | |
| lane->logConsole(env, text); | |
| return; | |
| } | |
| consoleWrite(vmFromEnv(env), consoleString(env, text)); | |
| } | |
| void consoleTime(wasm_exec_env_t env, uint32_t label) | |
| { | |
| if (WasmLane* lane = WasmLane::current()) | |
| { | |
| lane->consoleTimer(env, label, false, false); | |
| return; | |
| } | |
| consoleTimeStart(bootVmFromEnv(env), consoleString(env, label)); | |
| } | |
| void consoleTimeLog(wasm_exec_env_t env, uint32_t label) | |
| { | |
| if (WasmLane* lane = WasmLane::current()) | |
| { | |
| lane->consoleTimer(env, label, true, false); | |
| return; | |
| } | |
| consoleTimeReport(bootVmFromEnv(env), consoleString(env, label), false); | |
| } | |
| void consoleTimeEnd(wasm_exec_env_t env, uint32_t label) | |
| { | |
| if (WasmLane* lane = WasmLane::current()) | |
| { | |
| lane->consoleTimer(env, label, true, true); | |
| return; | |
| } | |
| consoleTimeReport(bootVmFromEnv(env), consoleString(env, label), true); | |
| } | |
| void envTrace( | |
| wasm_exec_env_t env, | |
| uint32_t message, | |
| int32_t count, | |
| double a0, | |
| double a1, | |
| double a2, | |
| double a3, | |
| double a4) | |
| { | |
| double args[5] = {a0, a1, a2, a3, a4}; | |
| traceWrite(vmFromEnv(env), consoleString(env, message), count, args); | |
| } | |
| NativeSymbol kEnvNatives[] = { | |
| {"console.log", (void*)consoleLog, "(i)", nullptr}, | |
| {"console.debug", (void*)consoleLog, "(i)", nullptr}, | |
| {"console.info", (void*)consoleLog, "(i)", nullptr}, | |
| {"console.warn", (void*)consoleLog, "(i)", nullptr}, | |
| {"console.error", (void*)consoleLog, "(i)", nullptr}, | |
| {"console.time", (void*)consoleTime, "(i)", nullptr}, | |
| {"console.timeLog", (void*)consoleTimeLog, "(i)", nullptr}, | |
| {"console.timeEnd", (void*)consoleTimeEnd, "(i)", nullptr}, | |
| {"trace", (void*)envTrace, "(iiFFFFF)", nullptr}, | |
| {"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}, | |
| }; | |
| #endif | |
| void deleteHostObject( | |
| WasmScriptingVM* vm, | |
| WasmScriptingVM::HandleTable::Tag tag, | |
| void* object); | |
| void releaseHostHandle( | |
| WasmScriptingVM* vm, | |
| uint32_t handle, | |
| WasmScriptingVM::HandleTable::Tag tag) | |
| { | |
| void* object = vm != nullptr ? vm->handles().resolve(handle, tag) : nullptr; | |
| if (object == nullptr) | |
| { | |
| return; | |
| } | |
| deleteHostObject(vm, tag, object); | |
| vm->handles().release(handle, tag); | |
| } | |
| // --- 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)); | |
| } | |
| void rebuildRenderPath(WasmScriptingVM* vm, HostPath* hostPath) | |
| { | |
| // makeRenderPath may take the arrays it is handed, and raw stays the | |
| // geometry add and the readbacks use. | |
| RawPath copy(hostPath->raw); | |
| hostPath->path = vm->factory()->makeRenderPath(copy, hostPath->fillRule); | |
| hostPath->renderDirty = false; | |
| } | |
| RenderPath* hostRenderPath(WasmScriptingVM* vm, HostPath* hostPath) | |
| { | |
| if (hostPath == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| if (hostPath->renderDirty) | |
| { | |
| rebuildRenderPath(vm, hostPath); | |
| } | |
| return hostPath->path.get(); | |
| } | |
| struct HostPaint | |
| { | |
| rcp<RenderPaint> paint; | |
| }; | |
| // Enums cross as ordinals; one out of range raises as a bad name does in the | |
| // Luau lane, since the renderer's switches treat other values as unreachable. | |
| bool validOrdinal( | |
| WasmScriptingVM* vm, | |
| uint32_t value, | |
| bool valid, | |
| const char* type) | |
| { | |
| if (!valid) | |
| { | |
| char message[64]; | |
| snprintf(message, sizeof(message), "%u is not a valid %s", value, type); | |
| vm->raiseModuleError(message); | |
| } | |
| return valid; | |
| } | |
| bool validBlendMode(WasmScriptingVM* vm, uint32_t value) | |
| { | |
| return validOrdinal( | |
| vm, | |
| value, | |
| value == (uint32_t)BlendMode::srcOver || | |
| value == (uint32_t)BlendMode::additive || | |
| (value >= (uint32_t)BlendMode::screen && | |
| value <= (uint32_t)BlendMode::luminosity), | |
| "BlendMode"); | |
| } | |
| // Backends index a sampler table with the key. | |
| bool validSamplerKey(WasmScriptingVM* vm, uint32_t key) | |
| { | |
| return validOrdinal( | |
| vm, | |
| key, | |
| key < ImageSampler::MAX_SAMPLER_PERMUTATIONS, | |
| "ImageSampler key"); | |
| } | |
| // RawPath walkers trust a stream to open with a move and its verbs to take | |
| // exactly its points, so guest geometry is checked before it becomes one. | |
| bool guestRawPath( | |
| WasmScriptingVM* vm, | |
| const uint8_t* verbs, | |
| uint32_t verbCount, | |
| const float* points, | |
| uint32_t floatCount, | |
| RawPath* out) | |
| { | |
| uint64_t floats = 0; | |
| bool valid = verbCount == 0 || verbs[0] == (uint8_t)PathVerb::move; | |
| for (uint32_t i = 0; valid && i < verbCount; i++) | |
| { | |
| switch ((PathVerb)verbs[i]) | |
| { | |
| case PathVerb::move: | |
| case PathVerb::line: | |
| floats += 2; | |
| continue; | |
| case PathVerb::quad: | |
| floats += 4; | |
| continue; | |
| case PathVerb::cubic: | |
| floats += 6; | |
| continue; | |
| case PathVerb::close: | |
| continue; | |
| } | |
| valid = false; | |
| } | |
| if (!valid || floats != floatCount) | |
| { | |
| vm->raiseModuleError("Path: verbs and points do not match"); | |
| return false; | |
| } | |
| *out = RawPath( | |
| Span<const PathVerb>((const PathVerb*)verbs, verbCount), | |
| Span<const Vec2D>((const Vec2D*)points, floatCount / 2)); | |
| return true; | |
| } | |
| 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 || | |
| !validOrdinal( | |
| vm, | |
| fillRule, | |
| fillRule <= (uint32_t)FillRule::clockwise, | |
| "FillRule") || | |
| !guestRawPath(vm, verbs, verbCount, points, floatCount, &hostPath->raw)) | |
| { | |
| return; | |
| } | |
| hostPath->fillRule = (FillRule)fillRule; | |
| rebuildRenderPath(vm, hostPath); | |
| } | |
| 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) | |
| { | |
| RawPath rawPath; | |
| if (vm == nullptr || vm->pathEffectOut() == nullptr || | |
| !guestRawPath(vm, verbs, verbCount, points, floatCount, &rawPath)) | |
| { | |
| return; | |
| } | |
| 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( | |
| WasmScriptingVM* vm, | |
| const uint8_t* verbs, | |
| uint32_t verbCount, | |
| const float* points, | |
| uint32_t floatCount) | |
| { | |
| auto path = std::make_unique<RawPath>(); | |
| if (vm == nullptr || | |
| !guestRawPath(vm, verbs, verbCount, points, floatCount, path.get())) | |
| { | |
| return nullptr; | |
| } | |
| return path; | |
| } | |
| uint32_t measurePathNewImpl( | |
| WasmScriptingVM* vm, | |
| const uint8_t* verbs, | |
| uint32_t verbCount, | |
| const float* points, | |
| uint32_t floatCount) | |
| { | |
| auto source = copyGeometry(vm, verbs, verbCount, points, floatCount); | |
| if (source == nullptr) | |
| { | |
| return 0; | |
| } | |
| auto measure = new HostMeasure(); | |
| measure->source = std::move(source); | |
| 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) | |
| { | |
| auto source = copyGeometry(vm, verbs, verbCount, points, floatCount); | |
| if (source == nullptr) | |
| { | |
| return 0; | |
| } | |
| auto measure = new HostMeasure(); | |
| measure->source = std::move(source); | |
| 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) | |
| { | |
| releaseHostHandle(vm, handle, WasmScriptingVM::HandleTable::Tag::measure); | |
| } | |
| void pathReleaseImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| releaseHostHandle(vm, 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) | |
| { | |
| releaseHostHandle(vm, 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) | |
| { | |
| auto paint = resolvePaint(vm, handle); | |
| if (paint != nullptr && validOrdinal( | |
| vm, | |
| value, | |
| value <= (uint32_t)StrokeJoin::bevel, | |
| "StrokeJoin")) | |
| { | |
| paint->join((StrokeJoin)value); | |
| } | |
| } | |
| void paintCapImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value) | |
| { | |
| auto paint = resolvePaint(vm, handle); | |
| if (paint != nullptr && validOrdinal( | |
| vm, | |
| value, | |
| value <= (uint32_t)StrokeCap::square, | |
| "StrokeCap")) | |
| { | |
| paint->cap((StrokeCap)value); | |
| } | |
| } | |
| void paintBlendModeImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t value) | |
| { | |
| auto paint = resolvePaint(vm, handle); | |
| if (paint != nullptr && validBlendMode(vm, value)) | |
| { | |
| // As ScriptedPaint::blendMode in the Luau lane: scripts have no amount. | |
| paint->blendMode((BlendMode)value); | |
| paint->additiveness(additivenessFor((BlendMode)value, 255)); | |
| } | |
| } | |
| void paintFeatherImpl(WasmScriptingVM* vm, uint32_t handle, float value) | |
| { | |
| if (auto paint = resolvePaint(vm, handle)) | |
| { | |
| paint->feather(value); | |
| } | |
| } | |
| struct HostShader | |
| { | |
| rcp<RenderShader> shader; | |
| }; | |
| // Both gradients read count colors and count stops from the module. | |
| bool resolveGradientStops( | |
| WasmScriptingVM* vm, | |
| uint32_t colorsPtr, | |
| uint32_t stopsPtr, | |
| uint32_t count, | |
| const ColorInt*& colors, | |
| const float*& stops) | |
| { | |
| // A larger count wraps the byte size, which then passes the bounds check. | |
| if (count > UINT32_MAX / 4) | |
| { | |
| return false; | |
| } | |
| colors = (const ColorInt*)vm->resolveModulePtr(colorsPtr, count * 4); | |
| stops = (const float*)vm->resolveModulePtr(stopsPtr, count * 4); | |
| return colors != nullptr && stops != nullptr; | |
| } | |
| 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; | |
| } | |
| const ColorInt* colors = nullptr; | |
| const float* stops = nullptr; | |
| if (!resolveGradientStops(vm, colorsPtr, stopsPtr, count, colors, stops)) | |
| { | |
| 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; | |
| } | |
| const ColorInt* colors = nullptr; | |
| const float* stops = nullptr; | |
| if (!resolveGradientStops(vm, colorsPtr, stopsPtr, count, colors, stops)) | |
| { | |
| 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) | |
| { | |
| releaseHostHandle(vm, 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); | |
| } | |
| void paintShaderTransformImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t paintHandle, | |
| const float* values, | |
| uint32_t floatCount) | |
| { | |
| if (values == nullptr || floatCount < 6) | |
| { | |
| return; | |
| } | |
| if (auto paint = resolvePaint(vm, paintHandle)) | |
| { | |
| paint->shaderTransform(Mat2D( | |
| values[0], | |
| values[1], | |
| values[2], | |
| values[3], | |
| values[4], | |
| values[5])); | |
| } | |
| } | |
| // 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. | |
| FileAsset* findFileAsset( | |
| WasmScriptingVM* vm, | |
| uint32_t objectHandle, | |
| const char* name, | |
| uint32_t length, | |
| uint16_t typeKey, | |
| bool (*accept)(FileAsset*)) | |
| { | |
| 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->isTypeOf(typeKey) || asset->name() != key) | |
| { | |
| continue; | |
| } | |
| if (accept == nullptr || accept(asset.get())) | |
| { | |
| return asset.get(); | |
| } | |
| } | |
| return nullptr; | |
| } | |
| template <typename T> | |
| T* findFileAsset( | |
| WasmScriptingVM* vm, | |
| uint32_t objectHandle, | |
| const char* name, | |
| uint32_t length, | |
| bool (*accept)(FileAsset*) = nullptr) | |
| { | |
| return static_cast<T*>( | |
| findFileAsset(vm, objectHandle, name, length, T::typeKey, accept)); | |
| } | |
| 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) | |
| { | |
| releaseHostHandle(vm, 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; | |
| // setBindGroup calls made before the first setPipeline. | |
| ore::DeferredBindGroups deferredBindGroups; | |
| ore::DrawBindings drawBindings; | |
| bool pipelineSet = false; | |
| }; | |
| struct HostGpuBuffer | |
| { | |
| rcp<ore::Buffer> buffer; | |
| // Ore allows no update to an immutable buffer. | |
| bool immutable; | |
| }; | |
| 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; | |
| } | |
| template <typename E> | |
| bool validOre(WasmScriptingVM* vm, uint32_t value, E last, const char* type) | |
| { | |
| return validOrdinal(vm, value, value <= (uint32_t)last, type); | |
| } | |
| bool guardPassed(WasmScriptingVM* vm, bool passed, const std::string& error) | |
| { | |
| if (!passed) | |
| { | |
| vm->raiseModuleError(error.c_str()); | |
| } | |
| return passed; | |
| } | |
| 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) | |
| { | |
| releaseHostHandle(vm, 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() || | |
| !oreContext->exposesTarget() || 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; | |
| // Guests built before the alignment field ask for 19. | |
| if (outCount < 20) | |
| { | |
| return 19; | |
| } | |
| out[19] = f.minUniformBufferOffsetAlignment; | |
| return 20; | |
| } | |
| uint32_t gpuPassBeginImpl( | |
| WasmScriptingVM* vm, | |
| const rive_gpu_pass_desc_v1* podDesc, | |
| uint32_t descByteCount, | |
| const uint8_t* blob, | |
| uint32_t blobCount) | |
| { | |
| ore::Context* oreContext = gpuOreContext(vm); | |
| // The label is optional, so a descriptor from before it still passes. | |
| if (vm == nullptr || oreContext == nullptr || | |
| descByteCount < offsetof(rive_gpu_pass_desc_v1, labelSize) || | |
| podDesc->colorCount > 4) | |
| { | |
| return 0; | |
| } | |
| const uint32_t labelSize = descByteCount >= sizeof(*podDesc) | |
| ? podDesc->labelSize | |
| : 0; | |
| const uint64_t colorBytes = uint64_t(podDesc->colorCount) * | |
| sizeof(rive_gpu_pass_color_attachment_v1); | |
| if (colorBytes + labelSize != blobCount) | |
| { | |
| return 0; | |
| } | |
| auto colors = reinterpret_cast<const rive_gpu_pass_color_attachment_v1*>( | |
| blob); | |
| std::string label; | |
| if (labelSize != 0) | |
| { | |
| label.assign(reinterpret_cast<const char*>(blob + colorBytes), labelSize); | |
| } | |
| 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; | |
| if (!validOre(vm, colors[i].loadOp, ore::LoadOp::dontCare, "LoadOp") || | |
| !validOre(vm, colors[i].storeOp, ore::StoreOp::discard, "StoreOp")) | |
| { | |
| return 0; | |
| } | |
| 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; | |
| } | |
| } | |
| if (!validOre(vm, podDesc->depthLoadOp, ore::LoadOp::dontCare, "LoadOp") || | |
| !validOre(vm, podDesc->depthStoreOp, ore::StoreOp::discard, "StoreOp") || | |
| !validOre(vm, podDesc->stencilLoadOp, ore::LoadOp::dontCare, "LoadOp") || | |
| !validOre(vm, podDesc->stencilStoreOp, ore::StoreOp::discard, "StoreOp")) | |
| { | |
| 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; | |
| desc.label = label.empty() ? nullptr : label.c_str(); | |
| 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; | |
| } | |
| if (std::string err; !host->drawBindings.validate(&err)) | |
| { | |
| vm->raiseModuleError(err.c_str()); | |
| 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) | |
| { | |
| // The pass resolves first: it is null with no VM, as during module start. | |
| auto* host = resolveHostPass(vm, passHandle); | |
| if (host == nullptr) | |
| { | |
| return; | |
| } | |
| auto pipeline = static_cast<HostGpuPipeline*>(vm->handles().resolve( | |
| pipelineHandle, | |
| WasmScriptingVM::HandleTable::Tag::gpuPipeline)); | |
| if (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()); | |
| host->deferredBindGroups.flush(host->pass.get()); | |
| if (oreContext != nullptr && !oreContext->lastError().empty()) | |
| { | |
| gpuRejected(vm, oreContext, "setPipeline"); | |
| return; | |
| } | |
| host->drawBindings.setPipeline(pipeline->pipeline.get()); | |
| host->pipelineSet = true; | |
| } | |
| void gpuPassSetVertexBufferImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t passHandle, | |
| uint32_t slot, | |
| uint32_t bufferHandle, | |
| uint32_t offset) | |
| { | |
| auto* pass = resolvePass(vm, passHandle); | |
| if (pass == nullptr) | |
| { | |
| return; | |
| } | |
| auto buffer = static_cast<HostGpuBuffer*>(vm->handles().resolve( | |
| bufferHandle, | |
| WasmScriptingVM::HandleTable::Tag::gpuBuffer)); | |
| if (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); | |
| if (pass == nullptr) | |
| { | |
| return; | |
| } | |
| auto buffer = static_cast<HostGpuBuffer*>(vm->handles().resolve( | |
| bufferHandle, | |
| WasmScriptingVM::HandleTable::Tag::gpuBuffer)); | |
| if (buffer == nullptr || | |
| !validOrdinal( | |
| vm, | |
| indexFormat, | |
| indexFormat == (uint32_t)ore::IndexFormat::uint16 || | |
| indexFormat == (uint32_t)ore::IndexFormat::uint32, | |
| "IndexFormat")) | |
| { | |
| 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* host = resolveHostPass(vm, passHandle); | |
| if (host == nullptr || host->pass == nullptr) | |
| { | |
| return; | |
| } | |
| auto bindGroup = static_cast<HostGpuBindGroup*>(vm->handles().resolve( | |
| bindGroupHandle, | |
| WasmScriptingVM::HandleTable::Tag::gpuBindGroup)); | |
| if (bindGroup == nullptr || dynamicOffsetByteCount % sizeof(uint32_t) != 0) | |
| { | |
| return; | |
| } | |
| uint32_t count = dynamicOffsetByteCount / (uint32_t)sizeof(uint32_t); | |
| // The checks the Luau binding makes, so a misuse raises on the call | |
| // instead of reaching a backend assert or a silent misbind. | |
| ore::Context* oreContext = gpuOreContext(vm); | |
| if (std::string err; !ore::validateSetBindGroup( | |
| groupIndex, | |
| bindGroup->bindGroup.get(), | |
| dynamicOffsets, | |
| count, | |
| ore::scriptDynamicOffsetAlignment(oreContext), | |
| &err)) | |
| { | |
| vm->raiseModuleError(err.c_str()); | |
| return; | |
| } | |
| if (!host->pipelineSet) | |
| { | |
| host->deferredBindGroups | |
| .defer(groupIndex, bindGroup->bindGroup.get(), dynamicOffsets, count); | |
| host->drawBindings.setBindGroup(groupIndex, bindGroup->bindGroup.get()); | |
| return; | |
| } | |
| if (oreContext != nullptr) | |
| { | |
| oreContext->clearLastError(); | |
| } | |
| host->pass->setBindGroup( | |
| groupIndex, | |
| bindGroup->bindGroup.get(), | |
| count != 0 ? dynamicOffsets : nullptr, | |
| count); | |
| if (oreContext != nullptr && !oreContext->lastError().empty()) | |
| { | |
| gpuRejected(vm, oreContext, "setBindGroup"); | |
| return; | |
| } | |
| host->drawBindings.setBindGroup(groupIndex, bindGroup->bindGroup.get()); | |
| } | |
| 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(); | |
| } | |
| host->drawBindings = {}; | |
| } | |
| void gpuPassReleaseImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| releaseHostHandle(vm, 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) || | |
| !validOre(vm, usage, ore::BufferUsage::uniform, "BufferUsage")) | |
| { | |
| 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), desc.immutable}); | |
| } | |
| 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 || host->immutable || dataCount == 0 || | |
| uint64_t(dstOffset) + dataCount > host->buffer->size()) | |
| { | |
| return; | |
| } | |
| host->buffer->update(data, dataCount, dstOffset); | |
| } | |
| void gpuBufferUpdateBlobImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t handle, | |
| uint32_t dstOffset, | |
| uint32_t blob, | |
| uint32_t srcOffset, | |
| uint32_t byteCount) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return; | |
| } | |
| // The lookup holds the blob through the update, which copies the bytes | |
| // into the buffer or the recording before it returns. | |
| auto found = SharedRegistry::find<SharedBlob>(blob, vm->sharedOwner()); | |
| if (found == nullptr || uint64_t(srcOffset) + byteCount > found->size()) | |
| { | |
| return; | |
| } | |
| gpuBufferUpdateImpl( | |
| vm, | |
| handle, | |
| dstOffset, | |
| found->bytes() + srcOffset, | |
| byteCount); | |
| } | |
| uint32_t gpuBufferNewBlobImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t usage, | |
| uint32_t immutable, | |
| uint32_t blob, | |
| uint32_t srcOffset, | |
| uint32_t byteCount) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return 0; | |
| } | |
| // The lookup holds the blob while the create copies its bytes. | |
| auto found = SharedRegistry::find<SharedBlob>(blob, vm->sharedOwner()); | |
| if (found == nullptr || uint64_t(srcOffset) + byteCount > found->size()) | |
| { | |
| return 0; | |
| } | |
| return gpuBufferNewImpl( | |
| vm, | |
| usage, | |
| byteCount, | |
| immutable, | |
| found->bytes() + srcOffset, | |
| byteCount); | |
| } | |
| void gpuBufferReleaseImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| releaseHostHandle(vm, 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); | |
| std::string error; | |
| if (oreContext == nullptr || descByteCount < sizeof(*podDesc) || | |
| !validOre( | |
| vm, | |
| podDesc->format, | |
| ore::TextureFormat::astc8x8, | |
| "TextureFormat") || | |
| !validOre( | |
| vm, | |
| podDesc->textureType, | |
| ore::TextureType::array2D, | |
| "TextureType") || | |
| !guardPassed( | |
| vm, | |
| ore::validateSampleCount(oreContext, podDesc->sampleCount, &error), | |
| error) || | |
| !guardPassed( | |
| vm, | |
| ore::validateTextureFormat( | |
| oreContext, | |
| (ore::TextureFormat)podDesc->format, | |
| &error), | |
| error)) | |
| { | |
| 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) | |
| { | |
| releaseHostHandle(vm, 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) || | |
| !validOre(vm, podDesc->minFilter, ore::Filter::linear, "Filter") || | |
| !validOre(vm, podDesc->magFilter, ore::Filter::linear, "Filter") || | |
| !validOre(vm, podDesc->mipmapFilter, ore::Filter::linear, "Filter") || | |
| !validOre(vm, podDesc->wrapU, ore::WrapMode::clampToEdge, "WrapMode") || | |
| !validOre(vm, podDesc->wrapV, ore::WrapMode::clampToEdge, "WrapMode") || | |
| !validOre(vm, podDesc->wrapW, ore::WrapMode::clampToEdge, "WrapMode") || | |
| (podDesc->compare != 0xFFFFFFFFu && !validOre( | |
| vm, | |
| podDesc->compare, | |
| ore::CompareFunction::always, | |
| "CompareFunction"))) | |
| { | |
| 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; | |
| std::string error; | |
| if (!guardPassed( | |
| vm, | |
| ore::validateSamplerDesc(oreContext, desc, &error), | |
| error)) | |
| { | |
| return 0; | |
| } | |
| 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) | |
| { | |
| releaseHostHandle(vm, 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; | |
| } | |
| if (!validOre( | |
| vm, | |
| podDesc->dimension, | |
| ore::TextureViewDimension::cubeArray, | |
| "TextureViewDimension") || | |
| !validOre( | |
| vm, | |
| podDesc->aspect, | |
| ore::TextureAspect::stencilOnly, | |
| "TextureAspect")) | |
| { | |
| 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) | |
| { | |
| releaseHostHandle( | |
| vm, | |
| 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 mintShaderModule( | |
| WasmScriptingVM* vm, | |
| ore::Context* oreContext, | |
| const ore::ShaderModuleDesc& desc) | |
| { | |
| 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)}); | |
| } | |
| // The module serving entry, built as every lane builds an asset's modules. | |
| static uint32_t mintAssetEntryModule( | |
| WasmScriptingVM* vm, | |
| ore::Context* oreContext, | |
| const ShaderAsset& asset, | |
| uint32_t entry) | |
| { | |
| uint32_t handle = 0; | |
| ore::visitShaderAssetModules( | |
| oreContext->shaderTarget(), | |
| asset, | |
| [&](const ore::ShaderModuleDesc& desc, | |
| uint32_t first, | |
| uint32_t count, | |
| const std::vector<ore::RstbEntryView>&) { | |
| if (entry < first || entry - first >= count) | |
| { | |
| return true; | |
| } | |
| handle = mintShaderModule(vm, oreContext, desc); | |
| return false; | |
| }); | |
| return handle; | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| // The same from a bare RSTB, which only edit time hosts take. | |
| static uint32_t mintRstbEntryModule( | |
| WasmScriptingVM* vm, | |
| ore::Context* oreContext, | |
| const uint8_t* rstb, | |
| uint32_t rstbCount, | |
| uint32_t assetId, | |
| uint32_t entry) | |
| { | |
| ShaderAsset asset; | |
| if (!ore::decodeBareRstb(asset, rstb, rstbCount)) | |
| { | |
| return 0; | |
| } | |
| asset.assetId(assetId); | |
| return mintAssetEntryModule(vm, oreContext, asset, entry); | |
| } | |
| #endif | |
| uint32_t gpuShaderModuleFromAssetImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t objectHandle, | |
| const char* name, | |
| uint32_t nameLength, | |
| uint32_t entry) | |
| { | |
| ore::Context* oreContext = gpuOreContext(vm); | |
| if (oreContext == nullptr) | |
| { | |
| return 0; | |
| } | |
| const ShaderAsset* fileAsset = findFileAsset<ShaderAsset>( | |
| vm, | |
| objectHandle, | |
| name, | |
| nameLength); | |
| #ifdef WITH_RIVE_TOOLS | |
| // Editor path first, matching shader_asset_bytes. | |
| if (const std::vector<uint8_t>* rstb = vm->findShaderRstb( | |
| std::string(name, nameLength))) | |
| { | |
| return mintRstbEntryModule( | |
| vm, | |
| oreContext, | |
| rstb->data(), | |
| (uint32_t)rstb->size(), | |
| fileAsset != nullptr ? fileAsset->assetId() : 0, | |
| entry); | |
| } | |
| #endif | |
| if (fileAsset == nullptr) | |
| { | |
| return 0; | |
| } | |
| return mintAssetEntryModule(vm, oreContext, *fileAsset, entry); | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| uint32_t gpuShaderModuleFromRstbImpl( | |
| WasmScriptingVM* vm, | |
| const uint8_t* rstb, | |
| uint32_t rstbCount, | |
| uint32_t assetId, | |
| uint32_t entry) | |
| { | |
| ore::Context* oreContext = gpuOreContext(vm); | |
| if (oreContext == nullptr) | |
| { | |
| return 0; | |
| } | |
| return mintRstbEntryModule(vm, oreContext, rstb, rstbCount, assetId, entry); | |
| } | |
| #endif | |
| void gpuShaderModuleReleaseImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| releaseHostHandle( | |
| vm, | |
| 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]; | |
| if (!validOre( | |
| vm, | |
| in.kind, | |
| ore::BindingKind::comparisonSampler, | |
| "BindingKind") || | |
| !validOrdinal( | |
| vm, | |
| in.visibility, | |
| in.visibility <= (ore::StageVisibility::kVertex | | |
| ore::StageVisibility::kFragment | | |
| ore::StageVisibility::kCompute), | |
| "StageVisibility") || | |
| !validOre( | |
| vm, | |
| in.textureViewDim, | |
| ore::TextureViewDimension::cubeArray, | |
| "TextureViewDimension") || | |
| !validOre( | |
| vm, | |
| in.textureSampleType, | |
| ore::BindGroupLayoutEntry::SampleType::uint, | |
| "SampleType")) | |
| { | |
| return 0; | |
| } | |
| 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) | |
| { | |
| releaseHostHandle( | |
| vm, | |
| 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 gpuBindGroupLayoutFromEntriesImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t vertexModule, | |
| const char* vertexEntry, | |
| uint32_t vertexEntryLength, | |
| uint32_t fragmentModule, | |
| const char* fragmentEntry, | |
| uint32_t fragmentEntryLength, | |
| uint32_t groupIndex) | |
| { | |
| 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; | |
| } | |
| const std::string vertexName(vertexEntry, vertexEntryLength); | |
| const std::string fragmentName(fragmentEntry, fragmentEntryLength); | |
| const ore::BindingMap map = ore::bindingMapForEntries( | |
| vertex, | |
| vertexName.c_str(), | |
| fragment, | |
| fragmentName.c_str()); | |
| bool bound = false; | |
| for (size_t i = 0; i < map.size() && !bound; ++i) | |
| { | |
| bound = map.at(i).group == groupIndex; | |
| } | |
| if (!bound) | |
| { | |
| return 0; | |
| } | |
| auto layout = ore::makeBindGroupLayoutFromBindingMap( | |
| *oreContext, | |
| map, | |
| groupIndex, | |
| nullptr, | |
| 0, | |
| vertex, | |
| fragment, | |
| vertexName.c_str(), | |
| fragmentName.c_str()); | |
| 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) | |
| { | |
| releaseHostHandle( | |
| vm, | |
| 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)); | |
| if (vertexModule == nullptr) | |
| { | |
| vm->raiseModuleError("GPUPipeline.new: 'vertex' is required"); | |
| return 0; | |
| } | |
| desc.vertexModule = vertexModule->shaderModule.get(); | |
| 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++) | |
| { | |
| const auto& in = colorTargets[i]; | |
| if (!validOre( | |
| vm, | |
| in.format, | |
| ore::TextureFormat::astc8x8, | |
| "TextureFormat") || | |
| !validOre( | |
| vm, | |
| in.srcColor, | |
| ore::BlendFactor::oneMinusBlendColor, | |
| "BlendFactor") || | |
| !validOre( | |
| vm, | |
| in.dstColor, | |
| ore::BlendFactor::oneMinusBlendColor, | |
| "BlendFactor") || | |
| !validOre(vm, in.colorOp, ore::BlendOp::max, "BlendOp") || | |
| !validOre( | |
| vm, | |
| in.srcAlpha, | |
| ore::BlendFactor::oneMinusBlendColor, | |
| "BlendFactor") || | |
| !validOre( | |
| vm, | |
| in.dstAlpha, | |
| ore::BlendFactor::oneMinusBlendColor, | |
| "BlendFactor") || | |
| !validOre(vm, in.alphaOp, ore::BlendOp::max, "BlendOp") || | |
| !validOre(vm, in.writeMask, ore::ColorWriteMask::all, "ColorWriteMask")) | |
| { | |
| return 0; | |
| } | |
| 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++) | |
| { | |
| if (!validOre( | |
| vm, | |
| attributePods[i].format, | |
| ore::VertexFormat::uint32, | |
| "VertexFormat")) | |
| { | |
| return 0; | |
| } | |
| 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 || | |
| !validOre( | |
| vm, | |
| bufferPods[i].stepMode, | |
| ore::VertexStepMode::instance, | |
| "VertexStepMode")) | |
| { | |
| 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; | |
| std::string error; | |
| if (!validOre( | |
| vm, | |
| podDesc->topology, | |
| ore::PrimitiveTopology::triangleStrip, | |
| "PrimitiveTopology") || | |
| !validOre( | |
| vm, | |
| podDesc->indexFormat, | |
| ore::IndexFormat::uint32, | |
| "IndexFormat") || | |
| !validOre(vm, podDesc->cullMode, ore::CullMode::back, "CullMode") || | |
| !validOre( | |
| vm, | |
| podDesc->winding, | |
| ore::FaceWinding::counterClockwise, | |
| "FaceWinding") || | |
| !validOre( | |
| vm, | |
| podDesc->depthFormat, | |
| ore::TextureFormat::astc8x8, | |
| "TextureFormat") || | |
| !validOre( | |
| vm, | |
| podDesc->depthCompare, | |
| ore::CompareFunction::always, | |
| "CompareFunction") || | |
| !validOre( | |
| vm, | |
| podDesc->stencilFrontCompare, | |
| ore::CompareFunction::always, | |
| "CompareFunction") || | |
| !validOre( | |
| vm, | |
| podDesc->stencilFrontFailOp, | |
| ore::StencilOp::decrementWrap, | |
| "StencilOp") || | |
| !validOre( | |
| vm, | |
| podDesc->stencilFrontDepthFailOp, | |
| ore::StencilOp::decrementWrap, | |
| "StencilOp") || | |
| !validOre( | |
| vm, | |
| podDesc->stencilFrontPassOp, | |
| ore::StencilOp::decrementWrap, | |
| "StencilOp") || | |
| !validOre( | |
| vm, | |
| podDesc->stencilBackCompare, | |
| ore::CompareFunction::always, | |
| "CompareFunction") || | |
| !validOre( | |
| vm, | |
| podDesc->stencilBackFailOp, | |
| ore::StencilOp::decrementWrap, | |
| "StencilOp") || | |
| !validOre( | |
| vm, | |
| podDesc->stencilBackDepthFailOp, | |
| ore::StencilOp::decrementWrap, | |
| "StencilOp") || | |
| !validOre( | |
| vm, | |
| podDesc->stencilBackPassOp, | |
| ore::StencilOp::decrementWrap, | |
| "StencilOp") || | |
| !guardPassed( | |
| vm, | |
| ore::validateSampleCount(oreContext, podDesc->sampleCount, &error), | |
| error) || | |
| !guardPassed( | |
| vm, | |
| ore::validateVertexLayout(oreContext, desc, &error), | |
| error)) | |
| { | |
| return 0; | |
| } | |
| 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) | |
| { | |
| releaseHostHandle(vm, 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 uint8_t*, | |
| 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 gpuBufferUpdateBlobImpl( | |
| WasmScriptingVM*, | |
| uint32_t, | |
| uint32_t, | |
| uint32_t, | |
| uint32_t, | |
| uint32_t) | |
| {} | |
| uint32_t gpuBufferNewBlobImpl( | |
| WasmScriptingVM*, | |
| uint32_t, | |
| uint32_t, | |
| uint32_t, | |
| uint32_t, | |
| uint32_t) | |
| { | |
| return 0; | |
| } | |
| 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 gpuShaderModuleFromAssetImpl( | |
| WasmScriptingVM*, | |
| uint32_t, | |
| const char*, | |
| uint32_t, | |
| uint32_t) | |
| { | |
| return 0; | |
| } | |
| #ifdef WITH_RIVE_TOOLS | |
| uint32_t gpuShaderModuleFromRstbImpl( | |
| WasmScriptingVM*, | |
| const uint8_t*, | |
| uint32_t, | |
| uint32_t, | |
| uint32_t) | |
| { | |
| return 0; | |
| } | |
| #endif | |
| 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 gpuBindGroupLayoutFromEntriesImpl( | |
| WasmScriptingVM*, | |
| uint32_t, | |
| const char*, | |
| uint32_t, | |
| uint32_t, | |
| const char*, | |
| 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 | |
| // A renderer the module draws through, with the saves it opened in the scope | |
| // in flight; it may restore only those, never the host's. | |
| struct HostRenderer | |
| { | |
| Renderer* renderer = nullptr; | |
| int saves = 0; | |
| // A trapped call returns with its saves open, and hosts reuse renderers | |
| // frame to frame. | |
| void restoreSaves() | |
| { | |
| for (; saves > 0; saves--) | |
| { | |
| renderer->restore(); | |
| } | |
| } | |
| }; | |
| HostRenderer* resolveHostRenderer(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return static_cast<HostRenderer*>(vm->handles().resolve( | |
| handle, | |
| WasmScriptingVM::HandleTable::Tag::renderer)); | |
| } | |
| #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; | |
| HostRenderer frameRenderer; | |
| 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->frameRenderer = {renderer}; | |
| host->rendererHandle = vm->handles().mint( | |
| WasmScriptingVM::HandleTable::Tag::renderer, | |
| &host->frameRenderer); | |
| 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->frameRenderer.restoreSaves(); | |
| 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; | |
| bool spent = false; | |
| }; | |
| 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 || | |
| !validOrdinal( | |
| vm, | |
| bufferType, | |
| bufferType <= (uint32_t)RenderBufferType::vertex, | |
| "RenderBufferType") || | |
| !validOrdinal( | |
| vm, | |
| flags, | |
| flags <= (uint32_t)RenderBufferFlags::mappedOnceAtInitialization, | |
| "RenderBufferFlags")) | |
| { | |
| 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; | |
| } | |
| if (host->spent) | |
| { | |
| vm->raiseModuleError( | |
| "RenderBuffer: a buffer mapped once at initialization takes one " | |
| "update"); | |
| return; | |
| } | |
| host->spent = enums::is_flag_set( | |
| host->buffer->flags(), | |
| RenderBufferFlags::mappedOnceAtInitialization); | |
| void* data = host->buffer->map(); | |
| if (data != nullptr) | |
| { | |
| memcpy(data, bytes, byteCount); | |
| host->buffer->unmap(); | |
| } | |
| } | |
| void bufferReleaseImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| releaseHostHandle(vm, 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) | |
| { | |
| releaseHostHandle( | |
| vm, | |
| handle, | |
| WasmScriptingVM::HandleTable::Tag::meshInstances); | |
| } | |
| Renderer* resolveRenderer(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| auto host = resolveHostRenderer(vm, handle); | |
| return host != nullptr ? host->renderer : nullptr; | |
| } | |
| 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 host = resolveHostRenderer(vm, handle)) | |
| { | |
| host->renderer->save(); | |
| host->saves++; | |
| } | |
| } | |
| void rendererRestoreImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| auto host = resolveHostRenderer(vm, handle); | |
| if (host == nullptr) | |
| { | |
| return; | |
| } | |
| if (host->saves == 0) | |
| { | |
| vm->raiseModuleError("Renderer save/restore stack was unbalanced by " | |
| "trying to restore more times than saved."); | |
| return; | |
| } | |
| host->renderer->restore(); | |
| host->saves--; | |
| } | |
| 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 || | |
| !validSamplerKey(vm, samplerKey) || !validBlendMode(vm, blend)) | |
| { | |
| 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 || !validSamplerKey(vm, samplerKey) || | |
| !validBlendMode(vm, blend)) | |
| { | |
| 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 || | |
| !validSamplerKey(vm, samplerKey)) | |
| { | |
| 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) | |
| { | |
| vm->reportError(budgetExceededLine((int)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 final : public ViewModelInstanceValueDelegate | |
| { | |
| WasmScriptingVM* vm = nullptr; | |
| uint32_t token = 0; | |
| void valueChanged() override { vm->notifyDataValueChanged(token); } | |
| }; | |
| struct HostInstanceValue | |
| { | |
| // Values and list items point back at their instance raw, so the handle | |
| // keeps it alive. | |
| rcp<ViewModelInstance> owner; | |
| rcp<ViewModelInstanceValue> value; | |
| HostValueDelegate* delegate = nullptr; | |
| // What string_get last handed the module, for string_changed. | |
| std::string sent; | |
| bool hasSent = false; | |
| explicit HostInstanceValue(ViewModelInstanceValue* instanceValue) | |
| : owner(ref_rcp(instanceValue->viewModelInstance())) | |
| , value(ref_rcp(instanceValue)) | |
| {} | |
| ~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) | |
| { | |
| releaseHostHandle( | |
| vm, | |
| handle, | |
| WasmScriptingVM::HandleTable::Tag::viewModelInstance); | |
| } | |
| uint32_t mintInstanceValue( | |
| WasmScriptingVM* vm, | |
| uint32_t vmiHandle, | |
| const char* name, | |
| uint32_t length, | |
| uint16_t typeKey) | |
| { | |
| 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->isTypeOf(typeKey)) | |
| { | |
| return 0; | |
| } | |
| return vm->handles().mint( | |
| WasmScriptingVM::HandleTable::Tag::instanceValue, | |
| new HostInstanceValue(value)); | |
| } | |
| template <typename T> | |
| uint32_t mintInstanceValue( | |
| WasmScriptingVM* vm, | |
| uint32_t vmiHandle, | |
| const char* name, | |
| uint32_t length) | |
| { | |
| return mintInstanceValue(vm, vmiHandle, name, length, T::typeKey); | |
| } | |
| 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) | |
| { | |
| releaseHostHandle( | |
| vm, | |
| handle, | |
| WasmScriptingVM::HandleTable::Tag::instanceValue); | |
| } | |
| // A ref, so the value outlives a native whose change notifies listeners that | |
| // release its last handle. | |
| rcp<ViewModelInstanceValue> resolveInstanceValue( | |
| WasmScriptingVM* vm, | |
| uint32_t handle, | |
| uint16_t typeKey) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| auto host = static_cast<HostInstanceValue*>(vm->handles().resolve( | |
| handle, | |
| WasmScriptingVM::HandleTable::Tag::instanceValue)); | |
| if (host == nullptr || !host->value->isTypeOf(typeKey)) | |
| { | |
| return nullptr; | |
| } | |
| return host->value; | |
| } | |
| template <typename T> | |
| rcp<T> resolveInstanceValue(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| return static_rcp_cast<T>(resolveInstanceValue(vm, handle, T::typeKey)); | |
| } | |
| 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; | |
| } | |
| // The swap notifies, and a listener can release the parent's last handle. | |
| auto parent = ref_rcp(value->parentViewModelInstance()); | |
| parent->replaceViewModelByProperty(value.get(), 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(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) | |
| { | |
| releaseHostHandle(vm, 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); | |
| } | |
| }; | |
| // A ref, so the artboard outlives a native whose script callbacks release it. | |
| rcp<HostArtboard> resolveArtboard(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return ref_rcp(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); | |
| auto renderer = resolveRenderer(vm, rendererHandle); | |
| if (host != nullptr && renderer != nullptr) | |
| { | |
| #ifdef WITH_RIVE_TOOLS | |
| host->artboard->settleIfImagesRevealed(); | |
| #endif | |
| host->artboard->drawInternal(renderer); | |
| } | |
| } | |
| namespace | |
| { | |
| struct WasmDrawVisit | |
| { | |
| WasmScriptingVM* vm; | |
| // Resolved around each visit, which may end the canvas frame behind it. | |
| uint32_t renderer; | |
| 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); | |
| // The host has the artboard's own saves open here, so each visit gets | |
| // a scope of its own and leaves the renderer as it found it. | |
| HostRenderer* host = resolveHostRenderer(visit->vm, visit->renderer); | |
| int outer = host != nullptr ? host->saves : 0; | |
| if (host != nullptr) | |
| { | |
| host->saves = 0; | |
| } | |
| visit->failed = !visit->vm->notifyDrawVisit(handle); | |
| host = resolveHostRenderer(visit->vm, visit->renderer); | |
| if (host != nullptr) | |
| { | |
| host->restoreSaves(); | |
| host->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 = resolveHostRenderer(vm, rendererHandle); | |
| if (host == nullptr || renderer == nullptr) | |
| { | |
| return; | |
| } | |
| WasmDrawVisit visit = {vm, rendererHandle}; | |
| #ifdef WITH_RIVE_TOOLS | |
| host->artboard->settleIfImagesRevealed(); | |
| #endif | |
| host->artboard | |
| ->drawInternal(renderer->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) | |
| { | |
| #ifdef WITH_RIVE_TOOLS | |
| host->artboard->settleIfImagesRevealed(); | |
| #endif | |
| 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; | |
| } | |
| uint32_t artboardScrollEventImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t handle, | |
| uint32_t pointerId, | |
| float x, | |
| float y, | |
| float dx, | |
| float dy, | |
| uint32_t phase, | |
| uint32_t precise, | |
| float timeStamp) | |
| { | |
| auto host = resolveArtboard(vm, handle); | |
| if (host == nullptr || host->stateMachine == nullptr || | |
| phase > (uint32_t)ScrollPhase::inertiaCancel) | |
| { | |
| return 0; | |
| } | |
| ScrollEvent event{Vec2D(dx, dy), (ScrollPhase)phase, precise != 0}; | |
| return (uint32_t)(int)host->stateMachine | |
| ->pointerScroll(Vec2D(x, y), event, timeStamp, (int)pointerId); | |
| } | |
| // 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{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, 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. | |
| releaseHostHandle(vm, 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.startX()), | |
| floatBits(text.lineTop(line)), | |
| floatBits(orderedLine.y()), | |
| floatBits(orderedLine.bottom())}); | |
| } | |
| // A font no run uses any more gives its handle back, or a text relaid | |
| // out with a fresh font variant every frame would keep one per frame. | |
| std::unordered_map<const Font*, uint32_t> fontHandles; | |
| for (const GlyphRun* run : host->runs) | |
| { | |
| auto found = fontHandles.find(run->font.get()); | |
| if (found == fontHandles.end()) | |
| { | |
| auto kept = host->fontHandles.find(run->font.get()); | |
| uint32_t handle; | |
| if (kept != host->fontHandles.end()) | |
| { | |
| handle = kept->second; | |
| host->fontHandles.erase(kept); | |
| } | |
| else | |
| { | |
| handle = mintFont(vm, run->font); | |
| } | |
| found = fontHandles.emplace(run->font.get(), handle).first; | |
| } | |
| host->runWords.insert( | |
| host->runWords.end(), | |
| {found->second, | |
| (uint32_t)(run->level & 1), | |
| floatBits(run->size), | |
| floatBits(run->lineHeight), | |
| floatBits(run->letterSpacing)}); | |
| } | |
| for (auto& pair : host->fontHandles) | |
| { | |
| dataFontReleaseImpl(vm, pair.second); | |
| } | |
| host->fontHandles = std::move(fontHandles); | |
| } | |
| 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) | |
| { | |
| releaseHostHandle(vm, 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, | |
| [](FileAsset* blob) { return !blob->as<BlobAsset>()->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_file_v1 and artboard properties: context:decodeFile --------------- | |
| // | |
| // The Luau lane's decodeFile, RiveFile, BindableArtboard and | |
| // Property<BindableArtboard> (lua_rive_file.cpp, ScriptedPropertyArtboard), | |
| // with the file and its bindables held host side. Like the Luau lane they | |
| // exist only where fetch() does; elsewhere every op answers none. | |
| struct HostRiveFile | |
| { | |
| rcp<File> file; | |
| }; | |
| using HostBindableArtboard = DecodedBindable; | |
| HostRiveFile* resolveRiveFile(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| return vm == nullptr ? nullptr | |
| : static_cast<HostRiveFile*>(vm->handles().resolve( | |
| handle, | |
| WasmScriptingVM::HandleTable::Tag::riveFile)); | |
| } | |
| HostBindableArtboard* resolveBindableArtboard( | |
| WasmScriptingVM* vm, | |
| uint32_t handle) | |
| { | |
| return vm == nullptr | |
| ? nullptr | |
| : static_cast<HostBindableArtboard*>(vm->handles().resolve( | |
| handle, | |
| WasmScriptingVM::HandleTable::Tag::bindableArtboard)); | |
| } | |
| // The VM decoding a file, which the file's own VMs collect with. | |
| thread_local WasmScriptingVM* s_decodingParent = nullptr; | |
| uint32_t fileDecodeImpl( | |
| WasmScriptingVM* vm, | |
| const uint8_t* bytes, | |
| uint32_t byteCount, | |
| uint32_t* statusOut, | |
| uint32_t statusCount) | |
| { | |
| auto status = [&](uint32_t value) { | |
| if (statusCount > 0) | |
| { | |
| statusOut[0] = value; | |
| } | |
| }; | |
| #ifdef WITH_RIVE_SCRIPTNET | |
| if (vm == nullptr || vm->factory() == nullptr) | |
| { | |
| status(FileDecodeWire::statusNoFactory); | |
| return 0; | |
| } | |
| ImportResult result = ImportResult::malformed; | |
| WasmScriptingVM* outer = s_decodingParent; | |
| s_decodingParent = vm; | |
| auto file = byteCount == 0 ? nullptr | |
| : importDecodedFile( | |
| Span<const uint8_t>(bytes, byteCount), | |
| vm->factory(), | |
| &result); | |
| s_decodingParent = outer; | |
| if (file == nullptr) | |
| { | |
| status( | |
| result == ImportResult::unsupportedVersion | |
| ? FileDecodeWire::statusUnsupportedVersion | |
| : FileDecodeWire::statusMalformed); | |
| return 0; | |
| } | |
| status(FileDecodeWire::statusOk); | |
| return vm->handles().mint( | |
| WasmScriptingVM::HandleTable::Tag::riveFile, | |
| new HostRiveFile{std::move(file)}); | |
| #else | |
| (void)vm; | |
| (void)bytes; | |
| (void)byteCount; | |
| status(FileDecodeWire::statusUnavailable); | |
| return 0; | |
| #endif | |
| } | |
| void fileReleaseImpl(WasmScriptingVM* vm, uint32_t file) | |
| { | |
| releaseHostHandle(vm, file, WasmScriptingVM::HandleTable::Tag::riveFile); | |
| } | |
| uint32_t fileArtboardCountImpl(WasmScriptingVM* vm, uint32_t file) | |
| { | |
| auto host = resolveRiveFile(vm, file); | |
| return host == nullptr ? 0 : (uint32_t)host->file->artboardCount(); | |
| } | |
| // One name at a time: names are file data, so no separator is safe. | |
| uint32_t fileArtboardNameImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t file, | |
| uint32_t index, | |
| char* buffer, | |
| uint32_t capacity) | |
| { | |
| auto host = resolveRiveFile(vm, file); | |
| if (host == nullptr || index >= host->file->artboardCount()) | |
| { | |
| return 0; | |
| } | |
| return fillModuleString( | |
| vm, | |
| host->file->artboard(index)->name(), | |
| buffer, | |
| capacity); | |
| } | |
| uint32_t fileBindableImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t file, | |
| const char* name, | |
| uint32_t nameLength, | |
| uint32_t useDefault) | |
| { | |
| auto host = resolveRiveFile(vm, file); | |
| if (host == nullptr) | |
| { | |
| return 0; | |
| } | |
| std::string named(name, nameLength); | |
| DecodedBindable decoded = decodedBindable( | |
| *host->file, | |
| useDefault != 0 ? nullptr : named.c_str()); | |
| if (decoded.artboard == nullptr) | |
| { | |
| return 0; | |
| } | |
| return vm->handles().mint( | |
| WasmScriptingVM::HandleTable::Tag::bindableArtboard, | |
| new HostBindableArtboard(std::move(decoded))); | |
| } | |
| void fileBindableReleaseImpl(WasmScriptingVM* vm, uint32_t bindable) | |
| { | |
| releaseHostHandle( | |
| vm, | |
| bindable, | |
| WasmScriptingVM::HandleTable::Tag::bindableArtboard); | |
| } | |
| uint32_t fileBindableNameImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t bindable, | |
| char* buffer, | |
| uint32_t capacity) | |
| { | |
| auto host = resolveBindableArtboard(vm, bindable); | |
| if (host == nullptr) | |
| { | |
| return 0; | |
| } | |
| return fillModuleString( | |
| vm, | |
| host->artboard->artboard()->name(), | |
| buffer, | |
| capacity); | |
| } | |
| uint32_t fileBindableDataImpl(WasmScriptingVM* vm, uint32_t bindable) | |
| { | |
| auto host = resolveBindableArtboard(vm, bindable); | |
| if (host == nullptr || host->viewModel == nullptr) | |
| { | |
| return 0; | |
| } | |
| return vm->handles().mint( | |
| WasmScriptingVM::HandleTable::Tag::viewModelInstance, | |
| new HostViewModelInstance{host->viewModel}); | |
| } | |
| uint32_t fileBindableEqualImpl(WasmScriptingVM* vm, uint32_t a, uint32_t b) | |
| { | |
| auto first = resolveBindableArtboard(vm, a); | |
| auto second = resolveBindableArtboard(vm, b); | |
| return first != nullptr && second != nullptr && | |
| first->artboard == second->artboard | |
| ? 1 | |
| : 0; | |
| } | |
| uint32_t dataVmiArtboardImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t vmiHandle, | |
| const char* name, | |
| uint32_t length) | |
| { | |
| #ifdef WITH_RIVE_SCRIPTNET | |
| return mintInstanceValue<ViewModelInstanceArtboard>( | |
| vm, | |
| vmiHandle, | |
| name, | |
| length); | |
| #else | |
| (void)vm; | |
| (void)vmiHandle; | |
| (void)name; | |
| (void)length; | |
| return 0; | |
| #endif | |
| } | |
| // Mirrors ScriptedPropertyArtboard::pushValue: nil while the property names | |
| // one of the host file's own artboards. | |
| uint32_t dataArtboardGetImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| auto value = resolveInstanceValue<ViewModelInstanceArtboard>(vm, handle); | |
| if (value == nullptr || value->asset() == nullptr) | |
| { | |
| return 0; | |
| } | |
| return vm->handles().mint( | |
| WasmScriptingVM::HandleTable::Tag::bindableArtboard, | |
| new HostBindableArtboard{ | |
| value->asset(), | |
| value->boundViewModelInstance()}); | |
| } | |
| // Mirrors ScriptedPropertyArtboard::setValue: the artboard's own view model | |
| // goes in first, since asset() notifies and whatever mounts the artboard | |
| // binds it to that instance. | |
| void dataArtboardSetImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t handle, | |
| uint32_t bindable) | |
| { | |
| auto value = resolveInstanceValue<ViewModelInstanceArtboard>(vm, handle); | |
| if (value == nullptr) | |
| { | |
| return; | |
| } | |
| auto host = resolveBindableArtboard(vm, bindable); | |
| value->boundViewModelInstance(host != nullptr ? host->viewModel : nullptr); | |
| value->asset(host != nullptr ? host->artboard : nullptr); | |
| } | |
| // --- rive_net_v1: fetch() -------------------------------------------------- | |
| uint32_t netFetchImpl( | |
| WasmScriptingVM* vm, | |
| const uint8_t* request, | |
| uint32_t requestCount, | |
| uint32_t token) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return 0; | |
| } | |
| return vm->startFetch(Span<const uint8_t>(request, requestCount), token) ? 1 | |
| : 0; | |
| } | |
| void netFetchCancelImpl(WasmScriptingVM* vm, uint32_t token) | |
| { | |
| if (vm != nullptr) | |
| { | |
| vm->cancelFetch(token); | |
| } | |
| } | |
| // --- 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, | |
| [](FileAsset* audio) { | |
| return audio->as<AudioAsset>()->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 | |
| } | |
| // Settles through the main lane's waits with the blob a job decoded. | |
| uint32_t audioSourceToBlobImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t handle, | |
| uint32_t sampleRate, | |
| uint32_t channels) | |
| { | |
| #ifdef WITH_RIVE_AUDIO | |
| auto host = resolveAudioSource(vm, handle); | |
| if (host == nullptr) | |
| { | |
| return 0; | |
| } | |
| return vm->startAudioDecode(host->source, sampleRate, channels); | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| #if RIVE_AUDIO_STREAMS() | |
| bool isSharedId(uint32_t id); | |
| // The fewest chunks a poll decodes per stream, well ahead of a frame's worth. | |
| static constexpr uint32_t kStreamChunksPerPump = 16; | |
| // Leads each stream record. Our std's AudioStreamReader reads it in place, | |
| // and published files carry that reader, so it never changes. | |
| struct StreamRecordHeader | |
| { | |
| // The seeks before it, which the processor drops records older than. | |
| uint32_t seek; | |
| uint32_t flags; | |
| // Its first frame's place in the source, at the stream's rate. | |
| double frame; | |
| }; | |
| static_assert(sizeof(StreamRecordHeader) == 16, "the std reads 16 bytes"); | |
| static constexpr size_t kRecordHeaderFloats = sizeof(StreamRecordHeader) / | |
| sizeof(float); | |
| static constexpr uint32_t kRecordLast = 1; | |
| // A seek dropped the frames before it; a loop's wrap is seamless, so is not. | |
| static constexpr uint32_t kRecordDiscontinuity = 2; | |
| } // namespace | |
| // Keeps a ring a processor drains topped up with records of decoded frames. | |
| struct rive::HostAudioStream | |
| { | |
| rcp<AudioReader> reader; | |
| rcp<SharedRing> ring; | |
| uint32_t ringId = 0; | |
| rcp<SharedSignal> seeks; | |
| uint32_t seeksId = 0; | |
| uint64_t owner = 0; | |
| // 0 once released, after which it decodes on while a reader holds the ring. | |
| uint32_t handle = 0; | |
| uint32_t chunkFrames = 0; | |
| bool discontinuity = false; | |
| double position = 0; | |
| // The record in chunk that has not fit the ring yet, 0 when none. | |
| uint32_t queuedBytes = 0; | |
| // The ring's bytes after the last top up, so a poll sees what was played. | |
| uint32_t usedAfterFill = 0; | |
| uint32_t fullRecordBytes = 0; | |
| bool queuedLast = false; | |
| bool loop = false; | |
| bool ended = false; | |
| // A header, then chunkFrames frames. | |
| std::vector<float> chunk; | |
| ~HostAudioStream() | |
| { | |
| SharedRegistry::release(ringId, owner); | |
| SharedRegistry::release(seeksId, owner); | |
| } | |
| void fill(uint32_t maxChunks) | |
| { | |
| for (uint32_t chunks = 0; !ended && chunks < maxChunks; chunks++) | |
| { | |
| if (queuedBytes == 0) | |
| { | |
| decodeRecord(); | |
| } | |
| if (!ring->write( | |
| Span<const uint8_t>((const uint8_t*)chunk.data(), queuedBytes))) | |
| { | |
| break; | |
| } | |
| queuedBytes = 0; | |
| ended = queuedLast; | |
| } | |
| usedAfterFill = ring->used(); | |
| } | |
| // Twice what was played since the last poll, so a processor reading | |
| // faster than real time keeps up. | |
| void pump() | |
| { | |
| uint32_t used = ring->used(); | |
| uint32_t played = usedAfterFill > used ? usedAfterFill - used : 0; | |
| fill(std::max(kStreamChunksPerPump, 2 * (played / fullRecordBytes) + 1)); | |
| } | |
| void decodeRecord() | |
| { | |
| float* samples = chunk.data() + kRecordHeaderFloats; | |
| uint64_t read = 0; | |
| bool decoded = reader->readInto(samples, chunkFrames, read); | |
| // Rewound once a pass, so a source with no frames ends rather than spins. | |
| if (decoded && read == 0 && loop && reader->rewind()) | |
| { | |
| position = 0; | |
| decoded = reader->readInto(samples, chunkFrames, read); | |
| } | |
| queuedLast = !decoded || read == 0; | |
| // The signal's own count, so a notify the host did not make drops nothing. | |
| StreamRecordHeader header = { | |
| (uint32_t)seeks->seq(), | |
| (discontinuity ? kRecordDiscontinuity : 0) | | |
| (queuedLast ? kRecordLast : 0), | |
| position}; | |
| memcpy(chunk.data(), &header, sizeof(header)); | |
| discontinuity = false; | |
| position += (double)read; | |
| queuedBytes = (uint32_t)(sizeof(header) + | |
| read * reader->channels() * sizeof(float)); | |
| } | |
| // Drops what the processor has not played, so it hears frame next. | |
| bool restart(uint64_t frame) | |
| { | |
| uint64_t landed = 0; | |
| if (!reader->seek(frame, &landed)) | |
| { | |
| return false; | |
| } | |
| // Before the drain, so the processor drops what it already took too. | |
| seeks->notify(); | |
| uint32_t capacity = (uint32_t)(chunk.size() * sizeof(float)); | |
| while (ring->read((uint8_t*)chunk.data(), capacity) != 0) | |
| { | |
| } | |
| queuedBytes = 0; | |
| ended = false; | |
| position = (double)landed; | |
| discontinuity = true; | |
| fill(kStreamChunksPerPump); | |
| return true; | |
| } | |
| // Answers whether its first records are queued. | |
| bool ready() { return ended || ring->used() != 0; } | |
| }; | |
| namespace | |
| { | |
| HostAudioStream* resolveAudioStream(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return nullptr; | |
| } | |
| return static_cast<HostAudioStream*>(vm->handles().resolve( | |
| handle, | |
| WasmScriptingVM::HandleTable::Tag::audioStream)); | |
| } | |
| void pumpAudioStreams(WasmScriptingVM* vm) | |
| { | |
| auto& streams = vm->audioStreams(); | |
| for (auto it = streams.begin(); it != streams.end();) | |
| { | |
| HostAudioStream* stream = *it; | |
| if (stream->handle == 0 && | |
| (stream->ended || | |
| SharedRegistry::holds(stream->ringId, stream->owner) <= 1)) | |
| { | |
| delete stream; | |
| it = streams.erase(it); | |
| continue; | |
| } | |
| stream->pump(); | |
| ++it; | |
| } | |
| } | |
| // Another hold on id for the caller, which answers it; else 0. | |
| uint32_t retainedId(uint32_t id, uint64_t owner) | |
| { | |
| return SharedRegistry::retain(id, owner) ? id : 0; | |
| } | |
| #endif | |
| uint32_t audioStreamImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t source, | |
| uint32_t sampleRate, | |
| uint32_t channels, | |
| uint32_t chunkFrames, | |
| uint32_t bufferFrames) | |
| { | |
| #if RIVE_AUDIO_STREAMS() | |
| auto host = resolveAudioSource(vm, source); | |
| if (host == nullptr || chunkFrames == 0) | |
| { | |
| return 0; | |
| } | |
| auto reader = host->source->makeReader(channels, sampleRate); | |
| if (reader == nullptr) | |
| { | |
| return 0; | |
| } | |
| uint64_t chunkBytes = sizeof(StreamRecordHeader) + | |
| (uint64_t)chunkFrames * reader->channels() * 4; | |
| if (chunkBytes > shared_layout::kSharedMaxBytes) | |
| { | |
| return 0; | |
| } | |
| uint64_t records = std::max<uint64_t>( | |
| 1, | |
| ((uint64_t)bufferFrames + chunkFrames - 1) / chunkFrames); | |
| uint64_t recordBytes = SharedRing::recordBytes((size_t)chunkBytes, 0); | |
| uint64_t capacity = records * recordBytes; | |
| if (capacity > shared_layout::kSharedMaxBytes) | |
| { | |
| return 0; | |
| } | |
| uint32_t ringId = sharedRingCreateImpl(vm, (uint32_t)capacity); | |
| if (!isSharedId(ringId)) | |
| { | |
| return 0; | |
| } | |
| uint64_t owner = vm->sharedOwner(); | |
| uint32_t seeksId = sharedSignalCreateImpl(vm); | |
| if (!isSharedId(seeksId)) | |
| { | |
| SharedRegistry::release(ringId, owner); | |
| return 0; | |
| } | |
| auto stream = new HostAudioStream(); | |
| stream->owner = owner; | |
| stream->ring = SharedRegistry::find<SharedRing>(ringId, owner); | |
| stream->ringId = ringId; | |
| stream->seeks = SharedRegistry::find<SharedSignal>(seeksId, owner); | |
| stream->seeksId = seeksId; | |
| stream->chunkFrames = chunkFrames; | |
| stream->fullRecordBytes = (uint32_t)recordBytes; | |
| stream->chunk.resize( | |
| kRecordHeaderFloats + (size_t)chunkFrames * reader->channels()); | |
| stream->reader = std::move(reader); | |
| stream->fill(kStreamChunksPerPump); | |
| stream->handle = vm->handles().mint( | |
| WasmScriptingVM::HandleTable::Tag::audioStream, | |
| stream); | |
| vm->audioStreams().push_back(stream); | |
| vm->watchLanes(); | |
| return vm->watchStream(stream->handle); | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| uint32_t audioStreamRingImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| #if RIVE_AUDIO_STREAMS() | |
| auto stream = resolveAudioStream(vm, handle); | |
| return stream != nullptr ? retainedId(stream->ringId, stream->owner) : 0; | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| uint32_t audioStreamSeeksImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| #if RIVE_AUDIO_STREAMS() | |
| auto stream = resolveAudioStream(vm, handle); | |
| return stream != nullptr ? retainedId(stream->seeksId, stream->owner) : 0; | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| uint32_t audioStreamChannelsImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| #if RIVE_AUDIO_STREAMS() | |
| auto stream = resolveAudioStream(vm, handle); | |
| return stream != nullptr ? stream->reader->channels() : 0; | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| uint32_t audioStreamSampleRateImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| #if RIVE_AUDIO_STREAMS() | |
| auto stream = resolveAudioStream(vm, handle); | |
| return stream != nullptr ? stream->reader->sampleRate() : 0; | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| uint32_t audioStreamSeekFrameImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t handle, | |
| double frame) | |
| { | |
| #if RIVE_AUDIO_STREAMS() | |
| auto stream = resolveAudioStream(vm, handle); | |
| return stream != nullptr && | |
| stream->restart( | |
| frame > 0 ? (uint64_t)std::min(frame, 1.8e19) : 0) | |
| ? 1 | |
| : 0; | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| void audioStreamSetLoopImpl(WasmScriptingVM* vm, uint32_t handle, uint32_t loop) | |
| { | |
| #if RIVE_AUDIO_STREAMS() | |
| auto stream = resolveAudioStream(vm, handle); | |
| if (stream == nullptr) | |
| { | |
| return; | |
| } | |
| stream->loop = loop != 0; | |
| if (!stream->loop) | |
| { | |
| return; | |
| } | |
| // A stream that ran out starts over, after what is queued. | |
| if (stream->ended && stream->reader->rewind()) | |
| { | |
| stream->ended = false; | |
| stream->position = 0; | |
| stream->fill(kStreamChunksPerPump); | |
| } | |
| // Its last record waits for room, so the next decode loops instead. | |
| else if ( | |
| stream->queuedLast && stream->queuedBytes == sizeof(StreamRecordHeader)) | |
| { | |
| stream->queuedBytes = 0; | |
| } | |
| #endif | |
| } | |
| uint32_t audioStreamEndedImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| #if RIVE_AUDIO_STREAMS() | |
| auto stream = resolveAudioStream(vm, handle); | |
| return stream != nullptr && stream->ended ? 1 : 0; | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| void audioStreamReleaseImpl(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| #if RIVE_AUDIO_STREAMS() | |
| releaseHostHandle(vm, handle, WasmScriptingVM::HandleTable::Tag::audioStream); | |
| #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) | |
| { | |
| uint64_t frame; | |
| uint32_t rate; | |
| if (WasmProcessors::offlineClock(frame, rate)) | |
| { | |
| return (float)((double)frame / rate); | |
| } | |
| #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) | |
| { | |
| uint64_t frame; | |
| uint32_t rate; | |
| if (WasmProcessors::offlineClock(frame, rate)) | |
| { | |
| return (double)frame; | |
| } | |
| #ifdef WITH_RIVE_AUDIO | |
| auto engine = AudioEngine::RuntimeEngine(true); | |
| return engine != nullptr ? (double)engine->timeInFrames() : 0.0; | |
| #else | |
| return 0.0; | |
| #endif | |
| } | |
| uint32_t audioProcessorSampleRateImpl(WasmScriptingVM* vm) | |
| { | |
| return WasmProcessors::format().sampleRate; | |
| } | |
| uint32_t audioProcessorChannelsImpl(WasmScriptingVM* vm) | |
| { | |
| return WasmProcessors::format().channels; | |
| } | |
| uint32_t audioSampleRateImpl(WasmScriptingVM* vm) | |
| { | |
| uint64_t frame; | |
| uint32_t rate; | |
| if (WasmProcessors::offlineClock(frame, rate)) | |
| { | |
| return rate; | |
| } | |
| #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; | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| bool initRuntime() | |
| { | |
| 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 env, | |
| void*) { | |
| const char* why = reason == MAX_SIZE_REACHED | |
| ? "the module hit its wasmMaxPages ceiling" | |
| : "the device could not provide the memory"; | |
| #ifdef RIVE_WASM_PARALLEL | |
| if (WasmParallel::runningParts()) | |
| { | |
| why = "the memory cannot grow during a parallel call"; | |
| } | |
| #endif | |
| reportScriptError( | |
| env != nullptr ? bootVmFromEnv(env) : nullptr, | |
| "wasm memory: growing " + std::to_string(currentBytes >> 20) + | |
| " MB by " + std::to_string(incPages) + " pages failed: " + why); | |
| }, | |
| 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; | |
| } | |
| registerLaneNatives(); | |
| registerJobNatives(); | |
| registerWorkerNatives(); | |
| return true; | |
| } | |
| bool ensureRuntime() | |
| { | |
| // Hosts boot on more than one thread, so a boot waits out another's init. | |
| static bool ok = initRuntime(); | |
| return ok; | |
| } | |
| #endif | |
| // --- rive_shared_v1: memory every lane shares ----------------------------- | |
| using shared_layout::kSharedMaxBytes; | |
| // The lane a native runs on, null on the main lane. | |
| WasmLane* callingLane(WasmScriptingVM* vm) | |
| { | |
| return vm == nullptr ? WasmLane::current() : nullptr; | |
| } | |
| WasmLane* callingProcessor(WasmScriptingVM* vm) | |
| { | |
| #if RIVE_PROCESSOR_AUDIO() | |
| WasmLane* lane = callingLane(vm); | |
| return lane != nullptr && lane->kind() == LaneKind::processor ? lane | |
| : nullptr; | |
| #else | |
| return nullptr; | |
| #endif | |
| } | |
| // Whose holds a call takes and gives back: the VM's, or its lane's. | |
| uint64_t sharedHolder(WasmScriptingVM* vm) | |
| { | |
| WasmLane* lane = callingLane(vm); | |
| return vm != nullptr ? vm->sharedOwner() | |
| : lane != nullptr ? lane->sharedOwner() | |
| : 0; | |
| } | |
| template <typename T> rcp<T> sharedFind(WasmScriptingVM* vm, uint32_t id) | |
| { | |
| return SharedRegistry::find<T>(id, sharedHolder(vm)); | |
| } | |
| bool isSharedId(uint32_t id) { return (id & shared_layout::kIdBit) != 0; } | |
| // The creator, charged size; else 0 with the create's answer in refusal, as | |
| // a processor never may create and a job makes only blobs. | |
| uint64_t sharedCreator( | |
| WasmScriptingVM* vm, | |
| uint64_t size, | |
| bool allowEmpty, | |
| bool blob, | |
| uint32_t& refusal) | |
| { | |
| WasmLane* lane = callingLane(vm); | |
| bool may = lane == nullptr || lane->kind() == LaneKind::worker || | |
| (blob && lane->kind() == LaneKind::job); | |
| uint64_t owner = may ? sharedHolder(vm) : 0; | |
| refusal = shared_layout::kCreateRefused; | |
| if (owner != 0 && ((size == 0 && !allowEmpty) || size > kSharedMaxBytes)) | |
| { | |
| refusal = shared_layout::kCreateBadSize; | |
| owner = 0; | |
| } | |
| if (owner != 0 && !SharedRegistry::charge(owner, size)) | |
| { | |
| refusal = shared_layout::kCreateNoMemory; | |
| owner = 0; | |
| } | |
| return owner; | |
| } | |
| // What a create answers for object, made for owner with size charged: its | |
| // id once in the registry, else why not, its charge refunded. | |
| uint32_t sharedCreated( | |
| WasmScriptingVM* vm, | |
| uint64_t owner, | |
| uint64_t size, | |
| rcp<SharedObject> object) | |
| { | |
| if (!object->made()) | |
| { | |
| SharedRegistry::refund(owner, size); | |
| return shared_layout::kCreateNoMemory; | |
| } | |
| object->charge = size; | |
| uint32_t id = SharedRegistry::add(std::move(object), owner); | |
| if (!isSharedId(id)) | |
| { | |
| SharedRegistry::refund(owner, size); | |
| return id; | |
| } | |
| if (vm == nullptr) | |
| { | |
| WasmLane::current()->addCreated(id); | |
| } | |
| return id; | |
| } | |
| // Whether a misuse warns: once per object, never on a processor, which may | |
| // be on the audio thread, so the text is built only for a warning. | |
| bool sharedWarns(WasmScriptingVM* vm, SharedObject& object) | |
| { | |
| WasmLane* lane = callingLane(vm); | |
| return (vm != nullptr || | |
| (lane != nullptr && lane->kind() != LaneKind::processor)) && | |
| object.firstWarning(); | |
| } | |
| #if defined(__GNUC__) || defined(__clang__) | |
| __attribute__((format(printf, 2, 3))) | |
| #endif | |
| void sharedWarn(WasmScriptingVM* vm, const char* format, ...) | |
| { | |
| char text[256]; | |
| va_list args; | |
| va_start(args, format); | |
| vsnprintf(text, sizeof(text), format, args); | |
| va_end(args); | |
| if (vm != nullptr) | |
| { | |
| rtLogImpl(vm, 0, text, (uint32_t)strlen(text)); | |
| } | |
| else | |
| { | |
| callingLane(vm)->log(text); | |
| } | |
| } | |
| // A processor may be on the audio thread, where taking or giving back a hold | |
| // could lock or free, so a value naming shared objects fails it instead. | |
| using lane_layout::kProcessorHolds; | |
| uint32_t processorRead(WasmLane* processor, uint32_t got) | |
| { | |
| if (got != SharedObject::kCarriesHolds) | |
| { | |
| return got; | |
| } | |
| processor->fail(kProcessorHolds); | |
| return 0; | |
| } | |
| uint32_t sharedRegionCreateImpl(WasmScriptingVM* vm, uint32_t size) | |
| { | |
| uint32_t refusal; | |
| uint64_t owner = sharedCreator(vm, size, false, false, refusal); | |
| if (owner == 0) | |
| { | |
| return refusal; | |
| } | |
| return sharedCreated(vm, owner, size, make_rcp<SharedRegion>(size)); | |
| } | |
| uint32_t sharedRegionSizeImpl(WasmScriptingVM* vm, uint32_t region) | |
| { | |
| auto found = sharedFind<SharedRegion>(vm, region); | |
| return found != nullptr ? found->size() : 0; | |
| } | |
| uint32_t sharedRegionLockImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t region, | |
| double timeoutSeconds) | |
| { | |
| auto found = sharedFind<SharedRegion>(vm, region); | |
| if (found == nullptr) | |
| { | |
| return shared_layout::kRegionLockGone; | |
| } | |
| // One region at a time keeps lock order acyclic. | |
| if (sharedHeldRegion() != 0) | |
| { | |
| return shared_layout::kRegionLockNested; | |
| } | |
| bool locked = false; | |
| { | |
| WasmScriptingVM::WaitScope wait(vm); | |
| // A lock begun in the time kept for the script gets nothing back, so | |
| // it spins for half of what is left, as a timed wait may oversleep. | |
| if (wait.usable() <= 0) | |
| { | |
| Clock::time_point until = deadlineAfter( | |
| std::min(timeoutSeconds, callSecondsLeft() / 2)); | |
| spinUntil([&]() { | |
| locked = found->lock(0); | |
| return locked || Clock::now() >= until; | |
| }); | |
| } | |
| else | |
| { | |
| locked = found->lock(timeoutSeconds); | |
| } | |
| } | |
| if (!locked) | |
| { | |
| return shared_layout::kRegionLockTimedOut; | |
| } | |
| setSharedHeldRegion(region, std::move(found)); | |
| return shared_layout::kRegionLockAcquired; | |
| } | |
| void sharedRegionUnlockImpl(WasmScriptingVM* vm, uint32_t region) | |
| { | |
| if (SharedRegion* held = sharedHeld(region)) | |
| { | |
| held->unlock(); | |
| setSharedHeldRegion(0, nullptr); | |
| } | |
| } | |
| uint32_t sharedRegionReadImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t region, | |
| uint32_t offset, | |
| uint8_t* out, | |
| uint32_t outByteCount) | |
| { | |
| SharedRegion* found = sharedHeld(region); | |
| if (found == nullptr || (uint64_t)offset + outByteCount > found->size()) | |
| { | |
| return 0; | |
| } | |
| if (outByteCount != 0) | |
| { | |
| memcpy(out, found->bytes() + offset, outByteCount); | |
| } | |
| return 1; | |
| } | |
| uint32_t sharedRegionWriteImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t region, | |
| uint32_t offset, | |
| const uint8_t* data, | |
| uint32_t dataByteCount) | |
| { | |
| SharedRegion* found = sharedHeld(region); | |
| if (found == nullptr || (uint64_t)offset + dataByteCount > found->size()) | |
| { | |
| return 0; | |
| } | |
| if (dataByteCount != 0) | |
| { | |
| memcpy(found->bytes() + offset, data, dataByteCount); | |
| } | |
| return 1; | |
| } | |
| uint32_t sharedRegionPoisonedImpl(WasmScriptingVM* vm, uint32_t region) | |
| { | |
| auto found = sharedFind<SharedRegion>(vm, region); | |
| return found != nullptr && found->poisoned() ? 1 : 0; | |
| } | |
| // -1 for a word outside the region or a region gone, which the std throws | |
| // for. | |
| template <typename Use> | |
| double sharedAtomic( | |
| WasmScriptingVM* vm, | |
| uint32_t region, | |
| uint32_t offset, | |
| Use use) | |
| { | |
| SharedRegion* found = sharedRecentRegion(region, sharedHolder(vm)); | |
| std::atomic<uint32_t>* word = found != nullptr ? found->word(offset) | |
| : nullptr; | |
| return word != nullptr ? (double)use(*word) : -1.0; | |
| } | |
| double sharedAtomicLoadImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t region, | |
| uint32_t offset) | |
| { | |
| return sharedAtomic(vm, region, offset, [](std::atomic<uint32_t>& word) { | |
| return word.load(); | |
| }); | |
| } | |
| double sharedAtomicStoreImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t region, | |
| uint32_t offset, | |
| uint32_t value) | |
| { | |
| return sharedAtomic(vm, region, offset, [value](std::atomic<uint32_t>& word) { | |
| word.store(value); | |
| return 0u; | |
| }); | |
| } | |
| double sharedAtomicAddImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t region, | |
| uint32_t offset, | |
| uint32_t value) | |
| { | |
| return sharedAtomic(vm, region, offset, [value](std::atomic<uint32_t>& word) { | |
| return word.fetch_add(value); | |
| }); | |
| } | |
| double sharedAtomicCasImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t region, | |
| uint32_t offset, | |
| uint32_t expected, | |
| uint32_t desired) | |
| { | |
| return sharedAtomic( | |
| vm, | |
| region, | |
| offset, | |
| [expected, desired](std::atomic<uint32_t>& word) { | |
| uint32_t held = expected; | |
| compareExchangeStrong(word, held, desired); | |
| return held; | |
| }); | |
| } | |
| // The holds a value in flight carries, retained here for it; an id already | |
| // gone is left out. | |
| #if RIVE_ONE_THREAD() | |
| [[clang::no_destroy]] std::vector<uint32_t> t_carried; | |
| #else | |
| thread_local std::vector<uint32_t> t_carried; | |
| #endif | |
| Span<const uint32_t> retainCarried(uint64_t owner, Span<const uint32_t> ids) | |
| { | |
| t_carried.clear(); | |
| for (uint32_t id : ids) | |
| { | |
| if (SharedRegistry::retain(id, owner)) | |
| { | |
| t_carried.push_back(id); | |
| } | |
| } | |
| return t_carried; | |
| } | |
| void releaseAll(uint64_t owner, Span<const uint32_t> ids) | |
| { | |
| for (uint32_t id : ids) | |
| { | |
| SharedRegistry::release(id, owner); | |
| } | |
| } | |
| // The main lane keeps a read's holds while its std decodes; a lane keeps | |
| // each until it ends or releases it. | |
| void handHolds(WasmScriptingVM* vm, Span<const uint32_t> ids) | |
| { | |
| if (ids.empty()) | |
| { | |
| return; | |
| } | |
| if (vm != nullptr) | |
| { | |
| vm->keepReadHolds(ids); | |
| } | |
| else if (WasmLane* lane = callingLane(vm)) | |
| { | |
| lane->adoptShared(ids); | |
| } | |
| } | |
| // What a read takes holds into before handing them over. | |
| #if RIVE_ONE_THREAD() | |
| [[clang::no_destroy]] std::vector<uint32_t> t_taken; | |
| #else | |
| thread_local std::vector<uint32_t> t_taken; | |
| #endif | |
| uint32_t sharedRingCreateImpl(WasmScriptingVM* vm, uint32_t capacity) | |
| { | |
| uint32_t refusal; | |
| uint64_t owner = sharedCreator(vm, capacity, false, false, refusal); | |
| if (owner == 0) | |
| { | |
| return refusal; | |
| } | |
| auto ring = make_rcp<SharedRing>(capacity); | |
| if (!ring->made()) | |
| { | |
| return sharedCreated(vm, owner, capacity, ring); | |
| } | |
| uint32_t data = SharedRegistry::add(ring->dataSignal(), owner); | |
| uint32_t space = SharedRegistry::add(ring->spaceSignal(), owner); | |
| ring->setSignalIds(data, space); | |
| // A signal that failed answers why. | |
| uint32_t id = isSharedId(data) ? space : data; | |
| if (isSharedId(id)) | |
| { | |
| id = sharedCreated(vm, owner, capacity, ring); | |
| } | |
| else | |
| { | |
| SharedRegistry::refund(owner, capacity); | |
| } | |
| if (!isSharedId(id)) | |
| { | |
| SharedRegistry::release(data, owner); | |
| SharedRegistry::release(space, owner); | |
| } | |
| return id; | |
| } | |
| uint32_t sharedRingWriteImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t ring, | |
| const uint8_t* data, | |
| uint32_t dataByteCount, | |
| const uint32_t* holds, | |
| uint32_t holdCount) | |
| { | |
| auto found = sharedFind<SharedRing>(vm, ring); | |
| if (found == nullptr) | |
| { | |
| return shared_layout::kRingWriteFull; | |
| } | |
| if (WasmLane* processor = callingProcessor(vm); | |
| processor != nullptr && holdCount != 0) | |
| { | |
| processor->fail(kProcessorHolds); | |
| return shared_layout::kRingWriteFull; | |
| } | |
| uint64_t owner = sharedHolder(vm); | |
| auto carried = retainCarried(owner, Span<const uint32_t>(holds, holdCount)); | |
| if (found->write(Span<const uint8_t>(data, dataByteCount), carried)) | |
| { | |
| return shared_layout::kRingWriteWritten; | |
| } | |
| releaseAll(owner, carried); | |
| uint64_t bytes = SharedRing::recordBytes(dataByteCount, holdCount); | |
| uint32_t capacity = found->block()->capacity; | |
| if (bytes > capacity) | |
| { | |
| if (sharedWarns(vm, *found)) | |
| { | |
| sharedWarn( | |
| vm, | |
| "Ring.send, Ring.writeF32 or Port.post: a %u byte record and its " | |
| "%llu %s never fit this %u byte ring; create it larger, or send " | |
| "large data as a Blob, which crosses by id", | |
| dataByteCount, | |
| (unsigned long long)(bytes - dataByteCount), | |
| holdCount == 0 ? "byte length" : "bytes of length and holds", | |
| capacity); | |
| } | |
| return shared_layout::kRingWriteNeverFits; | |
| } | |
| return shared_layout::kRingWriteFull; | |
| } | |
| uint32_t sharedRingReadImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t ring, | |
| uint8_t* out, | |
| uint32_t outByteCount) | |
| { | |
| auto found = sharedFind<SharedRing>(vm, ring); | |
| if (found == nullptr) | |
| { | |
| return 0; | |
| } | |
| if (WasmLane* processor = callingProcessor(vm)) | |
| { | |
| return processorRead(processor, found->read(out, outByteCount, nullptr)); | |
| } | |
| t_taken.clear(); | |
| uint32_t got = found->read(out, outByteCount, &t_taken); | |
| handHolds(vm, t_taken); | |
| return got; | |
| } | |
| uint32_t sharedRingDropImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t ring, | |
| uint32_t capacity) | |
| { | |
| auto found = sharedFind<SharedRing>(vm, ring); | |
| if (found == nullptr) | |
| { | |
| return 0; | |
| } | |
| if (WasmLane* processor = callingProcessor(vm)) | |
| { | |
| return processorRead(processor, found->drop(capacity)); | |
| } | |
| t_taken.clear(); | |
| uint32_t dropped = found->drop(capacity, &t_taken); | |
| releaseAll(sharedHolder(vm), t_taken); | |
| return dropped; | |
| } | |
| uint32_t sharedRingUsedImpl(WasmScriptingVM* vm, uint32_t ring) | |
| { | |
| auto found = sharedFind<SharedRing>(vm, ring); | |
| return found != nullptr ? found->used() : 0; | |
| } | |
| uint32_t sharedRingSignalImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t ring, | |
| uint32_t space) | |
| { | |
| auto found = sharedFind<SharedRing>(vm, ring); | |
| if (found == nullptr) | |
| { | |
| return 0; | |
| } | |
| return space != 0 ? found->spaceSignalId() : found->dataSignalId(); | |
| } | |
| uint32_t sharedSnapshotCreateImpl(WasmScriptingVM* vm, uint32_t capacity) | |
| { | |
| uint32_t refusal; | |
| uint64_t owner = sharedCreator( | |
| vm, | |
| (uint64_t)capacity * SharedSnapshot::kCopies, | |
| true, | |
| false, | |
| refusal); | |
| if (owner == 0) | |
| { | |
| return refusal; | |
| } | |
| return sharedCreated( | |
| vm, | |
| owner, | |
| (uint64_t)capacity * SharedSnapshot::kCopies, | |
| make_rcp<SharedSnapshot>(capacity)); | |
| } | |
| uint32_t sharedSnapshotPublishImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t snapshot, | |
| const uint8_t* data, | |
| uint32_t dataByteCount, | |
| const uint32_t* holds, | |
| uint32_t holdCount) | |
| { | |
| auto found = sharedFind<SharedSnapshot>(vm, snapshot); | |
| if (found == nullptr) | |
| { | |
| return 0; | |
| } | |
| Span<const uint8_t> value(data, dataByteCount); | |
| if (WasmLane* processor = callingProcessor(vm)) | |
| { | |
| if (holdCount == 0 && found->publish(value)) | |
| { | |
| return 1; | |
| } | |
| if (holdCount != 0 || dataByteCount <= found->block()->capacity) | |
| { | |
| processor->fail(kProcessorHolds); | |
| } | |
| return 0; | |
| } | |
| uint64_t owner = sharedHolder(vm); | |
| auto carried = retainCarried(owner, Span<const uint32_t>(holds, holdCount)); | |
| t_taken.clear(); | |
| if (found->publish(value, carried, &t_taken)) | |
| { | |
| releaseAll(owner, t_taken); | |
| return 1; | |
| } | |
| releaseAll(owner, carried); | |
| if (sharedWarns(vm, *found)) | |
| { | |
| sharedWarn( | |
| vm, | |
| "Snapshot.publish: a %u byte value does not fit this %u byte " | |
| "snapshot, so readers keep the last value that fit; create the " | |
| "snapshot larger, or send large data as a Blob, which crosses by id", | |
| dataByteCount, | |
| found->block()->capacity); | |
| } | |
| return 0; | |
| } | |
| uint32_t sharedSnapshotReadImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t snapshot, | |
| uint8_t* out, | |
| uint32_t outByteCount) | |
| { | |
| auto found = sharedFind<SharedSnapshot>(vm, snapshot); | |
| if (found == nullptr) | |
| { | |
| return 0; | |
| } | |
| if (WasmLane* processor = callingProcessor(vm)) | |
| { | |
| return processorRead(processor, found->read(out, outByteCount, nullptr)); | |
| } | |
| uint64_t owner = sharedHolder(vm); | |
| // A publish gives back the holds of the value it replaced once seq moves, | |
| // so holds taken before seq moved are on live objects. | |
| for (;;) | |
| { | |
| uint64_t seq = found->seq(); | |
| t_taken.clear(); | |
| uint32_t got = found->read(out, outByteCount, &t_taken); | |
| auto taken = retainCarried(owner, t_taken); | |
| if (t_taken.empty() || found->seq() == seq) | |
| { | |
| handHolds(vm, taken); | |
| return got; | |
| } | |
| releaseAll(owner, taken); | |
| } | |
| } | |
| double sharedSnapshotSeqImpl(WasmScriptingVM* vm, uint32_t snapshot) | |
| { | |
| auto found = sharedFind<SharedSnapshot>(vm, snapshot); | |
| return found != nullptr ? (double)found->seq() : 0.0; | |
| } | |
| uint32_t sharedSignalCreateImpl(WasmScriptingVM* vm) | |
| { | |
| uint32_t refusal; | |
| uint64_t owner = sharedCreator(vm, 0, true, false, refusal); | |
| if (owner == 0) | |
| { | |
| return refusal; | |
| } | |
| return sharedCreated(vm, owner, 0, make_rcp<SharedSignal>()); | |
| } | |
| void sharedSignalNotifyImpl(WasmScriptingVM* vm, uint32_t signal) | |
| { | |
| if (auto found = sharedFind<SharedSignal>(vm, signal)) | |
| { | |
| found->notify(); | |
| } | |
| } | |
| double sharedSignalSeqImpl(WasmScriptingVM* vm, uint32_t signal) | |
| { | |
| auto found = sharedFind<SharedSignal>(vm, signal); | |
| return found != nullptr ? (double)found->seq() : 0.0; | |
| } | |
| uint32_t sharedBlobCreateImpl( | |
| WasmScriptingVM* vm, | |
| const uint8_t* data, | |
| uint32_t dataByteCount) | |
| { | |
| uint32_t refusal; | |
| uint64_t owner = sharedCreator(vm, dataByteCount, true, true, refusal); | |
| if (owner == 0) | |
| { | |
| return refusal; | |
| } | |
| return sharedCreated( | |
| vm, | |
| owner, | |
| dataByteCount, | |
| make_rcp<SharedBlob>(Span<const uint8_t>(data, dataByteCount))); | |
| } | |
| uint32_t sharedBlobSizeImpl(WasmScriptingVM* vm, uint32_t blob) | |
| { | |
| auto found = sharedFind<SharedBlob>(vm, blob); | |
| return found != nullptr ? found->size() : 0; | |
| } | |
| uint32_t sharedBlobReadImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t blob, | |
| uint32_t offset, | |
| uint8_t* out, | |
| uint32_t outByteCount) | |
| { | |
| auto found = sharedFind<SharedBlob>(vm, blob); | |
| return found != nullptr ? found->read(offset, out, outByteCount) : 0; | |
| } | |
| void sharedReadDoneImpl(WasmScriptingVM* vm) | |
| { | |
| if (vm != nullptr) | |
| { | |
| vm->releaseReadHolds(); | |
| } | |
| } | |
| uint32_t sharedRetainImpl(WasmScriptingVM* vm, uint32_t object) | |
| { | |
| return vm != nullptr && SharedRegistry::retain(object, vm->sharedOwner()) ? 1 | |
| : 0; | |
| } | |
| // The main lane gives back a handle's hold; a lane only what it created or | |
| // holds, and a processor nothing, as its holds go with it. | |
| void sharedReleaseImpl(WasmScriptingVM* vm, uint32_t object) | |
| { | |
| if (vm != nullptr) | |
| { | |
| SharedRegistry::release(object, vm->sharedOwner()); | |
| return; | |
| } | |
| WasmLane* lane = WasmLane::current(); | |
| if (lane != nullptr && lane->kind() != LaneKind::processor && | |
| lane->dropShared(object)) | |
| { | |
| SharedRegistry::release(object, lane->sharedOwner()); | |
| } | |
| } | |
| // --- rive_lanes_v1: jobs, workers and audio processors ----------------------- | |
| struct HostLane | |
| { | |
| virtual ~HostLane() = default; | |
| WasmLane* lane = nullptr; | |
| }; | |
| template <class T> struct HostLaneOf : HostLane | |
| { | |
| explicit HostLaneOf(rcp<T> held) : owned(std::move(held)) | |
| { | |
| lane = owned.get(); | |
| } | |
| rcp<T> owned; | |
| }; | |
| WasmLane* resolveLane(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| auto* host = vm != nullptr ? static_cast<HostLane*>(vm->handles().resolve( | |
| handle, | |
| WasmScriptingVM::HandleTable::Tag::lane)) | |
| : nullptr; | |
| return host != nullptr ? host->lane : nullptr; | |
| } | |
| WasmJob* resolveJob(WasmScriptingVM* vm, uint32_t handle) | |
| { | |
| WasmLane* lane = resolveLane(vm, handle); | |
| return lane != nullptr && lane->kind() == LaneKind::job | |
| ? static_cast<WasmJob*>(lane) | |
| : nullptr; | |
| } | |
| // The encoded value a done job or worker hands its main lane. | |
| const std::vector<uint8_t>& laneResult(WasmLane* lane) | |
| { | |
| return lane->kind() == LaneKind::job | |
| ? static_cast<WasmJob*>(lane)->output() | |
| : static_cast<WasmWorker*>(lane)->result(); | |
| } | |
| template <class T> | |
| uint32_t mintLane(WasmScriptingVM* vm, rcp<T> lane, Span<const uint32_t> holds) | |
| { | |
| if (lane == nullptr) | |
| { | |
| return 0; | |
| } | |
| // The script's handles outlive this call, so a lane already running | |
| // sees them alive until it holds them. | |
| lane->holdShared(holds); | |
| vm->watchLanes(); | |
| return vm->handles().mint( | |
| WasmScriptingVM::HandleTable::Tag::lane, | |
| new HostLaneOf<T>(std::move(lane))); | |
| } | |
| uint32_t lanesStartImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t kind, | |
| uint32_t entry, | |
| const uint8_t* input, | |
| uint32_t inputByteCount, | |
| const uint32_t* holds, | |
| uint32_t holdCount) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return 0; | |
| } | |
| if (kind < 3 && !vm->laneLoadError((LaneKind)kind).empty()) | |
| { | |
| vm->refusedStart = (LaneKind)kind; | |
| return lane_layout::kStartFailed; | |
| } | |
| std::vector<uint8_t> bytes(input, input + inputByteCount); | |
| Span<const uint32_t> held(holds, holdCount); | |
| uint32_t pageBudgetMs = vm->pageThreadBudgetMs(); | |
| switch ((LaneKind)kind) | |
| { | |
| case LaneKind::job: | |
| return vm->jobs() != nullptr | |
| ? mintLane( | |
| vm, | |
| vm->jobs()->run(entry, std::move(bytes), pageBudgetMs), | |
| held) | |
| : 0; | |
| case LaneKind::worker: | |
| return vm->workers() != nullptr | |
| ? mintLane( | |
| vm, | |
| vm->workers() | |
| ->spawn(entry, std::move(bytes), pageBudgetMs), | |
| held) | |
| : 0; | |
| case LaneKind::processor: | |
| return vm->processors() != nullptr | |
| ? mintLane( | |
| vm, | |
| vm->processors()->start( | |
| entry, | |
| std::move(bytes), | |
| vm->limits().audioHardBlocks), | |
| held) | |
| : 0; | |
| } | |
| return 0; | |
| } | |
| uint32_t lanesStateImpl(WasmScriptingVM* vm, uint32_t lane) | |
| { | |
| WasmLane* found = resolveLane(vm, lane); | |
| if (found == nullptr) | |
| { | |
| return (uint32_t)LaneState::stopped; | |
| } | |
| LaneState state = found->state(); | |
| // What it printed lands ahead of whatever its ending makes the caller | |
| // print. | |
| if (state >= LaneState::done) | |
| { | |
| vm->deliverLaneOutput(found); | |
| } | |
| return (uint32_t)state; | |
| } | |
| uint32_t lanesFailedImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t lane, | |
| char* buffer, | |
| uint32_t capacity) | |
| { | |
| WasmLane* found = resolveLane(vm, lane); | |
| std::string failure; | |
| if (vm != nullptr && lane == lane_layout::kStartFailed) | |
| { | |
| failure = vm->laneLoadError(vm->refusedStart); | |
| } | |
| else if (found == nullptr || found->state() != LaneState::failed) | |
| { | |
| return 0; | |
| } | |
| else | |
| { | |
| failure = found->failure(); | |
| if (failure.empty()) | |
| { | |
| failure = found->name() + " failed"; | |
| } | |
| } | |
| if (buffer != nullptr && failure.size() <= capacity) | |
| { | |
| memcpy(buffer, failure.data(), failure.size()); | |
| } | |
| return (uint32_t)failure.size(); | |
| } | |
| void lanesStopImpl(WasmScriptingVM* vm, uint32_t lane) | |
| { | |
| if (WasmLane* found = resolveLane(vm, lane)) | |
| { | |
| found->stop(); | |
| } | |
| } | |
| void lanesReleaseImpl(WasmScriptingVM* vm, uint32_t lane) | |
| { | |
| WasmLane* found = resolveLane(vm, lane); | |
| if (found == nullptr) | |
| { | |
| return; | |
| } | |
| // Now rather than at the next poll, so a call releasing many lanes keeps | |
| // none it is done with. | |
| vm->deliverLaneOutput(found); | |
| releaseHostHandle(vm, lane, WasmScriptingVM::HandleTable::Tag::lane); | |
| switch (found->kind()) | |
| { | |
| case LaneKind::job: | |
| vm->jobs()->prune(found); | |
| break; | |
| case LaneKind::worker: | |
| vm->workers()->prune(found); | |
| break; | |
| case LaneKind::processor: | |
| vm->processors()->prune(found); | |
| break; | |
| } | |
| } | |
| uint32_t lanesResultImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t lane, | |
| uint32_t out, | |
| uint32_t outCount) | |
| { | |
| WasmLane* found = resolveLane(vm, lane); | |
| if (found == nullptr || found->state() != LaneState::done) | |
| { | |
| return 0; | |
| } | |
| const std::vector<uint8_t>& result = laneResult(found); | |
| if (result.size() <= outCount && !result.empty() && | |
| !vm->copyToModule(out, result.data(), (uint32_t)result.size())) | |
| { | |
| vm->raiseModuleError("lane result buffer is outside module memory"); | |
| } | |
| return (uint32_t)result.size(); | |
| } | |
| uint32_t lanesWatchImpl(WasmScriptingVM* vm, uint32_t lane) | |
| { | |
| return resolveLane(vm, lane) != nullptr ? vm->watchLane(lane) : 0; | |
| } | |
| uint32_t lanesWaitImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t signal, | |
| double after, | |
| double timeoutSeconds) | |
| { | |
| return vm != nullptr ? vm->waitOnMain(signal, after, timeoutSeconds) : 0; | |
| } | |
| void lanesCancelWaitImpl(WasmScriptingVM* vm, uint32_t token) | |
| { | |
| if (vm != nullptr) | |
| { | |
| vm->cancelMainWait(token); | |
| } | |
| } | |
| double lanesWaitWithinImpl( | |
| WasmScriptingVM* vm, | |
| uint32_t signal, | |
| double after, | |
| double timeoutSeconds) | |
| { | |
| if (vm == nullptr) | |
| { | |
| return lane_layout::kWaitGone; | |
| } | |
| // Waiting while holding a region could wait on a lane that waits for it. | |
| if (sharedHeldRegion() != 0) | |
| { | |
| return lane_layout::kWaitRefused; | |
| } | |
| // Without lanes nothing else notifies, and no call deadline is armed. | |
| double limit = vm->hasLanes() | |
| ? std::chrono::duration<double>(waitLimit()).count() | |
| : 0; | |
| uint64_t owner = vm->sharedOwner(); | |
| WasmScriptingVM::WaitScope wait(vm); | |
| return SignalWait( | |
| 0, | |
| owner, | |
| signal, | |
| SignalWait::find(signal, owner).get(), | |
| after, | |
| std::min({timeoutSeconds, wait.usable(), limit})) | |
| .block(); | |
| } | |
| // --- rive_parallel_v1: parallel calls ------------------------------------- | |
| uint32_t parallelSizeImpl(WasmScriptingVM* vm) | |
| { | |
| return vm != nullptr ? vm->parallelSize() : 1; | |
| } | |
| #ifdef RIVE_WASM_PARALLEL | |
| uint32_t parallelRunImpl(WasmScriptingVM* vm, uint32_t control) | |
| { | |
| if (vm == nullptr) | |
| { | |
| WasmParallel::refuseMemberCall(); | |
| return 0; | |
| } | |
| return vm->runParallel(control); | |
| } | |
| #endif | |
| // Only a caller that cannot park calls it, and natively every caller parks. | |
| void parallelWaitingImpl(WasmScriptingVM* vm, uint32_t control) {} | |
| // --- rive_jobs_v1: script jobs --------------------------------------------- | |
| uint32_t jobsThreadsImpl(WasmScriptingVM* vm) | |
| { | |
| return WasmJobs::pool().threadCount(); | |
| } | |
| uint32_t jobsJoinImpl(WasmScriptingVM* vm, uint32_t job, double timeoutSeconds) | |
| { | |
| WasmJob* found = resolveJob(vm, job); | |
| if (found == nullptr) | |
| { | |
| return (uint32_t)LaneState::stopped; | |
| } | |
| // Waiting while holding a region could wait on a job that waits for it. | |
| if (sharedHeldRegion() != 0) | |
| { | |
| return WasmJob::kJoinRefused; | |
| } | |
| { | |
| WasmScriptingVM::WaitScope wait(vm); | |
| vm->jobs()->join(found, timeoutSeconds, wait.usable()); | |
| } | |
| // Its lines land ahead of whatever the joiner prints next. | |
| vm->deliverLaneOutput(found); | |
| return (uint32_t)found->state(); | |
| } | |
| uint32_t jobsJoinAnyImpl( | |
| WasmScriptingVM* vm, | |
| const uint32_t* jobs, | |
| uint32_t count, | |
| double timeoutSeconds) | |
| { | |
| if (sharedHeldRegion() != 0) | |
| { | |
| return lane_layout::kJoinAnyRefused; | |
| } | |
| std::vector<WasmJob*> live; | |
| std::vector<uint32_t> indices; | |
| for (uint32_t i = 0; i < count; i++) | |
| { | |
| if (WasmJob* found = resolveJob(vm, jobs[i])) | |
| { | |
| live.push_back(found); | |
| indices.push_back(i); | |
| } | |
| } | |
| if (live.empty()) | |
| { | |
| return 0; | |
| } | |
| int found; | |
| { | |
| WasmScriptingVM::WaitScope wait(vm); | |
| found = vm->jobs()->joinAny(live, timeoutSeconds, wait.usable()); | |
| } | |
| if (found < 0) | |
| { | |
| return 0; | |
| } | |
| vm->deliverLaneOutput(live[found]); | |
| return indices[found] + 1; | |
| } | |
| void deleteHostObject( | |
| WasmScriptingVM* vm, | |
| WasmScriptingVM::HandleTable::Tag tag, | |
| void* object) | |
| { | |
| using Tag = WasmScriptingVM::HandleTable::Tag; | |
| switch (tag) | |
| { | |
| case Tag::instanceValue: | |
| delete static_cast<HostInstanceValue*>(object); | |
| break; | |
| case Tag::viewModelInstance: | |
| delete static_cast<HostViewModelInstance*>(object); | |
| break; | |
| case Tag::path: | |
| delete static_cast<HostPath*>(object); | |
| break; | |
| case Tag::measure: | |
| delete static_cast<HostMeasure*>(object); | |
| break; | |
| case Tag::paint: | |
| delete static_cast<HostPaint*>(object); | |
| break; | |
| case Tag::shader: | |
| delete static_cast<HostShader*>(object); | |
| break; | |
| case Tag::image: | |
| delete static_cast<HostImage*>(object); | |
| break; | |
| case Tag::font: | |
| delete static_cast<HostFont*>(object); | |
| break; | |
| case Tag::text: | |
| deleteHostText(vm, static_cast<HostText*>(object)); | |
| break; | |
| case Tag::buffer: | |
| delete static_cast<HostBuffer*>(object); | |
| break; | |
| case Tag::meshInstances: | |
| delete static_cast<HostMeshInstances*>(object); | |
| break; | |
| case Tag::dataContext: | |
| delete static_cast<HostDataContext*>(object); | |
| break; | |
| case Tag::artboard: | |
| // Refcounted: animations and nodes may still hold it. | |
| static_cast<HostArtboard*>(object)->unref(); | |
| break; | |
| case Tag::animation: | |
| delete static_cast<HostAnimation*>(object); | |
| break; | |
| case Tag::node: | |
| delete static_cast<HostNode*>(object); | |
| break; | |
| case Tag::riveFile: | |
| delete static_cast<HostRiveFile*>(object); | |
| break; | |
| case Tag::bindableArtboard: | |
| delete static_cast<HostBindableArtboard*>(object); | |
| break; | |
| #ifdef WITH_RIVE_AUDIO | |
| case Tag::audioSource: | |
| delete static_cast<HostAudioSource*>(object); | |
| break; | |
| case Tag::audioSound: | |
| delete static_cast<HostAudioSound*>(object); | |
| break; | |
| case Tag::audioStream: | |
| #if RIVE_AUDIO_STREAMS() | |
| // Its poll frees it, once no reader holds the ring. | |
| static_cast<HostAudioStream*>(object)->handle = 0; | |
| #endif | |
| break; | |
| #endif | |
| #ifdef RIVE_CANVAS | |
| case Tag::canvas: | |
| delete static_cast<HostCanvas*>(object); | |
| break; | |
| #endif | |
| #if defined(RIVE_CANVAS) && defined(RIVE_ORE) | |
| case Tag::gpuPass: | |
| delete static_cast<HostGpuPass*>(object); | |
| break; | |
| case Tag::gpuCanvas: | |
| delete static_cast<HostGpuCanvas*>(object); | |
| break; | |
| case Tag::gpuBuffer: | |
| delete static_cast<HostGpuBuffer*>(object); | |
| break; | |
| case Tag::gpuTexture: | |
| delete static_cast<HostGpuTexture*>(object); | |
| break; | |
| case Tag::gpuSampler: | |
| delete static_cast<HostGpuSampler*>(object); | |
| break; | |
| case Tag::gpuTextureView: | |
| delete static_cast<HostGpuTextureView*>(object); | |
| break; | |
| case Tag::gpuShaderModule: | |
| delete static_cast<HostGpuShaderModule*>(object); | |
| break; | |
| case Tag::gpuBindGroupLayout: | |
| delete static_cast<HostGpuBindGroupLayout*>(object); | |
| break; | |
| case Tag::gpuBindGroup: | |
| delete static_cast<HostGpuBindGroup*>(object); | |
| break; | |
| case Tag::gpuPipeline: | |
| delete static_cast<HostGpuPipeline*>(object); | |
| break; | |
| #endif | |
| case Tag::lane: | |
| { | |
| // A lane whose wrapper died is not waited for. | |
| auto* host = static_cast<HostLane*>(object); | |
| host->lane->stop(); | |
| delete host; | |
| break; | |
| } | |
| default: | |
| break; | |
| } | |
| } | |
| } // namespace | |
| uint32_t WasmScriptingVM::HandleTable::mint(Tag tag, void* object) | |
| { | |
| uint32_t slot; | |
| if (!freeSlots.empty()) | |
| { | |
| slot = freeSlots.back(); | |
| freeSlots.pop_back(); | |
| } | |
| else if (slots.size() < slotLimit) | |
| { | |
| slot = (uint32_t)slots.size(); | |
| slots.push_back({}); | |
| } | |
| else if (!retiredSlots.empty()) | |
| { | |
| // Callers allocate before minting, so a full table reissues old | |
| // handles rather than fail. | |
| slot = retiredSlots.front(); | |
| retiredSlots.pop_front(); | |
| } | |
| else | |
| { | |
| return 0; | |
| } | |
| 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; | |
| // Seven bits, so no handle sets bit 31, which marks shared ids. | |
| entry.generation = (entry.generation + 1) & 0x7f; | |
| uint32_t index = slot - 1; | |
| if (entry.generation == 0) | |
| { | |
| retiredSlots.push_back(index); | |
| if (retiredSlots.size() <= retiredLimit) | |
| { | |
| return; | |
| } | |
| index = retiredSlots.front(); | |
| retiredSlots.pop_front(); | |
| } | |
| freeSlots.push_back(index); | |
| } | |
| WasmScriptingVM::WasmScriptingVM() : m_decodedBy(s_decodingParent) | |
| { | |
| if (m_decodedBy != nullptr) | |
| { | |
| m_decodedBy->m_decodedVMs.push_back(this); | |
| } | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| 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)) | |
| { | |
| if (vm->m_error) | |
| { | |
| vm->m_error(kModuleStartFailed + vm->m_lastError); | |
| } | |
| else | |
| { | |
| outError = vm->m_lastError; | |
| } | |
| return nullptr; | |
| } | |
| return vm; | |
| } | |
| #endif | |
| 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. | |
| HostRenderer host{renderer}; | |
| uint32_t handle = m_handles.mint(HandleTable::Tag::renderer, &host); | |
| uint32_t args[3] = {m_L, (uint32_t)selfRef, handle}; | |
| callModule("host_obj_draw", 3, args); | |
| host.restoreSaves(); | |
| m_handles.release(handle, HandleTable::Tag::renderer); | |
| } | |
| #ifdef RIVE_WASM_PARALLEL | |
| uint32_t WasmScriptingVM::runParallel(uint32_t control) | |
| { | |
| if (m_parallel == nullptr) | |
| { | |
| m_parallel = std::make_unique<WasmParallel>(m_state->instance); | |
| } | |
| WasmParallel::Outcome outcome = m_parallel->run( | |
| m_state->execEnv, | |
| control, | |
| parallelSize() == 1); | |
| using Kind = WasmParallel::Outcome::Kind; | |
| switch (outcome.kind) | |
| { | |
| case Kind::ok: | |
| case Kind::callerFailed: | |
| break; | |
| case Kind::trapped: | |
| raiseModuleError(outcome.message.c_str()); | |
| break; | |
| case Kind::threw: | |
| { | |
| WasmStringArg message( | |
| this, | |
| outcome.message.data(), | |
| (uint32_t)outcome.message.size()); | |
| raiseThrown(std::string(message.data(), message.size()).c_str()); | |
| break; | |
| } | |
| case Kind::budget: | |
| reportError(budgetExceededLine(m_timeoutMs)); | |
| raiseBudgetExceeded(); | |
| break; | |
| case Kind::interrupted: | |
| // The watchdog's own report names the timeout. | |
| raiseModuleError("execution exceeded timeout"); | |
| break; | |
| } | |
| return outcome.threads; | |
| } | |
| #endif | |
| WasmScriptingVM::~WasmScriptingVM() | |
| { | |
| // First, so no poll reaches it while it goes. | |
| VMPollList::leave(m_laneList, this); | |
| VMPollList::leave(m_holdingList, this); | |
| for (const MainWait& wait : m_mainWaits) | |
| { | |
| abandonWait(wait); | |
| } | |
| // Workers and jobs may hold this VM's shared memory; they stop first. | |
| m_processors.reset(); | |
| m_workers.reset(); | |
| m_jobs.reset(); | |
| #ifndef __EMSCRIPTEN__ | |
| // Members share the instance's memory, so they go before it. | |
| m_parallel.reset(); | |
| #endif | |
| if (m_sharedOwner != 0) | |
| { | |
| SharedRegistry::releaseOwner(m_sharedOwner); | |
| } | |
| if (m_decodedBy != nullptr) | |
| { | |
| auto& siblings = m_decodedBy->m_decodedVMs; | |
| siblings.erase( | |
| std::remove(siblings.begin(), siblings.end(), this), | |
| siblings.end()); | |
| } | |
| for (WasmScriptingVM* decoded : m_decodedVMs) | |
| { | |
| decoded->m_decodedBy = nullptr; | |
| } | |
| 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. | |
| #ifdef __EMSCRIPTEN__ | |
| for (const auto& pending : m_pendingDecodes) | |
| { | |
| cancelBrowserImageDecode(pending.second); | |
| } | |
| #else | |
| if (m_decodeOwnerId != 0) | |
| { | |
| auto& pool = getGlobalWorkPoolIfExists(); | |
| if (pool != nullptr) | |
| { | |
| pool->cancelAllForOwner(m_decodeOwnerId); | |
| } | |
| } | |
| #endif | |
| #ifdef WITH_RIVE_SCRIPTNET | |
| // Same for fetches: their listeners hold this VM. | |
| if (m_netOwnerId != 0) | |
| { | |
| scriptnet::cancelAllForOwner(m_netOwnerId); | |
| } | |
| #endif | |
| #ifndef __EMSCRIPTEN__ | |
| // 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); | |
| } | |
| #endif | |
| // No script runs from here: disposing the swept artboards' scripts would | |
| // mint handles into the table the sweep walks. | |
| m_poisoned = true; | |
| // 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. | |
| // Indexed, and each slot cleared before its object goes, because host | |
| // teardown may still mint and grow the table. | |
| for (size_t i = 0; i < m_handles.slots.size(); ++i) | |
| { | |
| HandleTable::Slot& slot = m_handles.slots[i]; | |
| HandleTable::Tag tag = slot.tag; | |
| void* object = slot.object; | |
| slot.tag = HandleTable::Tag::empty; | |
| slot.object = nullptr; | |
| deleteHostObject(this, tag, object); | |
| } | |
| #if RIVE_AUDIO_STREAMS() | |
| for (HostAudioStream* stream : m_audioStreams) | |
| { | |
| delete stream; | |
| } | |
| #endif | |
| #ifndef __EMSCRIPTEN__ | |
| // Last, as the stats call above can still arm and register a slot. | |
| if (m_budgetSlot != nullptr) | |
| { | |
| ScriptWatchdog::instance().remove(m_budgetSlot.get()); | |
| } | |
| #endif | |
| } | |
| 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 --------------------------------- | |
| #if !defined(__EMSCRIPTEN__) && defined(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) | |
| { | |
| #ifdef __EMSCRIPTEN__ | |
| // Like the Luau lane on the web, the browser decodes. | |
| m_pendingDecodes[token] = startBrowserImageDecode( | |
| bytes, | |
| byteCount, | |
| [this, token]( | |
| uint32_t, | |
| uint32_t width, | |
| uint32_t height, | |
| Span<const uint8_t> pixels, | |
| const char* error) { | |
| if (error != nullptr) | |
| { | |
| rejectImageDecode(token, error); | |
| return; | |
| } | |
| resolveImageDecode( | |
| token, | |
| width, | |
| height, | |
| pixels.data(), | |
| (uint32_t)pixels.size()); | |
| }); | |
| return true; | |
| #elif !defined(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()) | |
| { | |
| #ifdef __EMSCRIPTEN__ | |
| cancelBrowserImageDecode(it->second); | |
| #else | |
| it->second->cancel(); | |
| #endif | |
| m_pendingDecodes.erase(it); | |
| } | |
| } | |
| void WasmScriptingVM::deliverDecodeResult(const DecodeResult& result) | |
| { | |
| if (result.ok) | |
| { | |
| uint32_t byteCount = (uint32_t)result.pixels.size(); | |
| // The listener's own buffer, where the module hands one out, saves | |
| // a second copy as big as the image. | |
| uint32_t bufferArgs[2] = {m_L, byteCount}; | |
| uint32_t pixelsPtr = 0; | |
| bool staged = callModuleChecked( | |
| "host_image_buffer", | |
| 2, | |
| bufferArgs, | |
| &pixelsPtr) != CallOutcome::ok; | |
| if (staged) | |
| { | |
| uint32_t sizeArgs[1] = {byteCount}; | |
| pixelsPtr = callModule("malloc", 1, sizeArgs); | |
| } | |
| if (pixelsPtr == 0) | |
| { | |
| DecodeResult failure; | |
| failure.token = result.token; | |
| failure.error = "failed to allocate decoded pixels"; | |
| deliverDecodeResult(failure); | |
| return; | |
| } | |
| copyToModule(pixelsPtr, result.pixels.data(), byteCount); | |
| uint32_t args[6] = | |
| {m_L, result.token, result.width, result.height, pixelsPtr, byteCount}; | |
| callModule("host_image_decoded", 6, args); | |
| if (staged) | |
| { | |
| 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) | |
| { | |
| if (inModuleCall()) | |
| { | |
| // The decoder frees its pixels once this returns. | |
| std::vector<uint8_t> owned(pixels, pixels + byteCount); | |
| holdOutcome([this, token, width, height, owned = std::move(owned)] { | |
| resolveImageDecode( | |
| token, | |
| width, | |
| height, | |
| owned.data(), | |
| (uint32_t)owned.size()); | |
| }); | |
| return; | |
| } | |
| if (m_pendingDecodes.erase(token) == 0 || !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) | |
| { | |
| if (inModuleCall()) | |
| { | |
| holdOutcome([this, token, error = std::string(message)] { | |
| rejectImageDecode(token, error.c_str()); | |
| }); | |
| return; | |
| } | |
| if (m_pendingDecodes.erase(token) == 0 || !valid()) | |
| { | |
| return; | |
| } | |
| DecodeResult result; | |
| result.token = token; | |
| result.error = message; | |
| deliverDecodeResult(result); | |
| } | |
| // --- fetch() over the module ABI -------------------------------------------- | |
| #ifdef WITH_RIVE_SCRIPTNET | |
| namespace | |
| { | |
| // The Luau lane's rejection value: "<code>: <message>". | |
| std::string fetchRejection( | |
| scriptnet::NetErrorCode code, | |
| const std::string& message) | |
| { | |
| return std::string(scriptnet::netErrorCodeName(code)) + ": " + message; | |
| } | |
| // Carries one request's outcome back to the VM that started it. scriptnet | |
| // delivers on this VM's thread from its poll; teardown cancels every | |
| // outstanding request first, which clears the VM pointer here. | |
| class WasmFetchListener : public scriptnet::FetchListener | |
| { | |
| public: | |
| WasmFetchListener(WasmScriptingVM* vm, uint32_t token) | |
| : m_vm(vm) | |
| , m_token(token) | |
| {} | |
| void onResponse(scriptnet::HttpResponse&& response) override | |
| { | |
| if (m_vm != nullptr) | |
| { | |
| m_vm->resolveFetch(m_token, std::move(response)); | |
| } | |
| } | |
| void onError(const scriptnet::NetError& error) override | |
| { | |
| if (m_vm != nullptr) | |
| { | |
| m_vm->rejectFetch( | |
| m_token, | |
| (uint32_t)error.code, | |
| fetchRejection(error.code, error.message)); | |
| } | |
| } | |
| void onCancel() override { m_vm = nullptr; } | |
| private: | |
| WasmScriptingVM* m_vm; | |
| uint32_t m_token; | |
| }; | |
| } // namespace | |
| #endif | |
| bool WasmScriptingVM::startFetch(Span<const uint8_t> wire, uint32_t token) | |
| { | |
| #ifdef WITH_RIVE_SCRIPTNET | |
| // Reusing a live token would leave its first request uncancellable. | |
| if (m_pendingFetches.count(token) != 0) | |
| { | |
| return false; | |
| } | |
| scriptnet::HttpRequest request; | |
| // The module's fetch() writes the stream from arguments it validated, so | |
| // a bad one is a bug; it goes out empty and the policy refuses it, | |
| // settling like any other refusal. | |
| if (!NetWire::decode(wire, request)) | |
| { | |
| request = scriptnet::HttpRequest(); | |
| } | |
| if (m_netOwnerId == 0) | |
| { | |
| m_netOwnerId = WorkPool::nextOwnerId(); | |
| } | |
| m_pendingFetches[token] = scriptnet::fetch( | |
| m_netOwnerId, | |
| std::move(request), | |
| make_rcp<WasmFetchListener>(this, token)); | |
| return true; | |
| #else | |
| (void)wire; | |
| (void)token; | |
| return false; | |
| #endif | |
| } | |
| void WasmScriptingVM::cancelFetch(uint32_t token) | |
| { | |
| auto it = m_pendingFetches.find(token); | |
| if (it == m_pendingFetches.end()) | |
| { | |
| return; | |
| } | |
| uint32_t id = it->second; | |
| m_pendingFetches.erase(it); | |
| #ifdef WITH_RIVE_SCRIPTNET | |
| scriptnet::cancel(id); | |
| #else | |
| (void)id; | |
| #endif | |
| } | |
| void WasmScriptingVM::resolveFetch( | |
| uint32_t token, | |
| scriptnet::HttpResponse&& response) | |
| { | |
| if (inModuleCall()) | |
| { | |
| holdOutcome([this, token, response = std::move(response)]() mutable { | |
| resolveFetch(token, std::move(response)); | |
| }); | |
| return; | |
| } | |
| // Cancelled while held. | |
| if (m_pendingFetches.erase(token) == 0 || !valid()) | |
| { | |
| return; | |
| } | |
| auto tooLarge = [&](const char* message) { | |
| #ifdef WITH_RIVE_SCRIPTNET | |
| deliverFetchFailure( | |
| token, | |
| (uint32_t)scriptnet::NetErrorCode::TooLarge, | |
| fetchRejection(scriptnet::NetErrorCode::TooLarge, message)); | |
| #else | |
| (void)message; | |
| #endif | |
| }; | |
| // The body stays in the host, so it reaches a lane without ever entering | |
| // module memory. | |
| uint32_t body = 0; | |
| if (m_fetchBlobBody) | |
| { | |
| uint64_t size = response.body.size(); | |
| uint64_t owner = sharedCreator(this, size, true, true, body); | |
| if (owner != 0) | |
| { | |
| body = sharedCreated( | |
| this, | |
| owner, | |
| size, | |
| make_rcp<SharedBlob>( | |
| Span<const uint8_t>(response.body.data(), response.body.size()))); | |
| } | |
| if (!isSharedId(body)) | |
| { | |
| tooLarge("the response body does not fit in shared memory"); | |
| return; | |
| } | |
| } | |
| NetWire::Response wire{response, body}; | |
| uint32_t byteCount = (uint32_t)NetWire::encodedSize(wire); | |
| uint32_t sizeArgs[1] = {byteCount}; | |
| uint32_t responsePtr = callModule("malloc", 1, sizeArgs); | |
| if (responsePtr == 0) | |
| { | |
| if (body != 0) | |
| { | |
| SharedRegistry::release(body, sharedOwner()); | |
| } | |
| tooLarge("the response does not fit in script memory"); | |
| return; | |
| } | |
| NetWire::encode( | |
| wire, | |
| (uint8_t*)resolveModuleWritePtr(responsePtr, byteCount)); | |
| uint32_t args[4] = {m_L, token, responsePtr, byteCount}; | |
| callModule("host_fetch_resolved", 4, args); | |
| guestFree(responsePtr); | |
| } | |
| void WasmScriptingVM::rejectFetch( | |
| uint32_t token, | |
| uint32_t code, | |
| const std::string& message) | |
| { | |
| if (inModuleCall()) | |
| { | |
| holdOutcome( | |
| [this, token, code, message] { rejectFetch(token, code, message); }); | |
| return; | |
| } | |
| if (m_pendingFetches.erase(token) != 0 && valid()) | |
| { | |
| deliverFetchFailure(token, code, message); | |
| } | |
| } | |
| void WasmScriptingVM::deliverFetchFailure( | |
| uint32_t token, | |
| uint32_t code, | |
| const std::string& message) | |
| { | |
| uint32_t messagePtr = guestString(message.c_str()); | |
| if (messagePtr == 0) | |
| { | |
| return; | |
| } | |
| uint32_t args[4] = {m_L, token, code, messagePtr}; | |
| callModule("host_fetch_failed", 4, args); | |
| guestFree(messagePtr); | |
| } | |
| bool WasmScriptingVM::inModuleCall() const { return m_callDepth != 0; } | |
| void WasmScriptingVM::holdOutcome(std::function<void()> deliver) | |
| { | |
| if (m_heldOutcomes.empty()) | |
| { | |
| VMPollList::join(m_holdingList, t_holdingVMs, this); | |
| } | |
| m_heldOutcomes.push_back(std::move(deliver)); | |
| } | |
| void WasmScriptingVM::attachLanes(Span<const uint8_t> module) | |
| { | |
| // Not generic, so the three kinds share one copy. | |
| auto load = [&](LaneKind kind) { | |
| std::string& error = m_laneLoadErrors[(int)kind]; | |
| rcp<WasmLaneModule> lanes = loadLaneModule(module, kind, error); | |
| if (!error.empty()) | |
| { | |
| consoleWrite( | |
| this, | |
| std::string("script ") + laneNoun(kind) + "s unavailable: " + error); | |
| } | |
| return lanes; | |
| }; | |
| auto attach = [&](auto& set, LaneKind kind) { | |
| rcp<WasmLaneModule> lanes = load(kind); | |
| if (lanes != nullptr) | |
| { | |
| set.reset(new std::remove_reference_t<decltype(*set)>(std::move(lanes))); | |
| set->setSharedOwner(sharedOwner()); | |
| } | |
| }; | |
| attach(m_jobs, LaneKind::job); | |
| attach(m_workers, LaneKind::worker); | |
| attach(m_processors, LaneKind::processor); | |
| } | |
| void WasmScriptingVM::laneTierLines(std::vector<std::string>& lines) const | |
| { | |
| #ifndef __EMSCRIPTEN__ | |
| auto add = [&](const auto& lanes, LaneKind kind) { | |
| if (lanes != nullptr) | |
| { | |
| lines.push_back( | |
| std::string(laneSpec(kind).module) + " tier: " + lanes->tier()); | |
| } | |
| }; | |
| add(m_jobs, LaneKind::job); | |
| add(m_workers, LaneKind::worker); | |
| add(m_processors, LaneKind::processor); | |
| #endif | |
| } | |
| void WasmScriptingVM::poison() | |
| { | |
| m_poisoned = true; | |
| auto stopAll = [](auto* set) { | |
| if (set == nullptr) | |
| { | |
| return; | |
| } | |
| for (const auto& lane : set->lanes()) | |
| { | |
| lane->stop(); | |
| } | |
| }; | |
| stopAll(m_jobs.get()); | |
| stopAll(m_workers.get()); | |
| stopAll(m_processors.get()); | |
| } | |
| void WasmScriptingVM::deliverAllLaneOutput() | |
| { | |
| t_laneVMs->deliver([](WasmScriptingVM* vm) { | |
| // At a poll no module call is live, so workers without threads may | |
| // run here. | |
| if (vm->m_workers != nullptr) | |
| { | |
| vm->m_workers->pump(); | |
| } | |
| vm->deliverLaneOutput(); | |
| }); | |
| } | |
| void WasmScriptingVM::watchLanes() | |
| { | |
| // Its lanes start only from one thread, whose polls reach their output. | |
| assert(m_laneList == nullptr || m_laneList == t_laneVMs); | |
| VMPollList::join(m_laneList, t_laneVMs, this); | |
| } | |
| // Decodes on a job thread into a blob the main lane registers as it settles. | |
| class rive::WasmAudioDecode : public JobTask | |
| { | |
| public: | |
| #ifdef WITH_RIVE_AUDIO | |
| WasmAudioDecode(rcp<AudioReader> reader, uint64_t maxFrames) | |
| : m_reader(std::move(reader)) | |
| , m_maxFrames(maxFrames) | |
| {} | |
| #endif | |
| // Bytes 0 when it did not decode; blob null when they cannot be one. | |
| uint64_t bytes = 0; | |
| rcp<SharedBlob> blob; | |
| // What the main lane charged its owner up front, refunded as it settles. | |
| uint64_t charged = 0; | |
| uint64_t owner = 0; | |
| // The main lane decodes it a slice per poll instead, as a pool with no | |
| // threads runs a job to its end. | |
| bool sliced = false; | |
| // True once it is done, else it stopped at until. | |
| bool decodeUntil(Clock::time_point until) | |
| { | |
| #ifdef WITH_RIVE_AUDIO | |
| bool done = false; | |
| do | |
| { | |
| if (m_stop.load(std::memory_order_relaxed) || | |
| !m_reader->readBlock(m_maxFrames, m_samples, done)) | |
| { | |
| m_samples = {}; | |
| return true; | |
| } | |
| } while (!done && Clock::now() < until); | |
| if (!done) | |
| { | |
| return false; | |
| } | |
| bytes = m_samples.size() * sizeof(float); | |
| if (bytes != 0 && bytes <= kSharedMaxBytes) | |
| { | |
| blob = make_rcp<SharedBlob>( | |
| Span<const uint8_t>((const uint8_t*)m_samples.data(), bytes)); | |
| } | |
| m_samples = {}; | |
| #endif | |
| return true; | |
| } | |
| protected: | |
| void run() override { decodeUntil(Clock::time_point::max()); } | |
| void interrupt() override { m_stop.store(true, std::memory_order_relaxed); } | |
| private: | |
| std::atomic<bool> m_stop{false}; | |
| #ifdef WITH_RIVE_AUDIO | |
| rcp<AudioReader> m_reader; | |
| uint64_t m_maxFrames; | |
| std::vector<float> m_samples; | |
| #endif | |
| }; | |
| uint32_t WasmScriptingVM::startAudioDecode( | |
| rcp<AudioSource> source, | |
| uint32_t sampleRate, | |
| uint32_t channels) | |
| { | |
| #ifdef WITH_RIVE_AUDIO | |
| // Made here, as the source caches its format lazily and unguarded. | |
| auto reader = source->makeReader(channels, sampleRate); | |
| if (reader == nullptr) | |
| { | |
| return 0; | |
| } | |
| watchLanes(); | |
| uint64_t frameBytes = sizeof(float) * (uint64_t)reader->channels(); | |
| uint64_t maxFrames = kSharedMaxBytes / frameBytes; | |
| // Charged now, so decodes running at once cannot outgrow the budget. | |
| uint64_t estimate = std::min(reader->lengthInFrames(), maxFrames) * | |
| frameBytes; | |
| uint64_t owner = sharedOwner(); | |
| uint32_t token = m_nextMainWait++; | |
| if (!SharedRegistry::charge(owner, estimate)) | |
| { | |
| wakeMain(token, shared_layout::kCreateNoMemory); | |
| return token; | |
| } | |
| auto decode = make_rcp<WasmAudioDecode>(std::move(reader), maxFrames); | |
| decode->charged = estimate; | |
| decode->owner = owner; | |
| decode->sliced = WasmJobs::pool().threadCount() == 0; | |
| if (!decode->sliced) | |
| { | |
| WasmJobs::pool().submit(decode); | |
| } | |
| m_mainWaits.push_back({SignalWait(token), 0, std::move(decode)}); | |
| return token; | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| uint32_t WasmScriptingVM::watchLane(uint32_t lane) | |
| { | |
| m_mainWaits.push_back({SignalWait(m_nextMainWait), lane}); | |
| return m_nextMainWait++; | |
| } | |
| uint32_t WasmScriptingVM::watchStream(uint32_t stream) | |
| { | |
| m_mainWaits.push_back({SignalWait(m_nextMainWait), 0, nullptr, stream}); | |
| return m_nextMainWait++; | |
| } | |
| uint32_t WasmScriptingVM::waitOnMain( | |
| uint32_t signal, | |
| double after, | |
| double timeoutSeconds) | |
| { | |
| watchLanes(); | |
| uint64_t owner = sharedOwner(); | |
| rcp<SharedSignal> found = SignalWait::find(signal, owner); | |
| m_mainWaits.push_back( | |
| {SignalWait( | |
| m_nextMainWait, | |
| owner, | |
| signal, | |
| found.get(), | |
| after, | |
| timeoutSeconds), | |
| 0}); | |
| return m_nextMainWait++; | |
| } | |
| void WasmScriptingVM::abandonWait(const MainWait& wait) | |
| { | |
| if (wait.decode != nullptr) | |
| { | |
| WasmJobs::pool().cancel(wait.decode.get()); | |
| SharedRegistry::refund(wait.decode->owner, wait.decode->charged); | |
| } | |
| #if RIVE_AUDIO_STREAMS() | |
| releaseHostHandle(this, wait.stream, HandleTable::Tag::audioStream); | |
| #endif | |
| } | |
| void WasmScriptingVM::cancelMainWait(uint32_t token) | |
| { | |
| for (const MainWait& wait : m_mainWaits) | |
| { | |
| if (wait.wait.id == token) | |
| { | |
| abandonWait(wait); | |
| // A promise that parked it still takes one wake, which says it went. | |
| wakeMain(token, 0); | |
| } | |
| } | |
| m_mainWaits.erase( | |
| std::remove_if( | |
| m_mainWaits.begin(), | |
| m_mainWaits.end(), | |
| [token](const MainWait& wait) { return wait.wait.id == token; }), | |
| m_mainWaits.end()); | |
| } | |
| static constexpr double kOfflineDecodeSeconds = 10; | |
| // The blob's id or why not, -1 when nothing decoded. | |
| static double settleAudioDecode(WasmScriptingVM* vm, WasmAudioDecode& decode) | |
| { | |
| SharedRegistry::refund(decode.owner, decode.charged); | |
| if (decode.bytes == 0) | |
| { | |
| return -1; | |
| } | |
| uint32_t answer = 0; | |
| uint64_t owner = sharedCreator(vm, decode.bytes, false, true, answer); | |
| if (owner == 0) | |
| { | |
| return answer; | |
| } | |
| return sharedCreated(vm, owner, decode.bytes, std::move(decode.blob)); | |
| } | |
| void WasmScriptingVM::settleMainWaits() | |
| { | |
| auto now = std::chrono::steady_clock::now(); | |
| auto sliceEnd = now + | |
| std::chrono::milliseconds(lane_timing::kCooperativeSliceMs); | |
| size_t resultBytes = 0; | |
| for (auto it = m_mainWaits.begin(); it != m_mainWaits.end();) | |
| { | |
| double value = 0; | |
| bool fire = false; | |
| if (it->decode != nullptr) | |
| { | |
| // Offline renders outrun real time, so one waits on what it decodes. | |
| #if RIVE_PROCESSOR_AUDIO() | |
| bool offline = WasmProcessors::renderOffline(); | |
| #else | |
| bool offline = false; | |
| #endif | |
| if (it->decode->sliced) | |
| { | |
| fire = it->decode->decodeUntil( | |
| offline ? Clock::time_point::max() : sliceEnd); | |
| } | |
| else | |
| { | |
| if (offline) | |
| { | |
| WasmJobs::pool().join(it->decode.get(), kOfflineDecodeSeconds); | |
| } | |
| fire = it->decode->finished(); | |
| } | |
| if (fire) | |
| { | |
| value = settleAudioDecode(this, *it->decode); | |
| } | |
| } | |
| #if RIVE_AUDIO_STREAMS() | |
| else if (it->stream != 0) | |
| { | |
| HostAudioStream* stream = resolveAudioStream(this, it->stream); | |
| fire = stream == nullptr || stream->ready(); | |
| value = stream != nullptr ? it->stream : 0; | |
| } | |
| #endif | |
| else if (it->lane != 0) | |
| { | |
| WasmLane* lane = resolveLane(this, it->lane); | |
| LaneState state = lane != nullptr ? lane->state() : LaneState::stopped; | |
| fire = state >= LaneState::done; | |
| value = (double)state; | |
| // Each result is decoded into the main heap as it wakes, so a | |
| // burst of large ones spreads over polls. | |
| if (fire && state == LaneState::done) | |
| { | |
| size_t bytes = laneResult(lane).size(); | |
| if (resultBytes != 0 && resultBytes + bytes > kLaneResultBytesPerPoll) | |
| { | |
| ++it; | |
| continue; | |
| } | |
| resultBytes += bytes; | |
| } | |
| } | |
| else | |
| { | |
| fire = it->wait.fired(now, value); | |
| } | |
| if (!fire) | |
| { | |
| ++it; | |
| continue; | |
| } | |
| uint32_t token = it->wait.id; | |
| it = m_mainWaits.erase(it); | |
| wakeMain(token, value); | |
| } | |
| } | |
| void WasmScriptingVM::wakeMain(uint32_t token, double value) | |
| { | |
| // Held even outside a call, so an await always resumes at a poll. | |
| holdOutcome([this, token, value] { | |
| double args[3] = {(double)m_L, (double)token, value}; | |
| double unused = 0; | |
| callF64("host_lane_wake", args, 3, &unused); | |
| }); | |
| } | |
| void WasmScriptingVM::deliverLaneOutput(WasmLane* lane) | |
| { | |
| lane->takeLogs( | |
| [](void* vm, std::string_view line) { | |
| rtLogImpl((WasmScriptingVM*)vm, 0, line.data(), (uint32_t)line.size()); | |
| }, | |
| this); | |
| // A job's failure is its joiner's result, so only workers and processors | |
| // announce theirs. | |
| if (lane->kind() == LaneKind::job || lane->reportedFailure || | |
| lane->state() != LaneState::failed) | |
| { | |
| return; | |
| } | |
| lane->reportedFailure = true; | |
| std::string line = lane->name() + " failed: " + lane->failure(); | |
| if (m_error) | |
| { | |
| m_error(line); | |
| } | |
| else | |
| { | |
| consoleWrite(this, line); | |
| } | |
| // A failed processor renders only silence, so it leaves the mix. | |
| lane->stop(); | |
| } | |
| void WasmScriptingVM::deliverLaneOutput() | |
| { | |
| settleMainWaits(); | |
| #if RIVE_AUDIO_STREAMS() | |
| pumpAudioStreams(this); | |
| #endif | |
| auto deliver = [this](auto* set) { | |
| if (set == nullptr) | |
| { | |
| return; | |
| } | |
| for (const auto& lane : set->lanes()) | |
| { | |
| deliverLaneOutput(lane.get()); | |
| } | |
| // Their output is out, and an idle slot keeps the watchdog polling. | |
| set->prune(); | |
| }; | |
| deliver(m_jobs.get()); | |
| deliver(m_workers.get()); | |
| deliver(m_processors.get()); | |
| } | |
| bool WasmScriptingVM::hasPendingAsyncWork() const | |
| { | |
| // A started job keeps hosts ticking until the script can see it finish, | |
| // and a worker only while it needs a step, never for being alive. | |
| return !m_pendingDecodes.empty() || !m_pendingFetches.empty() || | |
| !m_heldOutcomes.empty() || !m_mainWaits.empty() || | |
| (m_jobs != nullptr && m_jobs->hasUnfinished()) || | |
| (m_workers != nullptr && m_workers->due()) || | |
| (m_processors != nullptr && m_processors->starting()) || streaming(); | |
| } | |
| bool WasmScriptingVM::streaming() const | |
| { | |
| #if RIVE_AUDIO_STREAMS() | |
| // Only a poll tops a stream up, so one still decoding keeps hosts ticking. | |
| for (const HostAudioStream* stream : m_audioStreams) | |
| { | |
| if (!stream->ended) | |
| { | |
| return true; | |
| } | |
| } | |
| #endif | |
| return false; | |
| } | |
| uint64_t WasmScriptingVM::sharedOwner() | |
| { | |
| if (m_sharedOwner == 0) | |
| { | |
| m_sharedOwner = SharedRegistry::nextOwner(m_limits.sharedBytesPerFile); | |
| } | |
| return m_sharedOwner; | |
| } | |
| void WasmScriptingVM::keepReadHolds(Span<const uint32_t> ids) | |
| { | |
| m_readHolds.insert(m_readHolds.end(), ids.begin(), ids.end()); | |
| } | |
| void WasmScriptingVM::releaseReadHolds() | |
| { | |
| for (uint32_t id : m_readHolds) | |
| { | |
| SharedRegistry::release(id, m_sharedOwner); | |
| } | |
| m_readHolds.clear(); | |
| } | |
| void WasmScriptingVM::deliverHeldOutcomes() | |
| { | |
| if (inModuleCall() || m_heldOutcomes.empty()) | |
| { | |
| return; | |
| } | |
| VMPollList::leave(m_holdingList, this); | |
| // Deliveries can hold more, which wait for the next pass. | |
| std::vector<std::function<void()>> held; | |
| held.swap(m_heldOutcomes); | |
| for (auto& deliver : held) | |
| { | |
| deliver(); | |
| } | |
| } | |
| void WasmScriptingVM::deliverAllHeldOutcomes() | |
| { | |
| t_holdingVMs->deliver([](WasmScriptingVM* vm) { vm->deliverHeldOutcomes(); }); | |
| } | |
| void WasmScriptingVM::setBootHook(BootHook hook) | |
| { | |
| s_bootHook = std::move(hook); | |
| } | |
| void WasmScriptingVM::requireBounds(bool on) { s_requireBounds.store(on); } | |
| 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); | |
| } | |
| } | |
| std::string WasmScriptingVM::tierName( | |
| ExecutionTier tier, | |
| const std::string& interpReason) | |
| { | |
| switch (tier) | |
| { | |
| case ExecutionTier::interp: | |
| return interpReason.empty() ? "interp" : "interp, " + interpReason; | |
| case ExecutionTier::aotO0: | |
| return "aot -O0"; | |
| case ExecutionTier::aotO3: | |
| return "aot -O3"; | |
| } | |
| return "unknown"; | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| std::shared_ptr<void> WasmScriptingVM::loadSharedModule( | |
| Span<const uint8_t> bytes, | |
| ExecutionTier& tier, | |
| std::string& interpReason, | |
| std::string& outError, | |
| bool checkedBounds) | |
| { | |
| if (!ensureRuntime()) | |
| { | |
| outError = "wamr runtime init failed"; | |
| return nullptr; | |
| } | |
| // One content hash serves the AOT artifact lookups and the shared module | |
| // cache key; the key folds in which artifact actually loads so interp | |
| // and AOT modules never share an entry. | |
| const uint64_t plainKey = wasmModuleKey(bytes); | |
| uint64_t moduleKey = plainKey; | |
| tier = ExecutionTier::interp; | |
| // wasm_runtime_load keeps referencing the buffer, and may rewrite it. | |
| std::vector<uint8_t> moduleBytes; | |
| wasm_module_t loaded = nullptr; | |
| 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; | |
| auto setCompiledSpecies = [&](TierSpecies species) { | |
| haveHwAot = species == TierSpecies::hw; | |
| haveO0Aot = species == TierSpecies::o0; | |
| haveAot = !haveHwAot && !haveO0Aot; | |
| }; | |
| 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/%s", | |
| cacheDir, | |
| aotArtifactName(plainKey, ".hw").c_str()); | |
| struct stat hwAotStat; | |
| haveHwAot = !checkedBounds && stat(aotPath, &hwAotStat) == 0; | |
| #endif | |
| if (!haveHwAot) | |
| { | |
| snprintf( | |
| aotPath, | |
| sizeof(aotPath), | |
| "%s/%s", | |
| cacheDir, | |
| aotArtifactName(plainKey).c_str()); | |
| struct stat aotStat; | |
| haveAot = stat(aotPath, &aotStat) == 0; | |
| } | |
| if (!haveAot && !haveHwAot) | |
| { | |
| char dumpPath[1024]; | |
| snprintf( | |
| dumpPath, | |
| sizeof(dumpPath), | |
| "%s/%s", | |
| cacheDir, | |
| wasmModuleName(plainKey).c_str()); | |
| struct stat dumpStat; | |
| if (stat(dumpPath, &dumpStat) != 0) | |
| { | |
| if (FILE* dump = fopen(dumpPath, "wb")) | |
| { | |
| fwrite(bytes.data(), 1, bytes.size(), dump); | |
| fclose(dump); | |
| fprintf(stderr, "wasm aot: dumped %s for wamrc\n", dumpPath); | |
| } | |
| } | |
| } | |
| } | |
| // Tools builds boot compiled code: a ladder cache hit loads its | |
| // artifact and a miss compiles one on this thread, blocking the load. | |
| // RIVE_WASM_AOT_SYNC=o0 asks for the faster debug compile and =interp | |
| // for no compile at all. | |
| const char* tierEnv = getenv("RIVE_WASM_AOT_SYNC"); | |
| bool wantInterp = tierEnv != nullptr && strcmp(tierEnv, "interp") == 0; | |
| TierSpecies species = bootTierSpecies(); | |
| if (checkedBounds && species == TierSpecies::hw) | |
| { | |
| species = TierSpecies::o3; | |
| } | |
| bool ladderArtifact = false; | |
| if (!haveAot && !haveHwAot) | |
| { | |
| if (wantInterp) | |
| { | |
| interpReason = "RIVE_WASM_AOT_SYNC=interp"; | |
| } | |
| else if ( | |
| auto cached = leaseCachedModule( | |
| compiledModuleKey(plainKey, species), | |
| tier)) | |
| { | |
| interpReason.clear(); | |
| return cached; | |
| } | |
| else | |
| { | |
| TierCompile compile = ModuleTierLadder::instance().bootCompile( | |
| plainKey, | |
| bytes, | |
| species); | |
| if (!compile.path.empty() && compile.path.size() < sizeof(aotPath)) | |
| { | |
| memcpy(aotPath, compile.path.c_str(), compile.path.size() + 1); | |
| setCompiledSpecies(species); | |
| ladderArtifact = true; | |
| } | |
| else | |
| { | |
| interpReason = compile.reason; | |
| } | |
| } | |
| } | |
| #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( | |
| plainKey, | |
| bytes.size()); | |
| if (prelinked != nullptr) | |
| { | |
| moduleKey ^= 0x94d049bb133111ebull; | |
| } | |
| #endif | |
| if (haveAot || haveHwAot || haveO0Aot) | |
| { | |
| moduleKey = compiledModuleKey( | |
| moduleKey, | |
| haveHwAot ? TierSpecies::hw | |
| : haveO0Aot ? TierSpecies::o0 | |
| : TierSpecies::o3); | |
| } | |
| bool pinned = false; | |
| std::shared_ptr<void> lease = leaseCachedModule(moduleKey, tier); | |
| if (lease == nullptr) | |
| { | |
| if (haveAot || haveHwAot || haveO0Aot) | |
| { | |
| if (FILE* aot = fopen(aotPath, "rb")) | |
| { | |
| fseek(aot, 0, SEEK_END); | |
| long size = ftell(aot); | |
| fseek(aot, 0, SEEK_SET); | |
| moduleBytes.resize(size); | |
| size_t read = fread(moduleBytes.data(), 1, size, aot); | |
| fclose(aot); | |
| fprintf(stderr, "wasm aot: loaded %s (%zu bytes)\n", aotPath, read); | |
| tier = haveO0Aot ? ExecutionTier::aotO0 : ExecutionTier::aotO3; | |
| } | |
| } | |
| else | |
| { | |
| moduleBytes.assign(bytes.begin(), bytes.end()); | |
| } | |
| #if WASM_ENABLE_PRELINKED_AOT != 0 | |
| if (prelinked != nullptr) | |
| { | |
| loaded = (wasm_module_t) | |
| loadPrelinkedAotModule(*prelinked, error, sizeof(error)); | |
| if (loaded != nullptr) | |
| { | |
| tier = ExecutionTier::aotO3; | |
| pinned = true; | |
| fprintf( | |
| stderr, | |
| "wasm aot: prelinked module %016llx (%zu byte text)\n", | |
| (unsigned long long)plainKey, | |
| 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); | |
| moduleKey = plainKey; | |
| lease = leaseCachedModule(moduleKey, tier); | |
| } | |
| } | |
| if (loaded == nullptr && lease == nullptr) | |
| #endif | |
| { | |
| auto loadBytes = [&]() { | |
| loaded = wasm_runtime_load( | |
| moduleBytes.data(), | |
| (uint32_t)moduleBytes.size(), | |
| error, | |
| sizeof(error)); | |
| }; | |
| loadBytes(); | |
| if (loaded == nullptr && (haveAot || haveHwAot || haveO0Aot)) | |
| { | |
| // A stale artifact, say from another WAMR, must not fail the | |
| // file; drop to the wasm bytes under the plain key. | |
| fprintf( | |
| stderr, | |
| "wasm aot: %s rejected (%s); falling back\n", | |
| aotPath, | |
| error); | |
| haveAot = haveHwAot = haveO0Aot = false; | |
| tier = ExecutionTier::interp; | |
| interpReason = std::string("aot artifact rejected, ") + error; | |
| // Every later load would read and reject it again. | |
| if (ladderArtifact) | |
| { | |
| ModuleTierLadder::instance().rejectArtifact( | |
| plainKey, | |
| species, | |
| interpReason); | |
| } | |
| else | |
| { | |
| remove(aotPath); | |
| } | |
| moduleKey = plainKey; | |
| lease = leaseCachedModule(moduleKey, tier); | |
| if (lease == nullptr) | |
| { | |
| moduleBytes.assign(bytes.begin(), bytes.end()); | |
| loadBytes(); | |
| } | |
| } | |
| } | |
| if (loaded != nullptr) | |
| { | |
| auto entry = std::make_shared<SharedWasmModule>(); | |
| if (wasm_runtime_rive_module_hw_bounds(loaded)) | |
| { | |
| entry->hardwareBoundsWasm.assign(bytes.begin(), bytes.end()); | |
| } | |
| entry->bytes = std::move(moduleBytes); | |
| entry->module = loaded; | |
| entry->tier = tier; | |
| entry->pinned = pinned; | |
| // Like entry, declared before the lock so modules unload once it | |
| // is released, where they stall no other boot. | |
| std::vector<std::shared_ptr<SharedWasmModule>> evicted; | |
| std::lock_guard<std::mutex> lock(sharedModuleCacheMutex()); | |
| auto inserted = sharedModuleCache().try_emplace(moduleKey, entry); | |
| if (inserted.second) | |
| { | |
| lease = leaseLocked(entry); | |
| evictIdleModulesLocked(evicted); | |
| } | |
| else | |
| { | |
| // Another thread loaded the same bytes first, so entry | |
| // unloads this copy. | |
| tier = inserted.first->second->tier; | |
| lease = leaseLocked(inserted.first->second); | |
| } | |
| } | |
| } | |
| if (lease == nullptr) | |
| { | |
| outError = error; | |
| return nullptr; | |
| } | |
| // Prelinked code runs compiled whatever the ladder said. | |
| if (tier != ExecutionTier::interp) | |
| { | |
| interpReason.clear(); | |
| } | |
| return lease; | |
| } | |
| std::shared_ptr<void> WasmScriptingVM::loadFallbackModule( | |
| Span<const uint8_t> bytes, | |
| const char* what, | |
| ExecutionTier& tier, | |
| std::string& interpReason) | |
| { | |
| std::string error; | |
| auto lease = loadSharedModule(bytes, tier, interpReason, error, true); | |
| if (lease != nullptr) | |
| { | |
| fprintf( | |
| stderr, | |
| "wasm aot: no hardware bounds reservation left, %s falls back to " | |
| "software bounds on %s\n", | |
| what, | |
| tierName(tier, interpReason).c_str()); | |
| } | |
| else | |
| { | |
| fprintf( | |
| stderr, | |
| "wasm aot: no hardware bounds reservation left, and %s did not load " | |
| "on software bounds: %s\n", | |
| what, | |
| error.c_str()); | |
| } | |
| return lease; | |
| } | |
| Span<const uint8_t> WasmScriptingVM::hardwareBoundsWasm( | |
| const std::shared_ptr<void>& lease) | |
| { | |
| std::lock_guard<std::mutex> lock(sharedModuleCacheMutex()); | |
| for (const auto& cached : sharedModuleCache()) | |
| { | |
| const std::vector<uint8_t>& wasm = cached.second->hardwareBoundsWasm; | |
| if (cached.second->module == lease.get()) | |
| { | |
| return Span<const uint8_t>(wasm.data(), wasm.size()); | |
| } | |
| } | |
| return {}; | |
| } | |
| bool WasmScriptingVM::hardwareBounds() const | |
| { | |
| return m_state != nullptr && m_state->module != nullptr && | |
| wasm_runtime_rive_module_hw_bounds(m_state->module); | |
| } | |
| bool WasmScriptingVM::init(Span<const uint8_t> module) | |
| { | |
| // Unsigned modules are safe only as bytecode WAMR compiles itself; AOT | |
| // comes solely from our own artifacts, found by this bytecode's hash. | |
| if (module.size() < 4 || memcmp(module.data(), "\0asm", 4) != 0) | |
| { | |
| m_lastError = "script module is not wasm bytecode"; | |
| return false; | |
| } | |
| m_state = std::make_unique<WamrState>(); | |
| m_state | |
| ->lease = loadSharedModule(module, m_tier, m_interpReason, m_lastError); | |
| m_state->module = (wasm_module_t)m_state->lease.get(); | |
| if (m_state->module == nullptr) | |
| { | |
| m_lastError = "module load failed: " + m_lastError; | |
| 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); | |
| } | |
| } | |
| 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); | |
| noteModuleExport(moduleExport.name); | |
| } | |
| if (const char* refusal = WasmScriptingVMNatives::startRefusal(this)) | |
| { | |
| m_lastError = std::string("module instantiate failed: ") + refusal; | |
| return false; | |
| } | |
| // The top level may start lanes. | |
| attachLanes(module); | |
| // A top level that decodes a file starts another module inside this one. | |
| const std::function<void(const char*, size_t)>* outerPrint = s_bootPrint; | |
| WasmScriptingVM* outerBooting = s_booting; | |
| s_bootPrint = &m_print; | |
| s_booting = this; | |
| char error[256] = {0}; | |
| // Module start is a call into the module like any other: the top | |
| // level's probes need the bracket, its budget bounds it, and its trap | |
| // is the instantiate error. | |
| if (m_debugHooks != nullptr) | |
| { | |
| m_debugHooks->onCallBegin(*this); | |
| } | |
| // Set once: WAMR keeps it in a plain global, as ARMv7 Android loads no | |
| // ELF TLS, and the hook finds its VM through s_booting. | |
| static std::once_flag startHookOnce; | |
| std::call_once(startHookOnce, [] { | |
| wasm_runtime_rive_set_start_hook( | |
| [](wasm_exec_env_t env, bool begin, void*) { | |
| WasmScriptingVM* vm = s_booting; | |
| // A lane the top level joined starts on this thread too. | |
| if (vm == nullptr || WasmLane::current() != nullptr) | |
| { | |
| return; | |
| } | |
| if (begin) | |
| { | |
| // Trusted modules run their top level unbounded, like their | |
| // calls. | |
| if (vm->m_hostBudget && !vm->m_budgetArmed && vm->boundsCalls()) | |
| { | |
| vm->armWatchdog(env); | |
| } | |
| vm->armDeadline(); | |
| // A wait moves a metered module's fuel on with a call. | |
| vm->m_state->execEnv = env; | |
| } | |
| else | |
| { | |
| vm->m_state->execEnv = nullptr; | |
| if (vm->m_deadlineArmed) | |
| { | |
| vm->disarmDeadline(); | |
| } | |
| if (vm->m_budgetArmed && vm->m_budgetSlot->env == env) | |
| { | |
| vm->disarmWatchdog(); | |
| } | |
| } | |
| uint32_t fuelMs = (uint32_t)WasmScriptingVMNatives::startFuelMs(vm); | |
| wasm_module_inst_t inst = wasm_runtime_get_module_inst(env); | |
| wasm_function_inst_t arm = wasm_runtime_lookup_function( | |
| inst, | |
| "__riveFuelArm"); | |
| wasm_function_inst_t end = wasm_runtime_lookup_function( | |
| inst, | |
| "__riveFuelEnd"); | |
| // A start that trapped leaves no fuel to put back. | |
| if (fuelMs != 0 && arm != nullptr && end != nullptr && | |
| wasm_runtime_get_exception(inst) == nullptr) | |
| { | |
| wasm_runtime_call_wasm( | |
| env, | |
| begin ? arm : end, | |
| begin ? 1 : 0, | |
| &fuelMs); | |
| } | |
| }, | |
| nullptr); | |
| }); | |
| // A start nested in a call must not see the region it holds, and a top | |
| // level that trapped inside exclusive must not leave its region held. | |
| uint32_t outerRegionId; | |
| SharedRegion* outerRegion = stashHeldRegion(&outerRegionId); | |
| ensureWasmThreadEnv(); | |
| auto instantiate = [&]() { | |
| m_state->instance = wasm_runtime_instantiate( | |
| m_state->module, | |
| 512 * 1024, | |
| 0, | |
| error, | |
| sizeof(error)); | |
| }; | |
| instantiate(); | |
| if (m_state->instance == nullptr && | |
| strstr(error, WASM_RIVE_HW_RESERVE_FAILED) != nullptr) | |
| { | |
| if (auto checked = loadFallbackModule( | |
| module, | |
| "main module", | |
| m_tier, | |
| m_interpReason)) | |
| { | |
| m_state->lease = std::move(checked); | |
| m_state->module = (wasm_module_t)m_state->lease.get(); | |
| instantiate(); | |
| } | |
| } | |
| restoreHeldRegion(outerRegionId, outerRegion); | |
| if (m_debugHooks != nullptr) | |
| { | |
| m_debugHooks->onCallEnd( | |
| *this, | |
| m_state->instance == nullptr ? error : nullptr); | |
| } | |
| s_booting = outerBooting; | |
| s_bootPrint = outerPrint; | |
| if (m_state->instance == nullptr || m_poisoned) | |
| { | |
| // The runtime's exception buffer truncates what start threw, and AOT | |
| // code traps on a timeout as unreachable. | |
| std::string detail = m_poisoned ? "execution exceeded timeout" | |
| : m_moduleErrorDetail.empty() ? std::string(error) | |
| : m_moduleErrorDetail; | |
| m_lastError = "module instantiate failed: " + detail; | |
| return false; | |
| } | |
| m_state->findCallState(); | |
| 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); | |
| return bootModule(); | |
| } | |
| #endif | |
| void WasmScriptingVM::noteModuleExport(const char* name) | |
| { | |
| m_utf16Strings |= strcmp(name, "__riveUtf16Strings") == 0; | |
| m_utf16HostStrings |= strcmp(name, "__riveUtf16HostStrings") == 0; | |
| m_hostBudget |= strcmp(name, "__riveHostBudget") == 0; | |
| m_fuelBudget |= strcmp(name, "__riveFuelCheck") == 0; | |
| m_fuelShift |= strcmp(name, "__riveFuelShift") == 0; | |
| m_fetchBlobBody |= strcmp(name, "__riveFetchBlobBody") == 0; | |
| } | |
| bool WasmScriptingVM::bootModule() | |
| { | |
| // 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 = hasExport("riveRegister") && | |
| (leakEnv == nullptr || strcmp(leakEnv, "0") != 0); | |
| m_collectedRuntime = hasExport("__riveCollected"); | |
| m_handleWatch = m_leakWatch; | |
| // Frame collector modules scavenge at every boundary instead of | |
| // rewinding or finalizing. | |
| m_frameMinor = hasExport("__riveFrameMinor"); | |
| m_frameRelease = m_frameMinor && hasExport("__riveFrameRelease"); | |
| m_frameMajorsProbe = hasExport("__riveFrameMajors"); | |
| // Stub modules report their bump position, finer than page counts. | |
| m_heapUsedProbe = hasExport("__riveHeapUsed"); | |
| m_rawAllocExport = hasExport("__riveRawAlloc"); | |
| m_inputSlotExport = hasExport("host_obj_input_slot"); | |
| m_pathEffectV2 = hasExport("host_obj_path_effect_v2"); | |
| m_pointerEventV2 = hasExport("host_obj_pointer_event_v2"); | |
| m_layoutResizeV2 = hasExport("host_obj_layout_resize_v2"); | |
| m_transitionManagesToExport = hasExport("host_obj_transition_manages_to"); | |
| callModule("__wasm_call_ctors", 0, nullptr); | |
| m_L = callModule("host_newstate", 0, nullptr); | |
| if (m_L == 0) | |
| { | |
| m_lastError = "host_newstate failed"; | |
| return false; | |
| } | |
| // A deterministic run pins math.random, as ScriptingVM::init does for | |
| // native Luau. Older blobs without the export stay clock-seeded. | |
| if (File::deterministicMode && hasExport("host_randomseed")) | |
| { | |
| uint32_t seedArgs[2] = {m_L, (uint32_t)File::deterministicSeed}; | |
| callModule("host_randomseed", 2, seedArgs); | |
| } | |
| 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; | |
| } | |
| const char* WasmScriptingVM::frameBoundary() | |
| { | |
| const char* warning = collectFrame(); | |
| if (m_decodedVMs.empty()) | |
| { | |
| return warning; | |
| } | |
| m_frameReport = warning != nullptr ? warning : ""; | |
| // A file a script decoded has no host to give it boundaries. | |
| for (WasmScriptingVM* decoded : std::vector<WasmScriptingVM*>(m_decodedVMs)) | |
| { | |
| // Releasing one file can end another. | |
| if (std::find(m_decodedVMs.begin(), m_decodedVMs.end(), decoded) == | |
| m_decodedVMs.end()) | |
| { | |
| continue; | |
| } | |
| if (const char* notice = decoded->frameBoundary()) | |
| { | |
| joinFrameReport(m_frameReport, notice); | |
| } | |
| } | |
| return m_frameReport.empty() ? nullptr : m_frameReport.c_str(); | |
| } | |
| const char* WasmScriptingVM::frameBoundaryWarnings() | |
| { | |
| const char* report = frameBoundary(); | |
| std::string warnings; | |
| for (std::string_view rest = report != nullptr ? report : ""; !rest.empty();) | |
| { | |
| std::string_view line = rest.substr(0, rest.find('\n')); | |
| rest.remove_prefix(std::min(rest.size(), line.size() + 1)); | |
| if (line != kFrameGcNotice) | |
| { | |
| joinFrameReport(warnings, line); | |
| } | |
| } | |
| m_frameReport = std::move(warnings); | |
| return m_frameReport.empty() ? nullptr : m_frameReport.c_str(); | |
| } | |
| void WasmScriptingVM::joinFrameReport( | |
| std::string& joined, | |
| std::string_view report) | |
| { | |
| if (!joined.empty()) | |
| { | |
| joined += '\n'; | |
| } | |
| joined += report; | |
| } | |
| const char* WasmScriptingVM::collectFrame() | |
| { | |
| if (!valid()) | |
| { | |
| return nullptr; | |
| } | |
| releaseReadHolds(); | |
| if (m_frameMinor) | |
| { | |
| uint32_t promoted = 0; | |
| if (callModuleChecked("__riveFrameMinor", 0, nullptr, &promoted) == | |
| CallOutcome::trapped) | |
| { | |
| // Objects may be half moved, so nothing may run on the heap. | |
| poison(); | |
| return "script stopped: its collector trapped mid collection"; | |
| } | |
| if (getenv("RIVE_FRAME_GC_DEBUG") != nullptr) | |
| { | |
| fprintf(stderr, "frameMinor promoted=%u\n", promoted); | |
| callModule("__riveFrameVerify", 0, nullptr); | |
| } | |
| if (m_frameRelease) | |
| { | |
| // The handles the collection found dead. Releasing one can | |
| // destroy an artboard instance whose scripted objects call | |
| // back into the module (host_obj_dispose, host_unref), so this | |
| // runs after the collection, never inside it. (A host that | |
| // skips it gets them released at the next boundary.) | |
| callModule("__riveFrameRelease", 0, nullptr); | |
| } | |
| if (!m_frameMinorAnnounced) | |
| { | |
| m_frameMinorAnnounced = true; | |
| return kFrameGcNotice; | |
| } | |
| if (const char* warning = handleLeakWarning()) | |
| { | |
| return warning; | |
| } | |
| return heapGrowthWarning(); | |
| } | |
| if (const char* warning = handleLeakWarning()) | |
| { | |
| return warning; | |
| } | |
| return heapGrowthWarning(); | |
| } | |
| #ifdef __EMSCRIPTEN__ | |
| // A page cannot set the environment. | |
| static constexpr const char* kLeakWarnOffHint = ""; | |
| #else | |
| static constexpr const char* | |
| kLeakWarnOffHint = " RIVE_WASM_LEAK_WARN=0 silences this."; | |
| #endif | |
| const char* WasmScriptingVM::heapGrowthWarning() | |
| { | |
| if (!m_leakWatch || !m_advancedOnce) | |
| { | |
| return nullptr; | |
| } | |
| uint32_t pages = 0; | |
| uint32_t maxPages = 0; | |
| if (!memoryLimits(&pages, &maxPages)) | |
| { | |
| return nullptr; | |
| } | |
| // The stub bump position beats page counts when available. | |
| if (m_heapUsedProbe) | |
| { | |
| pages = std::max(1u, callModule("__riveHeapUsed", 0, nullptr) >> 16); | |
| } | |
| 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}; | |
| 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).%s", | |
| grownMB, | |
| frames, | |
| projection, | |
| kLeakWarnOffHint); | |
| } | |
| 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.%s", | |
| grownMB, | |
| frames, | |
| projection, | |
| kLeakWarnOffHint); | |
| } | |
| 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::audioStream: | |
| return "audioStream"; | |
| case Tag::drawable: | |
| return "drawable"; | |
| case Tag::riveFile: | |
| return "riveFile"; | |
| case Tag::bindableArtboard: | |
| return "bindableArtboard"; | |
| case Tag::lane: | |
| return "lane"; | |
| case Tag::empty: | |
| case Tag::count: | |
| break; | |
| } | |
| return "unknown"; | |
| } | |
| #ifndef __EMSCRIPTEN__ | |
| bool WasmScriptingVM::memoryLimits(uint32_t* pages, uint32_t* maxPages) const | |
| { | |
| if (m_state == nullptr || m_state->instance == nullptr) | |
| { | |
| return false; | |
| } | |
| wasm_memory_inst_t memory = wasm_runtime_get_default_memory( | |
| m_state->instance); | |
| if (memory == nullptr) | |
| { | |
| return false; | |
| } | |
| *pages = (uint32_t)wasm_memory_get_cur_page_count(memory); | |
| *maxPages = (uint32_t)wasm_memory_get_max_page_count(memory); | |
| return true; | |
| } | |
| #endif | |
| uint32_t WasmScriptingVM::memoryPages() const | |
| { | |
| uint32_t pages = 0; | |
| uint32_t maxPages = 0; | |
| return memoryLimits(&pages, &maxPages) ? pages : 0; | |
| } | |
| 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.live(); | |
| 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_handleCollects = 0; | |
| 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; | |
| } | |
| // A major already in flight keeps wrappers that died after it began, | |
| // so growth is judged by one requested once the first finishes. | |
| constexpr uint8_t kHandleJudgeCollects = 2; | |
| if (m_collectedRuntime) | |
| { | |
| uint32_t majors = frameMajors(); | |
| if (m_handleCollects != 0 && majors == m_handleCollectMajors) | |
| { | |
| return nullptr; | |
| } | |
| if (m_handleCollects < kHandleJudgeCollects) | |
| { | |
| m_handleCollects++; | |
| m_handleCollectMajors = majors; | |
| callModule("__riveCollect", 0, nullptr); | |
| return nullptr; | |
| } | |
| } | |
| m_handleCollects = 0; | |
| // 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().%s", | |
| grown, | |
| frames, | |
| counts[top], | |
| handleTagName((HandleTable::Tag)top), | |
| kLeakWarnOffHint); | |
| m_leakWarning = buffer; | |
| return m_leakWarning.c_str(); | |
| } | |
| 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; | |
| } | |
| copyToModule(namePtr, name.c_str(), (uint32_t)name.size() + 1); | |
| copyToModule(bcPtr, bytecode.data(), (uint32_t)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 ? resolveModuleString(messagePtr) | |
| : 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 --------------------- | |
| #ifndef __EMSCRIPTEN__ | |
| bool WasmScriptingVM::valid() const | |
| { | |
| return m_state != nullptr && m_state->execEnv != nullptr && m_L != 0 && | |
| !m_poisoned; | |
| } | |
| #endif | |
| 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) | |
| { | |
| double args[3] = {(double)m_L, (double)selfRef, (double)contextRef}; | |
| double status = 0; | |
| // An init can run a nested one; each reads only its own misses. | |
| bool outerMissing = m_missingRequestedData; | |
| m_missingRequestedData = false; | |
| CallOutcome outcome = callF64("host_obj_user_init", args, 3, &status); | |
| bool missing = m_missingRequestedData; | |
| m_missingRequestedData = outerMissing; | |
| switch (outcome) | |
| { | |
| case CallOutcome::missing: | |
| return InitResult::notImplemented; | |
| case CallOutcome::trapped: | |
| m_lastError = lastTrap().message; | |
| return InitResult::failed; | |
| case CallOutcome::ok: | |
| break; | |
| } | |
| if (status == 0) | |
| { | |
| return InitResult::notImplemented; | |
| } | |
| return status == 1 && !missing ? InitResult::succeeded : InitResult::failed; | |
| } | |
| bool WasmScriptingVM::callAdvance( | |
| ScriptedObject* object, | |
| int selfRef, | |
| float elapsedSeconds) | |
| { | |
| double args[3] = {(double)m_L, (double)selfRef, elapsedSeconds}; | |
| double keepGoing = 0; | |
| if (callF64("host_obj_advance", args, 3, &keepGoing) != CallOutcome::ok) | |
| { | |
| return false; | |
| } | |
| m_advancedOnce = true; | |
| return keepGoing != 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; | |
| } | |
| copyToModule(scratch, args, (uint32_t)(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(); | |
| return callPathEffectUpdate( | |
| object, | |
| selfRef, | |
| sourcePath, | |
| paint, | |
| shapePaint->parentTransformComponent(), | |
| outPath); | |
| } | |
| bool WasmScriptingVM::callPathEffectUpdate( | |
| ScriptedObject* object, | |
| int selfRef, | |
| const RawPath& sourcePath, | |
| const PathEffectPaintWire& paint, | |
| TransformComponent* shape, | |
| RawPath* outPath) | |
| { | |
| if (!valid() || outPath == nullptr) | |
| { | |
| return false; | |
| } | |
| 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 = m_pathEffectV2; | |
| size_t scopeStart = m_scopedNodes.size(); | |
| uint32_t nodeHandle = 0; | |
| 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::callHitExport( | |
| const char* name, | |
| const double* args, | |
| uint32_t argc, | |
| HitResult* outResult) | |
| { | |
| double hit = 0; | |
| if (callF64(name, args, argc, &hit) != CallOutcome::ok || hit == 0) | |
| { | |
| return false; | |
| } | |
| *outResult = (HitResult)((int)hit - 1); | |
| return true; | |
| } | |
| bool WasmScriptingVM::callPointerEvent( | |
| ScriptedObject* object, | |
| int selfRef, | |
| const char* method, | |
| int pointerId, | |
| Vec2D localPosition, | |
| ListenerType hitType, | |
| float timeStamp, | |
| HitResult* outResult) | |
| { | |
| if (!valid()) | |
| { | |
| return false; | |
| } | |
| GuestName methodCopy(this, method); | |
| uint32_t methodPtr = methodCopy.ptr; | |
| if (methodPtr == 0) | |
| { | |
| return false; | |
| } | |
| double args[8] = { | |
| (double)m_L, | |
| (double)selfRef, | |
| (double)methodPtr, | |
| (double)pointerId, | |
| localPosition.x, | |
| localPosition.y, | |
| (double)hitType, | |
| timeStamp}; | |
| // Modules built before _v2 take no listener type or time stamp. | |
| return m_pointerEventV2 | |
| ? callHitExport("host_obj_pointer_event_v2", args, 8, outResult) | |
| : callHitExport("host_obj_pointer_event", args, 6, outResult); | |
| } | |
| bool WasmScriptingVM::callScrollEvent( | |
| ScriptedObject* object, | |
| int selfRef, | |
| int pointerId, | |
| Vec2D localPosition, | |
| const ScrollEvent& event, | |
| float timeStamp, | |
| HitResult* outResult) | |
| { | |
| if (!valid()) | |
| { | |
| return false; | |
| } | |
| double args[10] = { | |
| (double)m_L, | |
| (double)selfRef, | |
| (double)pointerId, | |
| localPosition.x, | |
| localPosition.y, | |
| event.delta.x, | |
| event.delta.y, | |
| (double)event.phase, | |
| event.precise ? 1.0 : 0.0, | |
| timeStamp}; | |
| // Modules built before scroll input lack the export and never scroll. | |
| return callHitExport("host_obj_scroll_event", args, 10, outResult); | |
| } | |
| 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; | |
| } | |
| copyToModule(textPtr, bytes.data(), (uint32_t)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() || !m_transitionManagesToExport) | |
| { | |
| 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; | |
| } | |
| HostRenderer host{renderer}; | |
| uint32_t rendererHandle = m_handles.mint(HandleTable::Tag::renderer, &host); | |
| 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); | |
| host.restoreSaves(); | |
| releaseTransitionChild(fromHandle); | |
| releaseTransitionChild(toHandle); | |
| m_handles.release(rendererHandle, HandleTable::Tag::renderer); | |
| } | |
| void WasmScriptingVM::callLayoutResize( | |
| ScriptedObject* object, | |
| int selfRef, | |
| Vec2D size) | |
| { | |
| if (!valid()) | |
| { | |
| return; | |
| } | |
| double args[5] = | |
| {(double)m_L, (double)selfRef, size.x, size.y, displayScale()}; | |
| double unused = 0; | |
| // Modules built before _v2 take no surface scale. | |
| if (m_layoutResizeV2) | |
| { | |
| callF64("host_obj_layout_resize_v2", args, 5, &unused); | |
| } | |
| else | |
| { | |
| callF64("host_obj_layout_resize", args, 4, &unused); | |
| } | |
| } | |
| 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; | |
| } | |
| uint32_t namePtr = guestString(name); | |
| if (namePtr == 0) | |
| { | |
| return -1; | |
| } | |
| uint32_t args[3] = {m_L, (uint32_t)selfRef, namePtr}; | |
| uint32_t slot = 0; | |
| CallOutcome outcome = callModuleChecked( | |
| "host_obj_input_slot", | |
| 3, | |
| args, | |
| &slot); | |
| guestFree(namePtr); | |
| // A trap answers nothing, so the next lookup asks again. | |
| if (outcome != CallOutcome::ok) | |
| { | |
| return -1; | |
| } | |
| slots.emplace(name, (int32_t)slot); | |
| return (int32_t)slot; | |
| } | |
| bool WasmScriptingVM::legacyInputs() { return !m_inputSlotExport; } | |
| 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; | |
| } | |
| double args[4] = {(double)m_L, (double)selfRef, (double)arg, value}; | |
| double unused = 0; | |
| callF64("host_obj_set_number", args, 4, &unused); | |
| 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}; | |
| uint32_t unused = 0; | |
| if (callModuleChecked("host_obj_set_view_model", 4, args, &unused) != | |
| CallOutcome::ok) | |
| { | |
| releaseHostHandle(this, handle, HandleTable::Tag::viewModelInstance); | |
| } | |
| 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}; | |
| uint32_t unused = 0; | |
| if (callModuleChecked("host_obj_set_artboard", 4, args, &unused) != | |
| CallOutcome::ok) | |
| { | |
| releaseHostHandle(this, handle, HandleTable::Tag::artboard); | |
| } | |
| 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) | |
| { | |
| copyToModule(ptr, bytes.data(), (uint32_t)bytes.size()); | |
| } | |
| return ptr; | |
| } | |
| WasmScriptingVM::GuestName::GuestName(WasmScriptingVM* vm, const char* name) | |
| : m_vm(vm) | |
| { | |
| if (!vm->m_rawAllocExport) | |
| { | |
| ptr = vm->guestString(name); | |
| m_owned = true; | |
| return; | |
| } | |
| auto& names = vm->m_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); | |
| } | |
| #ifdef __EMSCRIPTEN__ | |
| // The runner sets the booting VM around a module's start. | |
| extern "C" EMSCRIPTEN_KEEPALIVE void rive_web_vm_booting(uint32_t handle) | |
| { | |
| WasmScriptingVMNatives::booting((WasmScriptingVM*)(uintptr_t)handle); | |
| } | |
| extern "C" EMSCRIPTEN_KEEPALIVE int rive_web_vm_start_fuel(uint32_t handle) | |
| { | |
| return WasmScriptingVMNatives::startFuelMs( | |
| (WasmScriptingVM*)(uintptr_t)handle); | |
| } | |
| extern "C" EMSCRIPTEN_KEEPALIVE const char* rive_web_vm_start_refusal( | |
| uint32_t handle) | |
| { | |
| return WasmScriptingVMNatives::startRefusal( | |
| (WasmScriptingVM*)(uintptr_t)handle); | |
| } | |
| // The std's console imports, by the method codes the page's env block uses. | |
| extern "C" void rive_web_console( | |
| uint32_t handle, | |
| uint32_t method, | |
| const char* text, | |
| uint32_t length) | |
| { | |
| WasmScriptingVM* vm = bootVmFromHandle(handle); | |
| WasmStringArg utf8(vm, text, length); | |
| std::string value(utf8.data(), utf8.size()); | |
| switch (method) | |
| { | |
| case 0: | |
| consoleWrite(vmFromHandle(handle), value); | |
| break; | |
| case 1: | |
| consoleTimeStart(vm, value); | |
| break; | |
| default: | |
| consoleTimeReport(vm, value, method == 3); | |
| break; | |
| } | |
| } | |
| extern "C" void rive_web_trace( | |
| uint32_t handle, | |
| const char* text, | |
| uint32_t length, | |
| int32_t count, | |
| double a0, | |
| double a1, | |
| double a2, | |
| double a3, | |
| double a4) | |
| { | |
| WasmStringArg utf8(bootVmFromHandle(handle), text, length); | |
| double args[5] = {a0, a1, a2, a3, a4}; | |
| traceWrite( | |
| vmFromHandle(handle), | |
| std::string(utf8.data(), utf8.size()), | |
| count, | |
| args); | |
| } | |
| #endif | |
| // Browser-lane exports over the same impl cores; nothing off emscripten. | |
| #include "wasm_natives_web_gen.hpp" | |
| #endif |