blob: e3c71485e32634b0419e48d695b2a20e50c6ad38 [file] [edit]
#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