| #ifdef WITH_RIVE_SCRIPTING_WASM | |
| #include "rive/wasm/wasm_processors.hpp" | |
| #include "rive/async/compare_exchange.hpp" | |
| #include "rive/wasm/wasm_shared.hpp" | |
| #ifdef WITH_RIVE_AUDIO | |
| #include "rive/audio/audio_engine.hpp" | |
| #endif | |
| #ifndef __EMSCRIPTEN__ | |
| #include "wasm_export.h" | |
| #else | |
| #include <emscripten/em_js.h> | |
| #include <emscripten/emscripten.h> | |
| #endif | |
| #include <algorithm> | |
| #include <cmath> | |
| #include <cstdio> | |
| #include <cstdlib> | |
| #include <cstring> | |
| using namespace rive; | |
| namespace | |
| { | |
| constexpr uint32_t kProcessorStack = 256 * 1024; | |
| #if defined(WITH_RIVE_AUDIO) && !defined(__EMSCRIPTEN__) | |
| class ProcessorRenderer : public AudioRenderer | |
| { | |
| public: | |
| explicit ProcessorRenderer(WasmProcessor* processor) : m_processor(processor) | |
| {} | |
| void render(float* out, uint32_t frames, uint64_t startFrame) override | |
| { | |
| m_processor->render(out, frames, startFrame); | |
| } | |
| private: | |
| WasmProcessor* m_processor; | |
| }; | |
| #endif | |
| #ifndef __EMSCRIPTEN__ | |
| // Bounded inits get a thread no long job delays, leaked as it may outlive | |
| // static destructors. | |
| JobPool& initPool() | |
| { | |
| static JobPool* pool = new JobPool(1); | |
| return *pool; | |
| } | |
| #endif | |
| struct OfflineRender | |
| { | |
| bool on = false; | |
| ProcessorFormat format; | |
| uint64_t frame = 0; | |
| }; | |
| #if RIVE_PROCESSOR_AUDIO() | |
| std::mutex& offlineMutex() | |
| { | |
| static std::mutex mutex; | |
| return mutex; | |
| } | |
| OfflineRender& offlineSlot() | |
| { | |
| static OfflineRender render; | |
| return render; | |
| } | |
| OfflineRender offlineState() | |
| { | |
| std::lock_guard<std::mutex> lock(offlineMutex()); | |
| return offlineSlot(); | |
| } | |
| #else | |
| OfflineRender offlineState() { return {}; } | |
| #endif | |
| } // namespace | |
| WasmProcessor::WasmProcessor( | |
| rcp<WasmLaneModule> module, | |
| uint32_t entry, | |
| std::vector<uint8_t> input, | |
| ProcessorFormat format, | |
| uint32_t hardBlocks) | |
| : WasmLane(LaneKind::processor, std::move(module), entry, std::move(input)) | |
| , m_format(format) | |
| , m_hardBlocks(hardBlocks) | |
| {} | |
| WasmProcessor::~WasmProcessor() | |
| { | |
| stop(); | |
| endInstance(); | |
| } | |
| #if defined(__EMSCRIPTEN__) && RIVE_PROCESSOR_AUDIO() | |
| static void stopRealtime(uint32_t id); | |
| #endif | |
| bool WasmProcessor::settle(LaneState state) | |
| { | |
| LaneState now = m_state.load(); | |
| while (now < LaneState::done) | |
| { | |
| if (compareExchangeWeak(m_state, now, state)) | |
| { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| void WasmProcessor::stop() | |
| { | |
| settle(LaneState::stopped); | |
| #if defined(WITH_RIVE_AUDIO) && !defined(__EMSCRIPTEN__) | |
| std::lock_guard<std::mutex> lock(m_nodeMutex); | |
| if (m_node != nullptr) | |
| { | |
| m_engine->detachRenderer(m_node); | |
| m_node = nullptr; | |
| // Nothing renders it again, and its watchdog slot keeps the watchdog | |
| // ticking. | |
| endInstance(); | |
| } | |
| m_engine = nullptr; | |
| #endif | |
| #if defined(__EMSCRIPTEN__) && RIVE_PROCESSOR_AUDIO() | |
| if (m_audioId != 0) | |
| { | |
| stopRealtime(m_audioId); | |
| m_audioId = 0; | |
| } | |
| #endif | |
| } | |
| #if RIVE_PROCESSOR_AUDIO() | |
| void WasmProcessor::run() | |
| { | |
| WasmLane* outerLane = WasmLane::setCurrent(this); | |
| bool ok = m_state.load() == LaneState::starting && startInstance(); | |
| WasmLane::setCurrent(outerLane); | |
| releaseTrappedRegion(); | |
| #if defined(WITH_RIVE_AUDIO) && !defined(__EMSCRIPTEN__) | |
| std::lock_guard<std::mutex> lock(m_nodeMutex); | |
| // Joined before it reads as running, so a running processor is in the | |
| // mix. It renders silence until then. | |
| if (ok && m_engine != nullptr && m_state.load() == LaneState::starting) | |
| { | |
| m_renderer = std::make_unique<ProcessorRenderer>(this); | |
| m_node = m_engine->attachRenderer(m_renderer.get()); | |
| if (m_node == nullptr) | |
| { | |
| fail("processor could not join the audio mix"); | |
| ok = false; | |
| } | |
| } | |
| #endif | |
| LaneState state = LaneState::starting; | |
| if (compareExchangeStrong( | |
| m_state, | |
| state, | |
| ok ? LaneState::running : LaneState::failed) && | |
| ok) | |
| { | |
| return; | |
| } | |
| // A failed init or a stop that landed during it never renders. | |
| #if defined(WITH_RIVE_AUDIO) && !defined(__EMSCRIPTEN__) | |
| if (m_node != nullptr) | |
| { | |
| m_engine->detachRenderer(m_node); | |
| m_node = nullptr; | |
| } | |
| #endif | |
| endInstance(); | |
| } | |
| void WasmProcessor::render(float* out, uint32_t frames, uint64_t startFrame) | |
| { | |
| uint32_t channels = m_format.channels; | |
| uint32_t done = 0; | |
| WasmLane* outerLane = WasmLane::setCurrent(this); | |
| while (done < frames && m_state.load() == LaneState::running) | |
| { | |
| uint32_t block = std::min(frames - done, m_format.maxFrames); | |
| // A native may fail it without a trap, as a refused shared read does. | |
| if (!process(out + (size_t)done * channels, block, startFrame + done) || | |
| hasFailure()) | |
| { | |
| settle(LaneState::failed); | |
| break; | |
| } | |
| done += block; | |
| } | |
| WasmLane::setCurrent(outerLane); | |
| if (done < frames) | |
| { | |
| releaseTrappedRegion(); | |
| } | |
| else | |
| { | |
| // A kept region could outlive its file and be freed on the audio thread. | |
| forgetRecentRegions(); | |
| } | |
| memset( | |
| out + (size_t)done * channels, | |
| 0, | |
| (size_t)(frames - done) * channels * sizeof(float)); | |
| } | |
| #else | |
| // Every start here fails, so no processor runs. | |
| void WasmProcessor::run() {} | |
| #endif | |
| // --- the WAMR backend -------------------------------------------------------- | |
| #ifndef __EMSCRIPTEN__ | |
| bool WasmProcessor::startInstance() | |
| { | |
| ensureWasmThreadEnv(); | |
| if (!instantiate(kProcessorStack)) | |
| { | |
| return false; | |
| } | |
| // Registered here, so the audio thread never takes the watchdog's lock. | |
| m_budget = std::make_unique<WasmCallBudget>(m_execEnv); | |
| auto init = [&](auto&& step) { | |
| m_budget->arm(lane_layout::kProcessorInitMs); | |
| // Disarming clears an interrupt, so a cancel between steps ends here. | |
| if (cancelRequested()) | |
| { | |
| interruptCall(); | |
| } | |
| bool ok = step(); | |
| bool fired = m_budget->disarm(); | |
| if (!ok) | |
| { | |
| failTrap(fired ? lane_layout::kProcessorInitOverran : ""); | |
| } | |
| return ok; | |
| }; | |
| // (input, length, sampleRate f64, maxFrames, channels): the f64 takes | |
| // two cells. | |
| uint32_t argv[6] = {}; | |
| if (!init([&] { return runStart(argv); })) | |
| { | |
| return false; | |
| } | |
| uint32_t at = argv[0]; | |
| double rate = (double)m_format.sampleRate; | |
| memcpy(&argv[2], &rate, sizeof(rate)); | |
| argv[4] = m_format.maxFrames; | |
| argv[5] = m_format.channels; | |
| if (!init( | |
| [&] { return call(m_module->entryNames[m_entry].c_str(), 6, argv); })) | |
| { | |
| return false; | |
| } | |
| argv[0] = at; | |
| if (!init([&] { return call(lane_layout::kLaneFreeExport, 1, argv); })) | |
| { | |
| return false; | |
| } | |
| // (frames, startFrame f64) answering the block's address. | |
| m_processFn = lookup(lane_layout::kProcessorProcessExport, 3, 3); | |
| m_collectFn = lookup(lane_layout::kFrameMinorExport, 0, 1, true); | |
| return m_processFn != nullptr && !hasFailure(); | |
| } | |
| bool WasmProcessor::process(float* out, uint32_t frames, uint64_t startFrame) | |
| { | |
| // The thread pulling audio can change, as when a device reroutes. | |
| ensureWasmThreadEnv(); | |
| auto env = (wasm_exec_env_t)m_execEnv; | |
| uint32_t budgetMs = lane_layout::processorBlockBudgetMs( | |
| m_hardBlocks, | |
| frames, | |
| (double)m_format.sampleRate, | |
| !m_warm); | |
| auto inst = (wasm_module_inst_t)m_instance; | |
| wasm_val_t args[2]; | |
| args[0].kind = WASM_I32; | |
| args[0].of.i32 = (int32_t)frames; | |
| args[1].kind = WASM_F64; | |
| args[1].of.f64 = (double)startFrame; | |
| wasm_val_t result; | |
| size_t bytes = (size_t)frames * m_format.channels * sizeof(float); | |
| if (budgetMs != 0) | |
| { | |
| m_budget->arm(budgetMs); | |
| } | |
| m_warm = true; | |
| bool ok = wasm_runtime_call_wasm_a( | |
| env, | |
| (wasm_function_inst_t)m_processFn, | |
| 1, | |
| &result, | |
| 2, | |
| args) && | |
| wasm_runtime_validate_app_addr(inst, result.of.i32, bytes); | |
| if (ok) | |
| { | |
| memcpy(out, wasm_runtime_addr_app_to_native(inst, result.of.i32), bytes); | |
| // Back at the host no frame is live, so the block's garbage goes. | |
| uint32_t argv[1] = {0}; | |
| ok = m_collectFn == nullptr || wasm_runtime_call_wasm( | |
| env, | |
| (wasm_function_inst_t)m_collectFn, | |
| 0, | |
| argv); | |
| } | |
| bool fired = m_budget->disarm(); | |
| if (!ok) | |
| { | |
| // Built in place, as this may be the audio thread. An interpreter's | |
| // tier says what to pass for compiled code. | |
| char overran[512] = ""; | |
| if (fired) | |
| { | |
| snprintf( | |
| overran, | |
| sizeof(overran), | |
| "%s on %s", | |
| lane_layout::kProcessorOverran, | |
| m_module->currentTier().c_str()); | |
| } | |
| failTrap(overran); | |
| return false; | |
| } | |
| return true; | |
| } | |
| void WasmProcessor::endInstance() | |
| { | |
| m_budget = nullptr; | |
| teardown(); | |
| } | |
| #else | |
| // --- the browser backend ----------------------------------------------------- | |
| // clang-format off | |
| EM_JS_DEPS(riveWebProcessors, "$stringToNewUTF8"); | |
| EM_JS(char*, riveWebAudioRefusal, (), { | |
| const refusal = Module['riveScripting']['audioRefusal'](); | |
| return refusal === null ? 0 : stringToNewUTF8(refusal); | |
| }); | |
| #if RIVE_PROCESSOR_AUDIO() | |
| // The starts and the render answer null, or a malloc'd error. | |
| EM_JS(char*, riveWebProcessorStart, | |
| (uint32_t lane, uint32_t moduleId, const uint8_t* module, | |
| uint32_t moduleSize, uint32_t entry, const uint8_t* input, | |
| uint32_t inputSize, uint32_t sampleRate, uint32_t maxFrames, | |
| uint32_t channels, double sharedOwner, uint32_t* idPtr), { | |
| const result = Module['riveScripting']['processorStart']( | |
| lane, | |
| moduleId, | |
| () => HEAPU8.slice(module, module + moduleSize), | |
| entry, | |
| HEAPU8.slice(input, input + inputSize), | |
| {sampleRate, maxFrames, channels}, | |
| sharedOwner); | |
| if (!result.ok) { | |
| return stringToNewUTF8(result.error); | |
| } | |
| HEAPU32[idPtr >> 2] = result.id; | |
| return 0; | |
| }); | |
| EM_JS(char*, riveWebProcessorRender, | |
| (uint32_t id, uint32_t frames, double startFrame, float* out), { | |
| const error = Module['riveScripting']['processorRender']( | |
| id, frames, startFrame, (block) => HEAPF32.set(block, out >> 2)); | |
| return error === null ? 0 : stringToNewUTF8(error); | |
| }); | |
| EM_JS(void, riveWebProcessorEnd, (uint32_t id), { | |
| Module['riveScripting']['processorEnd'](id); | |
| }); | |
| EM_JS(double, riveWebAudioSampleRate, (), { | |
| return Module['riveScripting']['audioSampleRate'](); | |
| }); | |
| EM_JS(char*, riveWebAudioStart, | |
| (uint32_t owner, uint32_t moduleId, const uint8_t* module, | |
| uint32_t moduleSize, uint32_t entry, const uint8_t* input, | |
| uint32_t inputSize, uint32_t sampleRate, uint32_t maxFrames, | |
| uint32_t channels, uint32_t hardBlocks, double sharedOwner, | |
| uint32_t* idPtr), { | |
| const result = Module['riveScripting']['audioStart']( | |
| owner, | |
| moduleId, | |
| () => HEAPU8.slice(module, module + moduleSize), | |
| entry, | |
| HEAPU8.slice(input, input + inputSize), | |
| {sampleRate, maxFrames, channels}, | |
| hardBlocks, | |
| sharedOwner); | |
| if (!result.ok) { | |
| return stringToNewUTF8(result.error); | |
| } | |
| HEAPU32[idPtr >> 2] = result.id; | |
| return 0; | |
| }); | |
| EM_JS(void, riveWebAudioStop, (uint32_t id), { | |
| Module['riveScripting']['audioStop'](id); | |
| }); | |
| #endif | |
| // clang-format on | |
| // A page's instance belongs to the thread that made it, so a processor | |
| // starts on the thread that renders it, as an AudioWorklet's would. | |
| bool WasmProcessor::startInstance() { return true; } | |
| #if RIVE_PROCESSOR_AUDIO() | |
| static void stopRealtime(uint32_t id) { riveWebAudioStop(id); } | |
| bool WasmProcessor::startRealtime() | |
| { | |
| std::vector<uint8_t> input = takeInput(); | |
| char* error = riveWebAudioStart( | |
| (uint32_t)(uintptr_t)this, | |
| m_module->id, | |
| m_module->bytes.data(), | |
| (uint32_t)m_module->bytes.size(), | |
| m_entry, | |
| input.data(), | |
| (uint32_t)input.size(), | |
| m_format.sampleRate, | |
| m_format.maxFrames, | |
| m_format.channels, | |
| m_hardBlocks, | |
| (double)sharedOwner(), | |
| &m_audioId); | |
| if (error != nullptr) | |
| { | |
| failTaken(error); | |
| m_state.store(LaneState::failed); | |
| return false; | |
| } | |
| return true; | |
| } | |
| void WasmProcessor::event(uint32_t kind, const char* text) | |
| { | |
| if (kind == kEventLog) | |
| { | |
| log(text); | |
| return; | |
| } | |
| if ((LaneState)kind == LaneState::failed) | |
| { | |
| fail(text); | |
| } | |
| settle((LaneState)kind); | |
| } | |
| extern "C" EMSCRIPTEN_KEEPALIVE void rive_web_processor_event( | |
| uint32_t processor, | |
| uint32_t kind, | |
| const char* text) | |
| { | |
| ((WasmProcessor*)(uintptr_t)processor)->event(kind, text); | |
| } | |
| bool WasmProcessor::startOnPage() | |
| { | |
| if (!riveWebAttach()) | |
| { | |
| fail("no script runner on this thread"); | |
| return false; | |
| } | |
| std::vector<uint8_t> input = takeInput(); | |
| char* error = riveWebProcessorStart( | |
| (uint32_t)(uintptr_t)(WasmLane*)this, | |
| m_module->id, | |
| m_module->bytes.data(), | |
| (uint32_t)m_module->bytes.size(), | |
| m_entry, | |
| input.data(), | |
| (uint32_t)input.size(), | |
| m_format.sampleRate, | |
| m_format.maxFrames, | |
| m_format.channels, | |
| (double)sharedOwner(), | |
| &m_webInstance); | |
| if (error != nullptr) | |
| { | |
| failTaken(error); | |
| return false; | |
| } | |
| return true; | |
| } | |
| // Only an offline render pulls blocks here, and as natively those have no | |
| // limit. | |
| bool WasmProcessor::process(float* out, uint32_t frames, uint64_t startFrame) | |
| { | |
| if (m_webInstance == 0 && !startOnPage()) | |
| { | |
| return false; | |
| } | |
| char* error = riveWebProcessorRender( | |
| m_webInstance, | |
| frames, | |
| (double)startFrame, | |
| out); | |
| if (error != nullptr) | |
| { | |
| failTaken(error); | |
| return false; | |
| } | |
| return true; | |
| } | |
| #endif | |
| void WasmProcessor::endInstance() | |
| { | |
| #if RIVE_PROCESSOR_AUDIO() | |
| if (m_webInstance != 0) | |
| { | |
| riveWebProcessorEnd(m_webInstance); | |
| m_webInstance = 0; | |
| } | |
| #endif | |
| } | |
| #endif | |
| // --- the set ----------------------------------------------------------------- | |
| WasmProcessors::~WasmProcessors() | |
| { | |
| for (auto& processor : m_lanes) | |
| { | |
| #ifndef __EMSCRIPTEN__ | |
| // Asked only for a queued init, so teardown never makes the pool. | |
| if (processor->state() == LaneState::starting) | |
| { | |
| initPool().cancel(processor.get()); | |
| } | |
| #endif | |
| processor->stop(); | |
| } | |
| } | |
| bool WasmProcessors::starting() const | |
| { | |
| #if RIVE_PROCESSOR_AUDIO() | |
| return std::any_of( | |
| m_lanes.begin(), | |
| m_lanes.end(), | |
| [](const rcp<WasmProcessor>& processor) { | |
| return processor->state() == LaneState::starting; | |
| }); | |
| #else | |
| // Each start fails before it returns. | |
| return false; | |
| #endif | |
| } | |
| namespace | |
| { | |
| // Where a processor started now would run, resolved on the VM's thread. | |
| struct ProcessorTarget | |
| { | |
| ProcessorFormat format; | |
| bool offline = false; | |
| // The page's worklet runs it, so init happens there. | |
| bool worklet = false; | |
| #if defined(WITH_RIVE_AUDIO) && !defined(__EMSCRIPTEN__) | |
| rcp<AudioEngine> engine; | |
| #endif | |
| std::string refusal = "real time audio has no audio engine"; | |
| bool plays() const | |
| { | |
| #if defined(WITH_RIVE_AUDIO) && !defined(__EMSCRIPTEN__) | |
| return offline || engine != nullptr; | |
| #else | |
| return offline || worklet; | |
| #endif | |
| } | |
| }; | |
| ProcessorTarget processorTarget() | |
| { | |
| ProcessorTarget target; | |
| OfflineRender offlineRender = offlineState(); | |
| target.offline = offlineRender.on; | |
| if (target.offline) | |
| { | |
| target.format = offlineRender.format; | |
| return target; | |
| } | |
| #ifdef __EMSCRIPTEN__ | |
| // A refused worklet is final: the page's own engine has no block limit. | |
| if (riveWebAttach()) | |
| { | |
| char* why = riveWebAudioRefusal(); | |
| if (why != nullptr) | |
| { | |
| target.refusal = why; | |
| free(why); | |
| } | |
| #if RIVE_PROCESSOR_AUDIO() | |
| else | |
| { | |
| target.worklet = true; | |
| target.format.sampleRate = (uint32_t)riveWebAudioSampleRate(); | |
| target.format.maxFrames = 128; | |
| } | |
| #endif | |
| } | |
| #elif defined(WITH_RIVE_AUDIO) | |
| target.engine = AudioEngine::RuntimeEngine(); | |
| if (target.engine != nullptr) | |
| { | |
| target.format.sampleRate = target.engine->sampleRate(); | |
| target.format.channels = target.engine->channels(); | |
| } | |
| #endif | |
| return target; | |
| } | |
| } // namespace | |
| rcp<WasmProcessor> WasmProcessors::start( | |
| uint32_t entry, | |
| std::vector<uint8_t> input, | |
| uint32_t hardBlocks) | |
| { | |
| if (entry >= entryCount()) | |
| { | |
| return nullptr; | |
| } | |
| ProcessorTarget target = processorTarget(); | |
| if (target.offline) | |
| { | |
| // Nothing plays it live, so no block of it can run late. | |
| hardBlocks = 0; | |
| } | |
| auto processor = make_rcp<WasmProcessor>( | |
| m_module, | |
| entry, | |
| std::move(input), | |
| target.format, | |
| hardBlocks); | |
| add(processor); | |
| #if defined(__EMSCRIPTEN__) && RIVE_PROCESSOR_AUDIO() | |
| if (target.worklet) | |
| { | |
| processor->startRealtime(); | |
| return processor; | |
| } | |
| #endif | |
| #if defined(WITH_RIVE_AUDIO) && !defined(__EMSCRIPTEN__) | |
| processor->m_engine = target.engine; | |
| #endif | |
| // Nothing would pull its blocks, so it would only seem to play. | |
| if (!target.plays()) | |
| { | |
| processor->fail(target.refusal); | |
| processor->settle(LaneState::failed); | |
| return processor; | |
| } | |
| #ifdef __EMSCRIPTEN__ | |
| // The page instantiates it at its first block, so init costs nothing. | |
| processor->run(); | |
| #else | |
| initPool().submit(processor); | |
| #endif | |
| return processor; | |
| } | |
| ProcessorFormat WasmProcessors::format() | |
| { | |
| ProcessorTarget target = processorTarget(); | |
| return target.plays() ? target.format : ProcessorFormat{0, 0, 0}; | |
| } | |
| #if RIVE_PROCESSOR_AUDIO() | |
| void WasmProcessors::setRenderOffline( | |
| bool offline, | |
| const ProcessorFormat& format) | |
| { | |
| std::lock_guard<std::mutex> lock(offlineMutex()); | |
| OfflineRender& render = offlineSlot(); | |
| render.on = offline; | |
| // A block of no frames would never finish a render. | |
| render.format = { | |
| std::max<uint32_t>(format.sampleRate, 1), | |
| std::max<uint32_t>(format.channels, 1), | |
| std::max<uint32_t>(format.maxFrames, 1)}; | |
| render.frame = 0; | |
| } | |
| bool WasmProcessors::renderOffline() { return offlineState().on; } | |
| void WasmProcessors::setOfflineFrame(uint64_t frame) | |
| { | |
| std::lock_guard<std::mutex> lock(offlineMutex()); | |
| offlineSlot().frame = frame; | |
| } | |
| bool WasmProcessors::offlineClock(uint64_t& frame, uint32_t& sampleRate) | |
| { | |
| OfflineRender render = offlineState(); | |
| frame = render.frame; | |
| sampleRate = render.format.sampleRate; | |
| return render.on; | |
| } | |
| #endif | |
| #endif |