| // The JS half of the browser scripting lane. Script modules run on the | |
| // browser's own wasm engine; librive reaches them through the EM_JS calls in | |
| // browser_scripting_vm.cpp, which forward to the runner installed here. | |
| // | |
| // Loading a file is two steps. File::import makes each script VM and | |
| // registers its module bytes here, without instantiating. The host then | |
| // awaits prepare(), which compiles and instantiates every registered module | |
| // asynchronously, and only then asks librive to start the file's scripts. | |
| // A host that skips prepare() still works where the browser allows | |
| // synchronous compilation on the calling thread: start() instantiates inline. | |
| // | |
| // Hosts link rive_scripting_pre.js, generated from this file, into their | |
| // emscripten build with --pre-js. It installs the runner by itself, so a | |
| // host only does: | |
| // ... import a file ... | |
| // await Module.riveScripting.prepare(); | |
| // ... start the file's scripts ... | |
| import { | |
| createRiveModuleImports, | |
| bindRiveHostCalls, | |
| unlinkedImport, | |
| } from './rive_module_imports.mjs'; | |
| import { | |
| kFrameMinorExport, | |
| kLaneEntryPrefixes, | |
| kLaneResetExport, | |
| kLaneSections, | |
| kLaneStates, | |
| kLiftedStartExport, | |
| kProcessorEventLog, | |
| kProcessorInitMs, | |
| kProcessorNode, | |
| kReuseMaxPages, | |
| kWorkerWakeExport, | |
| laneImportError, | |
| laneImportRefusal, | |
| } from './rive_lane_imports.mjs'; | |
| import { kHoldBlocks, kHoldStarting, kHoldWords } from './rive_shared_layout.mjs'; | |
| import { createSharedLaneImports, releaseSharedLane } from './rive_shared_lane.mjs'; | |
| import { createParallel } from './rive_parallel.mjs'; | |
| import { | |
| enterLane, | |
| errorText, | |
| instantiateLane, | |
| monotonicNow, | |
| startLane, | |
| startProcessorLane, | |
| } from './rive_lane.mjs'; | |
| // WasmScriptingVM::CallOutcome, then the browser's own host fault. | |
| const kOk = 0; | |
| const kMissing = 1; | |
| const kTrapped = 2; | |
| const kHostFault = 3; | |
| // One function returning v128.const zero, then one returning the relaxed | |
| // swizzle of two: the smallest modules each SIMD flavor needs. | |
| const kV128Zero = [0xfd, 0x0c, ...new Array(16).fill(0)]; | |
| const kSimdProbe = probeModule([...kV128Zero]); | |
| const kRelaxedSimdProbe = probeModule([ | |
| ...kV128Zero, | |
| ...kV128Zero, | |
| 0xfd, | |
| 0x80, | |
| 0x02, | |
| ]); | |
| function probeModule(body) { | |
| const code = [0, ...body, 0x0b]; | |
| return new Uint8Array([ | |
| 0x00, 0x61, 0x73, 0x6d, 1, 0, 0, 0, | |
| // One type, () -> v128, and one function of it. | |
| 1, 5, 1, 0x60, 0, 1, 0x7b, | |
| 3, 2, 1, 0, | |
| 10, code.length + 2, 1, code.length, ...code, | |
| ]); | |
| } | |
| // Index immediates of the 0xfc ops by sub-opcode: none for the saturating | |
| // truncations, then memory.init through table.fill. | |
| const kBulkIndices = [0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 2, 1, 2, 1, 2, 1, 1, 1]; | |
| // The ops past the variable accesses that take one LEB. | |
| const kOneLebOps = [0x0c, 0x0d, 0x10, 0x12, 0x3f, 0x40, 0x41, 0x42, 0xd0, 0xd2]; | |
| // The SIMD a module's types and code need: 2 for relaxed, 1 for plain, 0 for | |
| // none, or -1 when they hold what this walk does not read. | |
| function simdNeeded(bytes) { | |
| let needed = 0; | |
| let at = 8; | |
| const need = (flavor) => { | |
| needed = Math.max(needed, flavor); | |
| }; | |
| const next = () => { | |
| const [value, end] = readLeb(bytes, at); | |
| at = end; | |
| return value; | |
| }; | |
| const memarg = () => { | |
| if (next() & 0x40) { | |
| next(); | |
| } | |
| next(); | |
| }; | |
| // False for a reference type with a heap type, which takes more bytes. | |
| const valueType = () => { | |
| const type = bytes[at++]; | |
| if (type === 0x7b) { | |
| need(1); | |
| } | |
| return type !== 0x63 && type !== 0x64; | |
| }; | |
| const instruction = () => { | |
| const op = bytes[at++]; | |
| if (op === 0xfd) { | |
| const sub = next(); | |
| need(sub >= 0x100 && sub <= 0x113 ? 2 : 1); | |
| if (sub <= 0x0b || sub === 0x5c || sub === 0x5d) { | |
| memarg(); | |
| } else if (sub === 0x0c || sub === 0x0d) { | |
| at += 16; | |
| } else if (sub >= 0x15 && sub <= 0x22) { | |
| at += 1; | |
| } else if (sub >= 0x54 && sub <= 0x5b) { | |
| memarg(); | |
| at += 1; | |
| } | |
| } else if (op === 0xfc) { | |
| const sub = next(); | |
| if (sub >= kBulkIndices.length) { | |
| return false; | |
| } | |
| for (let i = 0; i < kBulkIndices[sub]; i++) { | |
| next(); | |
| } | |
| } else if (op === 0xfe) { | |
| // atomic.fence takes a byte, the other atomics a memarg. | |
| if (next() === 3) { | |
| at += 1; | |
| } else { | |
| memarg(); | |
| } | |
| } else if (op >= 0x28 && op <= 0x3e) { | |
| memarg(); | |
| } else if (op >= 0x02 && op <= 0x04) { | |
| // A block type: one byte, or a type index. | |
| if (bytes[at] === 0x7b) { | |
| need(1); | |
| } | |
| next(); | |
| } else if (op === 0x0e) { | |
| for (let labels = next(); labels >= 0 && at < bytes.length; labels--) { | |
| next(); | |
| } | |
| } else if (op === 0x1c) { | |
| for (let types = next(); types > 0 && at < bytes.length; types--) { | |
| if (!valueType()) { | |
| return false; | |
| } | |
| } | |
| } else if (op === 0x11 || op === 0x13) { | |
| next(); | |
| next(); | |
| } else if (op === 0x43 || op === 0x44) { | |
| at += op === 0x43 ? 4 : 8; | |
| } else if ((op >= 0x20 && op <= 0x26) || kOneLebOps.includes(op)) { | |
| next(); | |
| } else if (!(op <= 0x01 || op === 0x05 || op === 0x0b || op === 0x0f || | |
| op === 0x1a || op === 0x1b || op === 0xd1 || | |
| (op >= 0x45 && op <= 0xc4))) { | |
| return false; | |
| } | |
| return true; | |
| }; | |
| while (at < bytes.length) { | |
| const id = bytes[at]; | |
| const [size, body] = readLeb(bytes, at + 1); | |
| at = body; | |
| if (id === 1) { | |
| for (let types = next(); types > 0 && at < bytes.length; types--) { | |
| // Function types only: params, then results. | |
| if (bytes[at++] !== 0x60) { | |
| return -1; | |
| } | |
| for (let list = 0; list < 2; list++) { | |
| for (let n = next(); n > 0 && at < bytes.length; n--) { | |
| if (!valueType()) { | |
| return -1; | |
| } | |
| } | |
| } | |
| } | |
| } else if (id === 10) { | |
| for (let bodies = next(); bodies > 0 && at < bytes.length; bodies--) { | |
| const end = Math.min(next() + at, bytes.length); | |
| for (let locals = next(); locals > 0 && at < end; locals--) { | |
| next(); | |
| if (!valueType()) { | |
| return -1; | |
| } | |
| } | |
| while (at < end) { | |
| if (!instruction()) { | |
| return -1; | |
| } | |
| } | |
| } | |
| } | |
| at = body + size; | |
| } | |
| return needed; | |
| } | |
| function readLeb(bytes, at) { | |
| let value = 0; | |
| let shift = 0; | |
| let byte; | |
| do { | |
| byte = bytes[at++]; | |
| value |= (byte & 0x7f) << shift; | |
| shift += 7; | |
| } while (byte & 0x80); | |
| return [value >>> 0, at]; | |
| } | |
| function leb(value) { | |
| const out = []; | |
| do { | |
| const byte = value & 0x7f; | |
| value >>>= 7; | |
| out.push(value ? byte | 0x80 : byte); | |
| } while (value); | |
| return out; | |
| } | |
| // Each section of a module: its id, where it starts, its body and its end. | |
| function sectionsOf(bytes) { | |
| const sections = []; | |
| for (let at = 8; at < bytes.length; ) { | |
| const [size, body] = readLeb(bytes, at + 1); | |
| sections.push({ id: bytes[at], at, body, end: body + size }); | |
| at = body + size; | |
| } | |
| return sections; | |
| } | |
| const kLaneSectionNames = kLaneSections.map(([name]) => | |
| Array.from(name, (c) => c.charCodeAt(0)) | |
| ); | |
| // The lane modules a script module carries in its custom sections. | |
| function laneModulesOf(bytes) { | |
| const lanes = []; | |
| for (const section of sectionsOf(bytes)) { | |
| if (section.id !== 0) { | |
| continue; | |
| } | |
| const [length, name] = readLeb(bytes, section.body); | |
| const named = (wanted) => | |
| wanted.length === length && wanted.every((c, i) => bytes[name + i] === c); | |
| if (kLaneSectionNames.some(named)) { | |
| lanes.push(bytes.subarray(name + length, section.end)); | |
| } | |
| } | |
| return lanes; | |
| } | |
| // rasc runs every script's top level in the module's start function, and a | |
| // top level can pass memory to the host. The module's memory is an export, | |
| // which the page cannot reach until start has returned. So the start section | |
| // is dropped and its function exported, for the runner to call once the | |
| // instance and its memory exist. Modules without one come back untouched. | |
| // Lane modules are cached by these bytes, so they must match wasm_lane.cpp's. | |
| function liftStart(bytes) { | |
| const all = sectionsOf(bytes); | |
| const start = all.find((section) => section.id === 8); | |
| const sections = all.filter((section) => section !== start); | |
| if (start === undefined || !sections.some((section) => section.id === 7)) { | |
| return bytes; | |
| } | |
| const startFunction = readLeb(bytes, start.body)[0]; | |
| const parts = [bytes.subarray(0, 8)]; | |
| for (const section of sections) { | |
| let body = bytes.subarray(section.body, section.end); | |
| if (section.id === 7) { | |
| const [count, first] = readLeb(bytes, section.body); | |
| const name = Array.from(kLiftedStartExport, (c) => c.charCodeAt(0)); | |
| body = Uint8Array.from([ | |
| ...leb(count + 1), | |
| ...bytes.subarray(first, section.end), | |
| ...leb(name.length), | |
| ...name, | |
| 0, | |
| ...leb(startFunction), | |
| ]); | |
| } | |
| parts.push(Uint8Array.from([section.id, ...leb(body.length)]), body); | |
| } | |
| const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0)); | |
| let at = 0; | |
| for (const part of parts) { | |
| out.set(part, at); | |
| at += part.length; | |
| } | |
| return out; | |
| } | |
| // A parallel module declares a shared memory up to wasm32's limit, which a | |
| // browser would set aside whole and which needs cross origin isolation. Its | |
| // memory moves to an import instead, so the runner picks the memory to give | |
| // it: shared up to what the device can set aside, else unshared, where its | |
| // parts run one at a time. Every other module comes back untouched. | |
| const kMemoryImport = ['rive_parallel_v1', 'memory']; | |
| const kSharedTryPages = 32768; | |
| export function importMemory(bytes) { | |
| const all = sectionsOf(bytes); | |
| const memory = all.find((section) => section.id === 5); | |
| const imports = all.find((section) => section.id === 2); | |
| if (memory === undefined || imports === undefined) { | |
| return { bytes, memory: null }; | |
| } | |
| let [count, at] = readLeb(bytes, memory.body); | |
| const flags = bytes[at++]; | |
| if (count !== 1 || (flags & 2) === 0) { | |
| return { bytes, memory: null }; | |
| } | |
| const [initial, afterInitial] = readLeb(bytes, at); | |
| const declared = flags & 1 ? readLeb(bytes, afterInitial)[0] : 65536; | |
| const shared = | |
| typeof SharedArrayBuffer === 'function' && | |
| globalThis.crossOriginIsolated !== false; | |
| let maximum = declared; | |
| let created = null; | |
| if (shared) { | |
| // Halving down to what the module needs, as a 32-bit browser or one | |
| // short of address space refuses a large reservation. | |
| maximum = Math.min(declared, kSharedTryPages); | |
| for (;;) { | |
| try { | |
| // Quoted, as closure renames a key its externs lack. | |
| created = new WebAssembly.Memory({ | |
| 'initial': initial, | |
| 'maximum': maximum, | |
| 'shared': shared, | |
| }); | |
| break; | |
| } catch (error) { | |
| if (!(error instanceof RangeError) || maximum <= initial) { | |
| throw error; | |
| } | |
| maximum = Math.max(initial, maximum >>> 1); | |
| } | |
| } | |
| } else { | |
| created = new WebAssembly.Memory({ initial, maximum }); | |
| } | |
| const name = (text) => [...leb(text.length), ...Array.from(text, (c) => c.charCodeAt(0))]; | |
| const entry = [ | |
| ...name(kMemoryImport[0]), | |
| ...name(kMemoryImport[1]), | |
| 2, | |
| shared ? 3 : 1, | |
| ...leb(initial), | |
| ...leb(maximum), | |
| ]; | |
| const [importCount, first] = readLeb(bytes, imports.body); | |
| const parts = [bytes.subarray(0, 8)]; | |
| for (const section of all) { | |
| if (section === memory) { | |
| continue; | |
| } | |
| let body = bytes.subarray(section.body, section.end); | |
| if (section === imports) { | |
| body = Uint8Array.from([ | |
| ...leb(importCount + 1), | |
| ...bytes.subarray(first, section.end), | |
| ...entry, | |
| ]); | |
| } | |
| parts.push(Uint8Array.from([section.id, ...leb(body.length)]), body); | |
| } | |
| const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0)); | |
| let offset = 0; | |
| for (const part of parts) { | |
| out.set(part, offset); | |
| offset += part.length; | |
| } | |
| return { bytes: out, memory: created }; | |
| } | |
| // A member of a parallel module shares the memory the page's instance already | |
| // filled, so its data segments are made passive, which instantiating leaves | |
| // alone. Its start is already lifted out. | |
| export function passiveData(bytes) { | |
| const all = sectionsOf(bytes); | |
| const data = all.find((section) => section.id === 11); | |
| if (data === undefined) { | |
| return bytes; | |
| } | |
| let [count, at] = readLeb(bytes, data.body); | |
| const body = [Uint8Array.from(leb(count))]; | |
| for (let i = 0; i < count; i++) { | |
| let flags; | |
| [flags, at] = readLeb(bytes, at); | |
| if (flags === 2) { | |
| at = readLeb(bytes, at)[1]; | |
| } | |
| // The offset is one constant instruction and end. | |
| if (flags !== 1) { | |
| for (let op = bytes[at++]; op !== 0x0b; op = bytes[at++]) { | |
| at = readLeb(bytes, at)[1]; | |
| } | |
| } | |
| const [length, first] = readLeb(bytes, at); | |
| body.push(Uint8Array.from([1, ...leb(length)]), bytes.subarray(first, first + length)); | |
| at = first + length; | |
| } | |
| const parts = [bytes.subarray(0, 8)]; | |
| for (const section of all) { | |
| const sectionBody = | |
| section === data ? concatBytes(body) : bytes.subarray(section.body, section.end); | |
| parts.push(Uint8Array.from([section.id, ...leb(sectionBody.length)]), sectionBody); | |
| } | |
| return concatBytes(parts); | |
| } | |
| function concatBytes(parts) { | |
| const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0)); | |
| let at = 0; | |
| for (const part of parts) { | |
| out.set(part, at); | |
| at += part.length; | |
| } | |
| return out; | |
| } | |
| // Args by index: spreading a subarray of the heap cost more than the call. | |
| function callExport(fn, args, at, argc) { | |
| switch (argc) { | |
| case 0: | |
| return fn(); | |
| case 1: | |
| return fn(args[at]); | |
| case 2: | |
| return fn(args[at], args[at + 1]); | |
| case 3: | |
| return fn(args[at], args[at + 1], args[at + 2]); | |
| case 4: | |
| return fn(args[at], args[at + 1], args[at + 2], args[at + 3]); | |
| case 5: | |
| return fn(args[at], args[at + 1], args[at + 2], args[at + 3], | |
| args[at + 4]); | |
| case 6: | |
| return fn(args[at], args[at + 1], args[at + 2], args[at + 3], | |
| args[at + 4], args[at + 5]); | |
| case 7: | |
| return fn(args[at], args[at + 1], args[at + 2], args[at + 3], | |
| args[at + 4], args[at + 5], args[at + 6]); | |
| case 8: | |
| return fn(args[at], args[at + 1], args[at + 2], args[at + 3], | |
| args[at + 4], args[at + 5], args[at + 6], args[at + 7]); | |
| default: | |
| return fn(...args.subarray(at, at + argc)); | |
| } | |
| } | |
| export function installRiveScripting(Module) { | |
| // Primitives over the librive heap and exports, handed over by librive | |
| // itself on first use because only code inside it can see them. | |
| let host = null; | |
| // A pool thread's own clock starts late, so lanes read the page's, as | |
| // native lanes share one steady clock. | |
| let laneNow = monotonicNow; | |
| const vms = new Map(); | |
| // Bridge traffic, tallied only while a page measures it. | |
| const counters = { | |
| on: false, | |
| hostCalls: 0, | |
| bytesStaged: 0, | |
| bytesCopiedOut: 0, | |
| bytesRead: 0, | |
| bytesWritten: 0, | |
| }; | |
| // What the generated imports need from librive, for calls under ctx, | |
| // binding only that namespace when only names one. | |
| function librive(ctx, only = null) { | |
| return { | |
| calls: bindRiveHostCalls(host.hostCall, ctx, only), | |
| heapU8: host.heapU8, | |
| stackSave: host.stackSave, | |
| stackAlloc: host.stackAlloc, | |
| stackRestore: host.stackRestore, | |
| malloc: host.malloc, | |
| free: host.free, | |
| counters, | |
| }; | |
| } | |
| // Tests stand in for an engine without SIMD with their own validator, | |
| // and run offline processors on the natives a worklet uses. | |
| const validate = | |
| Module['riveScriptingValidate'] ?? | |
| ((bytes) => WebAssembly.validate(bytes)); | |
| const laneTwin = Module['riveScriptingLaneTwin'] === true; | |
| let features = null; | |
| // Why this engine refuses a module or a lane module it carries, when a | |
| // SIMD flavor it lacks is the reason; null otherwise. | |
| function missingSimd(bytes) { | |
| features ??= { | |
| simd: validate(kSimdProbe), | |
| relaxed: validate(kRelaxedSimdProbe), | |
| }; | |
| const modules = [bytes, ...laneModulesOf(bytes)]; | |
| if ( | |
| (features.simd && features.relaxed) || | |
| modules.every((module) => validate(module)) | |
| ) { | |
| return null; | |
| } | |
| const needed = Math.max(...modules.map(simdNeeded)); | |
| if (!features.simd && needed > 0) { | |
| return "this browser lacks WebAssembly SIMD, so this file's scripts " + | |
| 'are off; bake with wasmSimd: off to support it'; | |
| } | |
| if (!features.relaxed && needed === 2) { | |
| return "this browser lacks WebAssembly relaxed SIMD, so this file's " + | |
| 'scripts are off; bake with wasmSimd: on to support it'; | |
| } | |
| return null; | |
| } | |
| // The module's bytes once its memory is the runner's to give. | |
| function withMemory(entry) { | |
| const { bytes, memory } = importMemory(entry.bytes); | |
| entry.parallelMemory = memory; | |
| return bytes; | |
| } | |
| const sharedMemory = (entry) => | |
| typeof SharedArrayBuffer === 'function' && | |
| entry.parallelMemory?.buffer instanceof SharedArrayBuffer; | |
| // The bytes a team thread instantiates a member from, or null where a | |
| // call never gets more than one part. | |
| function memberBytes(entry, lifted) { | |
| return sharedMemory(entry) && teamWorkerOf !== null ? passiveData(lifted) : null; | |
| } | |
| // Members on the team once the page's own instance has started. | |
| function adoptMembers(entry, bytes) { | |
| if (bytes !== null && entry.instance !== null) { | |
| entry.parallel = parallel.setUp(entry, bytes, entry.parallelSize()); | |
| } | |
| } | |
| function importsFor(entry, module) { | |
| const imports = createRiveModuleImports( | |
| librive(entry.ctx), | |
| () => { | |
| if (entry.memory === null) { | |
| // Only a start function liftStart could not move. | |
| throw new Error( | |
| 'script passed memory to the host while its module ' + | |
| 'was still starting' | |
| ); | |
| } | |
| return entry.memory; | |
| } | |
| ); | |
| if (entry.parallelMemory !== null) { | |
| const space = (imports[kMemoryImport[0]] ??= {}); | |
| space[kMemoryImport[1]] = entry.parallelMemory; | |
| // Calls share the memory with a team of threads only where it is | |
| // shared; elsewhere a call gets one part. Where librive allows | |
| // more, the page picks how many. | |
| const size = space['size']; | |
| const shared = sharedMemory(entry); | |
| space['size'] = () => | |
| shared && size() > 1 ? parallel.parts(runner['parallelParts']) : 1; | |
| const rt = imports['rive_rt_v1']; | |
| const budget = rt['budget_exceeded']; | |
| // A budget the page's own part already reported is not reported | |
| // again for a member that ran out at the same time. | |
| rt['budget_exceeded'] = (ms) => { | |
| entry.budgeted = true; | |
| return budget(ms); | |
| }; | |
| const report = { | |
| thrown: (at, length) => rt['error'](at, length), | |
| budget: (ms) => (entry.budgeted ? undefined : budget(ms)), | |
| }; | |
| space['run'] = (control) => { | |
| entry.budgeted = false; | |
| return parallel.run(entry, entry.parallel, control >>> 0, report); | |
| }; | |
| space['waiting'] = parallel.waiting; | |
| entry.parallelSize = space['size']; | |
| } | |
| // The browser refuses a module with an import nothing provides. | |
| for (const wanted of WebAssembly.Module.imports(module)) { | |
| if (wanted['kind'] === 'function') { | |
| const space = (imports[wanted['module']] ??= {}); | |
| space[wanted['name']] ??= unlinkedImport( | |
| wanted['module'], | |
| wanted['name'] | |
| ); | |
| } | |
| } | |
| return imports; | |
| } | |
| // Before its lanes compile and a start liftStart left in place runs, as | |
| // top levels already pass strings and a refused module must do neither. | |
| function announceExports(entry, module) { | |
| const mark = host.stackSave(); | |
| for (const { name } of WebAssembly.Module.exports(module)) { | |
| if (!name.startsWith('__rive')) { | |
| continue; | |
| } | |
| const capacity = name.length * 3 + 1; | |
| const ptr = host.stackAlloc(capacity); | |
| host.writeUtf8(name, ptr, capacity); | |
| host.exports['_rive_web_vm_module_export'](entry.ctx.vm, ptr); | |
| } | |
| host.stackRestore(mark); | |
| const refusal = | |
| host.exports['_rive_web_vm_start_refusal'](entry.ctx.vm); | |
| if (refusal !== 0) { | |
| throw new Error(host.utf8(refusal)); | |
| } | |
| } | |
| function adopt(entry, instance) { | |
| entry.memory = instance.exports['memory']; | |
| entry.bytes = null; | |
| // State of the call in progress, which a trap unwinds without | |
| // restoring; a global the module cannot write is not state. | |
| entry.callState = Object.keys(instance.exports) | |
| .filter((name) => name.startsWith('__riveCall')) | |
| .map((name) => instance.exports[name]) | |
| .filter((global) => { | |
| if (!(global instanceof WebAssembly.Global)) { | |
| return false; | |
| } | |
| try { | |
| global.value = global.value; | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| }); | |
| // One stack for nested calls, so hot paths allocate nothing. | |
| entry.callSaved = []; | |
| // A metered module's own fuel bounds its top level, as it does its | |
| // calls. | |
| const fuelMs = host.exports['_rive_web_vm_start_fuel'](entry.ctx.vm); | |
| const arm = instance.exports['__riveFuelArm']; | |
| const end = instance.exports['__riveFuelEnd']; | |
| const fueled = fuelMs > 0 && arm !== undefined && end !== undefined; | |
| const booting = host.exports['_rive_web_vm_booting']; | |
| booting(entry.ctx.vm); | |
| const mark = host.stackSave(); | |
| // A wait in the top level moves a metered module's fuel on with a call. | |
| entry.instance = instance; | |
| try { | |
| if (fueled) arm(fuelMs); | |
| instance.exports[kLiftedStartExport]?.(); | |
| if (fueled) end(); | |
| } catch (error) { | |
| entry.instance = null; | |
| // A host fault skips the epilogues of the librive frames it unwinds. | |
| host.stackRestore(mark); | |
| entry.ctx.raised = null; | |
| throw error; | |
| } finally { | |
| booting(0); | |
| } | |
| } | |
| async function prepareEntry(entry) { | |
| try { | |
| const lifted = liftStart(withMemory(entry)); | |
| const module = await WebAssembly.compile(lifted); | |
| const members = memberBytes(entry, lifted); | |
| if (entry.ctx.vm === 0) { | |
| return; | |
| } | |
| announceExports(entry, module); | |
| const lanes = prepareLanes(module); | |
| const instance = await WebAssembly.instantiate( | |
| module, | |
| importsFor(entry, module) | |
| ); | |
| await lanes; | |
| // Released while instantiating. | |
| if (entry.ctx.vm === 0) { | |
| return; | |
| } | |
| adopt(entry, instance); | |
| adoptMembers(entry, members); | |
| if (entry.parallel !== null) { | |
| await parallel.ready(entry.parallel); | |
| } | |
| } catch (error) { | |
| entry.error = errorText(error); | |
| } | |
| } | |
| // Lanes run the lane modules a script module carries, on whichever thread | |
| // runs them, reaching only their own natives, shared memory and the console. | |
| // Compiled lane modules, oldest first, matched by id or by their lifted | |
| // bytes, which librive hands over; a dropped one only costs a recompile. | |
| const laneModules = []; | |
| const kLaneModuleCache = 8; | |
| // The page's idle pthread workers, on a pthread build. | |
| let threads = () => []; | |
| // A team thread's worker by thread, on a pthread build. | |
| let teamWorkerOf = null; | |
| const parallel = createParallel({ | |
| can: () => teamWorkerOf !== null, | |
| start() { | |
| const thread = host.exports['_rive_web_parallel_team_thread'](); | |
| return thread === 0 ? null : (teamWorkerOf(thread) ?? null); | |
| }, | |
| }); | |
| function sameBytes(a, b) { | |
| if (a.length !== b.length) { | |
| return false; | |
| } | |
| for (let i = 0; i < a.length; i++) { | |
| if (a[i] !== b[i]) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| function cacheLane(bytes, module, error) { | |
| // job is the instance a finished job left on this thread for the next. | |
| const compiled = { bytes, module, error, ids: new Set(), job: null }; | |
| laneModules.push(compiled); | |
| if (laneModules.length > kLaneModuleCache) { | |
| laneModules.shift(); | |
| } | |
| return compiled; | |
| } | |
| const laneCached = (bytes) => laneModules.some((lane) => sameBytes(lane.bytes, bytes)); | |
| // A thread that missed the page's compile compiles its own. | |
| function shareLane(bytes, module) { | |
| if (laneCached(bytes)) { | |
| return; | |
| } | |
| cacheLane(bytes, module, null); | |
| for (const worker of threads()) { | |
| worker.postMessage({ 'riveLane': { 'bytes': bytes, 'module': module } }); | |
| } | |
| } | |
| const takeLane = (data) => { | |
| if (parallel.message(data)) { | |
| return; | |
| } | |
| const lane = data?.['riveLane']; | |
| if (lane !== undefined) { | |
| shareLane(lane['bytes'], lane['module']); | |
| } | |
| }; | |
| const nodeThreads = globalThis.process?.getBuiltinModule?.('worker_threads'); | |
| if (nodeThreads?.isMainThread === false) { | |
| nodeThreads.parentPort.on('message', takeLane); | |
| } else if (globalThis.WorkerGlobalScope !== undefined) { | |
| globalThis.addEventListener('message', (event) => takeLane(event.data)); | |
| } | |
| function checkLaneImports(module, kind) { | |
| for (const wanted of WebAssembly.Module.imports(module)) { | |
| if (laneImportRefusal(kind, wanted['module'], wanted['name']) !== null) { | |
| throw new Error(laneImportError(wanted['module'], wanted['name'])); | |
| } | |
| } | |
| } | |
| // Compiles a main module's lane sections for every thread, off the page's | |
| // thread unless now. One that fails is left to fail where it first runs. | |
| function prepareLanes(main, now = false) { | |
| const compiles = []; | |
| for (const [name, kind] of kLaneSections) { | |
| for (const section of WebAssembly.Module.customSections(main, name)) { | |
| const bytes = liftStart(new Uint8Array(section)); | |
| if (laneCached(bytes)) { | |
| continue; | |
| } | |
| const share = (module) => { | |
| checkLaneImports(module, kind); | |
| shareLane(bytes, module); | |
| }; | |
| if (now) { | |
| try { | |
| share(new WebAssembly.Module(bytes)); | |
| } catch (error) {} | |
| } else { | |
| compiles.push(WebAssembly.compile(bytes).then(share).catch(() => {})); | |
| } | |
| } | |
| } | |
| return Promise.all(compiles); | |
| } | |
| function laneModule(id, bytes, kind) { | |
| let compiled = laneModules.find((lane) => lane.ids.has(id)); | |
| if (compiled === undefined) { | |
| const lifted = bytes(); | |
| compiled = laneModules.find((lane) => sameBytes(lane.bytes, lifted)); | |
| if (compiled === undefined) { | |
| compiled = cacheLane(lifted, null, null); | |
| try { | |
| const module = new WebAssembly.Module(lifted); | |
| checkLaneImports(module, kind); | |
| compiled.module = module; | |
| } catch (error) { | |
| compiled.error = errorText(error); | |
| } | |
| } | |
| compiled.ids.add(id); | |
| if (compiled.ids.size > kLaneModuleCache) { | |
| compiled.ids.delete(compiled.ids.values().next().value); | |
| } | |
| } | |
| return compiled; | |
| } | |
| // A lane's shared imports: librive's natives, or with twin Atomics on | |
| // librive's heap reaching owner's objects, as an audio worklet does. | |
| function sharedImports(twin = false, owner = 0) { | |
| return (memory, fail) => | |
| twin | |
| ? createSharedLaneImports( | |
| () => host.heapU8().buffer, | |
| host.exports['_rive_shared_table'](), | |
| owner, | |
| memory, | |
| null, | |
| fail | |
| ) | |
| : createRiveModuleImports( | |
| librive( | |
| { vm: 1, raised: null, hostFault: false, counters }, | |
| 'rive_shared_v1' | |
| ), | |
| memory, | |
| 'rive_shared_v1' | |
| )['rive_shared_v1']; | |
| } | |
| // A browser cannot interrupt a lane, so a metered one's fuel checks throw | |
| // why librive would. | |
| function laneFuel(lane) { | |
| const stop = host.laneStop(lane); | |
| if (stop !== null) { | |
| throw stop; | |
| } | |
| } | |
| // The instance a job left idle on this thread, or a new one, whose imports | |
| // reach whichever job it runs. | |
| function jobInstance(compiled) { | |
| const idle = compiled.job; | |
| compiled.job = null; | |
| if (idle !== null) { | |
| idle.timers.clear(); | |
| return idle; | |
| } | |
| const job = { lane: 0, cancelled: null, timers: new Map(), reset: false }; | |
| return Object.assign( | |
| job, | |
| instantiateLane({ | |
| module: compiled.module, | |
| natives: 'rive_job_v1', | |
| own: () => ({ 'cancelled': () => job.cancelled() }), | |
| shared: sharedImports(), | |
| log: (line) => host.laneLog(job.lane, line), | |
| timers: job.timers, | |
| now: laneNow, | |
| fuel: () => laneFuel(job.lane), | |
| }) | |
| ); | |
| } | |
| // A stop reaches a running job through its cancelled() and its fuel | |
| // checks. The output views the job's memory. | |
| function runJob(lane, id, bytes, entry, input, cancelled) { | |
| const compiled = laneModule(id, bytes, 'job'); | |
| if (compiled.error !== null) { | |
| return { ok: false, error: compiled.error }; | |
| } | |
| const job = jobInstance(compiled); | |
| if (job.error !== undefined) { | |
| return { ok: false, error: job.error }; | |
| } | |
| job.lane = lane; | |
| job.cancelled = cancelled; | |
| const entered = enterLane( | |
| job, | |
| { prefix: kLaneEntryPrefixes['job'], entry, input }, | |
| job.reset | |
| ); | |
| if (entered.error !== undefined) { | |
| return { ok: false, error: entered.error }; | |
| } | |
| try { | |
| const buffer = job.memory.buffer; | |
| const words = new Uint32Array(buffer, entered.result >>> 0, 2); | |
| const output = | |
| words[1] === 0 ? new Uint8Array(0) : new Uint8Array(buffer, words[0], words[1]); | |
| // Only a job that returned, unstopped, with small memory leaves its | |
| // instance; the next resets it once this output is copied out. | |
| if ( | |
| job.exports[kLaneResetExport] !== undefined && | |
| buffer.byteLength <= kReuseMaxPages * 65536 && | |
| !cancelled() | |
| ) { | |
| job.reset = true; | |
| compiled.job = job; | |
| } | |
| return { ok: true, output }; | |
| } catch (error) { | |
| return { ok: false, error: job.failure(error) }; | |
| } | |
| } | |
| // Workers keep their instance between wakes, on the thread that started | |
| // them; librive steps each one and owns its waits. | |
| const workers = new Map(); | |
| let nextWorker = 1; | |
| // A worker's natives call back into librive through natives. | |
| function workerStart(lane, id, bytes, entry, input, natives) { | |
| const compiled = laneModule(id, bytes, 'worker'); | |
| if (compiled.error !== null) { | |
| return { ok: false, error: compiled.error }; | |
| } | |
| const started = startLane({ | |
| module: compiled.module, | |
| natives: 'rive_worker_v1', | |
| // A view of the worker's memory, which librive copies once. | |
| own: (memory) => ({ | |
| 'finished': (ok, at, length) => | |
| natives.finished( | |
| ok, | |
| new Uint8Array(memory().buffer, at >>> 0, length >>> 0) | |
| ), | |
| 'wait': natives.wait, | |
| 'stopping': natives.stopping, | |
| }), | |
| onThrow: (text) => host.laneFail(lane, text), | |
| shared: sharedImports(), | |
| log: (line) => host.laneLog(lane, line), | |
| now: laneNow, | |
| fuel: () => laneFuel(lane), | |
| prefix: kLaneEntryPrefixes['worker'], | |
| entry, | |
| input, | |
| free: true, | |
| }); | |
| if (started.error !== undefined) { | |
| return { ok: false, error: started.error }; | |
| } | |
| const worker = nextWorker++; | |
| workers.set(worker, { | |
| wake: started.exports[kWorkerWakeExport], | |
| collect: started.exports[kFrameMinorExport], | |
| }); | |
| return { ok: true, id: worker }; | |
| } | |
| // Returns null once the worker took the wake, else the error. | |
| function workerWake(worker, wait, value) { | |
| try { | |
| workers.get(worker).wake(wait, value); | |
| return null; | |
| } catch (error) { | |
| return errorText(error); | |
| } | |
| } | |
| // Back at the host no frame is live, so the frame collector may run. | |
| // Returns null, or the error that ended the worker. | |
| function workerCollect(worker) { | |
| try { | |
| workers.get(worker)?.collect?.(); | |
| return null; | |
| } catch (error) { | |
| return errorText(error); | |
| } | |
| } | |
| function workerEnd(worker) { | |
| workers.delete(worker); | |
| } | |
| // A processor rendered offline: librive pulls each block. | |
| const processors = new Map(); | |
| let nextProcessor = 1; | |
| function processorStart(lane, id, bytes, entry, input, format, sharedOwner) { | |
| const compiled = laneModule(id, bytes, 'processor'); | |
| if (compiled.error !== null) { | |
| return { ok: false, error: compiled.error }; | |
| } | |
| const started = startProcessorLane( | |
| compiled.module, | |
| entry, | |
| input, | |
| format, | |
| sharedImports(laneTwin, sharedOwner), | |
| (line) => host.laneLog(lane, line) | |
| ); | |
| if (started.error !== undefined) { | |
| return { ok: false, error: started.error }; | |
| } | |
| const processor = nextProcessor++; | |
| processors.set(processor, started.lane); | |
| return { ok: true, id: processor }; | |
| } | |
| function processorRender(processor, frames, startFrame, sink) { | |
| return processors.get(processor).render(frames, startFrame, sink); | |
| } | |
| // Processors in real time run on an audio worklet that reaches librive's | |
| // heap through Atomics; README.md says what the host provides for it. | |
| // owners: the librive processor and module behind each worklet id. | |
| const audio = { output: null, owners: new Map(), next: 1 }; | |
| // Checks of the worklet's block counter that may see no block before the | |
| // output ends. A start outlasts init's budget, which ends a metered one. | |
| const kStallCheckMs = 50; | |
| const kStallChecks = 2; | |
| const kStartStallChecks = kProcessorInitMs / kStallCheckMs + 10; | |
| const kReadyChecks = 40; | |
| // A stuck browser worklet holds up only its own context, and a slow block | |
| // that returns fails on its budget, so a stall there must last longer. | |
| const kBrowserStallChecks = 20; | |
| // An ended NRDP thread ran on for up to 39 ms on the devices we measured. | |
| const kEndedThreadMs = 200; | |
| const kContextClosed = 'script audio context closed'; | |
| // A context the browser lets start does so well within this. | |
| const kGestureWaitMs = 1000; | |
| // NRDP's names are quoted so a closure compiled host keeps them. | |
| const nrdp = () => globalThis['nrdp']; | |
| const onNrdp = () => nrdp()?.['audio']?.['output'] !== undefined; | |
| // The worklet a host serves, else the one a threaded build carries, which | |
| // needs nothing served beside the runtime. NRDP only runs its own bundle. | |
| let carriedWorklet = null; | |
| function workletUrl() { | |
| const given = runner['audioWorkletUrl']; | |
| if (typeof given === 'string') { | |
| return given; | |
| } | |
| const source = Module['riveAudioWorkletSource']; | |
| if (onNrdp() || typeof source !== 'string') { | |
| return null; | |
| } | |
| carriedWorklet ??= URL.createObjectURL( | |
| new Blob([source], { type: 'text/javascript' }) | |
| ); | |
| return carriedWorklet; | |
| } | |
| // Null when processors can run in real time, else why not. | |
| function audioRefusal() { | |
| if (!onNrdp() && globalThis.crossOriginIsolated !== true) { | |
| return 'real time audio needs cross origin isolation'; | |
| } | |
| // The worklet reaches librive's heap through Atomics. | |
| if ( | |
| typeof SharedArrayBuffer === 'undefined' || | |
| !(host.memory().buffer instanceof SharedArrayBuffer) | |
| ) { | |
| return 'real time audio needs librive built with threads'; | |
| } | |
| if (workletUrl() === null) { | |
| return 'real time audio needs riveScripting.audioWorkletUrl'; | |
| } | |
| if (typeof AudioContext === 'undefined') { | |
| return 'real time audio needs Web Audio'; | |
| } | |
| const given = runner['audioContext']; | |
| if (given?.state === 'closed') { | |
| return 'real time audio cannot start on a closed AudioContext'; | |
| } | |
| // A stuck worklet holds every later addModule on its context. | |
| if (contextHolds.get(given)?.stalled) { | |
| return "real time audio stalled the host's AudioContext; hand riveScripting a new one"; | |
| } | |
| return null; | |
| } | |
| // What both outputs share: messages queue until the worklet can take | |
| // them, and those that never reach it fail their processors. | |
| function audioQueue() { | |
| let queue = []; | |
| let send = null; | |
| return { | |
| post(message) { | |
| if (send !== null) { | |
| send(message); | |
| } else { | |
| queue.push(message); | |
| } | |
| }, | |
| open(sender) { | |
| send = sender; | |
| for (const message of queue) { | |
| send(message); | |
| } | |
| queue = []; | |
| }, | |
| fail(text) { | |
| for (const message of queue) { | |
| audioEvent({ 'id': message['id'], 'kind': 'failed', 'text': text }); | |
| } | |
| queue = []; | |
| }, | |
| }; | |
| } | |
| // The worklets run uncompiled, so keys that cross to them are quoted. | |
| function workletOptions(holds) { | |
| return { | |
| 'numberOfInputs': 0, | |
| 'numberOfOutputs': 1, | |
| 'outputChannelCount': [2], | |
| 'processorOptions': { | |
| 'memory': host.memory(), | |
| 'table': host.exports['_rive_shared_table'](), | |
| 'counter': holds.buffer, | |
| }, | |
| }; | |
| } | |
| const newHolds = () => new Int32Array(new SharedArrayBuffer(kHoldWords * 4)); | |
| // Frees the lock and lane word a worklet that is surely gone left held. | |
| const releaseLane = (holds) => | |
| releaseSharedLane( | |
| () => host.memory().buffer, | |
| host.exports['_rive_shared_table'](), | |
| holds | |
| ); | |
| function failOwners(text) { | |
| for (const id of [...audio.owners.keys()]) { | |
| audioEvent({ 'id': id, 'kind': 'failed', 'text': text }); | |
| } | |
| } | |
| // Calls end with why once the worklet stops counting blocks for | |
| // stallChecks while processors wait on it, or once context closes. | |
| function watchBlocks(context, holds, readyChecks, stallChecks, end, closed = end) { | |
| let ready = false; | |
| let seen = -1; | |
| let stalled = 0; | |
| const timer = setInterval(() => { | |
| if (context.state === 'closed') { | |
| clearInterval(timer); | |
| closed(kContextClosed); | |
| return; | |
| } | |
| const now = Atomics.load(holds, kHoldBlocks); | |
| // A suspended context renders nothing without being stuck. A | |
| // stall outlives the stop of the processor that caused it. | |
| const waiting = | |
| (audio.owners.size > 0 || stalled > 0) && context.state === 'running'; | |
| stalled = now === seen && waiting ? stalled + 1 : 0; | |
| seen = now; | |
| const limit = !ready | |
| ? readyChecks | |
| : Atomics.load(holds, kHoldStarting) !== 0 | |
| ? kStartStallChecks | |
| : stallChecks; | |
| if (stalled >= limit) { | |
| clearInterval(timer); | |
| end(ready ? 'processor stalled the audio thread' : 'script audio worklet never answered'); | |
| } | |
| }, kStallCheckMs); | |
| return { | |
| ready() { | |
| ready = true; | |
| stalled = 0; | |
| }, | |
| stop: () => clearInterval(timer), | |
| }; | |
| } | |
| // An output the host's worklet runs. sent: the modules it has the bytes | |
| // of, so each crosses once; a worklet that lost them asks again. | |
| function audioOutputOf(context, queue) { | |
| return { context, post: queue.post, sent: new Set(), queue }; | |
| } | |
| // An output that failed or was ended: what it still queued fails, and the | |
| // next start makes a fresh one. Ends the context when it was ours. | |
| function dropOutput(output, text, close = true) { | |
| output.queue.fail(text); | |
| if (audio.output === output) { | |
| audio.output = null; | |
| } | |
| if (close && runner['audioContext'] === undefined) { | |
| output.context.close?.(); | |
| } | |
| } | |
| const noWorklet = (error) => | |
| 'script audio has no worklet: ' + errorText(error); | |
| // A browser context's worklet keeps one lane word until the context | |
| // closes, unless it stalled, as a stalled worklet may yet run. | |
| const contextHolds = new WeakMap(); | |
| function browserHolds(context) { | |
| let holds = contextHolds.get(context); | |
| if (holds === undefined) { | |
| holds = { words: newHolds(), stalled: false }; | |
| contextHolds.set(context, holds); | |
| context.addEventListener('statechange', () => { | |
| if (context.state === 'closed' && !holds.stalled) { | |
| releaseLane(holds.words); | |
| } | |
| }); | |
| } | |
| return holds; | |
| } | |
| // Browsers start audio only after a gesture, so a context only the runner | |
| // holds resumes on the page's first one, and warns once while it waits. | |
| function ownContext() { | |
| // iOS mutes the default ambient session at the silent switch, which | |
| // video plays through. | |
| const session = globalThis.navigator?.['audioSession']; | |
| if (session?.['type'] === 'auto') { | |
| session['type'] = 'playback'; | |
| } | |
| const context = new AudioContext(); | |
| const gestures = ['pointerdown', 'pointerup', 'keydown', 'touchend']; | |
| const resume = () => context.resume(); | |
| const waiting = setTimeout( | |
| () => console.warn('script audio waits for a click, tap or key press on the page'), | |
| kGestureWaitMs | |
| ); | |
| const started = () => { | |
| if (context.state === 'running' || context.state === 'closed') { | |
| clearTimeout(waiting); | |
| context.removeEventListener('statechange', started); | |
| for (const gesture of gestures) { | |
| globalThis.removeEventListener(gesture, resume, { capture: true }); | |
| } | |
| } | |
| }; | |
| for (const gesture of gestures) { | |
| globalThis.addEventListener(gesture, resume, { capture: true }); | |
| } | |
| context.addEventListener('statechange', started); | |
| started(); | |
| return context; | |
| } | |
| function browserOutput() { | |
| const context = runner['audioContext'] ?? ownContext(); | |
| const queue = audioQueue(); | |
| const output = audioOutputOf(context, queue); | |
| const holds = browserHolds(context); | |
| let node = null; | |
| const closed = (text) => { | |
| if (audio.output === output) { | |
| watch.stop(); | |
| failOwners(text); | |
| dropOutput(output, text, false); | |
| } | |
| }; | |
| // Nothing ends a worklet stuck in a block, so its processors fail and the node | |
| // leaves the mix in case it returns. Chrome's close would wait on that thread. | |
| const stalled = (text) => { | |
| holds.stalled = true; | |
| node.port.onmessage = null; | |
| node.port.postMessage({ 'kind': 'end' }); | |
| node.disconnect(); | |
| failOwners(text); | |
| dropOutput(output, text, false); | |
| }; | |
| const watch = watchBlocks( | |
| context, | |
| holds.words, | |
| Infinity, | |
| kBrowserStallChecks, | |
| stalled, | |
| closed | |
| ); | |
| context.addEventListener('statechange', () => { | |
| if (context.state === 'closed') { | |
| closed(kContextClosed); | |
| } | |
| }); | |
| context.audioWorklet | |
| .addModule(workletUrl()) | |
| .then(() => { | |
| node = new AudioWorkletNode( | |
| context, | |
| kProcessorNode, | |
| workletOptions(holds.words) | |
| ); | |
| node.port.onmessage = (event) => audioEvent(event.data); | |
| node.connect(context.destination); | |
| watch.ready(); | |
| queue.open((message) => node.port.postMessage(message)); | |
| }) | |
| .catch((error) => { | |
| watch.stop(); | |
| dropOutput(output, noWorklet(error)); | |
| }); | |
| return output; | |
| } | |
| // NRDP's worklet has no addModule: init binds a thread of ours to the | |
| // context, which runs the bundled script and adds the processor. | |
| function nrdpOutput() { | |
| const url = runner['audioWorkletUrl']; | |
| let context = runner['audioContext']; | |
| if (context === undefined) { | |
| // A context made before the output opens never renders. | |
| nrdp()['audio']['output']['open'](); | |
| context = new AudioContext(); | |
| } | |
| context['forceStart']?.(); | |
| const queue = audioQueue(); | |
| const output = audioOutputOf(context, queue); | |
| const holds = newHolds(); | |
| let thread = null; | |
| let ready = false; | |
| let ended = false; | |
| // A processor stuck in a block freezes every context; ending the | |
| // thread lets sample playback resume. | |
| const end = (text) => { | |
| if (ended) { | |
| return; | |
| } | |
| ended = true; | |
| watch.stop(); | |
| thread?.['terminate'](); | |
| if (ready) { | |
| // Waits here, where librive cannot meanwhile spin on a lock | |
| // the ended thread left held. | |
| for (const until = monotonicNow() + kEndedThreadMs; monotonicNow() < until; ) {} | |
| releaseLane(holds); | |
| failOwners(text); | |
| } | |
| dropOutput(output, text); | |
| }; | |
| const watch = watchBlocks(context, holds, kReadyChecks, kStallChecks, end); | |
| // A throw in a callback NRDP runs would escape the fetch's catch. | |
| const guard = | |
| (fn) => | |
| (...args) => { | |
| try { | |
| fn(...args); | |
| } catch (error) { | |
| end(noWorklet(error)); | |
| } | |
| }; | |
| const onReady = guard(() => { | |
| const node = new AudioWorkletNode( | |
| context, | |
| kProcessorNode, | |
| workletOptions(holds) | |
| ); | |
| node.connect(context.destination); | |
| ready = true; | |
| watch.ready(); | |
| queue.open((message) => thread['send'](message)); | |
| }); | |
| fetch(url) | |
| .then((response) => { | |
| if (!response.ok) { | |
| throw new Error(url + ' answered HTTP ' + response.status); | |
| } | |
| return response.text(); | |
| }) | |
| .then((source) => { | |
| if (ended) { | |
| return; | |
| } | |
| const worklet = context.audioWorklet; | |
| worklet['active'] = true; | |
| worklet['init']( | |
| guard(() => { | |
| if (ended) { | |
| return; | |
| } | |
| thread = nrdp()['thread']['start']({ | |
| 'url': url, | |
| 'source': source, | |
| 'audio': context, | |
| }); | |
| thread.addEventListener('raw-message', (message) => | |
| message['kind'] === 'ready' ? onReady() : audioEvent(message) | |
| ); | |
| }) | |
| ); | |
| }) | |
| .catch((error) => end(noWorklet(error))); | |
| return output; | |
| } | |
| function audioOutput() { | |
| audio.output ??= onNrdp() ? nrdpOutput() : browserOutput(); | |
| return audio.output; | |
| } | |
| // Forgets the oldest modules no processor runs, past the cache's size. | |
| function forgetModules(output) { | |
| const running = new Set([...audio.owners.values()].map((o) => o.moduleId)); | |
| for (const moduleId of output.sent) { | |
| if (output.sent.size <= kLaneModuleCache) { | |
| return; | |
| } | |
| if (!running.has(moduleId)) { | |
| output.sent.delete(moduleId); | |
| output.post({ 'kind': 'forget', 'moduleId': moduleId }); | |
| } | |
| } | |
| } | |
| // Bytes rather than the compiled module: a browser's worklet may sit in | |
| // another agent cluster, where a module cannot cross. | |
| function postStart(output, owned, withBytes) { | |
| const compiled = owned.compiled; | |
| output.sent.add(owned.moduleId); | |
| output.post({ | |
| ...owned.start, | |
| 'bytes': withBytes ? compiled.bytes : null, | |
| }); | |
| } | |
| // owner names the processor to librive when the worklet answers. | |
| function audioStart(owner, moduleId, bytes, entry, input, format, hardBlocks, sharedOwner) { | |
| const compiled = laneModule(moduleId, bytes, 'processor'); | |
| if (compiled.error !== null) { | |
| return { ok: false, error: compiled.error }; | |
| } | |
| const output = audioOutput(); | |
| const id = audio.next++; | |
| const owned = { | |
| owner, | |
| moduleId, | |
| compiled, | |
| start: { | |
| 'kind': 'start', | |
| 'id': id, | |
| 'moduleId': moduleId, | |
| 'entry': entry, | |
| 'input': input, | |
| 'format': { | |
| 'sampleRate': format.sampleRate, | |
| 'maxFrames': format.maxFrames, | |
| 'channels': format.channels, | |
| }, | |
| 'hardBlocks': hardBlocks, | |
| 'sharedOwner': sharedOwner, | |
| }, | |
| }; | |
| audio.owners.set(id, owned); | |
| postStart(output, owned, !output.sent.has(moduleId)); | |
| forgetModules(output); | |
| return { ok: true, id }; | |
| } | |
| function audioStop(id) { | |
| audio.owners.delete(id); | |
| if (audio.output !== null) { | |
| audio.output.post({ 'kind': 'stop', 'id': id }); | |
| forgetModules(audio.output); | |
| } | |
| } | |
| // The LaneState a processor reached, or a console line. | |
| const kProcessorEvents = { | |
| 'running': kLaneStates['running'], | |
| 'failed': kLaneStates['failed'], | |
| 'log': kProcessorEventLog, | |
| }; | |
| function audioEvent(event) { | |
| const owned = audio.owners.get(event['id']); | |
| if (owned === undefined) { | |
| return; | |
| } | |
| if (event['kind'] === 'resend') { | |
| if (audio.output !== null) { | |
| postStart(audio.output, owned, true); | |
| } | |
| return; | |
| } | |
| host.processorEvent(owned.owner, kProcessorEvents[event['kind']], event['text'] ?? ''); | |
| } | |
| function processorEnd(processor) { | |
| processors.delete(processor); | |
| } | |
| const runner = { | |
| attach(primitives) { | |
| if (host !== null) { | |
| return; | |
| } | |
| host = primitives; | |
| // Zero where the engine has no timeOrigin. | |
| const offset = performance.timeOrigin - host.pageOrigin || 0; | |
| laneNow = () => performance.now() + offset; | |
| }, | |
| attachThreads(workers, workerOf) { | |
| threads = workers; | |
| teamWorkerOf = workerOf; | |
| }, | |
| register(vm, bytes) { | |
| const copy = bytes.slice(); | |
| vms.set(vm, { | |
| ctx: { vm, raised: null, hostFault: false, counters }, | |
| bytes: copy, | |
| instance: null, | |
| memory: null, | |
| parallelMemory: null, | |
| parallelSize: null, | |
| parallel: null, | |
| preparing: null, | |
| error: missingSimd(copy), | |
| names: new Map(), | |
| }); | |
| }, | |
| release(vm) { | |
| const entry = vms.get(vm); | |
| if (entry) { | |
| // A module still instantiating may call imports afterwards. | |
| entry.ctx.vm = 0; | |
| vms.delete(vm); | |
| if (entry.parallel !== null) { | |
| parallel.release(entry.parallel); | |
| } | |
| } | |
| }, | |
| // The team a parallel call runs on, for tests to report. | |
| parallelState() { | |
| return parallel.state(); | |
| }, | |
| prepare() { | |
| const pending = []; | |
| for (const entry of vms.values()) { | |
| if (entry.instance === null && entry.error === null) { | |
| entry.preparing ??= prepareEntry(entry); | |
| pending.push(entry.preparing); | |
| } | |
| } | |
| return Promise.all(pending); | |
| }, | |
| // Returns null when the VM has an instance, else the error. | |
| start(vm) { | |
| const entry = vms.get(vm); | |
| if (!entry) { | |
| return 'script module was never registered'; | |
| } | |
| if (entry.instance === null && entry.error === null) { | |
| if (entry.preparing !== null) { | |
| return 'script module is still being prepared'; | |
| } | |
| try { | |
| const lifted = liftStart(withMemory(entry)); | |
| const module = new WebAssembly.Module(lifted); | |
| announceExports(entry, module); | |
| prepareLanes(module, true); | |
| adopt( | |
| entry, | |
| new WebAssembly.Instance(module, importsFor(entry, module)) | |
| ); | |
| // Members start in the background; calls before they | |
| // do get fewer parts. | |
| adoptMembers(entry, memberBytes(entry, lifted)); | |
| } catch (error) { | |
| entry.error = errorText(error); | |
| } | |
| } | |
| return entry.error; | |
| }, | |
| preparing(vm) { | |
| const entry = vms.get(vm); | |
| return entry?.instance === null && | |
| entry.error === null && | |
| entry.preparing !== null | |
| ? 1 | |
| : 0; | |
| }, | |
| hasExport(vm, namePtr) { | |
| const entry = vms.get(vm); | |
| return typeof entry?.instance?.exports[host.utf8(namePtr)] === | |
| 'function' | |
| ? 1 | |
| : 0; | |
| }, | |
| // Args arrive as doubles: a u32 is exact in one and the engine | |
| // converts each to the export's own parameter type. | |
| call(vm, namePtr, argc, argv, resultPtr, trapBuffer, trapCapacity) { | |
| const entry = vms.get(vm); | |
| if (!entry || entry.instance === null) { | |
| return kMissing; | |
| } | |
| // Export names reach librive's call seam as string literals, so | |
| // the pointer identifies the name. | |
| let fn = entry.names.get(namePtr); | |
| if (fn === undefined) { | |
| fn = entry.instance.exports[host.utf8(namePtr)] ?? null; | |
| entry.names.set(namePtr, fn); | |
| } | |
| if (typeof fn !== 'function') { | |
| return kMissing; | |
| } | |
| const callState = entry.callState; | |
| const saved = entry.callSaved; | |
| const base = saved.length; | |
| for (let i = 0; i < callState.length; i++) { | |
| saved.push(callState[i].value); | |
| } | |
| try { | |
| const result = callExport(fn, host.heapF64(), argv >> 3, argc); | |
| host.heapF64()[resultPtr >> 3] = | |
| typeof result === 'number' ? result : 0; | |
| return kOk; | |
| } catch (error) { | |
| entry.ctx.raised = null; | |
| host.writeUtf8( | |
| errorText(error), | |
| trapBuffer, | |
| trapCapacity | |
| ); | |
| return entry.ctx.hostFault ? kHostFault : kTrapped; | |
| } finally { | |
| for (let i = 0; i < callState.length; i++) { | |
| callState[i].value = saved[base + i]; | |
| } | |
| saved.length = base; | |
| } | |
| }, | |
| raise(vm, messagePtr) { | |
| const entry = vms.get(vm); | |
| if (entry) { | |
| entry.ctx.raised = host.utf8(messagePtr); | |
| } | |
| }, | |
| read(vm, appAddr, dst, size) { | |
| const memory = vms.get(vm)?.memory; | |
| if (!memory || appAddr + size > memory.buffer.byteLength) { | |
| return 0; | |
| } | |
| host.heapU8().set(new Uint8Array(memory.buffer, appAddr, size), dst); | |
| if (counters.on) counters.bytesRead += size; | |
| return 1; | |
| }, | |
| write(vm, appAddr, src, size) { | |
| const memory = vms.get(vm)?.memory; | |
| if (!memory || appAddr + size > memory.buffer.byteLength) { | |
| return 0; | |
| } | |
| new Uint8Array(memory.buffer, appAddr, size).set( | |
| host.heapU8().subarray(src, src + size) | |
| ); | |
| if (counters.on) counters.bytesWritten += size; | |
| return 1; | |
| }, | |
| // Length of the zero terminated string at appAddr, or -1. | |
| stringLength(vm, appAddr) { | |
| const memory = vms.get(vm)?.memory; | |
| if (!memory || appAddr >= memory.buffer.byteLength) { | |
| return -1; | |
| } | |
| const end = new Uint8Array(memory.buffer).indexOf(0, appAddr); | |
| return end < 0 ? -1 : end - appAddr; | |
| }, | |
| pages(vm) { | |
| const memory = vms.get(vm)?.memory; | |
| // Not >>> 16, which reads a full 4 GiB memory as none. | |
| return memory ? memory.buffer.byteLength / 65536 : 0; | |
| }, | |
| // File::frameBoundary for every live VM, for hosts that keep no list | |
| // of their files. Returns the warnings they raised. | |
| frameBoundary() { | |
| const warnings = []; | |
| for (const vm of vms.keys()) { | |
| const text = host.exports['_rive_web_vm_frame_boundary'](vm); | |
| if (text !== 0) { | |
| warnings.push(host.utf8(text)); | |
| } | |
| } | |
| return warnings; | |
| }, | |
| // Starts or stops tallying the bridge traffic, from zero. | |
| countHostCalls(on) { | |
| counters.on = !!on; | |
| counters.hostCalls = 0; | |
| counters.bytesStaged = 0; | |
| counters.bytesCopiedOut = 0; | |
| counters.bytesRead = 0; | |
| counters.bytesWritten = 0; | |
| }, | |
| // Calls into librive, bytes staged into its heap and copied back, and | |
| // bytes librive read and wrote through its memory seam. | |
| hostCallCounts() { | |
| return { | |
| 'hostCalls': counters.hostCalls, | |
| 'bytesStaged': counters.bytesStaged, | |
| 'bytesCopiedOut': counters.bytesCopiedOut, | |
| 'bytesRead': counters.bytesRead, | |
| 'bytesWritten': counters.bytesWritten, | |
| }; | |
| }, | |
| }; | |
| runner['runJob'] = runJob; | |
| runner['workerStart'] = workerStart; | |
| runner['workerWake'] = workerWake; | |
| runner['workerCollect'] = workerCollect; | |
| runner['workerEnd'] = workerEnd; | |
| runner['processorStart'] = processorStart; | |
| runner['processorRender'] = processorRender; | |
| runner['processorEnd'] = processorEnd; | |
| runner['audioRefusal'] = audioRefusal; | |
| runner['audioSampleRate'] = () => audioOutput().context.sampleRate; | |
| runner['audioStart'] = audioStart; | |
| runner['audioStop'] = audioStop; | |
| // For hosts and tests: where the real time output stands. | |
| runner['audioState'] = () => ({ | |
| 'context': audio.output?.context.state ?? 'none', | |
| 'processors': audio.owners.size, | |
| }); | |
| // The entries hosts call from outside this script. | |
| runner['prepare'] = runner.prepare; | |
| runner['frameBoundary'] = runner.frameBoundary; | |
| runner['countHostCalls'] = runner.countHostCalls; | |
| runner['hostCallCounts'] = runner.hostCallCounts; | |
| Module['riveScripting'] = runner; | |
| return runner; | |
| } |