- 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]>
230 lines
9.1 KiB
HTML
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>
|