SIMD
Single instruction, multiple data via WebAssembly’s native 128-bit vector type.
--enable-simd adds v128 with an
f32x4/i32x4 subset (splat, lanes, arithmetic, compares,
min/max, bit ops, load/store) and the lane shuffles:
v128.shuffle<T>, the per-width i8x16/i16x8/
i32x4/f32x4.shuffle, and i8x16.swizzle. Two
rules make one source serve two artifacts:
ASC_FEATURE_SIMDfolds to 1 under the flag and 0 without it, and constantifconditions never compile their dead branch — so scalar and wide backends live in the same file.v128cannot appear in any function signature without the flag, since declarations always type-check — keep wide code in function bodies behind the constant guard.
// Scalar without --enable-simd, four lanes wide with it (this repl
// compiles with the flag); the dead branch never compiles, so the
// scalar artifact stays v128-free and loads on non-simd runtimes.
// 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 artifacts
}
// f32x4.min/max diverge from the scalar ternary on NaN and ±0;
// compare + bitselect matches scalar bit-for-bit, which is what keeps
// differential harnesses exact (the Box2D W lane uses this shape).
// v128 in a signature is legal only under the flag.
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) {
// Fused, single-rounding: deterministic against an fmaf
// reference, but a different golden than mul+add.
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);
}
// 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.
--enable-relaxed-simd adds f32x4.relaxed_madd /
relaxed_nmadd and ASC_FEATURE_RELAXED_SIMD. Fused fma is
single-rounding deterministic, so the relaxed artifact carries its own bit-exact
contract — deterministic output stays byte-identical, just to a different
golden.