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

@ -447,7 +447,16 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
});
}
fs::path stdPcmDir = GetCacheDir()/(config.target+"-"+config.march);
// The std PCM is cached per target+march, but a wasm variant pass compiles
// it with extra codegen flags (e.g. -mrelaxed-simd) — its BMI must not be
// shared with the baseline's, or the consuming TUs see a target-feature
// mismatch. Suffix the cache dir with the variant flags when present.
std::string stdPcmKey = config.target + "-" + config.march;
for (const std::string& f : config.wasmVariantFlags) {
stdPcmKey += "+";
for (char c : f) stdPcmKey += (c == '/' || c == '\\') ? '_' : c;
}
fs::path stdPcmDir = GetCacheDir()/stdPcmKey;
if (!fs::exists(stdPcmDir)) {
fs::create_directories(stdPcmDir);
@ -494,6 +503,13 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
// same command line; quiet the unused-flag warning rather than split
// compile and link commands.
command += " -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL -Wno-unused-command-line-argument";
// Active variant pass (see the variant driver at the end of Build):
// extra codegen flags (e.g. -mrelaxed-simd) applied to every TU. Empty
// for the baseline build. Part of VariantId, so these objects/PCMs
// land in their own dir.
for (const std::string& f : config.wasmVariantFlags) {
command += " " + f;
}
}
if (config.target == "x86_64-w64-mingw32") {
// mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without
@ -1117,6 +1133,71 @@ BuildResult Crafter::Build(Configuration& config, std::unordered_map<fs::path, s
buildResult.libs.insert(std::format("-l{}", config.outputName));
}
// Browser-wasm feature-detected variants. The baseline outputName.wasm is
// now built (above). For each declared variant, recompile the entire build
// graph with the variant's extra codegen flags and drop the result next to
// the baseline as outputName.<label>.wasm. Relaxed-SIMD (and friends) is
// per-translation-unit codegen, not a link switch, so a full rebuild — incl.
// dep libs and the std PCM — is the only correct way to produce it.
//
// Guard: only fires on the top-level config that *declared* wasmVariants
// (deps leave it empty) and never on a variant pass itself (those carry
// wasmVariantFlags), so there's no recursion. Skipped on build error.
if (buildResult.result.empty()
&& config.target.starts_with("wasm32")
&& config.type == ConfigurationType::Executable
&& !config.wasmVariants.empty()
&& config.wasmVariantFlags.empty()) {
fs::path baselineBin = config.BinDir();
// The whole transitive graph, so the variant flags reach every TU.
std::vector<Configuration*> graph;
{
std::unordered_set<Configuration*> graphSeen;
std::function<void(Configuration*)> collect = [&](Configuration* c) {
if (!c || !graphSeen.insert(c).second) return;
graph.push_back(c);
for (Configuration* dep : c->dependencies) collect(dep);
};
collect(&config);
}
for (const WasmVariant& variant : config.wasmVariants) {
// A labelless or flagless "variant" is just the baseline under
// another name — nothing extra to compile.
if (variant.label.empty() || variant.flags.empty()) continue;
Progress::Task task(std::format("Building wasm variant {} ({})",
variant.label, config.outputName));
for (Configuration* c : graph) c->wasmVariantFlags = variant.flags;
fs::path variantBin = config.BinDir();
std::unordered_map<fs::path, std::shared_future<BuildResult>> variantDeps;
std::mutex variantMutex;
BuildResult vr = Build(config, variantDeps, variantMutex);
for (Configuration* c : graph) c->wasmVariantFlags.clear();
if (!vr.result.empty()) {
buildResult.result = std::format(
"wasm variant '{}' build failed: {}", variant.label, vr.result);
break;
}
std::error_code ec;
fs::path src = variantBin / (config.outputName + ".wasm");
fs::path dst = baselineBin / std::format("{}.{}.wasm", config.outputName, variant.label);
fs::copy_file(src, dst, fs::copy_options::overwrite_existing, ec);
if (ec) {
buildResult.result = std::format(
"copy wasm variant '{}' {} -> {}: {}",
variant.label, src.string(), dst.string(), ec.message());
break;
}
}
}
return buildResult;
}
@ -1250,6 +1331,50 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
m << "]";
}
// Variant manifest. runtime.js fetches this, runs each entry's probes in
// order, and instantiates the first whose probes all pass — so the
// preferred (most feature-rich) variants come first and the baseline last
// as the universal fallback (empty probes always pass). A variant with a
// non-empty label maps to outputName.<label>.wasm (produced by Build's
// variant driver); the baseline maps to outputName.wasm. Even with no
// declared variants we still emit a one-entry manifest so the runtime has a
// single, uniform code path.
auto jsonEscape = [](std::string_view s) {
std::string out;
for (char c : s) {
if (c == '"' || c == '\\') out += '\\';
out += c;
}
return out;
};
fs::path variantsPath = htmlOutDir / "variants.json";
{
std::ofstream v(variantsPath);
v << "[";
bool first = true;
auto emit = [&](const std::string& label, const std::vector<std::string>& probes) {
if (!first) v << ",";
first = false;
std::string url = label.empty()
? cfg.outputName + ".wasm"
: std::format("{}.{}.wasm", cfg.outputName, label);
v << "{\"label\":\"" << jsonEscape(label) << "\""
<< ",\"url\":\"" << jsonEscape(url) << "\""
<< ",\"probes\":[";
for (std::size_t i = 0; i < probes.size(); ++i) {
if (i) v << ",";
v << "\"" << jsonEscape(probes[i]) << "\"";
}
v << "]}";
};
for (const WasmVariant& variant : cfg.wasmVariants) {
if (variant.label.empty()) continue; // baseline emitted below
emit(variant.label, variant.probes);
}
emit("", {}); // baseline fallback, always last
v << "]";
}
std::ifstream in(htmlTemplate);
std::stringstream buf;
buf << in.rdbuf();
@ -1264,6 +1389,18 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
cfg.files.push_back(runtimeJs);
cfg.files.push_back(htmlPath);
cfg.files.push_back(manifestPath);
cfg.files.push_back(variantsPath);
}
void Crafter::EnableWasiRelaxedSimdVariant(Configuration& cfg) {
for (const WasmVariant& v : cfg.wasmVariants) {
if (v.label == "relaxed-simd") return; // idempotent
}
cfg.wasmVariants.push_back(WasmVariant{
.label = "relaxed-simd",
.flags = {"-mrelaxed-simd"},
.probes = {"relaxed-simd"},
});
}
std::string Crafter::HostTarget() {