blob: 708ed9a2d381a3ddab13ae9c9f34582b58d9ba19 [file]
#!/bin/bash
# Builds librive for the web with the wasm scripting backend and runs baked
# AnimaScript files on the JS engine under node, the browser lane's path.
set -e
cd "$(dirname "$0")"
# The link does not track its --pre-js input, so a changed bundle would
# otherwise be silently left out.
for BUNDLE in ../../src/wasm/web/rive_{scripting,audio_worklet,threads}_pre.js; do
for LINKED in out/wasm{,_threads}_debug/web_scripting_{harness,page}; do
if [[ $BUNDLE -nt $LINKED.mjs ]]; then
rm -f $LINKED.mjs $LINKED.wasm
fi
done
done
# A lane that never ends holds node's event loop, so a run that would wait on
# one fails under this alarm instead of hanging the suite.
guarded() {
perl -e 'alarm shift; exec @ARGV' 60 "$@"
}
# The audio worklet's shared memory protocol, on a heap laid out by hand.
node shared_lane_test.mjs
# The page side of the lanes and of real time audio, without librive.
guarded node web_host_test.mjs
# The browser audio runner when a browser exits early or cannot start.
node browser_audio_test.mjs
build_rive.sh wasm debug --with_rive_scripting --scripting_vm=wasm \
--with_rive_layout --with_rive_tools -- web_scripting_harness \
web_scripting_page
HARNESS=out/wasm_debug/web_scripting_harness.mjs
BASIC=(--expect "init ran on the web" --expect "clocks ok" --expect "random ok"
--expect "drew frame 2")
node run.mjs $HARNESS fixture/basic.riv "${BASIC[@]}"
# Without prepare() the module instantiates inline when scripts start.
node run.mjs $HARNESS fixture/basic.riv --sync "${BASIC[@]}"
# A host without a list of its files services every script VM through the
# runner once a frame, as the JS runtime does, which hands back warnings only.
node run.mjs $HARNESS fixture/basic.riv --page-boundary \
--expect "page boundary called __riveFrameMinor" \
--expect "page boundary warned []"
# A parallel module runs its parts one after another where there are no
# threads, with its memory shared or, without cross origin isolation,
# unshared. Its first call comes in its first step, which once trapped. Its
# second call throws in a part, and the call after it starts clean.
PARALLEL=(--until "parallel after a throw"
--expect "parallel fill wrong 0" --expect "parallel structs wrong 0"
--expect "parallel inplace ok" --expect "parallel globals ok"
--expect "parallel sync ok" --expect "parallel up to 2 ok"
--expect "parallel fixed parts ok" --expect "parallel claims wrong 0"
--expect "parallel exchange ok" --expect "parallel clock wrong 0"
--expect "parallel memory grew yes" --expect "parallel grown wrong 0"
--expect "wasm call trapped in host_obj_advance: the last part gave up"
--expect "parallel after a throw ok")
SERIAL=(--expect "parallel size 1" --expect "parallel threads no"
--expect "parallel parts all yes")
# Every part spins until the call's budget stops them all.
STOPPED=(--until "parallel after a stop"
--expect "wasm call trapped in host_obj_advance: execution exceeded timeout"
--expect "parallel after a stop ok")
# Parallel.inPart() is false on the main lane and true in parts, a PerPart
# grows to a call's parts before they run, and parts call a @parallel
# function value bit for bit as the main lane does.
DESIGN=(--until "design friction wrong" --expect "design in part ok"
--expect "design per part wrong 0 objects enough"
--expect "design friction wrong 0")
for MODE in "" --sync --no-isolation; do
node run.mjs $HARNESS fixture/parallel.riv $MODE "${SERIAL[@]}" "${PARALLEL[@]}"
node run.mjs $HARNESS fixture/parallel_design.riv $MODE "${DESIGN[@]}"
done
node run.mjs $HARNESS fixture/parallel_stop.riv "${STOPPED[@]}"
# Without threads a job runs at a later advance, as a budgeted call never runs
# one on its joiner, which rive-cli's script_jobs_test.cpp also asserts.
JOBS=(--expect "job threads 0" --expect "job square 9.0"
--expect "job tile munich 4 1.5 munich/inner" --expect "job error bad code 3"
--expect "job says 42" --expect "job matches 24"
--expect "unjoined job done 144.0" --expect "job says 7"
--until "job matches" --until "job says 7")
node run.mjs $HARNESS fixture/jobs.riv "${JOBS[@]}"
node run.mjs $HARNESS fixture/jobs.riv --sync "${JOBS[@]}"
# A job may run in the instance an earlier job left on its thread, and finds
# it as a fresh one. A job that threw or grew its memory far leaves none.
REUSE=(--until "reuse done" --expect "reuse fresh state 32 of 32"
--expect "reuse kept an instance yes"
--expect "reuse after a throw fresh yes: tripped 3"
--expect "reuse after a stop fresh yes"
--expect "reuse dropped the hungry instance yes")
ANEW=(--expect "reuse after a throw started anew yes")
node run.mjs $HARNESS fixture/job_reuse.riv "${REUSE[@]}" "${ANEW[@]}"
node run.mjs $HARNESS fixture/job_reuse.riv --sync "${REUSE[@]}" "${ANEW[@]}"
# Shared memory through jobs: atomics, the lock, a ring, a snapshot, and a
# trap inside exclusive that poisons and frees its region.
SHARED=(--until "shared lock freed" --expect "shared atomic 800 locked 400"
--expect "shared ring 20 sum 190.0" --expect "shared snapshot 4.0"
--expect "shared trap failed poisoned yes" --expect "shared lock freed"
--expect "shared rebuilt handle held its own"
--expect "shared value after release 7.0")
node run.mjs $HARNESS fixture/shared.riv "${SHARED[@]}"
# Workers without threads step between frames on the page's thread, and the
# main lane only watches them, so the run lets frames pass until they finish.
# The native lane asserts the same lines.
WORKERS=(--until "workers done" --expect "worker echo 10 last 20.0 count 10"
--expect "worker echo stopped finished" --expect "worker sleep completed"
--expect "worker threw failed with code" --expect "worker thrower failed: worker code 7"
--expect "worker adders agree yes" --expect "worker snapshot 5"
--expect "workers done")
node run.mjs $HARNESS fixture/workers.riv "${WORKERS[@]}"
node run.mjs $HARNESS fixture/workers.riv --sync "${WORKERS[@]}"
# A worker that awaits again after its stop is ended, and one that yields
# each pass gets a bounded slice per advance, so neither holds the page.
STOPS=(--until "worker stops done" --expect "ticker stopped yielder finished"
--expect "worker stops done")
node run.mjs $HARNESS fixture/worker_stops.riv "${STOPS[@]}"
node run.mjs $HARNESS fixture/worker_stops.riv --sync "${STOPS[@]}"
# Workers without threads step again while due within an advance's slice,
# and a parked one lets its host settle, as the native lane asserts.
SLICE=(--until "worker slice done"
--expect "yielder rounds past one per advance yes"
--expect "worker slice done" --expect "pending async work no")
node run.mjs $HARNESS fixture/worker_slice.riv "${SLICE[@]}"
node run.mjs $HARNESS fixture/worker_slice.riv --sync "${SLICE[@]}"
# Workers meet at a barrier whose participant count changes, and a break or a
# stop settles every call false, as the native lane asserts.
BARRIER=(--until "barrier worker 0 met" --until "barrier worker 1 met"
--until "barrier worker 2 met" --until "barrier worker 3 met"
--until "barrier break settled" --until "barrier stop settled"
--until "barrier stop settles a lone participant"
--expect "barrier worker 0 met 1056 rounds, 0 wrong"
--expect "barrier worker 1 met 1056 rounds, 0 wrong"
--expect "barrier worker 2 met 432 rounds, 0 wrong"
--expect "barrier worker 3 met 432 rounds, 0 wrong"
--expect "barrier break settled 3 waits, 1 at a gate, and 3 later calls false"
--expect "barrier stop settled 3 waits, 1 at a gate, false"
--expect "barrier stop settles a lone participant false"
--expect "barrier released settles false")
node run.mjs $HARNESS fixture/barrier.riv "${BARRIER[@]}"
node run.mjs $HARNESS fixture/barrier.riv --sync "${BARRIER[@]}"
# Only advances step a worker without threads, so its timed wait keeps a host
# that settles advancing until it wakes.
node run.mjs $HARNESS fixture/worker_timed.riv --while-pending \
--expect "sleeper woke"
# A lane without threads holds the page's thread, so a metered job or worker
# step that never ends fails at the page's call budget and frames go on.
SPINNER_TIMEOUT="execution exceeded timeout on the page's thread"
for FIXTURE in job_spin job_spin_fuel; do
guarded node run.mjs $HARNESS fixture/$FIXTURE.riv --until "job after" \
--expect "spinner failed: $SPINNER_TIMEOUT" \
--expect "job after the spinners 9.0"
done
for FIXTURE in worker_spin worker_spin_fuel; do
guarded node run.mjs $HARNESS fixture/$FIXTURE.riv --error-sink \
--until "spinner running" \
--expect-line "error sink: worker spinner failed: $SPINNER_TIMEOUT"
done
# The main lane awaits jobs, workers, signals and sleeps, and a worker's
# promise hands back its value, as the native lane asserts.
AWAITS=(--until "awaits done" --expect "joined promise Pending" --expect "promise square 16.0"
--expect "joined promise settled 25.0" --expect "promise rejected bad code 3"
--expect "main signal 1.0" --expect "main signal timed out yes"
--expect "main sleep completed" --expect "awaited square 36.0 then slept yes"
--expect "worker result answer 4 6.0" --expect "worker void done"
--expect "worker rejected with code" --expect "worker peek 6.0"
--expect "awaits done")
node run.mjs $HARNESS fixture/awaits.riv "${AWAITS[@]}"
# Ports to a service worker: replies awaited and drained, a full port
# refusing, a blob outliving its senders, as the native lane asserts.
PORTS=(--until "ports done" --expect "port full refused yes"
--expect "port awaited 30.0" --expect "cancelled job promise: job was stopped"
--expect "port drained 3 total 21.0"
--expect "port filled 20 total 190.0 refused some yes"
--expect "port blob sum 19900.0" --expect "port service answered 4"
--expect "ports done")
# Without threads a worker only steps between advances, so a wait inside one
# never sees its answer.
node run.mjs $HARNESS fixture/ports.riv "${PORTS[@]}" \
--expect "port answered within the call never"
# Audio processors render blocks on the engine's clock from their messages
# alone, so the page and the native lane must hear the same samples.
AUDIO=(--until "processor running" --audio 4 --expect "ramp init 2 channels"
--expect "processor 0 state 1 sums 496.32 -496.32"
--expect "processor 1 state 3 sums 0.00 0.00"
--expect "processor Thrower failed: bad block" --expect "processor threw bad block"
--expect "processor meter 512.0 gain 2.0")
node run.mjs $HARNESS fixture/audio.riv "${AUDIO[@]}"
node run.mjs $HARNESS fixture/audio.riv --sync "${AUDIO[@]}"
# The same samples through the shared memory natives an audio worklet uses:
# Atomics on librive's heap rather than librive's own natives.
node run.mjs $HARNESS fixture/audio.riv --lane-twin "${AUDIO[@]}"
# A processor's float recurrences and v128 path render the same bits on every
# tier, with fuel or without, as rive-cli's script_jobs_test.cpp expects.
SAME_SAMPLES="sums -3.45 650.69 hash 12aca437"
SAME_BITS=(--until "bank running" --audio 16
--expect "processor 0 state 1 $SAME_SAMPLES")
DETERMINISM=(determinism determinism_fuel determinism_release)
for FIXTURE in "${DETERMINISM[@]}"; do
node run.mjs $HARNESS fixture/$FIXTURE.riv "${SAME_BITS[@]}"
done
# Without cross origin isolation nothing would play a processor, so its start
# fails saying why instead of running silent.
node run.mjs $HARNESS fixture/audio.riv --until "processor threw" \
--expect "processor threw real time audio needs cross origin isolation" \
--expect "processor Thrower failed: real time audio needs cross origin isolation"
# A worklet reaches librive's heap through Atomics, which a build without
# threads cannot share.
node run.mjs $HARNESS fixture/audio.riv --nrdp-mock --until "processor threw" \
--expect "processor threw real time audio needs librive built with threads"
# A processor fails on a record naming shared objects and leaves it in the
# ring, through librive's natives or through Atomics.
HOLDS=(--pump-audio --until "ring keeps"
--expect "reader failed: a processor cannot pass shared objects through rings or snapshots"
--expect "ring keeps its record")
node run.mjs $HARNESS fixture/processor_holds.riv "${HOLDS[@]}"
node run.mjs $HARNESS fixture/processor_holds.riv --lane-twin "${HOLDS[@]}"
# A worker feeds a processor through a ring as the processor makes space.
FEED=(--pump-audio --expect "feed received 4096 sum 8386560.0")
node run.mjs $HARNESS fixture/feed.riv "${FEED[@]}"
node run.mjs $HARNESS fixture/feed.riv --lane-twin "${FEED[@]}"
# A blob crosses to jobs and processors as a handle; a job makes one for its
# result, and a processor plays one through librive or through Atomics. Each
# lane copies in and out by range.
BLOB=(--pump-audio --expect "blob bytes 1000 sum 125316.0"
--expect "blob part 50 read 30 sum 3525.0"
--expect "blob job sum 125316.0 slices 125316.0"
--expect "blob made 1000 doubled 125704.0"
--expect "sampler played 4096 sum 4193280.0")
node run.mjs $HARNESS fixture/blob.riv "${BLOB[@]}"
node run.mjs $HARNESS fixture/blob.riv --lane-twin "${BLOB[@]}"
# The main lane's audio clock is the frames the harness has pulled, so a note
# stamped against it plays on its frame, as in a rive-cli render.
CLOCK=(--pump-audio --expect "clock starts at 0.0" --expect "clock offset 0.0"
--expect "clock played 0.0 frames late")
node run.mjs $HARNESS fixture/clock.riv "${CLOCK[@]}"
# GPU buffers take ranges of a job's blob from librive's heap, by update and
# at their make, and the recording keeps the bytes once the blob is gone.
GPU_BLOB=(--until "gpu upload" --expect "skin uploaded"
--expect "gpu upload at 8 of 32 bytes sum 3024"
--expect "gpu buffer of 8 bytes immutable sum 660")
node run.mjs $HARNESS fixture/gpu_blob.riv "${GPU_BLOB[@]}"
# The same jobs on pthreads, the lane a cross origin isolated page runs: pool
# threads are browser workers, and only a pool thread can finish the job no
# one joins, since nothing pumps the queue when threads exist.
RIVE_OUT=out/wasm_threads_debug build_rive.sh wasm debug --with_rive_scripting \
--scripting_vm=wasm --with_rive_layout --with_rive_tools --web_threads \
-- web_scripting_harness web_scripting_page
THREADED=out/wasm_threads_debug/web_scripting_harness.mjs
# A build with threads refuses at once to load where it cannot share memory.
REFUSED="harness refused: this build runs threads, which need shared memory, and the page is not cross origin isolated; load the build without threads"
node run.mjs $THREADED fixture/basic.riv --no-isolation --expect "$REFUSED"
# A parallel call on pthreads runs one part on each of the page's team
# threads, as many as riveScripting.parallelParts asks with the page's own.
# The V8 in node 26 races its tier up of a call into JS against other threads
# making the same call, which browsers do not, so these runs leave that tier
# up off.
PARALLEL_NODE=(node --wasm-wrapper-tiering-budget=2000000000 run.mjs $THREADED)
TEAM=(--parallel-parts 4 --expect "parallel size 4" --expect "parallel threads yes")
"${PARALLEL_NODE[@]}" fixture/parallel.riv "${TEAM[@]}" "${PARALLEL[@]}" \
--expect "parallel parts all yes" --expect 'parallel team {"team":3,"waits":"async"}'
# Started without prepare(), the team starts in the background, and calls
# before it has get fewer parts.
"${PARALLEL_NODE[@]}" fixture/parallel.riv --sync "${TEAM[@]}" "${PARALLEL[@]}"
# A parked team thread without Atomics.waitAsync still takes its messages.
RIVE_PARALLEL_TIMED_WAITS=1 "${PARALLEL_NODE[@]}" fixture/parallel.riv \
"${TEAM[@]}" "${PARALLEL[@]}" --expect "parallel parts all yes" \
--expect 'parallel team {"team":3,"waits":"timed"}'
"${PARALLEL_NODE[@]}" fixture/parallel.riv --parallel-safe "${SERIAL[@]}" \
"${PARALLEL[@]}"
# One part turns parallel calls off for the page.
"${PARALLEL_NODE[@]}" fixture/parallel.riv --parallel-parts 1 "${SERIAL[@]}" \
"${PARALLEL[@]}" --expect 'parallel team {"team":0,"waits":"none"}'
"${PARALLEL_NODE[@]}" fixture/parallel_stop.riv --parallel-parts 4 "${STOPPED[@]}"
"${PARALLEL_NODE[@]}" fixture/parallel_design.riv --parallel-parts 4 "${DESIGN[@]}"
# Member setup that runs out of memory keeps the members it made, as natively,
# and a call runs on those.
"${PARALLEL_NODE[@]}" fixture/parallel_capped.riv --parallel-parts 4 \
--until "capped fewer parts" --expect "capped parts 3 total 3" \
--expect "capped fewer parts yes"
# A team thread that never takes its part, as one whose wake was lost, fails
# the call within its budget, or after a second without one, rather than
# hanging the page, and later calls run without it. A syncing call fails the
# same way.
LOST=(--until "parallel after a lost thread"
--expect "parallel after a lost thread parts 3 total 3")
export RIVE_PARALLEL_DEAF_THREAD=1
guarded "${PARALLEL_NODE[@]}" fixture/parallel_lost.riv --parallel-parts 4 "${LOST[@]}" \
--expect "wasm call trapped in host_obj_advance: execution exceeded timeout"
guarded "${PARALLEL_NODE[@]}" fixture/parallel_lost.riv --parallel-parts 4 "${LOST[@]}" \
--call-budget 0 --expect "wasm call trapped in host_obj_advance: a thread of the page's team did not start its part of a parallel call within 1000 ms"
guarded "${PARALLEL_NODE[@]}" fixture/parallel_frames.riv --parallel-parts 4 \
--expect "wasm call trapped in host_obj_advance: execution exceeded timeout"
# Part 0 waits at its sync on the page's thread, where the join cannot run,
# so it checks in with the page, and without a budget the call fails within a
# second there too.
perl -e 'alarm shift; exec @ARGV' 10 "${PARALLEL_NODE[@]}" fixture/parallel_frames.riv \
--parallel-parts 4 --call-budget 0 --expect "wasm call trapped in host_obj_advance: a thread of the page's team did not start its part of a parallel call within 1000 ms"
unset RIVE_PARALLEL_DEAF_THREAD
node run.mjs $THREADED fixture/jobs.riv "${JOBS[@]:2}"
# The page compiles each lane module once and hands it to the pool's threads,
# with or without prepare(), so none of them compiles its own.
for MODE in "" --sync; do
node --import ./worker_compiles.mjs run.mjs $THREADED fixture/jobs.riv \
$MODE "${JOBS[@]:2}" --expect "worker threads compiled 0 modules"
done
# Each pool thread keeps its own instance, so which job finds one varies.
node run.mjs $THREADED fixture/job_reuse.riv "${REUSE[@]}"
# Nothing interrupts the page's thread, so its join ends with the call's
# budget, and a cancel reaches a job through the flag it polls, fuel or not.
for FIXTURE in job_cancel job_cancel_release; do
node run.mjs $THREADED fixture/$FIXTURE.riv --until "spinner " \
--expect "long join ended within a second yes" \
--expect "spinner cancelled: job was stopped"
done
# A stopped job that never polls ends at its next fuel check, freeing its
# pool thread for the next job, as releasing its file does.
SPINNERS=(--until "job after" --expect "spinners stopped yes"
--expect "job after the spinners 9.0")
for FIXTURE in job_spin job_spin_fuel; do
node run.mjs $THREADED fixture/$FIXTURE.riv "${SPINNERS[@]}"
done
node run.mjs $THREADED fixture/jobs.riv --first fixture/job_spin_fuel.riv "${JOBS[@]:2}"
node run.mjs $THREADED fixture/shared.riv "${SHARED[@]}"
# A lock that ends with a top level's budget moves a metered module's fuel on,
# which takes a call into the module while it starts.
for MODE in "" --sync; do
node run.mjs $THREADED fixture/top_lock.riv $MODE \
--expect "top level lock timed out turns 2000"
done
# Workers on a small pool of pthreads.
node run.mjs $THREADED fixture/workers.riv "${WORKERS[@]}"
node run.mjs $THREADED fixture/worker_stops.riv "${STOPS[@]}"
node run.mjs $THREADED fixture/worker_slice.riv "${SLICE[@]}"
node run.mjs $THREADED fixture/barrier.riv "${BARRIER[@]}"
node run.mjs $THREADED fixture/awaits.riv "${AWAITS[@]}"
# A worker on its own pthread answers while the page's thread waits.
node run.mjs $THREADED fixture/ports.riv "${PORTS[@]}" \
--expect "port answered within the call 7.0"
# A stopped worker that never awaits again ends at a fuel check past its grace.
# One without fuel runs on, and its file's release waits only the grace for it.
SPIN=(--until "spinner running" --expect "spinner stop sent")
for FIXTURE in worker_spin worker_spin_fuel; do
node run.mjs $THREADED fixture/$FIXTURE.riv "${SPIN[@]}" \
--expect "spinner running after its stop no"
done
node run.mjs $THREADED fixture/worker_spin_release.riv "${SPIN[@]}" \
--expect "spinner running after its stop yes"
# A spinning worker must not starve the workers its runner stepped before,
# as the native lane asserts.
node run.mjs $THREADED fixture/worker_pinned.riv \
--until "pinned workers progress" --expect "pinned workers progress yes"
# Init runs on a pool thread, but the instance starts where blocks render.
node run.mjs $THREADED fixture/audio.riv "${AUDIO[@]}"
node run.mjs $THREADED fixture/audio.riv --lane-twin "${AUDIO[@]}"
for FIXTURE in "${DETERMINISM[@]}"; do
node run.mjs $THREADED fixture/$FIXTURE.riv "${SAME_BITS[@]}"
done
# The feeder runs on its own pthread, so the processor's read, through
# librive or through Atomics, must wake it across threads.
node run.mjs $THREADED fixture/feed.riv "${FEED[@]}"
node run.mjs $THREADED fixture/feed.riv --lane-twin "${FEED[@]}"
node run.mjs $THREADED fixture/blob.riv "${BLOB[@]}"
node run.mjs $THREADED fixture/blob.riv --lane-twin "${BLOB[@]}"
node run.mjs $THREADED fixture/gpu_blob.riv "${GPU_BLOB[@]}"
# NRDP's audio API, mocked as we measured it on devices, drives the runner's
# NRDP output and the worklet bundle NRDP's thread runs.
# These blocks render in real time, so no hard limit keeps a loaded machine
# from dropping a processor the run needs.
STEADY="--hard-blocks 0"
node run.mjs $THREADED fixture/audio.riv --nrdp-mock $STEADY --until "processor meter" \
--expect "ramp init 2 channels" --expect "processor running" \
--expect "processor threw bad block" --expect "processor meter"
node run.mjs $THREADED fixture/feed.riv --nrdp-mock $STEADY --until "feed received" \
--expect "feed received 4096 sum 8386560.0"
node run.mjs $THREADED fixture/processor_holds.riv --nrdp-mock $STEADY "${HOLDS[@]:1}"
for FIXTURE in overrun overrun_release; do
node run.mjs $THREADED fixture/$FIXTURE.riv --nrdp-mock --until "slow failed" \
--expect "slow failed: processor overran its block budget"
done
# A metered block stuck past its budget ends at its next fuel check and the
# mix plays on, as does one whose init never returns, offline too.
for FIXTURE in worklet_stall worklet_stall_fuel; do
guarded node run.mjs $THREADED fixture/$FIXTURE.riv --nrdp-mock --hard-blocks 20 \
--until "quiet rendered" \
--expect "stuck failed: processor overran its block budget" \
--expect "quiet rendered after stuck failed"
done
INIT_SPIN=(--until "quiet rendered"
--expect "spinner 0 failed: processor init overran its budget"
--expect "spinner 1 failed: processor init overran its budget"
--expect "quiet rendered after the spinners failed")
guarded node run.mjs $THREADED fixture/init_spin.riv --nrdp-mock $STEADY "${INIT_SPIN[@]}"
guarded node run.mjs $HARNESS fixture/init_spin.riv --pump-audio "${INIT_SPIN[@]}"
# The worklet bundle renders the bits harness_audio hashes.
for FIXTURE in "${DETERMINISM[@]}"; do
node run.mjs $THREADED fixture/$FIXTURE.riv --nrdp-mock $STEADY --hash 16 \
--until "worklet rendered" --expect "worklet rendered 16 blocks $SAME_SAMPLES"
done
# A processor stuck in a block freezes NRDP's audio, so the page ends the
# thread, frees the lock and lane word it held and fails the processor.
node run.mjs $THREADED fixture/stall.riv --nrdp-stall $STEADY \
--until "stall ring written after" \
--expect "stall failed: processor stalled the audio thread" \
--expect "stall ring written after yes" \
--expect "stall thread terminated yes" --expect "stall lock freed yes" \
--expect "stall lane freed yes"
# A worklet script that cannot be fetched, or a thread that never starts or
# never answers, fails the processors waiting on it.
node run.mjs $THREADED fixture/audio.riv --nrdp-fault start --until "processor threw" \
--expect "processor threw script audio has no worklet: thread start refused"
node run.mjs $THREADED fixture/audio.riv --nrdp-fault silent --until "processor threw" \
--expect "processor threw script audio worklet never answered"
node run.mjs $THREADED fixture/audio.riv --nrdp-fault fetch --until "processor threw" \
--expect "processor threw script audio has no worklet: rive_audio_worklet_nrdp.js answered HTTP 404"
# The worklet keeps only the modules the page has not told it to forget.
node run.mjs $THREADED fixture/audio.riv --nrdp-mock $STEADY --until "processor meter" \
--forget-modules --expect "processor meter" \
--expect "forgot modules 9000 9001 9002" --expect "forgotten module resent yes"
# A worklet node that refuses librive's memory fails the start.
node run.mjs $THREADED fixture/audio.riv --worklet-throws --until "processor threw" \
--expect "processor threw script audio has no worklet: memory could not be cloned"
# The worklet bundle runs a processor's relaxed SIMD, and a page whose engine
# lacks it turns the file's scripts off before any reaches the worklet.
node run.mjs $THREADED fixture/simd_processor.riv --nrdp-mock $STEADY \
--until "processor relaxed lane" --expect "processor relaxed lane 7"
node run.mjs $THREADED fixture/simd_processor.riv --nrdp-mock --lacks relaxed --vms 0 \
--expect "this browser lacks WebAssembly relaxed SIMD, so this file's scripts are off; bake with wasmSimd: on to support it" \
--expect "artboard drew 5 frames"
# A gradient crosses as raw module addresses. The script then traps in its
# third advance, which is reported while the page keeps drawing.
node run.mjs $HARNESS fixture/trap.riv \
--expect "gradient ready" \
--expect "wasm call trapped in host_obj_advance" \
--expect "last trap: unreachable" \
--expect "drew after advance 3.0" \
--expect "script vms alive after the frames: 1"
# A host's error sink takes the trap's line in place of stderr, as rive-cli's
# log does.
node run.mjs $HARNESS fixture/trap.riv --error-sink \
--expect-line "error sink: wasm call trapped in host_obj_advance: unreachable"
# Gradient stops far past the module's memory trap the call, as on WAMR,
# rather than staging gigabytes.
node run.mjs $HARNESS fixture/gradient_bounds.riv \
--expect "wasm call trapped in host_obj_user_init: out of bounds memory access" \
--expect "script vms alive after the frames: 1"
# Module addresses past 2 GiB reach JS as negative i32s, yet the staged
# imports and librive's memory seam still reach them, as on WAMR.
node run.mjs $HARNESS fixture/high_memory.riv --seam 2147484416 \
--expect "hi from past 2 GiB" \
--expect-line "managed past 2 GiB" \
--expect "trace: managed past 2 GiB 2" \
--expect "high mat4 inverse 1 0.5" \
--expect "high gradient minted" \
--expect "seam seam"
# A full 4 GiB memory still counts all of its pages, and the collector still
# carves chunks below its top.
node run.mjs $HARNESS fixture/top_memory.riv --expect "top gradient minted" \
--expect "top carve 300000"
# A trap leaves the spill zone to later calls: the runner puts the module's
# call state back after every call.
node run.mjs $HARNESS fixture/spill_trap.riv --expect "spill ok on frame 2"
# A collector that traps mid collection stops its module.
node run.mjs $HARNESS fixture/collector_trap.riv \
--expect "wasm call trapped in __riveFrameMinor" \
--expect "script vms alive after the frames: 0"
# Nesting that would run librive out of stack traps the call instead.
node run.mjs $HARNESS fixture/basic.riv --deep \
--expect "last trap: script calls nest too deep for the host stack" \
--expect "script vms alive after the frames: 1"
# A top level passes memory to the host, so its start function is lifted into
# an export. As on WAMR, only the ops that trap see the VM there.
TOP=(--expect "top level log" --expect "init after top level"
--expect "top level path handle 0"
--expect "top level join ran its job here no"
--expect "top level job 9.0 region 8" --until "top level job")
node run.mjs $HARNESS fixture/top_level.riv "${TOP[@]}"
node run.mjs $HARNESS fixture/top_level.riv --sync "${TOP[@]}"
# A top level that traps inside exclusive leaves nothing held for the files
# the page loads after it.
for MODE in "" --sync; do
for BUILD in $HARNESS $THREADED; do
node run.mjs $BUILD fixture/top_level.riv $MODE \
--first fixture/top_exclusive_trap.riv "${TOP[@]}" \
--expect "module instantiate failed: threw inside exclusive"
done
done
# A module whose start traps leaves the file, as a native one never joins it.
node run.mjs $HARNESS fixture/start_trap.riv --vms 0 \
--expect "wasm script module failed to start: module instantiate failed" \
--expect "script vms kept: 0"
# An error sink takes its line, as rive-cli's log does.
node run.mjs $HARNESS fixture/start_trap.riv --vms 0 --error-sink \
--expect-line "error sink: wasm script module failed to start: module instantiate failed: unreachable"
# A host fault in a prepared module's start fails it and leaves librive's
# stack where it was.
node run.mjs $HARNESS fixture/start_fault.riv --vms 0 --fault raise \
--expect "wasm script module failed to start: module instantiate failed: host fault injected" \
--expect "librive stack kept across prepare"
# A metered module's own fuel stops a top level that would hang the page.
RUNAWAY=(--vms 0
--expect "wasm script module failed to start: module instantiate failed: execution exceeded timeout")
node run.mjs $HARNESS fixture/runaway_top.riv "${RUNAWAY[@]}"
node run.mjs $HARNESS fixture/runaway_top.riv --sync "${RUNAWAY[@]}"
# A default release bake runs unbounded unless the page runs only what it can
# bound, as an editor does. Debug bakes run either way.
node run.mjs $HARNESS fixture/release.riv --expect "release init ran" \
--expect "a runaway script will hang the page"
UNBOUNDED=(--bounded --vms 0
--expect "wasm script module failed to start: module instantiate failed: this host runs only scripts it can bound"
--expect-line "script modules instantiated: 0")
node run.mjs $HARNESS fixture/release.riv "${UNBOUNDED[@]}"
node run.mjs $HARNESS fixture/release.riv --sync "${UNBOUNDED[@]}"
node run.mjs $HARNESS fixture/basic.riv --bounded "${BASIC[@]}"
# A release bake with wasmFuel stops its own runaway loop.
METERED=(--expect "metered release init"
--expect "wasm call trapped in host_obj_user_init: execution exceeded timeout")
node run.mjs $HARNESS fixture/release_fuel.riv "${METERED[@]}"
node run.mjs $HARNESS fixture/release_fuel.riv --sync "${METERED[@]}"
node run.mjs $HARNESS fixture/release_fuel.riv --bounded "${METERED[@]}"
# A bake whose main module has fuel meters its lanes too, so a page that runs
# only what it can bound runs its jobs as well.
node run.mjs $HARNESS fixture/jobs.riv --bounded "${JOBS[@]}"
node run.mjs $THREADED fixture/jobs.riv --bounded "${JOBS[@]:2}"
# An error sink takes the line its fuel reports and the failed start.
node run.mjs $HARNESS fixture/runaway_top.riv --vms 0 --error-sink \
--expect-line "error sink: script error: execution exceeded 200 millisecond timeout" \
--expect-line "error sink: wasm script module failed to start: module instantiate failed: execution exceeded timeout"
# Scripts started before the page prepared their module wait for it.
node run.mjs $HARNESS fixture/basic.riv --early "${BASIC[@]}" \
--expect "wasm script module is still being prepared"
# Modules baked before rive_rt_v1 carried the clocks still read them.
node run.mjs $HARNESS fixture/legacy_clock.riv --expect "legacy clocks ok"
# A module importing an op no host has still starts, like on WAMR; only the
# call traps, naming the import.
UNLINKED=(--expect "init without the missing op"
--expect "last trap: failed to call unlinked import function rive_web_gate_v1.missing")
node run.mjs $HARNESS fixture/unlinked.riv "${UNLINKED[@]}"
node run.mjs $HARNESS fixture/unlinked.riv --sync "${UNLINKED[@]}"
# This tools build links the raw shader op; runtime_wasm's own suite has no
# tools and checks that the call traps there.
node run.mjs $HARNESS fixture/raw_shader.riv \
--expect "init reaches the raw shader op" \
--expect "raw shader op returned 0"
# The browser decodes images for scripts, and its results arrive premultiplied
# on a later event loop turn.
DECODE=(--settle --expect "decoded 2x1 first pixel 100,128"
--expect "decode failed: failed to decode image data")
node run.mjs $HARNESS fixture/decode.riv "${DECODE[@]}"
# A decoder that throws as librive starts it only fails that decode.
node run.mjs $HARNESS fixture/decode.riv --decoder-throws \
--expect "decode failed: failed to decode image data" \
--expect "script vms alive after the frames: 1"
# Scroll crosses as typed args, so the web gets it through the same call.
node run.mjs $HARNESS fixture/scroll.riv --scroll \
--expect "scroll crossed intact" --expect "scroll claimed"
# console reaches the script log and keeps its timers on the web too.
node run.mjs $HARNESS fixture/console.riv \
--expect "console log on the web" --expect "boot: " \
--expect "Timer 'boot' does not exist"
# A null string logs an empty line, as on WAMR, rather than trapping, and one
# outside module memory traps the call as it does there.
node run.mjs $HARNESS fixture/console_null.riv \
--expect "before the null log" --expect-line "" \
--expect "after the null log" \
--expect "wasm call trapped in host_obj_user_init: out of bounds memory access"
# Each lane's null string logs an empty line too and its job, worker and
# processor run on, on the page, on pthreads and in the worklet bundle.
LANE_NULL=(--expect-line "" --expect-line "" --expect-line ""
--expect "job after the null log" --expect "worker after the null log"
--expect "processor after the null log")
node run.mjs $HARNESS fixture/lane_console_null.riv --pump-audio "${LANE_NULL[@]}"
node run.mjs $THREADED fixture/lane_console_null.riv --pump-audio "${LANE_NULL[@]}"
node run.mjs $THREADED fixture/lane_console_null.riv --nrdp-mock $STEADY \
--until "job after" --until "worker after" --until "processor after" \
"${LANE_NULL[@]}"
# trace reaches the script log too, formatted as on WAMR.
node run.mjs $HARNESS fixture/trace.riv \
--expect "trace: traced 1.5 42 NaN" --expect "after trace"
# A host fault in a nested script call also ends the call it nests in.
node run.mjs $HARNESS fixture/nested_fault.riv --fault raise \
--expect "wasm call trapped in host_data_value_changed: librive faulted in a host call" \
--expect "wasm call trapped in host_obj_user_init: librive faulted in a host call, so its script module stops: librive faulted in a nested script call" \
--expect "script vms alive after the frames: 0"
# A JS error thrown under librive frames is a host fault: unlike a script
# trap, the module stops for good.
node run.mjs $HARNESS fixture/trap.riv --fault read \
--expect "wasm call trapped in host_obj_user_init: librive faulted in a host call" \
--expect "last trap: librive faulted in a host call" \
--expect "script vms alive after the frames: 0"
# A SIMD module runs where the engine has SIMD. Where it lacks the flavor the
# module was baked with, the file's scripts are off with one message saying
# why, and the artboard still draws.
node run.mjs $HARNESS fixture/simd.riv --expect "simd lane 22"
node run.mjs $HARNESS fixture/relaxed_simd.riv --expect "relaxed lane 7"
node run.mjs $HARNESS fixture/scalar.riv --expect "scalar lane 3"
for MODE in "" --sync; do
node run.mjs $HARNESS fixture/simd.riv $MODE --lacks simd --vms 0 \
--expect "this browser lacks WebAssembly SIMD, so this file's scripts are off; bake with wasmSimd: off to support it" \
--expect "artboard drew 5 frames"
node run.mjs $HARNESS fixture/relaxed_simd.riv $MODE --lacks relaxed --vms 0 \
--expect "this browser lacks WebAssembly relaxed SIMD, so this file's scripts are off; bake with wasmSimd: on to support it" \
--expect "artboard drew 5 frames"
# A module refused for a feature other than the SIMD it uses carries the
# engine's own reason.
node run.mjs $HARNESS fixture/scalar.riv $MODE --lacks simd,bulk --vms 0 \
--expect "module instantiate failed: this engine lacks bulk memory" \
--expect "artboard drew 5 frames"
node run.mjs $HARNESS fixture/simd.riv $MODE --lacks relaxed,bulk --vms 0 \
--expect "module instantiate failed: this engine lacks bulk memory" \
--expect "artboard drew 5 frames"
done
# An engine validates only the main module, so relaxed SIMD that only a job
# or a processor uses turns the file's scripts off the same way. Plain SIMD
# always reaches the main module too.
node run.mjs $HARNESS fixture/simd_job.riv --expect "job relaxed lane 7"
node run.mjs $HARNESS fixture/simd_processor.riv --audio 1 \
--until "processor relaxed lane" --expect "processor relaxed lane 7"
for MODE in "" --sync; do
for FIXTURE in simd_job simd_processor; do
node run.mjs $HARNESS fixture/$FIXTURE.riv $MODE --lacks relaxed --vms 0 \
--expect "this browser lacks WebAssembly relaxed SIMD, so this file's scripts are off; bake with wasmSimd: on to support it" \
--expect "artboard drew 5 frames"
done
done
# Browser pages, in Chrome when it is installed, CHROME naming another binary,
# and in firefox and webkit when RIVE_WEB_BROWSERS names them.
CHROME="${CHROME:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}"
BROWSERS=($RIVE_WEB_BROWSERS)
if [[ -x "$CHROME" ]]; then
BROWSERS=(chrome "${BROWSERS[@]}")
else
echo "skipped Chrome's browser lanes: no Chrome at $CHROME"
fi
# Firefox can leave a page's audio context suspended after its click, so a
# failed page outside Chrome runs once more, with a warning on GitHub.
page() {
CHROME="$CHROME" node browser_audio.mjs "$@" --browser $BROWSER && return
if [[ $BROWSER == chrome ]]; then
return 1
fi
echo "::warning::$2 ($1 $3) failed once in $BROWSER and runs again"
CHROME="$CHROME" node browser_audio.mjs "$@" --browser $BROWSER
}
for BROWSER in "${BROWSERS[@]}"; do
# Script audio in real time on the page's audio worklet, which runs each
# processor's own module and reaches librive's heap through Atomics.
PAGE=out/wasm_threads_debug
RAMP=(--expect "ramp init 2 channels" --expect "processor running"
--expect "processor threw bad block" --expect "processor meter")
page $PAGE fixture/audio.riv $STEADY "${RAMP[@]}"
# Given no context the runner makes its own, which Chrome's autoplay flag
# starts at once. Elsewhere it warns, then starts on the page's click.
WAITS=()
if [[ $BROWSER != chrome ]]; then
WAITS=(--expect "warning: script audio waits for a click")
fi
page $PAGE fixture/audio.riv --runner-context $STEADY "${WAITS[@]}" "${RAMP[@]}"
page $PAGE fixture/feed.riv $STEADY --expect "feed received 4096 sum 8386560.0"
page $PAGE fixture/processor_holds.riv $STEADY "${HOLDS[@]:3}"
page $PAGE fixture/blob.riv $STEADY --expect "sampler played 4096 sum 4193280.0"
# A browser worker's own clock starts after the page's, unlike node's.
page $PAGE fixture/lane_clock.riv \
--expect "lane clock job agrees yes" --expect "lane clock worker agrees yes"
# A processor's null string does not end it on a real worklet either.
page $PAGE fixture/lane_console_null.riv $STEADY \
--expect "processor after the null log"
# A block past its budget ends at its next fuel check, or without fuel
# once it returns, failing as the native watchdog does.
for FIXTURE in overrun overrun_release; do
page $PAGE fixture/$FIXTURE.riv \
--expect "slow failed: processor overran its block budget"
done
# A metered block stuck past its budget ends there and the mix plays on,
# as do inits past their own budget, each of which the page waits out.
for FIXTURE in worklet_stall worklet_stall_fuel; do
page $PAGE fixture/$FIXTURE.riv --hard-blocks 20 \
--expect "stuck failed: processor overran its block budget" \
--expect "quiet rendered after stuck failed"
done
page $PAGE fixture/init_spin.riv $STEADY "${INIT_SPIN[@]:2}"
# Nothing can end a worklet stuck in a block without fuel, so the page
# fails every processor it runs once its blocks stop.
page $PAGE fixture/worklet_stall_release.riv $STEADY \
--expect "stuck failed: processor stalled the audio thread" \
--expect "quiet failed: processor stalled the audio thread"
# The browser's own worklet renders the samples harness_audio hashes,
# with SIMD or without.
for FIXTURE in "${DETERMINISM[@]}" determinism_scalar; do
page $PAGE fixture/$FIXTURE.riv $STEADY --hash 16 \
--expect "worklet rendered 16 blocks $SAME_SAMPLES"
done
# A page that names no worklet and serves none of its files renders the
# same samples on the one the threaded build carries.
page $PAGE fixture/determinism.riv $STEADY --hash 16 --carried-worklet \
--expect "worklet rendered 16 blocks $SAME_SAMPLES"
# Jobs on browser workers, and a page thread that cannot sleep spinning
# while it waits on a worker.
page $PAGE fixture/jobs.riv "${JOBS[@]:2}"
page $PAGE fixture/job_spin.riv "${SPINNERS[@]:2}"
page $PAGE fixture/ports.riv "${PORTS[@]}" \
--expect "port answered within the call 7.0"
page $PAGE fixture/parallel.riv "${TEAM[@]}" "${PARALLEL[@]}" \
--expect "parallel parts all yes"
page $PAGE fixture/parallel_stop.riv --parallel-parts 4 "${STOPPED[@]}"
page $PAGE fixture/parallel_design.riv --parallel-parts 4 "${DESIGN[@]}"
# Calls on every frame, each frame's first waking a parked team. WebKit
# can lose the wake of an async wait, which hung the page, so its team
# waits with a timeout. Without audio a failure runs only once.
WAKES=async
if [[ $BROWSER == webkit ]]; then
WAKES=timed
fi
for PARTS in 4 8; do
CHROME="$CHROME" node browser_audio.mjs $PAGE fixture/parallel_frames.riv \
--browser $BROWSER --parallel-parts $PARTS \
--expect "frames 600 parts $PARTS wrong 0" \
--expect "page parallel {\"team\":$((PARTS - 1)),\"waits\":\"$WAKES\"}"
done
page $PAGE fixture/basic.riv --no-isolation --expect "$REFUSED"
# A page without cross origin isolation loads the build without threads,
# which runs lanes on the page's thread and refuses real time audio.
PAGE=out/wasm_debug
page $PAGE fixture/jobs.riv --no-isolation "${JOBS[@]}"
page $PAGE fixture/workers.riv --no-isolation "${WORKERS[@]}"
page $PAGE fixture/parallel.riv --no-isolation "${SERIAL[@]}" "${PARALLEL[@]}"
page $PAGE fixture/parallel_stop.riv --no-isolation "${STOPPED[@]}"
page $PAGE fixture/parallel_design.riv --no-isolation "${DESIGN[@]}"
page $PAGE fixture/job_spin.riv --no-isolation \
--expect "spinner failed: $SPINNER_TIMEOUT" \
--expect "job after the spinners 9.0"
page $PAGE fixture/worker_spin.riv --no-isolation \
--expect "worker spinner failed: $SPINNER_TIMEOUT"
for FIXTURE in "${DETERMINISM[@]}"; do
page $PAGE fixture/$FIXTURE.riv --no-isolation --offline 16 \
--expect "processor 0 state 1 $SAME_SAMPLES"
done
page $PAGE fixture/audio.riv --no-isolation \
--expect "processor threw real time audio needs cross origin isolation"
done