feat: feature-detected browser-wasm variants (relaxed-SIMD) + variant-aware runtime
Some checks failed
CI / build-test-release (pull_request) Failing after 7m19s

The browser wasm pipeline hardcoded -msimd128 for every wasm32 target and
baked a single wasm URL into index.html, so newer codegen features that
aren't yet baseline across engines (relaxed SIMD today; threads, future SIMD
revisions later) couldn't be adopted without dropping the browsers that lack
them.

Add a general, feature-parameterized mechanism owned entirely by
Crafter.Build:

- Configuration::wasmVariants declares N codegen variants (label, extra -m
  flags, runtime probes). Build() compiles the baseline plus one
  outputName.<label>.wasm per variant, recompiling the whole graph (incl.
  dep libs + std PCM) with the variant's flags — relaxed-SIMD is per-TU
  codegen, not a link switch. wasmVariantFlags folds into VariantId so each
  variant's objects/PCMs land in their own build+bin dir.
- EnableWasiBrowserRuntime emits a variants.json manifest (label -> url +
  probes), preferred-first with the baseline as the universal fallback.
- The shipped runtime.js runs inlined wasm-feature-detect probes
  (relaxed-simd, simd, tail-call, bulk-memory, exception-handling, threads),
  picks the first variant whose probes all pass, and falls back to the single
  baked CRAFTER_WASM_URL when no manifest is present (backward compatible).
- EnableWasiRelaxedSimdVariant registers the relaxed-SIMD variant — the
  motivating case (Chrome 114+/Firefox 120+ enable it by default; Safari
  still flag-gates it as of mid-2026).

Verified end to end: a wasm32-wasip1 build emits both wasi-hello.wasm and
wasi-hello.relaxed-simd.wasm + variants.json; Firefox selects the
relaxed-simd variant and runs it.

Resolves #24

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
catbot 2026-06-15 15:14:18 +00:00
commit 022ada70a6
7 changed files with 395 additions and 5 deletions

View file

@ -820,9 +820,71 @@ class Wasi {
}
}
const wasmUrl = window.CRAFTER_WASM_URL;
// ─── wasm feature-detect probes ────────────────────────────────────────
// Each probe validates a ~dozen-byte module that only validates on an engine
// implementing the named feature — the same technique as
// GoogleChromeLabs/wasm-feature-detect, inlined so we ship no dependency. A
// variant in variants.json lists the probe names that must all pass for that
// variant to be served; an unknown name is treated as unsupported (we fall
// through to a less demanding variant, ultimately the baseline). Keep the keys
// in sync with the `probes` a WasmVariant declares in Crafter.Build.
const WASM_PROBES = {
// relaxed SIMD: i8x16.relaxed_swizzle (Chrome 114+, Firefox 120+; Safari
// still flag-gated as of mid-2026).
"relaxed-simd": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,15,1,13,0,65,1,253,15,65,2,253,15,253,128,2,11])),
// fixed-width SIMD128 (v128.const + i8x16.popcnt) — the current baseline,
// here for completeness / future baseline shifts.
"simd": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,5,1,96,0,1,123,3,2,1,0,10,10,1,8,0,65,0,253,15,253,98,11])),
// tail calls (return_call).
"tail-call": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,6,1,4,0,18,0,11])),
// bulk memory ops (memory.init).
"bulk-memory": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,3,1,0,1,10,14,1,12,0,65,0,65,0,65,0,252,10,0,0,11])),
// legacy exception handling (try/catch).
"exception-handling": () => WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,8,1,6,0,6,64,25,11,11])),
// threads/atomics — also requires SharedArrayBuffer (cross-origin isolated).
"threads": () => {
try {
if (typeof SharedArrayBuffer === "undefined") return false;
return WebAssembly.validate(new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,5,4,1,3,1,1,10,11,1,9,0,65,0,254,16,2,0,26,11]));
} catch (e) { return false; }
},
};
function variantSupported(probes) {
return (probes || []).every((name) => {
const probe = WASM_PROBES[name];
if (!probe) {
console.warn(`[wasi] unknown wasm probe '${name}' — treating variant as unsupported`);
return false;
}
try { return !!probe(); } catch (e) { return false; }
});
}
// Pick the wasm URL: prefer the variants.json manifest (first entry whose
// probes all pass), else fall back to the single baked window.CRAFTER_WASM_URL.
async function pickWasmUrl() {
let variants = null;
try {
const r = await fetch("variants.json");
if (r.ok) variants = await r.json();
} catch (e) {
// No manifest (older build) — fall back below.
}
if (Array.isArray(variants) && variants.length) {
for (const v of variants) {
if (variantSupported(v.probes)) {
if (v.label) console.log(`[wasi] selected wasm variant '${v.label}' (${(v.probes || []).join(", ")})`);
return v.url;
}
}
}
return window.CRAFTER_WASM_URL;
}
const wasmUrl = await pickWasmUrl();
if (!wasmUrl) {
throw new Error("runtime.js: window.CRAFTER_WASM_URL is not set (set it in index.html before loading runtime.js)");
throw new Error("runtime.js: no wasm variant selected and window.CRAFTER_WASM_URL is not set");
}
// Preload asset files listed in files.json (emitted by