mandelbrot/tests/pixel-contract.mjs

62 lines
2.4 KiB
JavaScript
Raw Normal View History

2026-08-22 20:55:27 +09:00
import '../kernels.js';
const K = globalThis.MANDEL_KERNELS;
const decode = b64 => Uint8Array.from(atob(b64), c => c.charCodeAt(0));
function instantiate() {
for (const [name, payload] of [['simd', K.WASM_SIMD_B64], ['scalar', K.WASM_SCALAR_B64]]) {
try {
const instance = new WebAssembly.Instance(new WebAssembly.Module(decode(payload)), {});
return { name, ex: instance.exports };
} catch {}
}
throw new Error('Neither shallow WASM backend could be instantiated.');
}
function jsPixel(cre, cim, span, width, height, x, y, iter) {
const scale = span / width;
// Match the public ABI operation order: JavaScript shifts the center once,
// then the kernel applies the historical integer-grid formula. The
// algebraically equivalent single expression can round differently at a
// chaotic boundary and is not a useful backend-equivalence oracle.
const shiftedRe = cre + scale * 0.5;
const shiftedIm = cim - scale * 0.5;
const cr = shiftedRe + scale * (x - width * 0.5);
const ci = shiftedIm + scale * (height * 0.5 - y);
let zr = 0, zi = 0, zr2 = 0, zi2 = 0, n = 0;
while (n < iter && zr2 + zi2 <= 4) {
zi = 2 * zr * zi + ci;
zr = zr2 - zi2 + cr;
zr2 = zr * zr;
zi2 = zi * zi;
n++;
}
return n;
}
const backend = instantiate();
const scenes = [
{ cre: -0.5, cim: 0, span: 3.4, width: 31, height: 19, iter: 350 },
{ cre: -0.743643887037151, cim: 0.13182590420533, span: 4e-4, width: 37, height: 23, iter: 700 }
];
let samples = 0;
let mismatches = 0;
const mismatchDetails = [];
for (const s of scenes) {
const scale = s.span / s.width;
const npx = backend.ex.render_rows(s.cre + scale * 0.5, s.cim - scale * 0.5, s.span, s.width, s.height, 0, s.height, s.iter);
const counts = new Uint32Array(backend.ex.memory.buffer, backend.ex.counts_ptr(), npx);
for (let y = 0; y < s.height; y++) for (let x = 0; x < s.width; x++) {
samples++;
const wasmCount = counts[y * s.width + x];
const jsCount = jsPixel(s.cre, s.cim, s.span, s.width, s.height, x, y, s.iter);
if (wasmCount !== jsCount) {
mismatches++;
if (mismatchDetails.length < 12) mismatchDetails.push({ scene: scenes.indexOf(s), x, y, wasmCount, jsCount });
}
}
}
if (mismatches) throw new Error(`Pixel contract mismatch: ${mismatches}/${samples} ${JSON.stringify(mismatchDetails)}`);
console.log(JSON.stringify({ status: 'pass', backend: backend.name, samples, mismatches, contract: '(x+0.5,y+0.5)' }));