// 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 and ../../ops-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 has no `browser webgpu: key=` line with mismatches=0 errors=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 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, "/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 ok = report.split("\n").some((l) => /^browser webgpu: key=\S+ .* mismatches=0 errors=0$/.test(l)); const bad = errs.length > 0 || /MISMATCH|DEVICE ERROR|page error|did not parse/.test(report) || report.split("\n").some((l) => /^browser webgpu: key=/.test(l) && !/mismatches=0 errors=0$/.test(l)); if (bad || (REQUIRE && !ok)) process.exitCode = 1; } finally { if (browser) await browser.close(); server.close(); }