open-standards/wai/wai-vector/browser/page.html
Transaction Science 5189e5f50b wai-vector: the wasm exports and the three-engine browser leg (D7-8)
- wai/wai-vector/wasm: the sink's exports (wv_alloc, wv_free, wv_open,
  wv_close, wv_render, wv_out_ptr, wv_out_len, wv_sha256, wv_cost,
  wv_transcript, wv_projection), no_std with its own allocator, built for
  wasm32v1-none so the module is WebAssembly 1.0 only; check-mvp.mjs refuses
  any post-1.0 feature, and fails on the wasm32-unknown-unknown build.
- build-pkg.sh: the package a page serves.
- browser/: a harness that renders every listed frame of every vector in
  Chromium, WebKit and Firefox, hashes it in the page twice, re-hashes sampled
  frames in Node, recomputes the arithmetic vectors and authoring forms, and
  compares each a11y payload's transcript and projections with its text record.
  A missing engine fails the run.
- wai-vector.yml: the browser job, in a Playwright container pinned by digest,
  on the linux runner.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 02:44:44 -04:00

230 lines
9.1 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>wai-vector in this engine</title>
</head>
<body>
<p id="state">loading</p>
<script type="module">
// The sink of wai.graphics.vector in this page's engine. run.mjs serves this page,
// the two modules and the corpus, and drives it; nothing here draws to a canvas.
//
// /pkg/wai_vector.wasm the package: the sink's exports alone, WebAssembly 1.0
// /recompute.wasm the recomputation of the conformance vectors
// /corpus/… wai/vector-conformance
// POST /frame/<id> a frame's bytes, for run.mjs to hash for itself
//
// window.__wai_overlay is the hook a page gives its tests: frame(size, rate, g)
// returns the renderer's own buffer, RGBA8P, copied out of the module's memory, and
// transcript() and projection(rate, g) return the sink's own JSON.
// window.__wai_harness is what run.mjs calls. window.__wai_ready settles when both
// exist.
const u32 = (v) => v >>> 0;
const hex = (b) => Array.from(b, (v) => v.toString(16).padStart(2, '0')).join('');
const text = (b) => new TextDecoder().decode(b);
// The sink's JSON as a string, losing nothing: bytes that are not UTF-8 throw, and a
// leading U+FEFF is kept, so the string encodes back to the same bytes.
const json = (b) => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(b);
const utf8 = (s) => new TextEncoder().encode(s);
async function bytesAt(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(`${url}: HTTP ${r.status}`);
return new Uint8Array(await r.arrayBuffer());
}
// A module's exports, over byte arrays. The module imports nothing. Its memory moves
// when it grows, so no view of it outlives the call that made it.
async function moduleAt(url) {
const { instance } = await WebAssembly.instantiate(await bytesAt(url), {});
const x = instance.exports;
const put = (b) => {
const p = u32(x.wv_alloc(b.length));
new Uint8Array(x.memory.buffer, p, b.length).set(b);
return p;
};
return {
x,
put,
// The output of the last call, copied.
out: () => new Uint8Array(x.memory.buffer, u32(x.wv_out_ptr()), u32(x.wv_out_len())).slice(),
// f(pointer, length) over a copy of b in the module's memory, freed afterwards.
over(b, f) {
const p = put(b);
try {
return f(p, b.length);
} finally {
x.wv_free(p, b.length);
}
},
};
}
// The hook, over the package. One payload is open at a time.
function overlay(m) {
let handle = 0;
const refused = (status) => {
const code = status === 1 ? text(m.out()) : 'no_payload_open';
return Object.assign(new Error(code), { code });
};
return {
// Hold an outline set a payload may name.
addSet(bytes) {
m.over(bytes, (p, n) => m.x.wv_add_set(p, n));
},
// Open a payload; the one open before it is closed. Returns 'accepted', or the
// refusal's code.
open(bytes) {
if (handle !== 0) m.x.wv_close(handle);
handle = u32(m.over(bytes, (p, n) => m.x.wv_open(p, n)));
return handle !== 0 ? 'accepted' : text(m.out());
},
// Frame g at the size [w, h] and the rate [num, den]: RGBA8P, 4·w·h bytes. A
// request the payload does not list throws an Error whose code names why.
frame(size, rate, g) {
if (!Number.isSafeInteger(g) || g < 0) throw new RangeError(`frame ${g}`);
const status = m.x.wv_render(handle, size[0], size[1], rate[0], rate[1], g % 4294967296, Math.floor(g / 4294967296));
if (status !== 0) throw refused(status);
return m.out();
},
// The per-frame cost the payload states for the size [w, h].
cost(size) {
const status = m.x.wv_cost(handle, size[0], size[1]);
if (status !== 0) throw refused(status);
return Number(text(m.out()));
},
// The transcript: every block that is not decorative, with the ticks over which
// it is active, as the sink's JSON.
transcript() {
const status = m.x.wv_transcript(handle);
if (status !== 0) throw refused(status);
return json(m.out());
},
// The projection at frame g of the rate [num, den]: the blocks active at that
// frame, as the sink's JSON. A request the payload does not list throws an Error
// whose code names why.
projection(rate, g) {
if (!Number.isSafeInteger(g) || g < 0) throw new RangeError(`frame ${g}`);
const status = m.x.wv_projection(handle, rate[0], rate[1], g % 4294967296, Math.floor(g / 4294967296));
if (status !== 0) throw refused(status);
return json(m.out());
},
// SHA-256, computed in the module.
sha256(bytes) {
m.over(bytes, (p, n) => m.x.wv_sha256(p, n));
return m.out();
},
};
}
window.__wai_ready = (async () => {
const [pkg, rec] = await Promise.all([moduleAt('/pkg/wai_vector.wasm'), moduleAt('/recompute.wasm')]);
const hook = overlay(pkg);
window.__wai_overlay = hook;
const harness = {
// Every set under sets/, held by both modules.
async sets(names) {
for (const name of names) {
const b = await bytesAt(`/corpus/sets/${encodeURIComponent(name)}`);
window.__wai_overlay.addSet(b);
rec.over(b, (p, n) => rec.x.wv_add_set(p, n));
}
},
// Two SHA-256 digests of the same bytes: the module's, and the platform's.
async digests(bytes) {
if (!globalThis.crypto || !crypto.subtle) throw new Error('this page has no platform digest (crypto.subtle)');
const inModule = hex(window.__wai_overlay.sha256(bytes));
const platform = hex(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)));
return { inModule, platform };
},
// One frame through the hook, with its two digests.
async check(size, rate, g) {
const bytes = window.__wai_overlay.frame(size, rate, g);
return { bytes, ...(await this.digests(bytes)) };
},
// Hand a frame's bytes to run.mjs.
async send(id, bytes) {
const r = await fetch(`/frame/${id}`, { method: 'POST', headers: { 'content-type': 'application/octet-stream' }, body: bytes });
if (!r.ok) throw new Error(`/frame/${id}: HTTP ${r.status}`);
},
// One payload through the package, by plan:
// sizes [[w, h], …] each one's cost is asked
// frames [{ size, rate, g, send }, …] each rendered and hashed twice;
// sent when `send` is an id
// requests [{ size, rate, g }, …] requests expected to be refused
// text null, or { projections: [{ rate, g }, …], requests: [{ rate, g }, …] }:
// the transcript, each projection, and requests expected to be refused
async payload(path, plan) {
const bytes = await bytesAt(`/corpus/${path}`);
const r = { instr: await this.digests(bytes), code: window.__wai_overlay.open(bytes), costs: [], frames: [], requests: [], text: null };
if (r.code !== 'accepted') return r;
for (const size of plan.sizes) r.costs.push(window.__wai_overlay.cost(size));
for (const f of plan.frames) {
const c = await this.check(f.size, f.rate, f.g);
if (f.send !== null) await this.send(f.send, c.bytes);
r.frames.push({ bytes: c.bytes.length, inModule: c.inModule, platform: c.platform });
}
for (const q of plan.requests) {
try {
window.__wai_overlay.frame(q.size, q.rate, q.g);
r.requests.push('rendered');
} catch (e) {
r.requests.push(e.code || String(e));
}
}
if (plan.text !== null) {
const t = { transcript: window.__wai_overlay.transcript(), projections: [], requests: [] };
for (const q of plan.text.projections) t.projections.push(window.__wai_overlay.projection(q.rate, q.g));
for (const q of plan.text.requests) {
try {
window.__wai_overlay.projection(q.rate, q.g);
t.requests.push('projected');
} catch (e) {
t.requests.push(e.code || String(e));
}
}
r.text = t;
}
return r;
},
// One vector through the recomputation: its expected file, and for a payload its
// authoring form.
async recompute(group, name, ext) {
const data = await bytesAt(`/corpus/vectors/${group}/${encodeURIComponent(name)}.${ext}`);
const [g, e] = [utf8(group), utf8(ext)];
const [pg, pe, pd] = [rec.put(g), rec.put(e), rec.put(data)];
try {
const status = rec.x.wv_recompute(pg, g.length, pe, e.length, pd, data.length);
const r = { status, expected: text(rec.out()), form: null };
if (ext === 'wvg1') {
rec.x.wv_form(pd, data.length);
r.form = text(rec.out());
}
return r;
} finally {
rec.x.wv_free(pg, g.length);
rec.x.wv_free(pe, e.length);
rec.x.wv_free(pd, data.length);
}
},
};
window.__wai_harness = harness;
document.getElementById('state').textContent = 'ready';
return {
package: Object.keys(pkg.x).sort(),
recompute: Object.keys(rec.x).sort(),
platformDigest: Boolean(globalThis.crypto && crypto.subtle),
};
})();
</script>
</body>
</html>