// The ops-conformance graph vectors through a browser's WebGPU.
//
// Build the page's module first:
//
// cd wai/wai-ops-gpu/browser
// cargo build --locked --release --target wasm32-unknown-unknown
// wasm-bindgen --target web --out-dir pkg --out-name wai_ops_gpu_browser \
// target/wasm32-unknown-unknown/release/wai_ops_gpu_browser.wasm
//
// then, with Playwright installed under PLAYWRIGHT_HOST:
//
// [WAI_OPS_GPU_REQUIRE=1] [WAI_OPS_GPU_BROWSER_CHANNEL=chrome] \
// PLAYWRIGHT_HOST=
/ node wai/wai-ops-gpu/browser/run.mjs
//
// It serves this directory, ../../ops-conformance and ../../wgsl-conformance on
// 127.0.0.1, opens index.html in a headless browser and prints the page's report. It
// fails on a page error, a mismatch or a device error, and, under WAI_OPS_GPU_REQUIRE=1,
// when the page found no adapter, or the report lacks a `browser webgpu: key=` line
// with mismatches=0 errors=0, the `wasm reference:` line saying the committed WGSL
// semantics vectors are equal, or a `browser webgpu semantics: key=` line with differ=0.
// Headless, macOS offers a page a hardware adapter only when the browser draws through
// Metal (`--use-angle=metal`); WAI_OPS_GPU_BROWSER_ARGS (space-separated) replaces the
// launch arguments.
import http from "node:http";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const CORPUS = path.resolve(HERE, "../../ops-conformance");
const WGSL_VECTORS = path.resolve(HERE, "../../wgsl-conformance");
const REQUIRE = process.env.WAI_OPS_GPU_REQUIRE === "1";
const PORT = Number(process.env.PORT || 8797);
const TIMEOUT_MS = Number(process.env.WAI_OPS_GPU_TIMEOUT_MS || 20 * 60 * 1000);
const ARGS = process.env.WAI_OPS_GPU_BROWSER_ARGS !== undefined
? process.env.WAI_OPS_GPU_BROWSER_ARGS.split(/\s+/).filter(Boolean)
: (process.platform === "darwin" ? ["--use-angle=metal"] : []).concat(["--enable-unsafe-webgpu"]);
const { chromium } = createRequire(process.env.PLAYWRIGHT_HOST || HERE + "/")("playwright");
const files = ["vectors", "graphs"].flatMap((d) =>
fs.readdirSync(path.join(CORPUS, d)).filter((f) => f.endsWith(".jsonl")).sort().map((f) => `${d}/${f}`));
const ROOTS = { "/ops-conformance/": CORPUS, "/wgsl-conformance/": WGSL_VECTORS, "/browser/": HERE };
const MIME = { ".js": "text/javascript", ".wasm": "application/wasm", ".html": "text/html", ".json": "application/json", ".jsonl": "text/plain" };
const server = http.createServer((req, res) => {
const url = decodeURIComponent(req.url.split("?")[0]);
if (url === "/browser/files.json") {
res.writeHead(200, { "content-type": "application/json" });
return res.end(JSON.stringify(files));
}
const prefix = Object.keys(ROOTS).find((p) => url.startsWith(p));
const root = prefix && ROOTS[prefix];
const f = root && path.resolve(root, "." + url.slice(prefix.length - 1));
if (!f || !f.startsWith(root + path.sep) || !fs.existsSync(f) || !fs.statSync(f).isFile()) {
res.writeHead(404);
return res.end("not found");
}
res.writeHead(200, { "content-type": MIME[path.extname(f)] || "application/octet-stream" });
fs.createReadStream(f).pipe(res);
});
let browser;
try {
if (!fs.existsSync(path.join(HERE, "pkg", "wai_ops_gpu_browser.js"))) {
console.error("no pkg/wai_ops_gpu_browser.js; build it as this file's header says");
process.exit(2);
}
await new Promise((r) => server.listen(PORT, "127.0.0.1", r));
const channel = process.env.WAI_OPS_GPU_BROWSER_CHANNEL || undefined;
browser = await chromium.launch({ args: ARGS, channel });
console.log(`browser: ${channel || "chromium"} ${browser.version()} args=${ARGS.join(" ")}`);
const page = await browser.newPage();
const errs = [];
page.on("pageerror", (e) => errs.push(String(e)));
await page.goto(`http://127.0.0.1:${PORT}/browser/index.html`, { waitUntil: "domcontentloaded", timeout: 30000 });
await page.waitForFunction(() => window.opsGpu && window.opsGpu.done === true, null, { timeout: TIMEOUT_MS, polling: 1000 });
const report = await page.evaluate(() => window.opsGpu.report);
const info = await page.evaluate(async () => {
if (!navigator.gpu) return "navigator.gpu is absent";
const a = await navigator.gpu.requestAdapter();
if (!a) return "requestAdapter gave none";
const i = a.info || {};
return `vendor=${i.vendor} architecture=${i.architecture} device=${i.device} description=${i.description}`;
});
console.log(`adapter info (navigator.gpu): ${info}`);
console.log(report);
for (const e of errs) console.log(`page error: ${e}`);
const lines = report.split("\n");
const ok = lines.some((l) => /^browser webgpu: key=\S+ .* mismatches=0 errors=0$/.test(l)) &&
lines.some((l) => /^wasm reference: target=wasm32 .* cases=[1-9][0-9]* equal$/.test(l)) &&
lines.some((l) => /^browser webgpu semantics: key=\S+ .* guarded=[1-9][0-9]* differ=0 /.test(l));
const bad = errs.length > 0 || /MISMATCH|DEVICE ERROR|page error|did not parse/.test(report) ||
lines.some((l) => /^browser webgpu: key=/.test(l) && !/mismatches=0 errors=0$/.test(l)) ||
lines.some((l) => /^browser webgpu semantics: key=/.test(l) && !/ differ=0 /.test(l));
if (bad || (REQUIRE && !ok)) process.exitCode = 1;
} finally {
if (browser) await browser.close();
server.close();
}