blob: b602e7b2f2360fd15bb995af7c1612cd5f77a12e [file] [edit]
// Instances of the lane modules a script module carries, started the same way
// by the page runner and, through createProcessorMixer, by the audio worklets.
import {
kFrameMinorExport,
kLaneAllocExport,
kLaneConsole,
kLaneEntryPrefixes,
kLaneFreeExport,
kLaneResetExport,
kLiftedStartExport,
kProcessorInitMs,
kProcessorInitOverran,
kProcessorOverran,
kProcessorProcessExport,
processorBlockBudgetMs,
} from './rive_lane_imports.mjs';
import { kHoldBlocks, kHoldStarting } from './rive_shared_layout.mjs';
import { createSharedLaneImports, managedLength, utf16Text } from './rive_shared_lane.mjs';
export function errorText(error) {
return String(error?.message ?? error);
}
// Chrome's worklet scope has no performance; Date's milliseconds still catch a
// block of several.
export const monotonicNow =
globalThis.performance !== undefined ? () => globalThis.performance.now() : Date.now;
// The console imports: each line is a std string, its byte length before it.
// Only jobs and workers import the timers.
export function laneConsole(memory, log, timers = new Map()) {
const text = (ptr) => {
const buffer = memory().buffer;
return utf16Text(buffer, ptr, managedLength(buffer, ptr));
};
const env = {};
for (const name of kLaneConsole) {
env[name] = (ptr) => log(text(ptr));
}
const report = (end) => (ptr) => {
const label = text(ptr);
const started = timers.get(label);
if (started === undefined) {
log(`Timer '${label}' does not exist`);
return;
}
if (end) {
timers.delete(label);
}
log(`${label}: ${(monotonicNow() - started).toFixed(3)}ms`);
};
env['console.time'] = (ptr) => timers.set(text(ptr), monotonicNow());
env['console.timeLog'] = report(false);
env['console.timeEnd'] = report(true);
return env;
}
const noFuelCheck = () => {};
// An instance of lane.module whose imports reach the lane's own natives, log,
// timers, clock and fuel check. Returns { exports, memory, failure } or
// { error }.
export function instantiateLane(lane) {
let memory = null;
let thrown = null;
const failure = (error) => thrown ?? errorText(error);
const imports = {
'env': laneConsole(() => memory, lane.log, lane.timers),
'rive_shared_v1': lane.shared(() => memory),
'rive_rt_v1': { 'now': lane.now ?? monotonicNow },
'rive_lane_v1': {
'error': (ptr, byteLength) => {
thrown = utf16Text(memory.buffer, ptr, byteLength);
lane.onThrow?.(thrown);
},
'fuel_check': lane.fuel ?? noFuelCheck,
},
};
if (lane.natives !== undefined) {
imports[lane.natives] = lane.own(() => memory);
}
try {
const exports = new WebAssembly.Instance(lane.module, imports).exports;
memory = exports['memory'];
return { exports, memory, failure };
} catch (error) {
return { error: failure(error) };
}
}
// Runs the instance's start, or its reset when a job ran in it before, then
// prefix + entry with a copy of input and args, freeing the copy after when
// lane.free. Returns { result } or { error }.
export function enterLane(instance, lane, reset = false) {
try {
const exports = instance.exports;
exports[reset ? kLaneResetExport : kLiftedStartExport]?.();
const input = lane.input;
const at = exports[kLaneAllocExport](input.length) >>> 0;
new Uint8Array(instance.memory.buffer, at, input.length).set(input);
const result = exports[lane.prefix + lane.entry](
at,
input.length,
...(lane.args ?? [])
);
if (lane.free) {
exports[kLaneFreeExport](at);
}
return { result };
} catch (error) {
return { error: instance.failure(error) };
}
}
// A new instance, entered. Returns { exports, memory, result, failure } or
// { error }.
export function startLane(lane) {
const instance = instantiateLane(lane);
const entered = instance.error === undefined ? enterLane(instance, lane) : instance;
return entered.error === undefined ? { ...instance, result: entered.result } : entered;
}
// One script audio processor, whose init may take kProcessorInitMs. Returns
// { lane } or { error }; shared(memory, fail) answers its shared imports.
export function startProcessorLane(module, entry, input, format, shared, log) {
// As natively, a native that fails the processor lets the block finish.
let failed = null;
const fail = (text) => {
failed ??= text;
};
// When its fuel checks end it and why, made once so a block allocates
// nothing on the audio thread.
const bounds = { until: monotonicNow() + kProcessorInitMs, reason: kProcessorInitOverran };
const started = startLane({
module,
shared: (memory) => shared(memory, fail),
log,
fuel: () => {
if (monotonicNow() > bounds.until) {
throw bounds.reason;
}
},
prefix: kLaneEntryPrefixes['processor'],
entry,
input,
args: [format.sampleRate, format.maxFrames, format.channels],
free: true,
});
if (started.error !== undefined || failed !== null) {
return { error: started.error ?? failed };
}
bounds.until = Infinity;
bounds.reason = kProcessorOverran;
const exports = started.exports;
const memory = started.memory;
// Rebuilt only when the block moves or memory grows, so a block
// allocates nothing on the audio thread.
let view = null;
return {
lane: {
// Ends the block past budget ms, if any; sink gets its samples
// before a collection can move them. Null, or why it failed.
render(frames, startFrame, sink, budget = 0) {
if (budget > 0) {
bounds.until = monotonicNow() + budget;
}
try {
const at = exports[kProcessorProcessExport](frames, startFrame) >>> 0;
if (failed !== null) {
return failed;
}
const length = frames * format.channels;
if (
view === null ||
view.buffer !== memory.buffer ||
view.byteOffset !== at ||
view.length !== length
) {
view = new Float32Array(memory.buffer, at, length);
}
sink(view);
exports[kFrameMinorExport]?.();
return null;
} catch (error) {
return started.failure(error);
} finally {
bounds.until = Infinity;
}
},
},
};
}
// Every processor a real time output runs, as the page's messages say, with
// answers through post. It counts blocks and starts in holds, the Hold words.
export function createProcessorMixer(heap, table, post, holds) {
const lanes = new Map();
// What render walks, rebuilt only when lanes change, so a block does not
// allocate an iterator.
let order = [];
const relist = () => {
order = [...lanes.entries()];
};
// Null while compiling, then the module or the compile's error text,
// until the page forgets it.
const modules = new Map();
// Starts not yet run, behind one whose module still compiles.
const waiting = [];
// The output's add and where in its block a chunk goes, set as render
// walks, so sinks allocate nothing.
let add = null;
let offset = 0;
let frame = -1;
const fail = (id, text) => {
lanes.delete(id);
relist();
post({ id, kind: 'failed', text });
};
const start = (message) => {
const id = message.id;
const module = modules.get(message.moduleId);
if (typeof module === 'string') {
post({ id, kind: 'failed', text: module });
return;
}
const format = message.format;
const started = startProcessorLane(
module,
message.entry,
message.input,
format,
(moduleMemory, fail) =>
createSharedLaneImports(heap, table, message.sharedOwner, moduleMemory, holds, fail),
(text) => post({ id, kind: 'log', text })
);
if (started.error !== undefined) {
post({ id, kind: 'failed', text: started.error });
return;
}
const channels = format.channels;
lanes.set(id, {
lane: started.lane,
maxFrames: format.maxFrames,
// A block past its hard limit, where there is one, ends at its
// next fuel check, or without fuel once it returns.
hardBlocks: message.hardBlocks,
sampleRate: format.sampleRate,
warm: false,
sink: (block) => add(block, channels, offset),
});
relist();
post({ id, kind: 'running' });
};
// Starts in arrival order, up to the first whose module still compiles.
const drain = () => {
while (waiting.length > 0 && modules.get(waiting[0].moduleId) !== null) {
start(waiting.shift());
// Each start gets the page's whole start limit, and the checks it
// took must not count under the shorter limit after it.
Atomics.add(holds, kHoldBlocks, 1);
}
if (waiting.length === 0) {
Atomics.store(holds, kHoldStarting, 0);
}
};
return {
receive(message) {
if (message.kind === 'forget') {
modules.delete(message.moduleId);
return;
}
if (message.kind === 'stop') {
// A stop beats the start it follows when that still waits.
const at = waiting.findIndex((queued) => queued.id === message.id);
if (at >= 0) {
waiting.splice(at, 1);
drain();
} else {
lanes.delete(message.id);
relist();
}
return;
}
if (message.kind === 'end') {
// The page gave up on this output and failed what it ran.
lanes.clear();
relist();
waiting.length = 0;
return;
}
const moduleId = message.moduleId;
if (!modules.has(moduleId) && message.bytes === null) {
// A fresh worklet lacks modules the page sent the one before.
post({ id: message.id, kind: 'resend' });
return;
}
if (!modules.has(moduleId)) {
// Compiling on the audio thread would stall every processor.
modules.set(moduleId, null);
const settle = (value) => {
if (modules.has(moduleId)) {
modules.set(moduleId, value);
}
};
WebAssembly.compile(message.bytes)
.then(settle, (error) => settle(errorText(error)))
.then(drain);
}
Atomics.store(holds, kHoldStarting, 1);
waiting.push(message);
drain();
},
// Hands into(block, channels, offset) each processor's interleaved
// samples, in chunks of at most its block, offset frames in.
render(frames, firstFrame, into) {
if (frame < 0) {
frame = firstFrame;
}
add = into;
// A failure relists, which must not shift the rest of this pass.
const list = order;
for (let i = 0; i < list.length; i++) {
const [id, entry] = list[i];
for (offset = 0; offset < frames; offset += entry.maxFrames) {
const chunk = Math.min(entry.maxFrames, frames - offset);
const budget = processorBlockBudgetMs(
entry.hardBlocks,
chunk,
entry.sampleRate,
!entry.warm
);
const began = monotonicNow();
let error = entry.lane.render(chunk, frame + offset, entry.sink, budget);
if (error === null && budget > 0 && monotonicNow() - began > budget) {
error = kProcessorOverran;
}
entry.warm = true;
if (error !== null) {
fail(id, error);
break;
}
}
}
frame += frames;
Atomics.add(holds, kHoldBlocks, 1);
},
// The output went away: every processor it ran ends with text.
failAll(text) {
for (const id of [...lanes.keys()]) {
fail(id, text);
}
for (const message of waiting.splice(0)) {
post({ id: message.id, kind: 'failed', text });
}
},
};
}