w
This commit is contained in:
parent
03ddaa09f2
commit
caa90775f9
89 changed files with 7126 additions and 0 deletions
29
tests/analytic-interior.mjs
Normal file
29
tests/analytic-interior.mjs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
const fromFraction = (numerator, denominator, bits) => numerator * (1n << BigInt(bits)) / denominator;
|
||||
|
||||
function fixedAnalyticInterior(cr, ci, bits) {
|
||||
const scale = 1n << BigInt(bits);
|
||||
const x = cr - (scale >> 2n), y = ci, q = x * x + y * y;
|
||||
if (4n * q * (q + x * scale) <= y * y * scale * scale) return true;
|
||||
const d = cr + scale;
|
||||
return 16n * (d * d + y * y) <= scale * scale;
|
||||
}
|
||||
|
||||
const cases = [
|
||||
{ id: 'origin-cardioid', re: [0n, 1n], im: [0n, 1n], expected: true },
|
||||
{ id: 'cardioid-cusp', re: [1n, 4n], im: [0n, 1n], expected: true },
|
||||
{ id: 'period2-center', re: [-1n, 1n], im: [0n, 1n], expected: true },
|
||||
{ id: 'period2-boundary', re: [-5n, 4n], im: [0n, 1n], expected: true },
|
||||
{ id: 'right-exterior', re: [13n, 50n], im: [0n, 1n], expected: false },
|
||||
{ id: 'period3-not-analytic', re: [-123n, 1000n], im: [745n, 1000n], expected: false },
|
||||
{ id: 'far-exterior', re: [1n, 1n], im: [1n, 1n], expected: false }
|
||||
];
|
||||
|
||||
let checked = 0;
|
||||
for (const bits of [256, 320]) for (const sample of cases) {
|
||||
const cr = fromFraction(sample.re[0], sample.re[1], bits);
|
||||
const ci = fromFraction(sample.im[0], sample.im[1], bits);
|
||||
const actual = fixedAnalyticInterior(cr, ci, bits);
|
||||
if (actual !== sample.expected) throw new Error(`${sample.id} at ${bits} bit: expected ${sample.expected}, got ${actual}`);
|
||||
checked++;
|
||||
}
|
||||
console.log(JSON.stringify({ status: 'pass', checked, precisions: [256, 320], proof: 'integer cardioid/period-2 inequalities' }));
|
||||
27
tests/browser-benchmark.html
Normal file
27
tests/browser-benchmark.html
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Mandelbrot v23 browser benchmark</title>
|
||||
<style>body{font:14px system-ui;margin:20px;background:#111827;color:#eef2ff}button{padding:8px 14px}iframe{position:fixed;left:-20000px;top:0;border:0}pre{white-space:pre-wrap}</style>
|
||||
<h1>v23 browser benchmark</h1>
|
||||
<p>HTTP(S)でこのworkspaceを配信して実行します。iframeの実寸と実DPRを結果へ記録します。</p>
|
||||
<button id="run">Run corpus</button>
|
||||
<pre id="output">Ready.</pre>
|
||||
<iframe id="app" width="1440" height="900"></iframe>
|
||||
<script type="module">
|
||||
const out=document.querySelector('#output'),frame=document.querySelector('#app');
|
||||
const wait=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
function fixed(decimal,bits){let s=String(decimal).toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);let[m,e='0']=s.split('e'),[i,f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-Number(e);if(places<0){digits+='0'.repeat(-places);places=0}let v=BigInt(digits)*(1n<<BigInt(bits))/(10n**BigInt(places));return neg?-v:v}
|
||||
function bitsFor(span){const n=Math.abs(Number(span));return Number.isFinite(n)&&n>0?Math.max(256,Math.ceil(-Math.log2(n))+256):1280}
|
||||
function hash(scene){const bits=bitsFor(scene.span),p=new URLSearchParams({v:'23',b:String(bits),re:String(fixed(scene.re,bits)),im:String(fixed(scene.im,bits)),sp:String(fixed(scene.span,bits)),pal:'0',cy:'.008',sh:'.18',it:'350',ad:'1'});return'#'+p}
|
||||
async function poll(fn,timeout,label){const started=performance.now();while(performance.now()-started<timeout){let value;try{value=fn()}catch{}if(value)return{value,ms:performance.now()-started};await wait(25)}throw new Error('timeout: '+label)}
|
||||
const p95=values=>values.slice().sort((a,b)=>a-b)[Math.min(values.length-1,Math.floor(values.length*.95))]||0;
|
||||
async function canvasHash(win){const blob=await new Promise(resolve=>win.document.querySelector('#view').toBlob(resolve,'image/png'));const digest=await crypto.subtle.digest('SHA-256',await blob.arrayBuffer());return[...new Uint8Array(digest)].map(x=>x.toString(16).padStart(2,'0')).join('')}
|
||||
function accessibilityAudit(win){const doc=win.document,failures=[],interactive=[...doc.querySelectorAll('button,select,input,summary')];for(const el of interactive){const target=el.type==='checkbox'?el.closest('label')||el:el,r=target.getBoundingClientRect();if(r.width<44||r.height<44)failures.push('target:'+el.id);if(el.matches('input:not([type=checkbox]),select')&&el.id&&!doc.querySelector(`label[for="${el.id}"]`))failures.push('label:'+el.id)}if(!doc.querySelector('#view[tabindex][aria-label]'))failures.push('canvas-keyboard-name');if(!doc.querySelector('[aria-live]'))failures.push('live-status');if(/user-scalable\s*=\s*no/i.test(doc.querySelector('meta[name=viewport]')?.content||''))failures.push('page-zoom-disabled');const css=[...doc.querySelectorAll('style')].map(x=>x.textContent).join('\n');if(!css.includes('prefers-reduced-motion'))failures.push('reduced-motion');if(!css.includes('prefers-reduced-transparency'))failures.push('reduced-transparency');return{pass:failures.length===0,failures,interactiveCount:interactive.length}}
|
||||
async function interactionAudit(win){const canvas=win.document.querySelector('#view'),before=win.__MANDEL_DIAG__.snapshot(),durations=[];for(let i=0;i<16;i++){const t=performance.now();canvas.dispatchEvent(new win.WheelEvent('wheel',{deltaY:0,clientX:canvas.clientWidth/2,clientY:canvas.clientHeight/2,cancelable:true}));durations.push(performance.now()-t)}const during=win.__MANDEL_DIAG__.snapshot();await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d&&!d.rendering&&!d.scheduler.wheelActive&&d.lastPass==='preview'?d:null},120000,'wheel settle preview');await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d&&!d.rendering&&d.coverage>=.999&&['COVERED','RESOLVING','REFINING','REFINED'].includes(d.drawState)?d:null},180000,'wheel settle covered');return{dispatchP95Ms:p95(durations),renderStartsDuringGesture:during.runtimeMetrics.renderStartsDuringGesture-before.runtimeMetrics.renderStartsDuringGesture,renderStartsBeforeSettle:during.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts}}
|
||||
async function modeAudit(win){const select=win.document.querySelector('#processMode'),hq=win.document.querySelector('#hq');select.value='power';select.dispatchEvent(new win.Event('change',{bubbles:true}));await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d.automaticTarget==='PREVIEW'&&!d.rendering&&d.lastPass==='preview'?d:null},120000,'power preview');const powerBefore=win.__MANDEL_DIAG__.snapshot();await wait(900);const power=win.__MANDEL_DIAG__.snapshot(),powerPass=power.automaticTarget==='PREVIEW'&&power.lastPass==='preview'&&!power.rendering&&!power.scheduler.timer&&!power.detailActive&&!hq.checked&&power.screen.pixelBudget<=1048576&&power.runtimeMetrics.renderStarts===powerBefore.runtimeMetrics.renderStarts;select.value='standard';select.dispatchEvent(new win.Event('change',{bubbles:true}));await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d.automaticTarget==='COVERED'&&!d.rendering&&d.lastPass==='covered'&&d.coverage>=.999?d:null},180000,'standard covered');await wait(900);const standard=win.__MANDEL_DIAG__.snapshot(),standardPass=standard.automaticTarget==='COVERED'&&standard.lastPass==='covered'&&!standard.detailActive&&!standard.scheduler.unknownTimer&&!hq.checked&&standard.screen.pixelBudget<=4194304;return{pass:powerPass&&standardPass,power:{pass:powerPass,target:power.automaticTarget,pixelBudget:power.screen.pixelBudget,renderStartsAfterStable:power.runtimeMetrics.renderStarts-powerBefore.runtimeMetrics.renderStarts},standard:{pass:standardPass,target:standard.automaticTarget,pixelBudget:standard.screen.pixelBudget,detailActive:standard.detailActive,unknownTimer:standard.scheduler.unknownTimer}}}
|
||||
async function recolorAudit(win){const select=win.document.querySelector('#palette'),before=win.__MANDEL_DIAG__.snapshot();select.value=before.palette===1?'2':'1';select.dispatchEvent(new win.Event('change',{bubbles:true}));await new Promise(resolve=>win.requestAnimationFrame(()=>win.requestAnimationFrame(resolve)));const after=win.__MANDEL_DIAG__.snapshot();return{pass:after.palette!==before.palette&&after.runtimeMetrics.renderStarts===before.runtimeMetrics.renderStarts,renderStarts:after.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts}}
|
||||
async function exportAudit(win){const doc=win.document,captured=[],nativeCreate=win.URL.createObjectURL.bind(win.URL),nativeClick=win.HTMLAnchorElement.prototype.click;win.URL.createObjectURL=blob=>{const url=nativeCreate(blob);captured.push({url,blob});return url};win.HTMLAnchorElement.prototype.click=function(){};try{doc.querySelector('#exportScale').value='0';doc.querySelector('#exportWidth').value='64';doc.querySelector('#exportAA').value='1';doc.querySelector('#exportPrecision').value='balanced';doc.querySelector('#exportStart').click();await poll(()=>!win.__MANDEL_DIAG__.snapshot().exporting&&captured.length>=2,120000,'small export');const png=captured.find(x=>x.blob.type==='image/png')?.blob,json=captured.find(x=>x.blob.type==='application/json')?.blob;if(!png||!json)throw new Error('export files missing');const bytes=new Uint8Array(await png.arrayBuffer()),width=(bytes[16]<<24)|(bytes[17]<<16)|(bytes[18]<<8)|bytes[19],height=(bytes[20]<<24)|(bytes[21]<<16)|(bytes[22]<<8)|bytes[23],meta=JSON.parse(await json.text()),completed={width,height,metadataComplete:meta.determinism?.allTilesCompleted===true,sampleCount:meta.sampleCount,kernelHashes:!!meta.kernelSha256};const prior=captured.length;doc.querySelector('#exportWidth').value='256';doc.querySelector('#exportPrecision').value='validated';doc.querySelector('#exportStart').click();doc.querySelector('#exportCancel').click();await poll(()=>!win.__MANDEL_DIAG__.snapshot().exporting,120000,'export cancel');return{pass:width===64&&height===Math.round(64*win.document.querySelector('#view').height/win.document.querySelector('#view').width)&&captured.length===prior,completed,cancelledWithoutDownload:captured.length===prior}}finally{win.URL.createObjectURL=nativeCreate;win.HTMLAnchorElement.prototype.click=nativeClick}}
|
||||
async function runScene(scene,viewport){frame.width=viewport.cssWidth;frame.height=viewport.cssHeight;const loaded=new Promise((resolve,reject)=>{frame.onload=resolve;frame.onerror=reject});const started=performance.now();frame.src='../index.html'+hash(scene);await loaded;const win=frame.contentWindow;const label=viewport.id+'/'+scene.id;const preview=await poll(()=>{const d=win.__MANDEL_DIAG__?.snapshot();return d&&d.drawState==='PREVIEW'&&!d.rendering&&d.lastPass==='preview'?d:null},120000,'preview '+label);const previewSnapshot=preview.value,covered=await poll(()=>{const d=win.__MANDEL_DIAG__?.snapshot();return d&&['COVERED','RESOLVING','REFINING','REFINED','VALIDATING','VALIDATED','VALIDATION_INCOMPLETE'].includes(d.drawState)&&d.coverage>=.999?d:null},180000,'covered '+label);const refined=await poll(()=>{const d=win.__MANDEL_DIAG__?.snapshot();return d&&!d.detailActive&&!d.rendering&&!d.validating&&['COVERED','REFINED','VALIDATED','VALIDATION_INCOMPLETE'].includes(d.drawState)?d:null},240000,'refined '+label);const exercise=viewport.id==='desktop'&&scene.id==='z0',interaction=exercise?await interactionAudit(win):null,modes=exercise?await modeAudit(win):null,recolor=exercise?await recolorAudit(win):null,exportResult=exercise?await exportAudit(win):null,accessibility=exercise?accessibilityAudit(win):null,visualSha256=await canvasHash(win),resources=win.performance.getEntriesByType('resource').map(e=>String(e.name)),assetRequests={deep:resources.filter(x=>/\/(deep|bla|color)-/.test(x)),allWasm:resources.filter(x=>/\.wasm(?:$|\?)/.test(x))};await wait(2000);const before=win.__MANDEL_DIAG__.snapshot();await wait(1000);const after=win.__MANDEL_DIAG__.snapshot(),idle={raf:after.scheduler.raf,timer:after.scheduler.timer,pointerSettle:after.scheduler.pointerSettle,unknownTimer:after.scheduler.unknownTimer,backgroundJobs:after.scheduler.backgroundJobs,canvasWrites:after.runtimeMetrics.canvasWrites-before.runtimeMetrics.canvasWrites,domWrites:after.runtimeMetrics.domWrites-before.runtimeMetrics.domWrites,renderStarts:after.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts};return{id:scene.id,profile:viewport.id,requestedViewport:viewport,actualViewport:{cssWidth:win.innerWidth,cssHeight:win.innerHeight},actualDevicePixelRatio:win.devicePixelRatio,totalMs:performance.now()-started,previewMs:preview.ms,coveredMs:covered.ms,refinedMs:refined.ms,preview:previewSnapshot,final:after,idle,visualSha256,assetRequests,interaction,modes,recolor,export:exportResult,accessibility}}
|
||||
function evaluate(results){const exercise=results.find(x=>x.interaction),checks={idle:results.every(x=>!x.idle.raf&&!x.idle.timer&&!x.idle.pointerSettle&&!x.idle.unknownTimer&&x.idle.backgroundJobs===0&&x.idle.canvasWrites===0&&x.idle.domWrites===0&&x.idle.renderStarts===0),covered:results.every(x=>x.final.coverage>=.999&&x.final.hqTarget===x.final.frame),memory:results.every(x=>x.final.memory.managedBytes<=x.final.memory.budget),visual:results.every(x=>/^[0-9a-f]{64}$/.test(x.visualSha256)),startup:results.filter(x=>x.id==='z0').every(x=>x.assetRequests.deep.length===0),interaction:!!exercise&&exercise.interaction.dispatchP95Ms<16&&exercise.interaction.renderStartsDuringGesture===0&&exercise.interaction.renderStartsBeforeSettle===0,modes:exercise?.modes?.pass===true,recolor:exercise?.recolor?.pass===true,export:exercise?.export?.pass===true,accessibility:exercise?.accessibility?.pass===true,profileFidelity:results.every(x=>x.actualViewport.cssWidth===x.requestedViewport.cssWidth&&x.actualViewport.cssHeight===x.requestedViewport.cssHeight&&Math.abs(x.actualDevicePixelRatio-x.requestedViewport.dpr)<.01),previewBudget:results.every(x=>x.preview.lastRender<120)};const failures=Object.entries(checks).filter(([,pass])=>!pass).map(([name])=>name);return{pass:failures.length===0,checks,failures}}
|
||||
document.querySelector('#run').onclick=async()=>{document.querySelector('#run').disabled=true;try{const corpus=await(await fetch('./scenes.json',{cache:'no-store'})).json(),results=[];for(const viewport of corpus.viewports)for(const scene of corpus.scenes){out.textContent='Running '+viewport.id+'/'+scene.id+'…\n'+JSON.stringify(results,null,2);results.push(await runScene(scene,viewport))}const report={format:'mandelbrot-browser-baseline-v23',generatedUtc:new Date().toISOString(),userAgent:navigator.userAgent,hostDevicePixelRatio:devicePixelRatio,profiles:corpus.viewports,results,acceptance:evaluate(results)};out.textContent=JSON.stringify(report,null,2);globalThis.__BENCHMARK_RESULT__=report}catch(error){out.textContent=String(error?.stack||error)}finally{document.querySelector('#run').disabled=false}};
|
||||
</script>
|
||||
152
tests/js-syntax.mjs
Normal file
152
tests/js-syntax.mjs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const root = new URL('../', import.meta.url);
|
||||
const kernels = await fs.readFile(new URL('kernels.js', root), 'utf8');
|
||||
let app = await fs.readFile(new URL('script.js', root), 'utf8');
|
||||
const browserHarness = await fs.readFile(new URL('tests/browser-benchmark.html', root), 'utf8');
|
||||
const browserModule = browserHarness.match(/<script type="module">([\s\S]*?)<\/script>/)?.[1];
|
||||
if (!browserModule) throw new Error('Browser benchmark module not found.');
|
||||
new vm.Script(browserModule, { filename: 'browser-benchmark-module.js' });
|
||||
app = app.replace(/\}\)\(\);\s*$/, `
|
||||
const __fieldProbe = makeField(2, 100);
|
||||
putField(__fieldProbe, 0, 7, 16, FIELD_ESCAPED);
|
||||
putField(__fieldProbe, 1, 100, 0, FIELD_UNKNOWN);
|
||||
globalThis.__MANDEL_FIELD_PROBE__ = {
|
||||
iterations: Array.from(__fieldProbe.iterations),
|
||||
classes: Array.from(__fieldProbe.classes)
|
||||
};
|
||||
globalThis.__MANDEL_WORKER_SOURCES__ = {
|
||||
shallow: shallowWorkerSource(),
|
||||
deep: deepWorkerSource()
|
||||
};
|
||||
globalThis.__MANDEL_MODE_PROBE__ = {
|
||||
targets: Object.fromEntries(Object.keys(MODE_TARGET).map(mode => [mode, modeTarget(mode)])),
|
||||
coldDeepPreview: targetSize(RENDER_PROFILE[RENDER_PASS.PREVIEW], true)
|
||||
};
|
||||
globalThis.__MANDEL_REFERENCE_PROBE__ = async () => {
|
||||
const bits = 256, refLen = 40, rr = new Float64Array(refLen + 1), ri = new Float64Array(refLen + 1);
|
||||
const ref = { bits, re: 0n, im: 0n, rr, ri, escape: 0, version: 1, checkpointVersion: 0, checkpointBits: 0, checkpointCount: 0, checkpointMismatch: false };
|
||||
const run = () => new Promise(resolve => verifyReferenceCheckpoints(ref, refLen, state.token, ok => resolve(ok)));
|
||||
const agreement = await run();
|
||||
ref.rr[16] = 1; ref.checkpointVersion = 0; ref.checkpointMismatch = false;
|
||||
const catchesMismatch = !(await run());
|
||||
return { agreement, catchesMismatch, count: ref.checkpointCount, bits: ref.checkpointBits };
|
||||
};
|
||||
})();`);
|
||||
|
||||
const controls = new Map();
|
||||
function makeContext2d() {
|
||||
return {
|
||||
fillStyle: '', imageSmoothingEnabled: true, imageSmoothingQuality: 'high',
|
||||
fillRect() {}, drawImage() {}, putImageData() {}, save() {}, restore() {},
|
||||
setTransform() {}, translate() {}, scale() {},
|
||||
createImageData(width, height) {
|
||||
return { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
||||
},
|
||||
getImageData(_x, _y, width, height) {
|
||||
return { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
||||
}
|
||||
};
|
||||
}
|
||||
function makeElement(id = '') {
|
||||
return {
|
||||
id, value: '', checked: false, disabled: false, hidden: false,
|
||||
width: 800, height: 600, clientWidth: 800, clientHeight: 600,
|
||||
textContent: '', innerHTML: '', style: {}, dataset: {},
|
||||
classList: { add() {}, remove() {}, toggle() {} },
|
||||
addEventListener() {}, setAttribute() {}, click() {}, close() {}, showModal() {},
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }),
|
||||
getContext: () => makeContext2d(),
|
||||
toBlob(callback) { callback(new Blob()); }
|
||||
};
|
||||
}
|
||||
function element(id) {
|
||||
if (!controls.has(id)) controls.set(id, makeElement(id));
|
||||
return controls.get(id);
|
||||
}
|
||||
|
||||
Object.assign(element('processMode'), { value: 'standard' });
|
||||
Object.assign(element('palette'), { value: '0' });
|
||||
Object.assign(element('cycle'), { value: '.008' });
|
||||
Object.assign(element('shift'), { value: '.18' });
|
||||
Object.assign(element('iters'), { value: '350' });
|
||||
Object.assign(element('adaptive'), { checked: true });
|
||||
Object.assign(element('hq'), { checked: true });
|
||||
Object.assign(element('exportScale'), { value: '1' });
|
||||
Object.assign(element('exportAA'), { value: '1' });
|
||||
Object.assign(element('exportPrecision'), { value: 'balanced' });
|
||||
|
||||
let rafId = 0;
|
||||
const sandbox = {
|
||||
console, WebAssembly, BigInt, Blob, URL, URLSearchParams,
|
||||
Uint8Array, Uint8ClampedArray, Uint32Array, Float32Array, Float64Array,
|
||||
ArrayBuffer, Map, Set, Math, Date, JSON, Promise, performance,
|
||||
atob, btoa,
|
||||
innerWidth: 800, innerHeight: 600, devicePixelRatio: 1,
|
||||
navigator: { hardwareConcurrency: 4, deviceMemory: 8, clipboard: { writeText: async () => {} } },
|
||||
location: { protocol: 'http:', origin: 'http://localhost', hash: '', href: 'http://localhost/' },
|
||||
history: { pushState() {}, replaceState() {} },
|
||||
localStorage: { getItem: () => null, setItem() {} },
|
||||
matchMedia: () => ({ matches: false }),
|
||||
requestAnimationFrame: () => ++rafId,
|
||||
cancelAnimationFrame() {}, requestIdleCallback: () => 1,
|
||||
setTimeout: () => 1, clearTimeout() {}, queueMicrotask() {},
|
||||
addEventListener() {}, removeEventListener() {},
|
||||
document: {
|
||||
hidden: false,
|
||||
body: makeElement('body'),
|
||||
querySelector(selector) { return element(selector.replace(/^#/, '')); },
|
||||
createElement() { return makeElement(); }
|
||||
}
|
||||
};
|
||||
sandbox.window = sandbox;
|
||||
sandbox.globalThis = sandbox;
|
||||
|
||||
const context = vm.createContext(sandbox);
|
||||
new vm.Script(kernels, { filename: 'kernels.js' }).runInContext(context);
|
||||
new vm.Script(app, { filename: 'script.js' }).runInContext(context);
|
||||
|
||||
const sources = sandbox.__MANDEL_WORKER_SOURCES__;
|
||||
if (!sources?.shallow || !sources?.deep) throw new Error('Worker source extraction failed.');
|
||||
new vm.Script(sources.shallow, { filename: 'shallow-worker.js' });
|
||||
new vm.Script(sources.deep, { filename: 'deep-worker.js' });
|
||||
|
||||
let deepReady = null;
|
||||
const workerSandbox = {
|
||||
WebAssembly, Uint8Array, Uint8ClampedArray, Uint32Array, Float32Array, Float64Array,
|
||||
ArrayBuffer, Map, Set, Math, Date, JSON, Promise, performance,
|
||||
postMessage(message) { deepReady = message; }
|
||||
};
|
||||
workerSandbox.self = workerSandbox;
|
||||
const workerContext = vm.createContext(workerSandbox);
|
||||
new vm.Script(sources.deep, { filename: 'deep-worker.js' }).runInContext(workerContext);
|
||||
const moduleFiles = { deep: 'deep-simd.wasm', bla: 'bla-simd.wasm', color: 'color-simd.wasm' };
|
||||
const modules = {};
|
||||
for (const [key, file] of Object.entries(moduleFiles)) {
|
||||
const bytes = await fs.readFile(new URL(`dist/wasm/${file}`, root));
|
||||
modules[key] = { module: structuredClone(await WebAssembly.compile(bytes)), simd: true };
|
||||
}
|
||||
await workerSandbox.onmessage({ data: { type: 'init', modules } });
|
||||
if (deepReady?.type !== 'ready' || deepReady.error) throw new Error(`Deep Worker module init failed: ${deepReady?.error || 'no reply'}`);
|
||||
|
||||
const diagnostics = sandbox.__MANDEL_DIAG__?.snapshot();
|
||||
if (diagnostics?.rendererVersion !== 23) throw new Error('Application initialization failed.');
|
||||
if (diagnostics.lastPass !== 'preview') throw new Error('Discrete preview profile did not initialize.');
|
||||
if (diagnostics.automaticTarget !== 'COVERED') throw new Error('Standard mode must stop automatically at Covered.');
|
||||
if (JSON.stringify(sandbox.__MANDEL_MODE_PROBE__?.targets) !== JSON.stringify({ power: 'PREVIEW', standard: 'COVERED', fine: 'REFINED', validate: 'VALIDATED' })) throw new Error('Processing-mode completion targets failed.');
|
||||
if (sandbox.__MANDEL_MODE_PROBE__?.coldDeepPreview?.[0] > 48) throw new Error('Cold deep Preview exceeded its conservative width cap.');
|
||||
if (sandbox.__MANDEL_FIELD_PROBE__?.iterations?.join(',') !== '7,100') throw new Error('Field iteration channel failed.');
|
||||
sandbox.requestAnimationFrame = callback => { queueMicrotask(() => callback(performance.now())); return ++rafId; };
|
||||
const referenceCheckpoints = await sandbox.__MANDEL_REFERENCE_PROBE__();
|
||||
if (!referenceCheckpoints.agreement || !referenceCheckpoints.catchesMismatch || referenceCheckpoints.bits !== 320) throw new Error('P/P+64 reference checkpoint verification failed.');
|
||||
|
||||
console.log(JSON.stringify({
|
||||
status: 'pass', rendererVersion: diagnostics.rendererVersion, lastPass: diagnostics.lastPass, automaticTarget: diagnostics.automaticTarget,
|
||||
fieldIterations: sandbox.__MANDEL_FIELD_PROBE__.iterations,
|
||||
modeProbe: sandbox.__MANDEL_MODE_PROBE__,
|
||||
referenceCheckpoints,
|
||||
deepWorkerModuleInit: true,
|
||||
browserHarnessSyntax: true,
|
||||
sources: { appBytes: app.length, shallowWorkerBytes: sources.shallow.length, deepWorkerBytes: sources.deep.length }
|
||||
}));
|
||||
68
tests/kernel-golden.mjs
Normal file
68
tests/kernel-golden.mjs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const root = path.resolve(process.argv[2] || 'build/wasm-v23');
|
||||
const load = async name => {
|
||||
const bytes = await fs.readFile(path.join(root, name));
|
||||
return (await WebAssembly.instantiate(bytes, {})).instance.exports;
|
||||
};
|
||||
const assert = (ok, message) => { if (!ok) throw new Error(message); };
|
||||
const close = (a, b, tolerance) => Math.abs(a - b) <= tolerance * Math.max(1, Math.abs(a), Math.abs(b));
|
||||
|
||||
function direct(cr, ci, limit) {
|
||||
let zr=0, zi=0, zr2=0, zi2=0, n=0;
|
||||
while (n < limit && zr2 + zi2 <= 4) {
|
||||
zi = 2*zr*zi + ci; zr = zr2 - zi2 + cr; zr2 = zr*zr; zi2 = zi*zi; n++;
|
||||
}
|
||||
return [n, n < limit ? zr2 + zi2 : 0];
|
||||
}
|
||||
|
||||
async function shallowGolden() {
|
||||
const cores = await Promise.all(['wasm-simd.wasm','wasm-scalar.wasm'].map(load));
|
||||
const width=19, height=11, iter=320, re=-0.5, im=0, span=3.4, scale=span/width;
|
||||
let baseline;
|
||||
for (const [variant, ex] of [['simd',cores[0]],['scalar',cores[1]]]) {
|
||||
const npx=ex.render_rows(re+scale*.5,im-scale*.5,span,width,height,0,height,iter);
|
||||
assert(npx===width*height, `shallow ${variant}: output size`);
|
||||
const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),npx);
|
||||
const mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),npx);
|
||||
const copy=Uint32Array.from(counts); if (!baseline) baseline=copy;
|
||||
for(let y=0;y<height;y++)for(let x=0;x<width;x++){
|
||||
const i=y*width+x,shiftedRe=re+scale*.5,shiftedIm=im-scale*.5,cr=shiftedRe+scale*(x-width*.5),ci=shiftedIm+scale*(height*.5-y),[n,m]=direct(cr,ci,iter);
|
||||
assert(counts[i]===n, `shallow ${variant}: count mismatch at ${x},${y}`);
|
||||
if(n<iter)assert(close(mags[i],m,1e-12),`shallow ${variant}: magnitude mismatch at ${x},${y}: wasm=${mags[i]} direct=${m}`);
|
||||
assert(copy[i]===baseline[i],`shallow SIMD/scalar mismatch at ${i}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function colorGolden() {
|
||||
const cores=await Promise.all(['color-simd.wasm','color-scalar.wasm'].map(load));
|
||||
const inputs=[4.0000001,4.1,8,32,1e4,1e100]; let baseline;
|
||||
for(const [variant,ex] of [['simd',cores[0]],['scalar',cores[1]]]){
|
||||
const mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),corr=new Float32Array(ex.memory.buffer,ex.corr_ptr(),65536);mags.set(inputs);assert(ex.smooth_batch(inputs.length)===inputs.length,`color ${variant}: output size`);const copy=Float32Array.from(corr.subarray(0,inputs.length));if(!baseline)baseline=copy;
|
||||
for(let i=0;i<inputs.length;i++){const expected=1-Math.log2(.5*Math.log2(inputs[i]));assert(close(copy[i],expected,3e-5),`color ${variant}: correction ${i}`);assert(close(copy[i],baseline[i],1e-7),`color SIMD/scalar mismatch ${i}`)}
|
||||
}
|
||||
}
|
||||
|
||||
function reference(cr,ci,limit){const rr=new Float64Array(limit+1),ri=new Float64Array(limit+1);let zr=0,zi=0;for(let n=0;n<=limit;n++){rr[n]=zr;ri[n]=zi;const nr=zr*zr-zi*zi+cr;zi=2*zr*zi+ci;zr=nr}return{rr,ri}}
|
||||
|
||||
async function deepGolden(){
|
||||
const deep=await Promise.all(['deep-simd.wasm','deep-scalar.wasm'].map(load));
|
||||
const blas=await Promise.all(['bla-simd.wasm','bla-scalar.wasm'].map(load));
|
||||
const width=13,height=9,iter=600,re=-.75,im=.1,span=1e-7,scale=span/width,ref=reference(re,im,iter);
|
||||
let baseline;
|
||||
for(const [variant,ex] of [['simd',deep[0]],['scalar',deep[1]]]){
|
||||
new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ref.rr);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ref.ri);
|
||||
const npx=ex.render_perturb_rebase_rect(span,0,scale*.5,-scale*.5,0,iter,width,height,width*.5,height*.5,0,0,width,height,iter,0,0,0,0,0,0,0);
|
||||
assert(npx===width*height,`deep ${variant}: output size`);const counts=Uint32Array.from(new Uint32Array(ex.memory.buffer,ex.counts_ptr(),npx));if(!baseline)baseline=counts;
|
||||
for(let y=0;y<height;y++)for(let x=0;x<width;x++){const i=y*width+x,cr=re+(x+.5-width*.5)*scale,ci=im+(height*.5-y-.5)*scale,[n]=direct(cr,ci,iter);assert(counts[i]===n,`deep ${variant}: count mismatch at ${x},${y}`);assert(counts[i]===baseline[i],`deep SIMD/scalar mismatch ${i}`)}
|
||||
}
|
||||
for(const [variant,ex] of [['simd',blas[0]],['scalar',blas[1]]]){
|
||||
new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ref.rr);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ref.ri);assert(ex.build_bla(iter,Math.hypot(span*.5,span*height/(2*width)),2**-32)>0,`BLA ${variant}: build`);
|
||||
const npx=ex.render_bla_rect_v2(span,scale*.5,-scale*.5,re,im,iter,width,height,0,0,width,height,iter,0,0,1);assert(npx===width*height,`BLA ${variant}: output size`);const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),npx);for(let i=0;i<npx;i++)assert(counts[i]===baseline[i]||counts[i]===0xfffffffe,`BLA ${variant}: mismatch ${i}`)
|
||||
}
|
||||
}
|
||||
|
||||
await shallowGolden();await colorGolden();await deepGolden();
|
||||
console.log(JSON.stringify({status:'pass',generatedDirectory:root,suites:['shallow','color','deep','bla']}));
|
||||
7
tests/kernel-golden.ps1
Normal file
7
tests/kernel-golden.ps1
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
param([Parameter(Mandatory=$true)][string]$GeneratedDirectory)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$node = Get-Command node -ErrorAction SilentlyContinue
|
||||
if (-not $node) { throw 'Node.js is required to execute generated WASM golden vectors.' }
|
||||
$directory = (Resolve-Path -LiteralPath $GeneratedDirectory).Path
|
||||
& $node.Source (Join-Path $PSScriptRoot 'kernel-golden.mjs') $directory
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Kernel golden vectors failed.' }
|
||||
16
tests/kernel-source-contract.ps1
Normal file
16
tests/kernel-source-contract.ps1
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
$ErrorActionPreference = 'Stop'
|
||||
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||
$abi = Get-Content -LiteralPath (Join-Path $workspace 'src\abi.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$lock = Get-Content -LiteralPath (Join-Path $workspace 'toolchain.lock.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($abi.format -ne 'mandelbrot-kernel-abi-v23') { throw 'Unexpected kernel ABI version.' }
|
||||
if ($lock.version -ne '17.0.6') { throw 'The pinned compiler version changed.' }
|
||||
foreach ($module in $abi.modules.psobject.Properties) {
|
||||
$source = Join-Path $workspace "src\$($module.Value.source)"
|
||||
if (-not (Test-Path -LiteralPath $source)) { throw "Missing source: $source" }
|
||||
$text = Get-Content -LiteralPath $source -Raw -Encoding UTF8
|
||||
foreach ($export in $module.Value.exports) {
|
||||
if ($export -eq 'memory') { continue }
|
||||
if ($text -notmatch [regex]::Escape("export_name(`"$export`"") ) { throw "Missing $($module.Name) export $export" }
|
||||
}
|
||||
}
|
||||
[ordered]@{ status='pass'; modules=@($abi.modules.psobject.Properties).Count; compiler=$lock.version; pixelContract=$abi.pixelContract } | ConvertTo-Json
|
||||
22
tests/module-clone.mjs
Normal file
22
tests/module-clone.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import fs from 'node:fs/promises';
|
||||
|
||||
const root = new URL('../dist/wasm/', import.meta.url);
|
||||
const assets = [
|
||||
['deep-simd.wasm', ['render_perturb_rebase_rect']],
|
||||
['bla-simd.wasm', ['build_bla', 'render_bla_rect_v2']],
|
||||
['color-simd.wasm', ['smooth_batch']]
|
||||
];
|
||||
|
||||
const results = [];
|
||||
for (const [name, requiredExports] of assets) {
|
||||
const bytes = await fs.readFile(new URL(name, root));
|
||||
const compiled = await WebAssembly.compile(bytes);
|
||||
const cloned = structuredClone(compiled);
|
||||
const instance = await WebAssembly.instantiate(cloned, {});
|
||||
for (const symbol of requiredExports) {
|
||||
if (typeof instance.exports[symbol] !== 'function') throw new Error(`${name}: missing ${symbol}`);
|
||||
}
|
||||
results.push({ name, bytes: bytes.length, exports: requiredExports });
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({ status: 'pass', structuredClone: true, results }));
|
||||
62
tests/pixel-contract.mjs
Normal file
62
tests/pixel-contract.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
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)' }));
|
||||
12
tests/pixel-mapping.mjs
Normal file
12
tests/pixel-mapping.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const assert=(ok,message)=>{if(!ok)throw new Error(message)};
|
||||
const close=(a,b)=>Math.abs(a-b)<=1e-15*Math.max(1,Math.abs(a),Math.abs(b));
|
||||
const view={re:-.743643887037151,im:.13182590420533,span:3.4e-12,w:37,h:23};
|
||||
const world=(x,y,w=view.w,h=view.h,span=view.span)=>[view.re+(x+.5-w*.5)*span/w,view.im+(h*.5-y-.5)*span/w];
|
||||
const shallow=(x,y)=>{const scale=view.span/view.w,shiftedRe=view.re+scale*.5,shiftedIm=view.im-scale*.5;return[shiftedRe+(x-view.w*.5)*scale,shiftedIm+(view.h*.5-y)*scale]};
|
||||
const deep=(x,y)=>{const scale=view.span/view.w,offR=scale*.5,offI=-scale*.5;return[view.re+offR+scale*(x-view.w*.5),view.im+offI+scale*(view.h*.5-y)]};
|
||||
const bla=(x,y)=>{const scale=view.span/view.w,offR=scale*.5,offI=-scale*.5;return[view.re+offR+view.span*(x/view.w-.5),view.im+offI+view.span*((.5*view.h-y)/view.w)]};
|
||||
let samples=0;for(let y=0;y<view.h;y++)for(let x=0;x<view.w;x++){const expected=world(x,y);for(const [name,actual]of[['shallow',shallow(x,y)],['deep',deep(x,y)],['bla',bla(x,y)]]){assert(close(expected[0],actual[0])&&close(expected[1],actual[1]),`${name} mapping mismatch ${x},${y}`)}samples++}
|
||||
for(const sampleScale of[2,4]){const tile={x:7,y:5,w:11,h:9},W=view.w*sampleScale,H=view.h*sampleScale;for(let phase=0;phase<sampleScale*sampleScale;phase++){const left=(phase%sampleScale)*tile.w,top=((phase/sampleScale)|0)*tile.h;for(let yy=0;yy<tile.h;yy++)for(let xx=0;xx<tile.w;xx++){const gx=tile.x*sampleScale+left+xx,gy=tile.y*sampleScale+top+yy,expected=world(gx,gy,W,H,view.span);const direct=[view.re+(gx+.5-W*.5)*view.span/W,view.im+(H*.5-gy-.5)*view.span/W];assert(close(expected[0],direct[0])&&close(expected[1],direct[1]),`tile seam ${sampleScale}x phase ${phase}`)}}}
|
||||
const first=world(0,0),last=world(view.w-1,view.h-1);assert(close((first[0]+last[0])/2,view.re),'view center moved on x');assert(close((first[1]+last[1])/2,view.im),'view center moved on y');
|
||||
const result={status:'pass',samples,backends:['shallow','deep','bla'],subsampleScales:[2,4],contract:'(x+0.5,y+0.5)'};
|
||||
console.log(JSON.stringify(result));export default result;
|
||||
45
tests/precision-reference.mjs
Normal file
45
tests/precision-reference.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
function fromDecimal(text, bits) {
|
||||
let s = String(text).trim(), neg = s.startsWith('-');
|
||||
if (neg) s = s.slice(1);
|
||||
const [mantissa, exponentText] = s.toLowerCase().split('e');
|
||||
const exponent = exponentText ? Number.parseInt(exponentText, 10) : 0;
|
||||
const [whole = '0', fraction = ''] = mantissa.split('.');
|
||||
let digits = `${whole}${fraction}`.replace(/^0+(?=\d)/, '') || '0';
|
||||
let places = fraction.length - exponent;
|
||||
if (places < 0) { digits += '0'.repeat(-places); places = 0; }
|
||||
const value = BigInt(digits) * (1n << BigInt(bits)) / (10n ** BigInt(places));
|
||||
return neg ? -value : value;
|
||||
}
|
||||
|
||||
function roundShift(value, bits) {
|
||||
const negative = value < 0n, absolute = negative ? -value : value;
|
||||
const rounded = (absolute + (1n << (BigInt(bits) - 1n))) >> BigInt(bits);
|
||||
return negative ? -rounded : rounded;
|
||||
}
|
||||
|
||||
function iterate(reText, imText, bits, limit) {
|
||||
const cr = fromDecimal(reText, bits), ci = fromDecimal(imText, bits), four = 4n << BigInt(bits);
|
||||
let zr = 0n, zi = 0n;
|
||||
for (let n = 0; n < limit; n++) {
|
||||
const zr2 = roundShift(zr * zr, bits), zi2 = roundShift(zi * zi, bits);
|
||||
if (zr2 + zi2 > four) return n;
|
||||
zi = roundShift(2n * zr * zi, bits) + ci;
|
||||
zr = zr2 - zi2 + cr;
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
const cases = [
|
||||
{ id: 'outside', re: '1', im: '0', limit: 100 },
|
||||
{ id: 'boundary-escape', re: '-0.75', im: '0.1', limit: 5000 },
|
||||
{ id: 'period3-center', re: '-0.122561166876', im: '0.744861766619', limit: 4000 }
|
||||
];
|
||||
|
||||
const results = cases.map(test => {
|
||||
const p = iterate(test.re, test.im, 256, test.limit);
|
||||
const guarded = iterate(test.re, test.im, 320, test.limit);
|
||||
return { id: test.id, p, guarded, stable: p === guarded };
|
||||
});
|
||||
if (results.some(result => !result.stable)) throw new Error(`Precision checkpoint mismatch: ${JSON.stringify(results)}`);
|
||||
if (roundShift(-123456789n, 8) !== -roundShift(123456789n, 8)) throw new Error('Round-to-nearest lost sign symmetry.');
|
||||
console.log(JSON.stringify({ status: 'pass', precisions: [256, 320], results }));
|
||||
41
tests/runtime-budget.mjs
Normal file
41
tests/runtime-budget.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
|
||||
const wasmDir=path.join(root,'build','wasm-v23');
|
||||
const load=async name=>(await WebAssembly.instantiate(await fs.readFile(path.join(wasmDir,name)),{})).instance.exports;
|
||||
const median=values=>values.slice().sort((a,b)=>a-b)[values.length>>1];
|
||||
const assert=(ok,message)=>{if(!ok)throw new Error(message)};
|
||||
|
||||
async function benchmarkShallow(){
|
||||
const ex=await load('wasm-simd.wasm'),cases=[
|
||||
{id:'overview-350',re:-.5,im:0,span:3.4,w:640,h:400,iter:350},
|
||||
{id:'boundary-900',re:-.743643887037151,im:.13182590420533,span:4e-4,w:512,h:320,iter:900}
|
||||
],results=[];
|
||||
for(const c of cases){const scale=c.span/c.w,rows=Math.max(1,Math.floor(65536/c.w)),run=()=>{let sum=0;for(let y=0;y<c.h;y+=rows){const n=ex.render_rows(c.re+scale*.5,c.im-scale*.5,c.span,c.w,c.h,y,Math.min(rows,c.h-y),c.iter);assert(n===c.w*Math.min(rows,c.h-y),`shallow output size: ${c.id}`);const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),n);for(let i=0;i<n;i++)sum+=counts[i]}return sum};run();const samples=[];let sum=0;for(let i=0;i<5;i++){const t=performance.now();sum=run();samples.push(performance.now()-t)}const ms=median(samples),pixels=c.w*c.h;results.push({id:c.id,width:c.w,height:c.h,iterations:c.iter,pixels,medianMs:ms,msPerMegapixel:ms*1e6/pixels,meanIterations:sum/pixels,samplesMs:samples})}
|
||||
return results
|
||||
}
|
||||
|
||||
function reference(cr,ci,limit){const rr=new Float64Array(limit+1),ri=new Float64Array(limit+1);let zr=0,zi=0;for(let n=0;n<=limit;n++){rr[n]=zr;ri[n]=zi;const nr=zr*zr-zi*zi+cr;zi=2*zr*zi+ci;zr=nr}return{rr,ri}}
|
||||
async function benchmarkDeep(){
|
||||
const ex=await load('bla-simd.wasm'),c={id:'seahorse-bla-2000',re:-.743643887037151,im:.13182590420533,span:3.4e-14,w:256,h:144,iter:2000},ref=reference(c.re,c.im,c.iter),scale=c.span/c.w;
|
||||
new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ref.rr);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ref.ri);
|
||||
const tBuild=performance.now(),entries=ex.build_bla(c.iter,Math.hypot(c.span*.5,c.span*c.h/(2*c.w)),2**-32),buildMs=performance.now()-tBuild;assert(entries>0,'BLA table build failed');
|
||||
const run=()=>ex.render_bla_rect_v2(c.span,scale*.5,-scale*.5,c.re,c.im,c.iter,c.w,c.h,0,0,c.w,c.h,c.iter,0,0,1);run();const samples=[];for(let i=0;i<5;i++){const t=performance.now();assert(run()===c.w*c.h,'BLA output size');samples.push(performance.now()-t)}const ms=median(samples),counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),c.w*c.h),unresolved=counts.reduce((n,v)=>n+(v>=0xfffffffe),0);return{id:c.id,width:c.w,height:c.h,iterations:c.iter,pixels:c.w*c.h,blaEntries:entries,buildMs,medianMs:ms,msPerMegapixel:ms*1e6/(c.w*c.h),unresolved,samplesMs:samples}
|
||||
}
|
||||
|
||||
const source=await fs.readFile(path.join(root,'script.js'),'utf8'),contracts={
|
||||
previewBudget110:/budgetMs:110/.test(source),
|
||||
modeTargets:/power:'PREVIEW',standard:'COVERED',fine:'REFINED',validate:'VALIDATED'/.test(source),
|
||||
screenBudgets:/processMode==='power'\)return 1\*1048576/.test(source)&&/lowMemory\|\|small\?2:4/.test(source)&&/lowMemory\|\|small\?4:8/.test(source),
|
||||
boundedContinuation:/deep\?384:4096/.test(source),
|
||||
coldDeepCap:/deep&&!profile\.covered&&measured<=0\)\{nominal=Math\.min\(nominal,48\)/.test(source),
|
||||
measuredDeepBudget:/measuredMPP=renderPerf\.deepMPP\|\|\.03/.test(source)&&/Math\.round\(1400\/measuredMPP\)/.test(source),
|
||||
viewportIndependentFloor:/minDpr=Math\.min\(1,64\/Math\.max\(cssW,cssH\)\)/.test(source)
|
||||
};
|
||||
assert(Object.values(contracts).every(Boolean),`runtime budget contract failed: ${JSON.stringify(contracts)}`);
|
||||
const report={format:'mandelbrot-node-runtime-budget-v23',generatedUtc:new Date().toISOString(),node:process.version,contracts,shallow:await benchmarkShallow(),deep:await benchmarkDeep()};
|
||||
const outputArg=process.argv.indexOf('--out');if(outputArg>=0){const target=path.resolve(process.argv[outputArg+1]);await fs.writeFile(target,JSON.stringify(report,null,2)+'\n','utf8')}
|
||||
console.log(JSON.stringify(report));
|
||||
18
tests/scenes.json
Normal file
18
tests/scenes.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"format": "mandelbrot-scene-corpus-v1",
|
||||
"rendererVersion": 23,
|
||||
"pixelContract": "centered",
|
||||
"scenes": [
|
||||
{"id":"z0","re":"-0.5","im":"0","span":"3.4","tags":["shallow","overview"]},
|
||||
{"id":"seahorse-z14","re":"-0.743643887037151","im":"0.13182590420533","span":"3.4e-14","tags":["deep","boundary"]},
|
||||
{"id":"seahorse-z20","re":"-0.743643887037151","im":"0.13182590420533","span":"3.4e-20","tags":["deep","boundary","warm-reference"]},
|
||||
{"id":"seahorse-z100","re":"-0.743643887037151","im":"0.13182590420533","span":"3.4e-100","tags":["deep","precision"]},
|
||||
{"id":"period3-interior","re":"-0.122561166876","im":"0.744861766619","span":"1e-8","tags":["interior","periodic"]},
|
||||
{"id":"deep-cliff-e280","re":"-0.743643887037151","im":"0.13182590420533","span":"1e-280","tags":["deep","scaled-bla-boundary"]}
|
||||
],
|
||||
"viewports": [
|
||||
{"id":"mobile","cssWidth":390,"cssHeight":844,"dpr":3},
|
||||
{"id":"desktop","cssWidth":1440,"cssHeight":900,"dpr":2},
|
||||
{"id":"4k","cssWidth":3840,"cssHeight":2160,"dpr":1}
|
||||
]
|
||||
}
|
||||
94
tests/source-contract.ps1
Normal file
94
tests/source-contract.ps1
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
$ErrorActionPreference = 'Stop'
|
||||
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||
$script = Get-Content -LiteralPath (Join-Path $workspace 'script.js') -Raw -Encoding UTF8
|
||||
$html = Get-Content -LiteralPath (Join-Path $workspace 'index.html') -Raw -Encoding UTF8
|
||||
$hosted = Get-Content -LiteralPath (Join-Path $workspace 'hosted-loader.js') -Raw -Encoding UTF8
|
||||
$kernels = Get-Content -LiteralPath (Join-Path $workspace 'kernels.js') -Raw -Encoding UTF8
|
||||
|
||||
function Assert-Contains([string]$Text, [string]$Pattern, [string]$Message) {
|
||||
if ($Text -notmatch $Pattern) { throw $Message }
|
||||
}
|
||||
function Assert-NotContains([string]$Text, [string]$Pattern, [string]$Message) {
|
||||
if ($Text -match $Pattern) { throw $Message }
|
||||
}
|
||||
|
||||
Assert-NotContains $script 'paintFrame\(\);updateStats\(\);requestAnimationFrame\(loop\)' 'Perpetual RAF loop returned.'
|
||||
Assert-NotContains $script 'setTimeout\(\(\)=>ensureDeepPool\(\),180\)' 'Deep pool is eager again.'
|
||||
Assert-Contains $script '2\*x\+1-w' 'Centered BigInt pixel mapping is missing.'
|
||||
Assert-Contains $script 'cre\+scale\*\.5,cim-scale\*\.5' 'Centered shallow WASM mapping is missing.'
|
||||
Assert-Contains $script "\?'COVERED':'PREVIEW'" 'Covered completion state is missing.'
|
||||
Assert-Contains $script 'RENDER_PASS=Object\.freeze' 'Render pass enum is missing.'
|
||||
Assert-Contains $script 'if\(profile\.covered\)return null' 'Covered is allowed to reuse reprojected pixels.'
|
||||
Assert-Contains $script 'const RENDER_PROFILE=Object\.freeze' 'Discrete render profiles are missing.'
|
||||
Assert-Contains $script 'MODE_TARGET=Object\.freeze\(\{power:''PREVIEW'',standard:''COVERED'',fine:''REFINED'',validate:''VALIDATED''\}\)' 'Processing modes are not bound to explicit automatic completion targets.'
|
||||
Assert-Contains $script 'budgetMs:110' 'Preview time budget regressed above the 80-120 ms target.'
|
||||
Assert-Contains $script 'deep&&!profile\.covered&&measured<=0\)\{nominal=Math\.min\(nominal,48\)' 'Cold deep Preview has no conservative 48px first-frame cap.'
|
||||
Assert-Contains $script 'deep&&!profile\.covered\?32:96' 'Cold deep Preview still inherits the 96px minimum height.'
|
||||
Assert-Contains $script 'function adaptStandardDeepBudget' 'Standard deep rendering is not adapted from measured milliseconds per pixel.'
|
||||
Assert-Contains $script 'deep=deepEngineNeeded\(snap,Math\.max\(1,canvas\.width\)\)' 'Frame completion still infers the deep engine from a display label or Preview width.'
|
||||
Assert-Contains $script 'measuredMPP=renderPerf\.deepMPP\|\|\.03' 'Unmeasured/reprojected deep views can bypass the conservative runtime budget.'
|
||||
Assert-Contains $script 'Math\.round\(1400/measuredMPP\)' 'Standard deep Covered budget is not tied to its 1.4 second target.'
|
||||
Assert-Contains $script 'minDpr=Math\.min\(1,64/Math\.max\(cssW,cssH\)\)' 'Effective DPR floor still prevents 4K deep scenes from meeting the runtime budget.'
|
||||
Assert-Contains $script 'state\.processMode===''power''\|\|state\.dirty' 'Power mode still advances automatically to a full Covered render.'
|
||||
Assert-Contains $script 'state\.processMode!==''fine''' 'Automatic unknown-pixel continuation is not limited to Fine mode.'
|
||||
Assert-Contains $script 'deep\?384:4096' 'Unknown-pixel continuation has no bounded deep/shallow sample cap.'
|
||||
Assert-NotContains $script 'lastQuality' 'Legacy continuous render quality state returned.'
|
||||
Assert-Contains $script 'FIELD_INTERIOR_LIKELY' 'Packed field classes are missing.'
|
||||
Assert-Contains $script 'unresolved&&d\.covered' 'Preview BLA work caps are still repaired eagerly.'
|
||||
Assert-NotContains $script 'likely=n===0xfffffffe' 'BLA work-cap status is still classified as interior likely.'
|
||||
Assert-Contains $script 'iterations:new Uint32Array' 'Packed field escape iteration channel is missing.'
|
||||
Assert-Contains $script 'iterationBuffer' 'Worker iteration buffer recycling is missing.'
|
||||
Assert-Contains $script 'function fixedAnalyticInterior' 'Exact fixed-point analytic interior proof is missing.'
|
||||
Assert-Contains $script 'fixedAnalyticPixelProven\(snap,fv\.w,fv\.h,x,y\)' 'Validation does not use the exact analytic proof.'
|
||||
Assert-Contains $script 'classes\[i\]=likely\(cr,ci\)\?3:4' 'f64 worker interior must remain likely, not proven.'
|
||||
Assert-Contains $script 'resolveSubsampleField' 'Linear-light detail resolve is missing.'
|
||||
Assert-Contains $script 'sampleScale=tile\.score>=1\.15\?4:2' 'Adaptive 2x/4x AA is missing.'
|
||||
Assert-NotContains $script 'n<36' 'Fixed 36-tile refinement cap returned.'
|
||||
Assert-Contains $script 'detailCacheBudget\(\)' 'Byte-budget detail cache is missing.'
|
||||
Assert-Contains $script 'function memoryLedger' 'Logical memory ledger is missing.'
|
||||
Assert-Contains $script 'function deepWisdomStorageKey' 'Versioned per-device wisdom persistence is missing.'
|
||||
Assert-Contains $script 'promoteState\(required\+32-available\);invalidateReferenceOrbit\(\)' 'Orbit-condition precision promotion does not rebuild the reference.'
|
||||
Assert-Contains $script 'function verifyReferenceCheckpoints' 'P/P+64 reference-orbit checkpoint verification is missing.'
|
||||
Assert-Contains $script 'state\.processMode!==''validate''' 'Reference-orbit checkpoint verification is not gated to precision-first rendering.'
|
||||
Assert-Contains $script 'promoteState\(32\);invalidateReferenceOrbit\(\)' 'Reference checkpoint mismatch does not rebuild the full reference at higher precision.'
|
||||
Assert-Contains $script 'deepTelemetry\.badRatio>1e-4' 'Deep-engine selection ignores measured orbit/glitch risk.'
|
||||
Assert-Contains $script 'w\.postMessage\(\{type:''init'',modules:deepModuleBundle\}\)' 'Compiled deep modules are not structured-cloned to workers.'
|
||||
Assert-Contains $script 'function prepareDeepModules' 'Shared deep-module compile gate is missing.'
|
||||
Assert-NotContains $script 'const SIMD=\$\{JSON\.stringify\(DEEP_SIMD_B64\)\}' 'Deep payloads are duplicated into the Worker source.'
|
||||
Assert-Contains $script 'highPrecisionDirectPixelAsync' 'Yielding high-precision direct verifier is missing.'
|
||||
Assert-Contains $script "state\.processMode==='validate'\|\|task\.tile\.score>1\.35" 'Validated detail subsamples are not guarded-direct.'
|
||||
Assert-Contains $script 'kernelSha256:globalThis\.MANDEL_KERNEL_META' 'Export kernel identity metadata is missing.'
|
||||
Assert-NotContains $script 'exactDeepPixel' 'Misleading exactDeepPixel alias returned.'
|
||||
Assert-Contains $script 'deepEngineNeeded' 'ULP-based engine selection is missing.'
|
||||
Assert-NotContains $script 'scheduleRealWisdom' 'Default active Real Wisdom benchmark returned.'
|
||||
Assert-Contains $script 'fieldBuffer' 'Worker buffer recycling is missing.'
|
||||
Assert-Contains $hosted 'compileStreaming' 'Hosted shallow WASM is not streamed.'
|
||||
Assert-Contains $hosted 'MANDEL_KERNEL_META' 'Hosted kernel identity injection point is missing.'
|
||||
Assert-Contains $html 'aria-live="polite"' 'Live status is missing.'
|
||||
Assert-Contains $html 'for="iters"' 'Form labels are not associated.'
|
||||
Assert-Contains $html 'id="processMode"' 'Processing mode UI is missing.'
|
||||
Assert-NotContains $html 'id="hq" type="checkbox" checked' 'Boundary AA is still enabled by default in Standard mode.'
|
||||
Assert-NotContains $html 'user-scalable=no' 'Page zoom was disabled again.'
|
||||
Assert-Contains $html 'button\{[^}]*min-height:44px' 'Primary controls are smaller than the 44px target.'
|
||||
|
||||
$manifestPath = Join-Path $workspace 'dist\wasm\manifest.json'
|
||||
if (-not (Test-Path -LiteralPath $manifestPath)) { throw 'WASM checksum manifest is missing.' }
|
||||
$manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
if ($manifest.payloads.Count -ne 8) { throw "Expected 8 WASM payloads, found $($manifest.payloads.Count)." }
|
||||
foreach ($payload in $manifest.payloads) {
|
||||
$path = Join-Path (Split-Path $manifestPath) $payload.file
|
||||
$actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
if ($actual -ne $payload.sha256) { throw "Checksum mismatch: $($payload.file)" }
|
||||
$metaPattern = "'$([regex]::Escape($payload.symbol))':'$([regex]::Escape($payload.sha256))'"
|
||||
Assert-Contains $kernels $metaPattern "Kernel metadata mismatch: $($payload.symbol)"
|
||||
}
|
||||
$kernelContract = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'tests\kernel-source-contract.ps1') | ConvertFrom-Json
|
||||
if ($kernelContract.status -ne 'pass') { throw 'Kernel source contract failed.' }
|
||||
|
||||
[ordered]@{
|
||||
status = 'pass'
|
||||
rendererVersion = 23
|
||||
wasmPayloads = $manifest.payloads.Count
|
||||
scriptBytes = (Get-Item -LiteralPath (Join-Path $workspace 'script.js')).Length
|
||||
htmlBytes = (Get-Item -LiteralPath (Join-Path $workspace 'index.html')).Length
|
||||
} | ConvertTo-Json
|
||||
Loading…
Add table
Add a link
Reference in a new issue