feat: feature-detected browser-wasm variants (relaxed-SIMD) + variant-aware runtime
Some checks failed
CI / build-test-release (pull_request) Failing after 7m19s
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:
parent
dbee23c564
commit
022ada70a6
7 changed files with 395 additions and 5 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue