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

@ -44,6 +44,21 @@ export namespace Crafter {
std::string value;
};
// A browser-wasm codegen variant. The same wasm32 target is compiled more
// than once, each pass adding `flags` to every translation unit, so newer
// ISA features (relaxed SIMD today; threads, future SIMD revisions, … later)
// can be adopted without dropping engines that lack them. Each non-empty
// `label` emits an additional `outputName.<label>.wasm` alongside the
// baseline `outputName.wasm`; `probes` names the runtime feature-detect
// checks (see wasi-runtime/runtime.js's probe registry) that must all pass
// for a browser to be served this variant. The first variant whose probes
// pass wins; the baseline is the universal fallback.
struct WasmVariant {
std::string label;
std::vector<std::string> flags;
std::vector<std::string> probes;
};
enum class ConfigurationType {
Executable,
LibraryStatic,
@ -157,6 +172,21 @@ export namespace Crafter {
std::vector<ExternalDependency> externalDependencies;
std::vector<std::string> compileFlags;
std::vector<std::string> linkFlags;
// Browser-wasm feature-detected variants. Empty (the default) builds a
// single outputName.wasm with the baseline wasm flag set. Non-empty
// builds the baseline plus one outputName.<label>.wasm per entry, each
// recompiling the whole build graph with that entry's `flags`;
// EnableWasiBrowserRuntime emits a variants.json manifest the shipped
// runtime.js uses to pick the right variant per browser. Populate via
// EnableWasiRelaxedSimdVariant for the common case.
std::vector<WasmVariant> wasmVariants;
// Extra wasm codegen flags injected into this configuration's compile +
// link command for the active variant pass. Set across the whole build
// graph by the variant driver inside Build(); part of VariantId so each
// variant's objects/PCMs land in their own build+bin dir and never
// clobber the baseline's. Not for direct project use — declare
// wasmVariants instead.
std::vector<std::string> wasmVariantFlags;
std::vector<Test> tests;
CRAFTER_API void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved
@ -199,6 +229,13 @@ export namespace Crafter {
compileKey += "|F:";
compileKey += f;
}
// Wasm variant codegen flags perturb every object/PCM, so they must
// key into a distinct build+bin dir from the baseline (and from
// each other) to avoid clobbering.
for (const std::string& f : wasmVariantFlags) {
compileKey += "|W:";
compileKey += f;
}
std::size_t configHash = std::hash<std::string>{}(compileKey);
return std::format("{}-{}-{}-{}-{:08x}", name, target, march, mtune, configHash);
}
@ -237,6 +274,17 @@ export namespace Crafter {
// outputName so renaming the binary later requires another call.
CRAFTER_API void EnableWasiBrowserRuntime(Configuration& cfg);
// Register the relaxed-SIMD codegen variant on cfg. Builds an additional
// outputName.relaxed-simd.wasm compiled with -mrelaxed-simd (FMA,
// dot-product, relaxed swizzle/laneselect, …); the shipped runtime serves
// it only to engines that report relaxed-SIMD support (Chrome 114+, Firefox
// 120+) and falls back to the baseline outputName.wasm everywhere else
// (e.g. Safari, which still gates relaxed SIMD behind a flag as of mid
// 2026). Idempotent. Call before EnableWasiBrowserRuntime so the emitted
// variants.json manifest includes it. A no-op for non-wasm targets at build
// time (no wasm is produced), but harmless to declare unconditionally.
CRAFTER_API void EnableWasiRelaxedSimdVariant(Configuration& cfg);
// View over the project's args with simple query helpers. Has(flag) for
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
// returns the substring after the equals). Definitions are out-of-line