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)) {
|
if (!fs::exists(stdPcmDir)) {
|
||||||
fs::create_directories(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
|
// same command line; quiet the unused-flag warning rather than split
|
||||||
// compile and link commands.
|
// 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";
|
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") {
|
if (config.target == "x86_64-w64-mingw32") {
|
||||||
// mingw libstdc++ defines TLS via __emutls_v.* (emulated TLS); without
|
// 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));
|
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;
|
return buildResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1250,6 +1331,50 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
||||||
m << "]";
|
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::ifstream in(htmlTemplate);
|
||||||
std::stringstream buf;
|
std::stringstream buf;
|
||||||
buf << in.rdbuf();
|
buf << in.rdbuf();
|
||||||
|
|
@ -1264,6 +1389,18 @@ void Crafter::EnableWasiBrowserRuntime(Configuration& cfg) {
|
||||||
cfg.files.push_back(runtimeJs);
|
cfg.files.push_back(runtimeJs);
|
||||||
cfg.files.push_back(htmlPath);
|
cfg.files.push_back(htmlPath);
|
||||||
cfg.files.push_back(manifestPath);
|
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() {
|
std::string Crafter::HostTarget() {
|
||||||
|
|
|
||||||
|
|
@ -778,6 +778,12 @@ std::string Crafter::BuildStdPcm(const Configuration& config, fs::path stdPcm) {
|
||||||
std::string archFlags = isWasm
|
std::string archFlags = isWasm
|
||||||
? std::string(" -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL")
|
? std::string(" -fno-exceptions -msimd128 -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL")
|
||||||
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
: std::format(" -march={} -mtune={}", config.march, config.mtune);
|
||||||
|
// Match the variant codegen flags the consuming TUs are built with
|
||||||
|
// (e.g. -mrelaxed-simd) so the std BMI is feature-compatible. wasm only;
|
||||||
|
// wasmVariantFlags is empty on every other path.
|
||||||
|
if (isWasm) {
|
||||||
|
for (const std::string& f : config.wasmVariantFlags) archFlags += " " + f;
|
||||||
|
}
|
||||||
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) {
|
if(!fs::exists(stdPcm) || fs::last_write_time(stdPcm) < fs::last_write_time(stdCppm)) {
|
||||||
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()));
|
return RunCommand(std::format("clang++ --target={} -std=c++26 -stdlib=libc++{}{} -O3 -Wno-reserved-identifier -Wno-reserved-module-identifier --precompile {} -o {}", config.target, sysrootFlag, archFlags, stdCppm, stdPcm.string()));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,21 @@ export namespace Crafter {
|
||||||
std::string value;
|
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 {
|
enum class ConfigurationType {
|
||||||
Executable,
|
Executable,
|
||||||
LibraryStatic,
|
LibraryStatic,
|
||||||
|
|
@ -157,6 +172,21 @@ export namespace Crafter {
|
||||||
std::vector<ExternalDependency> externalDependencies;
|
std::vector<ExternalDependency> externalDependencies;
|
||||||
std::vector<std::string> compileFlags;
|
std::vector<std::string> compileFlags;
|
||||||
std::vector<std::string> linkFlags;
|
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;
|
std::vector<Test> tests;
|
||||||
CRAFTER_API void GetInterfacesAndImplementations(std::span<fs::path> interfaces, std::span<fs::path> implementations);
|
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
|
// Declare a test. Sources default to `tests/<name>/main.cpp` resolved
|
||||||
|
|
@ -199,6 +229,13 @@ export namespace Crafter {
|
||||||
compileKey += "|F:";
|
compileKey += "|F:";
|
||||||
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);
|
std::size_t configHash = std::hash<std::string>{}(compileKey);
|
||||||
return std::format("{}-{}-{}-{}-{:08x}", name, target, march, mtune, configHash);
|
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.
|
// outputName so renaming the binary later requires another call.
|
||||||
CRAFTER_API void EnableWasiBrowserRuntime(Configuration& cfg);
|
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
|
// View over the project's args with simple query helpers. Has(flag) for
|
||||||
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
|
// boolean switches; Get(prefix) for valued options (e.g. Get("--prefix=")
|
||||||
// returns the substring after the equals). Definitions are out-of-line
|
// returns the substring after the equals). Definitions are out-of-line
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
|
||||||
cfg.AddTest("TestRunnerSpec").Dependencies({ crafterBuildLib.get() });
|
cfg.AddTest("TestRunnerSpec").Dependencies({ crafterBuildLib.get() });
|
||||||
cfg.AddTest("VariantId").Dependencies({ crafterBuildLib.get() });
|
cfg.AddTest("VariantId").Dependencies({ crafterBuildLib.get() });
|
||||||
cfg.AddTest("WasiBrowserRuntime").Dependencies({ crafterBuildLib.get() });
|
cfg.AddTest("WasiBrowserRuntime").Dependencies({ crafterBuildLib.get() });
|
||||||
|
cfg.AddTest("WasmVariants").Dependencies({ crafterBuildLib.get() });
|
||||||
cfg.AddTest("RunSingleTestExit").Dependencies({ crafterBuildLib.get() });
|
cfg.AddTest("RunSingleTestExit").Dependencies({ crafterBuildLib.get() });
|
||||||
// LoadProject dlopens the synthesized project.so, which references
|
// LoadProject dlopens the synthesized project.so, which references
|
||||||
// Crafter:: symbols (HostTarget, Configuration ctors) that have to be
|
// Crafter:: symbols (HostTarget, Configuration ctors) that have to be
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,8 @@ int main() {
|
||||||
EnableWasiBrowserRuntime(cfg);
|
EnableWasiBrowserRuntime(cfg);
|
||||||
|
|
||||||
// Three files registered: runtime.js, generated index.html, files.json.
|
// Three files registered: runtime.js, generated index.html, files.json.
|
||||||
if (cfg.files.size() != 3) {
|
if (cfg.files.size() != 4) {
|
||||||
std::println(std::cerr, "expected 3 cfg.files entries, got {}", cfg.files.size());
|
std::println(std::cerr, "expected 4 cfg.files entries, got {}", cfg.files.size());
|
||||||
for (const auto& f : cfg.files) std::println(std::cerr, " - {}", f.string());
|
for (const auto& f : cfg.files) std::println(std::cerr, " - {}", f.string());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
@ -63,10 +63,12 @@ int main() {
|
||||||
const fs::path* runtimeJs = findByName("runtime.js");
|
const fs::path* runtimeJs = findByName("runtime.js");
|
||||||
const fs::path* indexHtml = findByName("index.html");
|
const fs::path* indexHtml = findByName("index.html");
|
||||||
const fs::path* manifest = findByName("files.json");
|
const fs::path* manifest = findByName("files.json");
|
||||||
|
const fs::path* variants = findByName("variants.json");
|
||||||
|
|
||||||
if (!runtimeJs) { std::println(std::cerr, "runtime.js not registered"); return 1; }
|
if (!runtimeJs) { std::println(std::cerr, "runtime.js not registered"); return 1; }
|
||||||
if (!indexHtml) { std::println(std::cerr, "index.html not registered"); return 1; }
|
if (!indexHtml) { std::println(std::cerr, "index.html not registered"); return 1; }
|
||||||
if (!manifest) { std::println(std::cerr, "files.json not registered"); return 1; }
|
if (!manifest) { std::println(std::cerr, "files.json not registered"); return 1; }
|
||||||
|
if (!variants) { std::println(std::cerr, "variants.json not registered"); return 1; }
|
||||||
|
|
||||||
// The generated index.html and manifest live under the project's build/
|
// The generated index.html and manifest live under the project's build/
|
||||||
// wasi-runtime/<name>/ directory and must actually exist on disk.
|
// wasi-runtime/<name>/ directory and must actually exist on disk.
|
||||||
|
|
@ -78,6 +80,25 @@ int main() {
|
||||||
std::println(std::cerr, "files.json not produced at {}", manifest->string());
|
std::println(std::cerr, "files.json not produced at {}", manifest->string());
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
if (!fs::exists(*variants)) {
|
||||||
|
std::println(std::cerr, "variants.json not produced at {}", variants->string());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With no declared variants, variants.json is a single baseline entry
|
||||||
|
// pointing at outputName.wasm with no probes.
|
||||||
|
{
|
||||||
|
std::ifstream vin(*variants);
|
||||||
|
std::string vjson{std::istreambuf_iterator<char>(vin), std::istreambuf_iterator<char>()};
|
||||||
|
if (vjson.find("\"wasi-helper.wasm\"") == std::string::npos) {
|
||||||
|
std::println(std::cerr, "variants.json missing baseline url: {}", vjson);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (vjson.find("relaxed-simd") != std::string::npos) {
|
||||||
|
std::println(std::cerr, "variants.json unexpectedly mentions relaxed-simd: {}", vjson);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The template's {{WASM}} placeholder should have been replaced with
|
// The template's {{WASM}} placeholder should have been replaced with
|
||||||
// the configured outputName.
|
// the configured outputName.
|
||||||
|
|
|
||||||
115
tests/WasmVariants/main.cpp
Normal file
115
tests/WasmVariants/main.cpp
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import std;
|
||||||
|
import Crafter.Build;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
using namespace Crafter;
|
||||||
|
|
||||||
|
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||||
|
|
||||||
|
// Covers the browser-wasm variant plumbing that does NOT require a wasm
|
||||||
|
// toolchain run: EnableWasiRelaxedSimdVariant's registration, the
|
||||||
|
// variants.json manifest EnableWasiBrowserRuntime emits, and the VariantId
|
||||||
|
// perturbation that keeps each variant's objects in their own build dir. The
|
||||||
|
// actual multi-.wasm production is exercised by a real wasm build (manual /
|
||||||
|
// integration), not here — same boundary the WasiBrowserRuntime test draws.
|
||||||
|
namespace {
|
||||||
|
int failures = 0;
|
||||||
|
void Check(bool cond, std::string_view msg) {
|
||||||
|
if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++failures; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// ── EnableWasiRelaxedSimdVariant: registration + idempotency ──────────
|
||||||
|
{
|
||||||
|
Configuration cfg;
|
||||||
|
cfg.target = "wasm32-wasip1";
|
||||||
|
EnableWasiRelaxedSimdVariant(cfg);
|
||||||
|
Check(cfg.wasmVariants.size() == 1, "one variant registered");
|
||||||
|
if (!cfg.wasmVariants.empty()) {
|
||||||
|
const WasmVariant& v = cfg.wasmVariants.front();
|
||||||
|
Check(v.label == "relaxed-simd", "label is relaxed-simd");
|
||||||
|
Check(v.flags.size() == 1 && v.flags.front() == "-mrelaxed-simd", "flag is -mrelaxed-simd");
|
||||||
|
Check(v.probes.size() == 1 && v.probes.front() == "relaxed-simd", "probe is relaxed-simd");
|
||||||
|
}
|
||||||
|
EnableWasiRelaxedSimdVariant(cfg);
|
||||||
|
Check(cfg.wasmVariants.size() == 1, "second call is idempotent");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── wasmVariantFlags perturbs VariantId / BuildDir / BinDir ──────────
|
||||||
|
// Configuration owns move-only members (unique_ptr<Module>), so build the
|
||||||
|
// two cases independently rather than copying.
|
||||||
|
{
|
||||||
|
auto make = [] {
|
||||||
|
Configuration c;
|
||||||
|
c.path = "/tmp/wasm-variant-id-test";
|
||||||
|
c.name = "wv"; c.outputName = "wv";
|
||||||
|
c.target = "wasm32-wasip1";
|
||||||
|
c.type = ConfigurationType::Executable;
|
||||||
|
return c;
|
||||||
|
};
|
||||||
|
Configuration a = make();
|
||||||
|
Configuration b = make();
|
||||||
|
b.wasmVariantFlags = {"-mrelaxed-simd"};
|
||||||
|
Check(a.VariantId() != b.VariantId(), "wasmVariantFlags perturbs VariantId");
|
||||||
|
Check(a.BuildDir() != b.BuildDir(), "wasmVariantFlags perturbs BuildDir");
|
||||||
|
Check(a.BinDir() != b.BinDir(), "wasmVariantFlags perturbs BinDir");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── variants.json manifest contents ──────────────────────────────────
|
||||||
|
{
|
||||||
|
fs::path repoWasi = fs::current_path() / "wasi-runtime";
|
||||||
|
fs::path systemHome = "/usr/local/share/crafter-build";
|
||||||
|
if (fs::exists(repoWasi / "runtime.js")) {
|
||||||
|
setenv("CRAFTER_BUILD_HOME", fs::current_path().c_str(), 1);
|
||||||
|
} else if (fs::exists(systemHome / "wasi-runtime" / "runtime.js")) {
|
||||||
|
setenv("CRAFTER_BUILD_HOME", systemHome.c_str(), 1);
|
||||||
|
} else {
|
||||||
|
std::println(std::cerr, "wasi-runtime assets not found; skipping manifest check");
|
||||||
|
return failures ? 1 : 77;
|
||||||
|
}
|
||||||
|
|
||||||
|
Configuration cfg;
|
||||||
|
cfg.path = fs::temp_directory_path() / "crafter-build-wasm-variants-test";
|
||||||
|
cfg.name = "wvhelper";
|
||||||
|
cfg.outputName = "wvhelper";
|
||||||
|
cfg.target = "wasm32-wasip1";
|
||||||
|
cfg.type = ConfigurationType::Executable;
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
fs::remove_all(cfg.path, ec);
|
||||||
|
fs::create_directories(cfg.path);
|
||||||
|
|
||||||
|
EnableWasiRelaxedSimdVariant(cfg);
|
||||||
|
EnableWasiBrowserRuntime(cfg);
|
||||||
|
|
||||||
|
const fs::path* variants = nullptr;
|
||||||
|
for (const fs::path& f : cfg.files) {
|
||||||
|
if (f.filename() == "variants.json") variants = &f;
|
||||||
|
}
|
||||||
|
Check(variants != nullptr, "variants.json registered");
|
||||||
|
if (variants) {
|
||||||
|
std::ifstream vin(*variants);
|
||||||
|
std::string j{std::istreambuf_iterator<char>(vin), std::istreambuf_iterator<char>()};
|
||||||
|
// relaxed-simd entry, with its labelled url + probe.
|
||||||
|
Check(j.find("\"label\":\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd label");
|
||||||
|
Check(j.find("\"wvhelper.relaxed-simd.wasm\"") != std::string::npos, "manifest has labelled url");
|
||||||
|
Check(j.find("\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd probe");
|
||||||
|
// baseline fallback entry.
|
||||||
|
Check(j.find("\"wvhelper.wasm\"") != std::string::npos, "manifest has baseline url");
|
||||||
|
// The relaxed-simd variant must precede the baseline so the runtime
|
||||||
|
// prefers it when probes pass.
|
||||||
|
auto relaxedPos = j.find("relaxed-simd");
|
||||||
|
auto baselinePos = j.find("\"wvhelper.wasm\"");
|
||||||
|
Check(relaxedPos != std::string::npos && baselinePos != std::string::npos && relaxedPos < baselinePos,
|
||||||
|
"relaxed-simd entry precedes baseline");
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_all(cfg.path, ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures > 0) {
|
||||||
|
std::println(std::cerr, "{} assertions failed", failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
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
|
// Preload asset files listed in files.json (emitted by
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue