SIMD
Single instruction, multiple data via WebAssembly’s native 128-bit vector type.
A v128 value holds 128 bits, which the f32x4 and
i32x4 builtins read as four 32 bit lanes. One SIMD instruction works
on all four lanes at once, so a loop over floats can handle four of them per
step. v128 works as locals, params, returns, and class fields.
The wasmSimd setting
SIMD is on by default in a Rive project. wasmSimd in
rive.yaml chooses which SIMD the module may use:
on, the default, allows the SIMD on this page.offbuilds a module without SIMD, which also runs in browsers that lack WebAssembly SIMD, such as Safari before 16.4. A browser that lacks the SIMD a file was built with runs none of that file’s scripts, and says so on the console.relaxedalso allows fused multiply-adds, whose results can round differently by platform. Natively a relaxed module runs only as compiled machine code and fails to load on the interpreter, and browsers need relaxed SIMD support.
A script that uses no SIMD builds the same either way. Jobs, workers and audio processors build with the same setting as the main module. See Build Settings.
What is supported
The compiler has part of WebAssembly SIMD:
v128loads and stores, the bitwise operations (not,and,andnot,or,xor,bitselect), lane shuffles andany_true.- Loads and stores of a single lane:
v128.load8_lane,load16_lane,load32_laneandload64_lane, the matchingstore8_lanetostore64_lane, andv128.load_lane<T>andv128.store_lane<T>, which take the width fromT. A lane load reads one value from memory into that lane and keeps the other lanes; a lane store writes that one lane, so scattering a vector’s floats costs one instruction each. splatand lane access on every shape:extract_laneandreplace_laneonf32x4,i32x4,f64x2andi64x2, andextract_lane_s,extract_lane_uandreplace_laneoni8x16andi16x8.- On
f32x4:add,sub,mul,div,neg,abs,sqrt, the comparisonseq,ne,lt,gt,leandge, andmin,max,pminandpmax. - On
i32x4:add,sub,mul,eq,ne,all_trueandbitmask. - Shuffles at every width:
v128.shuffle<T>andi8x16,i16x8,i32x4andf32x4.shuffle, plus the byte swizzlei8x16.swizzle. Beyond lane access,i16x8has only the shuffle,i8x16only the shuffle and the swizzle, andf64x2andi64x2no arithmetic.
There are no shifts, no rounding, no conversions between float and integer
lanes, no lt, gt, le or ge on
i32x4, and no loads that splat, widen or zero the other lanes.
There is no v128.const either, so there are no
v128 globals: build a constant vector with splat and
replace_lane.
v128.load, v128.store and the lane loads and
stores read and write raw memory, so a function that calls them must be
unsafe. A lane index is a constant
below the lane count of its width.
Keeping a scalar path
A project built with wasmSimd: off has no SIMD, so code that must
also run there keeps a scalar path beside the SIMD one. Two rules make one source
serve both builds:
ASC_FEATURE_SIMDis 1 whenwasmSimdisonorrelaxedand 0 when it isoff, and anifon a constant never compiles its dead branch, so the scalar and SIMD paths live in the same file.v128cannot appear in any function signature when SIMD is off, since every declaration is type checked, so keep SIMD code in function bodies behind the constant guard.
Make the scalar path add things up in the same order as the SIMD one, so both builds give the same results.
// Scalar with wasmSimd: off, four lanes wide otherwise (this example
// compiles with SIMD on); the dead branch never compiles, so the
// scalar module has no v128 and loads in browsers without SIMD.
// Raw loads put this behind the unsafe gate.
export unsafe function integrate(xs: usize, vs: usize,
dt: f32, count: i32): void {
if (ASC_FEATURE_SIMD) {
for (let i = 0; i < count; i += 4) {
let x = v128.load(xs + (<usize>i << 2));
let v = v128.load(vs + (<usize>i << 2));
v128.store(xs + (<usize>i << 2),
f32x4.add(x, f32x4.mul(v, f32x4.splat(dt))));
}
return;
}
for (let i = 0; i < count; i++) {
let p = xs + (<usize>i << 2);
store<f32>(p, load<f32>(p) + load<f32>(vs + (<usize>i << 2)) * dt);
}
}
export unsafe function demo(): f32 {
let xs = __alloc(16 << 2);
let vs = __alloc(16 << 2);
for (let i = 0; i < 16; i++) {
store<f32>(xs + (<usize>i << 2), f32(i));
store<f32>(vs + (<usize>i << 2), 0.5);
}
integrate(xs, vs, 2.0, 16);
let sum: f32 = 0;
for (let i = 0; i < 16; i++) {
sum += load<f32>(xs + (<usize>i << 2));
}
return sum; // 120 + 16 × 0.5 × 2.0 = 136, both builds
}
// f32x4.min/max differ from the scalar ternary on NaN and ±0;
// compare + bitselect matches it bit for bit, so the SIMD and
// scalar builds give the same results.
// v128 in a signature compiles only with SIMD on.
function min4(a: v128, b: v128): v128 {
return v128.bitselect(a, b, f32x4.le(a, b));
}
export function step(px: f32, py: f32): f32 {
let c = f32x4.splat(px);
c = f32x4.replace_lane(c, 1, py);
let best = min4(c, f32x4.splat(100.0));
if (ASC_FEATURE_RELAXED_SIMD) {
// One rounding instead of two: results can differ from
// mul+add, and by platform.
best = f32x4.relaxed_madd(best, f32x4.splat(0.5), f32x4.splat(1.0));
} else {
best = f32x4.add(f32x4.mul(best, f32x4.splat(0.5)), f32x4.splat(1.0));
}
return f32x4.extract_lane(best, 0);
}
Shuffles
// A shuffle picks lanes from its two operands laid end to end: 0..7 for
// f32 lanes, 0..31 for bytes. Every lane index is a compile-time constant
// and every width lowers to the one wasm byte shuffle. i8x16.swizzle takes
// its byte selectors from a vector at run time; a selector past 15 reads 0.
export function spread(): f32 {
// Two points as (x0, y0, x1, y1); a 2D transform wants the xs and the
// ys spread across lanes, one shuffle each.
let p = f32x4.replace_lane(f32x4.replace_lane(
f32x4.replace_lane(f32x4.splat(1), 1, 2), 2, 3), 3, 4);
let xs = v128.shuffle<f32>(p, p, 0, 0, 2, 2); // (1, 1, 3, 3)
let ys = v128.shuffle<f32>(p, p, 1, 1, 3, 3); // (2, 2, 4, 4)
let r = f32x4.add(f32x4.mul(xs, f32x4.splat(10)), ys);
return f32x4.extract_lane(r, 2) + f32x4.extract_lane(r, 3); // 34 + 34
}
export function reverse(): i32 {
// Byte i holds i; lanes 16..31 would read from the second operand.
let v = i32x4.replace_lane(i32x4.replace_lane(i32x4.replace_lane(
i32x4.splat(0x03020100), 1, 0x07060504), 2, 0x0b0a0908), 3, 0x0f0e0d0c);
let r = i8x16.shuffle(v, v, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0);
return i32x4.extract_lane(r, 0); // bytes 15..12, 0x0c0d0e0f
}
A lane index that is not a constant, or is outside the doubled lane count, is a
compile error at that argument; v128.shuffle without its lane type is
one too.
Relaxed SIMD
wasmSimd: relaxed adds f32x4.relaxed_madd, which
computes a * b + c, and f32x4.relaxed_nmadd, which
computes -(a * b) + c, and sets ASC_FEATURE_RELAXED_SIMD
to 1. A fused multiply-add can round once where a multiply and an add round twice,
so its results can differ slightly from mul and add,
and from one platform to another. Keep to mul and add
where the output must match everywhere, such as an audio processor whose rendered
file is a test reference.