slim
This commit is contained in:
parent
8a2b1f6d4b
commit
0e1c1f04fd
158 changed files with 2578 additions and 15117 deletions
|
|
@ -1,29 +0,0 @@
|
|||
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' }));
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Mandelbrot v23 browser acceptance runner</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 acceptance runner</h1>
|
||||
<p>
|
||||
HTTP(S)で実行します。1回のrunは1 profile・1 buildだけを検査します。
|
||||
例: <code>?profile=desktop&target=hosted</code>。
|
||||
iframe寸法ではDPRを模擬できないため、要求DPRを持つbrowser contextごとに実行してください。
|
||||
</p>
|
||||
<button id="run">Run selected profile</button>
|
||||
<pre id="output">Ready.</pre>
|
||||
<iframe id="app" width="1440" height="900"></iframe>
|
||||
<script type="module">
|
||||
const out=document.querySelector('#output');
|
||||
const frame=document.querySelector('#app');
|
||||
const query=new URLSearchParams(location.search);
|
||||
const selectedProfile=query.get('profile')||'desktop';
|
||||
const target=query.get('target')||'hosted';
|
||||
const previewTrials=Math.max(30,Number(query.get('trials'))||30);
|
||||
if(!['hosted','standalone'].includes(target))throw new Error('target must be hosted or standalone');
|
||||
const appPath=target==='hosted'?'../dist/hosted/index.html':'../index.html';
|
||||
const wait=ms=>new Promise(resolve=>setTimeout(resolve,ms));
|
||||
const nextFrame=win=>new Promise(resolve=>win.requestAnimationFrame(resolve));
|
||||
function fixed(decimal,bits){
|
||||
let s=String(decimal).toLowerCase(),neg=s.startsWith('-');
|
||||
if(neg)s=s.slice(1);
|
||||
let pair=s.split('e'),mantissa=pair[0],exponent=Number(pair[1]||0),parts=mantissa.split('.');
|
||||
let digits=((parts[0]||'0')+(parts[1]||'')).replace(/^0+(?=\d)/,'')||'0';
|
||||
let places=(parts[1]||'').length-exponent;
|
||||
if(places<0){digits+='0'.repeat(-places);places=0}
|
||||
let value=BigInt(digits)*(1n<<BigInt(bits))/(10n**BigInt(places));
|
||||
return neg?-value:value
|
||||
}
|
||||
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);
|
||||
const 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.ceil(values.length*.95)-1)]||0;
|
||||
async function sha256(bytes){
|
||||
const digest=await crypto.subtle.digest('SHA-256',bytes);
|
||||
return[...new Uint8Array(digest)].map(x=>x.toString(16).padStart(2,'0')).join('')
|
||||
}
|
||||
async function canvasAudit(win,scene,profile){
|
||||
const canvas=win.document.querySelector('#view');
|
||||
const context=canvas.getContext('2d',{willReadFrequently:true});
|
||||
const pixels=context.getImageData(0,0,canvas.width,canvas.height).data;
|
||||
const hashValue=await sha256(pixels.buffer);
|
||||
let min=255,max=0;
|
||||
for(let i=0;i<pixels.length;i+=4){min=Math.min(min,pixels[i],pixels[i+1],pixels[i+2]);max=Math.max(max,pixels[i],pixels[i+1],pixels[i+2])}
|
||||
const expected=scene.visualGoldens?.[target]?.[profile.id]||null;
|
||||
return{hash:hashValue,expected,nonBlank:max>min,pass:!!expected&&hashValue===expected&&max>min}
|
||||
}
|
||||
async function accessibilityAudit(win){
|
||||
const doc=win.document,failures=[],interactive=[...doc.querySelectorAll('button,select,input,summary')];
|
||||
for(const el of interactive){
|
||||
const targetEl=el.type==='checkbox'?el.closest('label')||el:el,r=targetEl.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)
|
||||
}
|
||||
const canvas=doc.querySelector('#view');
|
||||
if(!canvas?.matches('[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');
|
||||
canvas.focus();
|
||||
const before=win.location.hash;
|
||||
canvas.dispatchEvent(new win.KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true,cancelable:true}));
|
||||
const keyboard=await poll(()=>win.location.hash!==before,2000,'keyboard navigation').then(()=>true).catch(()=>false);
|
||||
if(!keyboard)failures.push('keyboard-navigation');
|
||||
if(doc.activeElement!==canvas)failures.push('canvas-focus');
|
||||
return{
|
||||
pass:failures.length===0,failures,interactiveCount:interactive.length,keyboard,
|
||||
pendingExternal:['visible-focus visual review','browser page-zoom and canvas-pinch coexistence','screen-reader announcement order']
|
||||
}
|
||||
}
|
||||
async function interactionAudit(win){
|
||||
const canvas=win.document.querySelector('#view'),before=win.__MANDEL_DIAG__.snapshot(),paintDurations=[];
|
||||
for(let i=0;i<16;i++){
|
||||
const writes=win.__MANDEL_DIAG__.snapshot().runtimeMetrics.canvasWrites,t=performance.now();
|
||||
canvas.dispatchEvent(new win.WheelEvent('wheel',{
|
||||
deltaY:i%2?6:-4,clientX:canvas.clientWidth/2,clientY:canvas.clientHeight/2,cancelable:true
|
||||
}));
|
||||
await poll(()=>win.__MANDEL_DIAG__.snapshot().runtimeMetrics.canvasWrites>writes,1000,'wheel transform paint');
|
||||
paintDurations.push(performance.now()-t);
|
||||
await nextFrame(win)
|
||||
}
|
||||
const during=win.__MANDEL_DIAG__.snapshot();
|
||||
const preview=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>=1&&d.drawState==='COVERED'?d:null
|
||||
},180000,'wheel settle covered');
|
||||
const after=win.__MANDEL_DIAG__.snapshot();
|
||||
return{
|
||||
paintP95Ms:p95(paintDurations),
|
||||
previewAfterSettleMs:Math.max(0,preview.ms-110),
|
||||
renderStartsDuringGesture:during.runtimeMetrics.renderStartsDuringGesture-before.runtimeMetrics.renderStartsDuringGesture,
|
||||
renderStartsBeforeSettle:during.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts,
|
||||
longTasks:after.runtimeMetrics.longTasks-before.runtimeMetrics.longTasks
|
||||
}
|
||||
}
|
||||
async function modeAudit(win){
|
||||
const select=win.document.querySelector('#processMode'),hq=win.document.querySelector('#hq'),previewAfterSettle=[];
|
||||
select.value='power';
|
||||
select.dispatchEvent(new win.Event('change',{bubbles:true}));
|
||||
for(let i=0;i<previewTrials;i++){
|
||||
const canvas=win.document.querySelector('#view'),started=performance.now();
|
||||
canvas.dispatchEvent(new win.WheelEvent('wheel',{
|
||||
deltaY:1,clientX:canvas.clientWidth/2,clientY:canvas.clientHeight/2,cancelable:true
|
||||
}));
|
||||
await poll(()=>{
|
||||
const d=win.__MANDEL_DIAG__.snapshot();
|
||||
return d.automaticTarget==='PREVIEW'&&!d.rendering&&!d.scheduler.wheelActive&&d.lastPass==='preview'?d:null
|
||||
},120000,'power preview trial');
|
||||
previewAfterSettle.push(Math.max(0,performance.now()-started-110))
|
||||
}
|
||||
const powerBefore=win.__MANDEL_DIAG__.snapshot();
|
||||
await wait(900);
|
||||
const power=win.__MANDEL_DIAG__.snapshot();
|
||||
const 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>=1&&d.drawState==='COVERED'?d:null
|
||||
},180000,'standard covered');
|
||||
await wait(900);
|
||||
const standard=win.__MANDEL_DIAG__.snapshot();
|
||||
const standardPass=standard.automaticTarget==='COVERED'&&standard.lastPass==='covered'&&!standard.detailActive&&!standard.scheduler.unknownTimer&&!hq.checked&&standard.screen.pixelBudget<=4194304;
|
||||
select.value='fine';
|
||||
select.dispatchEvent(new win.Event('change',{bubbles:true}));
|
||||
const fineResult=await poll(()=>{
|
||||
const d=win.__MANDEL_DIAG__.snapshot();
|
||||
return d.automaticTarget==='REFINED'&&!d.rendering&&!d.detailActive&&d.drawState==='REFINED'?d:null
|
||||
},240000,'fine refined');
|
||||
const fine=fineResult.value;
|
||||
const finePass=fine.coverage>=1&&fine.drawState==='REFINED';
|
||||
return{
|
||||
pass:powerPass&&standardPass&&finePass&&previewAfterSettle.length>=30&&p95(previewAfterSettle)<120,
|
||||
previewTrials:previewAfterSettle.length,previewP95AfterSettleMs:p95(previewAfterSettle),
|
||||
power:{pass:powerPass,target:power.automaticTarget,pixelBudget:power.screen.pixelBudget},
|
||||
standard:{pass:standardPass,target:standard.automaticTarget,pixelBudget:standard.screen.pixelBudget},
|
||||
fine:{pass:finePass,target:fine.automaticTarget,refinedMs:fineResult.ms}
|
||||
}
|
||||
}
|
||||
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 nextFrame(win);await nextFrame(win);
|
||||
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,progress:Number(doc.querySelector('#exportProgress').value),active:win.__MANDEL_DIAG__.snapshot().exporting});
|
||||
return url
|
||||
};
|
||||
win.HTMLAnchorElement.prototype.click=function(){};
|
||||
try{
|
||||
async function balancedExport(){
|
||||
const start=captured.length;
|
||||
doc.querySelector('#exportScale').value='0';
|
||||
doc.querySelector('#exportWidth').value='64';
|
||||
doc.querySelector('#exportAA').value='2';
|
||||
doc.querySelector('#exportPrecision').value='balanced';
|
||||
doc.querySelector('#exportStart').click();
|
||||
await poll(()=>!win.__MANDEL_DIAG__.snapshot().exporting&&captured.length>=start+2,120000,'small export');
|
||||
return captured.slice(start)
|
||||
}
|
||||
const first=await balancedExport(),second=await balancedExport();
|
||||
const png=first.find(x=>x.blob.type==='image/png'),json=first.find(x=>x.blob.type==='application/json');
|
||||
const png2=second.find(x=>x.blob.type==='image/png');
|
||||
if(!png||!json||!png2)throw new Error('export files missing');
|
||||
const bytes=new Uint8Array(await png.blob.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];
|
||||
const meta=JSON.parse(await json.blob.text()),expectedHeight=Math.round(64*doc.querySelector('#view').height/doc.querySelector('#view').width);
|
||||
const deterministic=await sha256(await png.blob.arrayBuffer())===await sha256(await png2.blob.arrayBuffer());
|
||||
const completed=meta.determinism?.allTilesCompleted===true&&meta.sampleCount===width*height*4&&!!meta.kernelSha256&&typeof meta.unresolvedSamples==='number';
|
||||
const noEarlyDownload=[...first,...second].every(x=>x.progress>=1);
|
||||
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');
|
||||
const cancelledWithoutDownload=captured.length===prior;
|
||||
return{pass:width===64&&height===expectedHeight&&completed&&noEarlyDownload&&deterministic&&cancelledWithoutDownload,width,height,completed,noEarlyDownload,deterministic,cancelledWithoutDownload}
|
||||
}finally{
|
||||
win.URL.createObjectURL=nativeCreate;
|
||||
win.HTMLAnchorElement.prototype.click=nativeClick
|
||||
}
|
||||
}
|
||||
async function observedMemory(win){
|
||||
if(typeof win.performance.measureUserAgentSpecificMemory!=='function')return{status:'unsupported'};
|
||||
try{
|
||||
const result=await win.performance.measureUserAgentSpecificMemory();
|
||||
return{status:'measured',bytes:result.bytes}
|
||||
}catch(error){return{status:'error',error:String(error?.message||error)}}
|
||||
}
|
||||
async function runScene(scene,profile){
|
||||
frame.width=profile.cssWidth;frame.height=profile.cssHeight;
|
||||
const loaded=new Promise((resolve,reject)=>{frame.onload=resolve;frame.onerror=reject});
|
||||
const started=performance.now();
|
||||
frame.src=appPath+hash(scene);
|
||||
await loaded;
|
||||
const win=frame.contentWindow,label=profile.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 covered=await poll(()=>{
|
||||
const d=win.__MANDEL_DIAG__?.snapshot();
|
||||
return d&&d.drawState==='COVERED'&&d.coverage>=1&&!d.rendering?d:null
|
||||
},180000,'covered '+label);
|
||||
const initial=covered.value,visual=await canvasAudit(win,scene,profile);
|
||||
const exercise=scene.id==='z0';
|
||||
const interaction=exercise?await interactionAudit(win):null;
|
||||
const modes=exercise?await modeAudit(win):null;
|
||||
const recolor=exercise?await recolorAudit(win):null;
|
||||
const exportResult=exercise?await exportAudit(win):null;
|
||||
const accessibility=exercise?await accessibilityAudit(win):null;
|
||||
const resources=win.performance.getEntriesByType('resource').map(e=>String(e.name));
|
||||
const 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();
|
||||
const 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:profile.id,target,requestedViewport:profile,
|
||||
actualViewport:{cssWidth:win.innerWidth,cssHeight:win.innerHeight},actualDevicePixelRatio:win.devicePixelRatio,
|
||||
totalMs:performance.now()-started,previewMs:preview.ms,coveredMs:covered.ms,
|
||||
preview:preview.value,initial,final:after,idle,visual,assetRequests,interaction,modes,recolor,
|
||||
export:exportResult,accessibility,memoryObserved:await observedMemory(win)
|
||||
}
|
||||
}
|
||||
function evaluate(results,profile){
|
||||
const exercise=results.find(x=>x.interaction);
|
||||
const 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.initial.coverage>=1&&x.initial.hqTarget===x.initial.frame),
|
||||
managedMemory:results.every(x=>x.final.memory.managedBytes<=x.final.memory.budget),
|
||||
visual:results.every(x=>x.visual.pass),
|
||||
startup:results.filter(x=>x.id==='z0').every(x=>target==='hosted'?x.assetRequests.deep.length===0:x.initial.deepAssets.workers===0),
|
||||
interaction:!!exercise&&exercise.interaction.paintP95Ms<16&&exercise.interaction.renderStartsDuringGesture===0&&exercise.interaction.renderStartsBeforeSettle===0&&exercise.interaction.longTasks===0,
|
||||
preview:exercise?.modes?.previewTrials>=30&&exercise?.modes?.previewP95AfterSettleMs<120,
|
||||
modes:exercise?.modes?.pass===true,
|
||||
recolor:exercise?.recolor?.pass===true,
|
||||
export:exercise?.export?.pass===true,
|
||||
accessibilityAutomated:exercise?.accessibility?.pass===true,
|
||||
profileFidelity:results.every(x=>x.actualViewport.cssWidth===profile.cssWidth&&x.actualViewport.cssHeight===profile.cssHeight&&Math.abs(x.actualDevicePixelRatio-profile.dpr)<.01)
|
||||
};
|
||||
const failures=Object.entries(checks).filter(([,pass])=>!pass).map(([name])=>name);
|
||||
const pendingExternal=[
|
||||
'observed browser/GPU peak memory review',
|
||||
'visible-focus and rendered-output visual review',
|
||||
'page zoom and canvas pinch coexistence',
|
||||
'screen-reader live announcement order'
|
||||
];
|
||||
return{pass:false,automatedPass:failures.length===0,checks,failures,pendingExternal}
|
||||
}
|
||||
document.querySelector('#run').onclick=async()=>{
|
||||
document.querySelector('#run').disabled=true;
|
||||
try{
|
||||
const corpus=await(await fetch('./scenes.json',{cache:'no-store'})).json();
|
||||
const profile=corpus.viewports.find(x=>x.id===selectedProfile);
|
||||
if(!profile)throw new Error('Unknown profile: '+selectedProfile);
|
||||
const results=[];
|
||||
for(const scene of corpus.scenes){
|
||||
out.textContent='Running '+profile.id+'/'+scene.id+' on '+target+'…\n'+JSON.stringify(results,null,2);
|
||||
results.push(await runScene(scene,profile))
|
||||
}
|
||||
const report={
|
||||
format:'mandelbrot-browser-baseline-v23',generatedUtc:new Date().toISOString(),target,
|
||||
userAgent:navigator.userAgent,hostDevicePixelRatio:devicePixelRatio,profile,results,
|
||||
acceptance:evaluate(results,profile)
|
||||
};
|
||||
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>
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
$ErrorActionPreference = 'Stop'
|
||||
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||
|
||||
function Assert-Contract([bool]$Condition, [string]$Message) {
|
||||
if (-not $Condition) { throw $Message }
|
||||
}
|
||||
|
||||
function Decimal-Fraction([string]$Text) {
|
||||
if ($Text -notmatch '^([+-]?)(\d+)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$') {
|
||||
throw "Invalid decimal: $Text"
|
||||
}
|
||||
$negative = $Matches[1] -eq '-'
|
||||
$fraction = if ($null -eq $Matches[3]) { '' } else { $Matches[3] }
|
||||
$digits = ($Matches[2] + $fraction).TrimStart('0')
|
||||
if (-not $digits) { $digits = '0' }
|
||||
$numerator = [System.Numerics.BigInteger]::Parse($digits)
|
||||
if ($negative) { $numerator = -$numerator }
|
||||
$exponent = if ($Matches[4]) { [int]$Matches[4] } else { 0 }
|
||||
$places = $fraction.Length - $exponent
|
||||
if ($places -ge 0) {
|
||||
$denominator = [System.Numerics.BigInteger]::Pow([System.Numerics.BigInteger]10, $places)
|
||||
} else {
|
||||
$numerator *= [System.Numerics.BigInteger]::Pow([System.Numerics.BigInteger]10, -$places)
|
||||
$denominator = [System.Numerics.BigInteger]1
|
||||
}
|
||||
[pscustomobject]@{ numerator=$numerator; denominator=$denominator }
|
||||
}
|
||||
|
||||
function Assert-ExactCoordinate($Scene, [string]$Axis) {
|
||||
$decimal = Decimal-Fraction ([string]$Scene.$Axis)
|
||||
$prefix = if ($Axis -eq 're') { 're' } else { 'im' }
|
||||
$numerator = [System.Numerics.BigInteger]::Parse([string]$Scene.coordinate.($prefix + 'Numerator'))
|
||||
$denominator = [System.Numerics.BigInteger]::Parse([string]$Scene.coordinate.($prefix + 'Denominator'))
|
||||
Assert-Contract ($denominator -gt 0) "$($Scene.id): $Axis denominator must be positive."
|
||||
Assert-Contract ($decimal.numerator * $denominator -eq $numerator * $decimal.denominator) "$($Scene.id): $Axis decimal does not match its exact rational."
|
||||
}
|
||||
|
||||
$proposal = Get-Content -LiteralPath (Join-Path $workspace 'IMPROVEMENT_PROPOSAL.md') -Raw -Encoding UTF8
|
||||
Assert-Contract ($proposal -match 'Document status: historical-v22 / implemented-v23') 'Proposal status/provenance is missing.'
|
||||
Assert-Contract ($proposal -match 'sample coverage.+UNRESOLVED.+resolved coverage') 'Coverage semantics are not separated.'
|
||||
Assert-Contract ($proposal -match 'Balanced Export.+Validated Export') 'Export precision tiers are not separated.'
|
||||
Assert-Contract ($proposal -match 'exact half.+away-from-zero') 'Fixed-point tie rounding is not specified.'
|
||||
Assert-Contract ($proposal -match 'Phase 0A' -and $proposal -match 'Phase 0B') 'Browserless and browser gates are not separated.'
|
||||
Assert-Contract ($proposal -match 'threshold.+pass') 'Undefined acceptance thresholds must fail.'
|
||||
|
||||
$scenePath = Join-Path $workspace 'tests\scenes.json'
|
||||
$scenes = Get-Content -LiteralPath $scenePath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$policy = Get-Content -LiteralPath (Join-Path $workspace 'tests\numeric-policy-v23.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
Assert-Contract ($scenes.format -eq 'mandelbrot-scene-corpus-v2') 'Unexpected scene corpus version.'
|
||||
Assert-Contract ($scenes.pixelContract -eq 'centered-rational-ties-away-from-zero') 'Unexpected scene pixel contract.'
|
||||
Assert-Contract ($scenes.numericPolicy -eq $policy.id) 'Scene corpus and numeric policy IDs differ.'
|
||||
Assert-Contract ($policy.validated.falseEscapedMax -eq 0 -and $policy.validated.falseInteriorProvenMax -eq 0 -and $policy.validated.unresolvedMax -eq 0) 'Validated zero-error thresholds changed.'
|
||||
|
||||
$ids = @{}
|
||||
$maxViewportPixels = ($scenes.viewports | ForEach-Object { [double]$_.cssWidth * [double]$_.dpr } | Measure-Object -Maximum).Maximum
|
||||
$viewportDigits = [Math]::Ceiling([Math]::Log10($maxViewportPixels))
|
||||
foreach ($scene in $scenes.scenes) {
|
||||
Assert-Contract (-not $ids.ContainsKey($scene.id)) "Duplicate scene ID: $($scene.id)"
|
||||
$ids[$scene.id] = $true
|
||||
Assert-Contract ($null -ne $scene.coordinate -and $null -ne $scene.oracle) "$($scene.id): coordinate/oracle metadata missing."
|
||||
if ($scene.coordinate.kind -eq 'exact-rational') {
|
||||
Assert-ExactCoordinate $scene 're'
|
||||
Assert-ExactCoordinate $scene 'im'
|
||||
} elseif ($scene.coordinate.kind -eq 'decimal') {
|
||||
$reDigits = ([regex]::Replace([string]$scene.re, '[^0-9]', '')).TrimStart('0').Length
|
||||
$imDigits = ([regex]::Replace([string]$scene.im, '[^0-9]', '')).TrimStart('0').Length
|
||||
$actualDigits = [Math]::Min($reDigits, $imDigits)
|
||||
Assert-Contract ($actualDigits -eq [int]$scene.coordinate.significantDigits) "$($scene.id): significantDigits metadata is stale."
|
||||
$span = [Math]::Abs([double]::Parse([string]$scene.span, [Globalization.CultureInfo]::InvariantCulture))
|
||||
$depthDigits = [Math]::Ceiling(-[Math]::Log10($span))
|
||||
$guard = $actualDigits - $depthDigits - $viewportDigits
|
||||
Assert-Contract ($guard -eq [int]$scene.coordinate.guardDigitsAtViewport -and $guard -ge 4) "$($scene.id): coordinate guard digits are insufficient or stale."
|
||||
} else {
|
||||
throw "$($scene.id): unsupported coordinate kind $($scene.coordinate.kind)"
|
||||
}
|
||||
}
|
||||
foreach ($required in @('z0','period2-cusp-z14','swirly-seahorses-z12','period2-cusp-z20','period2-cusp-z100','period3-interior','period2-cusp-e280')) {
|
||||
Assert-Contract ($ids.ContainsKey($required)) "Required scene missing: $required"
|
||||
}
|
||||
$runtimeTest = Get-Content -LiteralPath (Join-Path $workspace 'tests\runtime-budget.mjs') -Raw -Encoding UTF8
|
||||
Assert-Contract ($runtimeTest -match "sceneById\('swirly-seahorses-z12'\)" -and $runtimeTest -match "acceptance:false") 'Runtime characterization must use the published mixed-boundary scene and remain non-acceptance evidence.'
|
||||
|
||||
$runner = Get-Content -LiteralPath (Join-Path $workspace 'tests\browser-benchmark.html') -Raw -Encoding UTF8
|
||||
Assert-Contract ($runner -match "target==='hosted'.+dist/hosted/index\.html") 'Browser runner does not target the Hosted build.'
|
||||
Assert-Contract ($runner -match "selectedProfile=query\.get\('profile'\)" -and $runner -notmatch 'for\s*\(const profile of corpus\.viewports\)') 'Browser runner must execute one real-DPR profile per run.'
|
||||
Assert-Contract ($runner -notmatch 'deltaY\s*:\s*0') 'Wheel audit still uses a zero-delta event.'
|
||||
Assert-Contract ($runner -match 'previewTrials>=30' -and $runner -match "drawState==='REFINED'") 'Preview p95 or actual Refined checks are missing.'
|
||||
Assert-Contract ($runner -match 'visualGoldens' -and $runner -match 'hashValue===expected') 'Visual acceptance lacks a golden comparison.'
|
||||
Assert-Contract ($runner -match 'interaction\.longTasks===0') 'Long tasks are not part of browser acceptance.'
|
||||
Assert-Contract ($runner -match 'completed&&noEarlyDownload&&deterministic&&cancelledWithoutDownload') 'Export evidence is not part of the pass condition.'
|
||||
Assert-Contract ($runner -match 'pendingExternal' -and $runner -match 'return\{pass:false,automatedPass') 'Browser-only/manual residual gates can be falsely marked complete.'
|
||||
|
||||
$baseline = Get-Content -LiteralPath (Join-Path $workspace 'audit\v23-source-baseline.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
Assert-Contract ($baseline.hosted.deepRequestMeasurementStatus -eq 'not-run') 'Static deep-request contract is mislabeled as a measurement.'
|
||||
$records = @($baseline.hosted.firstViewFiles) + @($baseline.standalone.files) + @($baseline.sourceBuild.files)
|
||||
foreach ($record in $records) {
|
||||
$path = Join-Path $workspace ([string]$record.file).Replace('/', [System.IO.Path]::DirectorySeparatorChar)
|
||||
Assert-Contract (Test-Path -LiteralPath $path) "Baseline file missing: $($record.file)"
|
||||
$item = Get-Item -LiteralPath $path
|
||||
$hash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
Assert-Contract ($item.Length -eq [long]$record.bytes -and $hash -eq [string]$record.sha256) "Baseline bytes/hash mismatch: $($record.file)"
|
||||
}
|
||||
|
||||
$implementation = Get-Content -LiteralPath (Join-Path $workspace 'IMPLEMENTATION_REPORT.md') -Raw -Encoding UTF8
|
||||
Assert-Contract ($implementation -match 'Hosted first view ([\d,]+) bytes') 'Hosted implementation size statement missing.'
|
||||
$hostedDoc = [long](($Matches[1] -replace ',', ''))
|
||||
Assert-Contract ($implementation -match 'Standalone ([\d,]+) bytes') 'Standalone implementation size statement missing.'
|
||||
$standaloneDoc = [long](($Matches[1] -replace ',', ''))
|
||||
Assert-Contract ($hostedDoc -eq [long]$baseline.hosted.firstViewUncompressedBytes -and $standaloneDoc -eq [long]$baseline.standalone.uncompressedBytes) 'Implementation size statement is stale.'
|
||||
|
||||
$completion = Get-Content -LiteralPath (Join-Path $workspace 'COMPLETION_AUDIT.md') -Raw -Encoding UTF8
|
||||
Assert-Contract ($completion -notmatch '0\.91|36\.6') 'Completion audit cites performance values absent from the saved v23 JSON.'
|
||||
$buildDoc = Get-Content -LiteralPath (Join-Path $workspace 'BUILD_REPRODUCIBILITY.md') -Raw -Encoding UTF8
|
||||
Assert-Contract ($buildDoc -match 'Provenance status: tool archive hash not retained') 'Toolchain provenance boundary is overstated.'
|
||||
|
||||
$browserless = Get-Content -LiteralPath (Join-Path $workspace 'audit\v23-browserless-baseline.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
Assert-Contract ($browserless.status -eq 'pass' -and $browserless.scope -eq 'browserless-current-artifacts' -and -not $browserless.fullAcceptance) 'Browserless audit scope/status is misleading.'
|
||||
Assert-Contract ($browserless.node.version -eq 'v22.18.0' -and $browserless.node.archive.sha256 -eq 'c95d8a7e1c99e669cc08c9f1176e068c1f50847c37908fcb8c35b62482366511') 'Pinned Node provenance is missing.'
|
||||
foreach ($test in $browserless.executableTests.psobject.Properties) {
|
||||
Assert-Contract ($test.Value.status -eq 'pass' -or $test.Name -eq 'runtime-budget.mjs') "Browserless executable gate failed: $($test.Name)"
|
||||
}
|
||||
Assert-Contract ($browserless.runtime.status -eq 'measured-not-acceptance' -and $browserless.v22Comparison.status -eq 'not-verifiable' -and $browserless.browserAcceptance.status -eq 'not-run') 'Residual gates are overstated.'
|
||||
|
||||
[ordered]@{
|
||||
status='pass'
|
||||
proposalContract='revised-v23'
|
||||
scenes=@($scenes.scenes).Count
|
||||
viewports=@($scenes.viewports).Count
|
||||
numericPolicy=$policy.id
|
||||
hashedFiles=$records.Count
|
||||
browserRunner='static-contract-pass'
|
||||
browserlessEvidence='pass-with-explicit-limits'
|
||||
} | ConvertTo-Json
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
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');
|
||||
const appSource = await fs.readFile(new URL('script.js', root), 'utf8');
|
||||
let app = appSource;
|
||||
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: Buffer.byteLength(appSource),
|
||||
shallowWorkerCharacters: sources.shallow.length,
|
||||
deepWorkerCharacters: sources.deep.length
|
||||
}
|
||||
}));
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
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']}));
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
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.' }
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
$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
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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 }));
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-numeric-policy-v1",
|
||||
"id": "numeric-policy-v23",
|
||||
"rendererVersion": 23,
|
||||
"pixelContract": {
|
||||
"mapping": "centered-rational",
|
||||
"fixedPointRounding": "nearest-ties-away-from-zero",
|
||||
"fixedPointIntegerMismatchMax": 0,
|
||||
"float64CoordinateUlpMax": 4,
|
||||
"tileSeamPixelMismatchMax": 0
|
||||
},
|
||||
"validated": {
|
||||
"falseEscapedMax": 0,
|
||||
"falseInteriorProvenMax": 0,
|
||||
"unresolvedMax": 0,
|
||||
"escapeIterationAbsMax": 0,
|
||||
"magnitudeRelativeErrorMax": 1e-12,
|
||||
"referenceGuardBits": 64
|
||||
},
|
||||
"balanced": {
|
||||
"falseEscapedMax": 0,
|
||||
"falseInteriorProvenMax": 0,
|
||||
"interiorLikelyMismatch": "record",
|
||||
"unresolved": "record"
|
||||
},
|
||||
"color": {
|
||||
"smoothCorrectionRelativeErrorMax": 0.00003,
|
||||
"simdScalarRelativeErrorMax": 1e-7
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
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)' }));
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
const assert=(ok,message)=>{if(!ok)throw new Error(message)};
|
||||
const bitsBuffer=new ArrayBuffer(8),bitsView=new DataView(bitsBuffer);
|
||||
function ulp(value){
|
||||
if(!Number.isFinite(value))return Infinity;
|
||||
if(value===0)return Number.MIN_VALUE;
|
||||
bitsView.setFloat64(0,value,false);
|
||||
let bits=bitsView.getBigUint64(0,false);
|
||||
bits+=value>0?1n:-1n;
|
||||
bitsView.setBigUint64(0,bits,false);
|
||||
return Math.abs(bitsView.getFloat64(0,false)-value)
|
||||
}
|
||||
const closeUlp=(a,b,limit=4)=>Math.abs(a-b)<=limit*Math.max(Number.MIN_VALUE,ulp(a),ulp(b));
|
||||
const roundDiv=(value,denominator)=>{
|
||||
const negative=value<0n,absolute=negative?-value:value;
|
||||
const quotient=(absolute+denominator/2n)/denominator;
|
||||
return negative?-quotient:quotient
|
||||
};
|
||||
assert(roundDiv(1n,2n)===1n&&roundDiv(-1n,2n)===-1n,'half ties must round away from zero');
|
||||
assert(roundDiv(3n,2n)===2n&&roundDiv(-3n,2n)===-2n,'roundDiv must be sign symmetric');
|
||||
|
||||
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(closeUlp(expected[0],actual[0])&&closeUlp(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(closeUlp(expected[0],direct[0])&&closeUlp(expected[1],direct[1]),'tile seam '+sampleScale+'x phase '+phase)
|
||||
}
|
||||
}
|
||||
}
|
||||
const first=world(0,0),last=world(view.w-1,view.h-1);
|
||||
assert(closeUlp((first[0]+last[0])/2,view.re),'view center moved on x');
|
||||
assert(closeUlp((first[1]+last[1])/2,view.im),'view center moved on y');
|
||||
|
||||
const fixedBits=320n,fixedOne=1n<<fixedBits;
|
||||
const fixedView={re:-3n*fixedOne/4n,im:0n,span:1n<<220n,w:37,h:23};
|
||||
const fixedWorld=(x,y,w=fixedView.w,h=fixedView.h)=>[
|
||||
fixedView.re+roundDiv(fixedView.span*BigInt(2*x+1-w),BigInt(2*w)),
|
||||
fixedView.im+roundDiv(fixedView.span*BigInt(h-2*y-1),BigInt(2*w))
|
||||
];
|
||||
for(let y=0;y<fixedView.h;y++)for(let x=0;x<fixedView.w;x++){
|
||||
const point=fixedWorld(x,y),mirror=fixedWorld(fixedView.w-1-x,fixedView.h-1-y);
|
||||
assert(point[0]+mirror[0]===2n*fixedView.re,'fixed x symmetry mismatch '+x+','+y);
|
||||
assert(point[1]+mirror[1]===2n*fixedView.im,'fixed y symmetry mismatch '+x+','+y)
|
||||
}
|
||||
|
||||
const result={
|
||||
status:'pass',samples,backends:['shallow','deep','bla'],subsampleScales:[2,4],
|
||||
contract:'centered-rational',float64UlpMax:4,fixedPointRounding:'nearest-ties-away-from-zero'
|
||||
};
|
||||
console.log(JSON.stringify(result));
|
||||
export default result;
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
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 }));
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
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 corpus=JSON.parse(await fs.readFile(path.join(root,'tests','scenes.json'),'utf8'));
|
||||
const sceneById=id=>{
|
||||
const scene=corpus.scenes.find(candidate=>candidate.id===id);
|
||||
if(!scene)throw new Error('Scene missing: '+id);
|
||||
return scene
|
||||
};
|
||||
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 scene=sceneById('swirly-seahorses-z12'),ex=await load('bla-simd.wasm'),c={id:'swirly-seahorses-z12-bla-2000',re:Number(scene.re),im:Number(scene.im),span:Number(scene.span),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,status:'measured',acceptance:false,sceneCorpus:corpus.format,measurementNote:'Runtime characterization only; browser acceptance has separate thresholds and evidence.',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));
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
$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 'DEEP_WISDOM_TTL_MS=30\*24\*60\*60\*1000' 'Persisted wisdom has no 30-day TTL.'
|
||||
Assert-Contains $script 'savedAt:Date\.now\(\)' 'Persisted wisdom has no save timestamp.'
|
||||
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-Contains $script 'exportForcePrecision&&unresolvedSamples' 'Validated Export can emit a final file with unresolved samples.'
|
||||
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
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const s=await fs.readFile(new URL('./webgpu-acceptance.js',import.meta.url),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/corrected production frame produced UNKNOWN/.test(s),'corrected screen UNKNOWN=0 gate missing');
|
||||
must(/forceDeep:true,correctUnknown:true/.test(s),'forced corrected deep dense probe missing');
|
||||
must(/dense DS correction left known regression pixel UNKNOWN/.test(s),'dense DS recovery gate missing');
|
||||
must(/falseEscaped/.test(s)&&/falseBounded/.test(s)&&/guardMismatch/.test(s),'membership/reference gates missing');
|
||||
must(/backend router ignored measured 15% speed margin/.test(s)&&/viewProbe/.test(s),'view-specific router acceptance gate missing');
|
||||
must(/renderMode:'fast'/.test(s)&&/fast mode did not select Direct/.test(s),'manual fast acceptance gate missing');
|
||||
must(/renderMode:'accurate'/.test(s)&&/accurate mode did not select Deep/.test(s),'manual accurate acceptance gate missing');
|
||||
must(/referenceReused/.test(s)&&/pan did not reuse Deep reference/.test(s),'Deep pan reference reuse acceptance gate missing');
|
||||
must(/corrected production frame produced UNKNOWN/.test(s),'corrected Deep UNKNOWN gate missing');
|
||||
must(/forceDeep:true,correctUnknown:true/.test(s),'forced corrected Deep probe missing');
|
||||
must(/exportSmoke\.unresolved!==0/.test(s)&&/exportSmokeAA\.unresolved!==0/.test(s),'corrected export unresolved gate missing');
|
||||
must(/uncapturedErrors/.test(s),'uncaptured WebGPU error gate missing');
|
||||
must(/runFixed96Experiment/.test(s)&&/fixed96 experiment smoke failed/.test(s)&&/queueIntegrity/.test(s)&&/dispatchRate/.test(s),'fixed96 sparse-queue real-GPU smoke gate missing');
|
||||
console.log(JSON.stringify({status:'pass',checks:8,correctedUnknownMax:0,routerSpeedMargin:.15,fixed96Smoke:true},null,2));
|
||||
console.log(JSON.stringify({status:'pass',checks:6,manualModes:true,panReferenceReuse:true},null,2));
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
const REQUIRED=8,DEFAULT=128,MARGIN=.85,UNKNOWN_MAX=.005;
|
||||
function choose(ratio,viewProbe=null){const eligible=Number.isFinite(ratio)&&ratio>=REQUIRED;if(!eligible)return'deep';if(ratio>=DEFAULT)return'direct';if(!viewProbe)return'probe';if(!viewProbe.qualityPass||viewProbe.classDisagreement!==0||viewProbe.deepUnknownRate>UNKNOWN_MAX)return'deep';if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*MARGIN)return'deep';return'direct'}
|
||||
const good=(d,u=0)=>({qualityPass:true,classDisagreement:0,deepUnknownRate:u,predictedDirectMs:100,predictedDeepMs:d});
|
||||
const cases=[
|
||||
[0.5,null,'deep'],[7.999,null,'deep'],[8,null,'probe'],[64,null,'probe'],[128,null,'direct'],
|
||||
[32,good(80),'deep'],[32,good(86),'direct'],[32,good(60,.01),'deep'],
|
||||
[32,{...good(60),qualityPass:false},'deep'],[32,{...good(60),classDisagreement:1},'deep']
|
||||
];
|
||||
for(const [ratio,p,want] of cases){const got=choose(ratio,p);if(got!==want)throw new Error(JSON.stringify({ratio,p,want,got}))}
|
||||
console.log(JSON.stringify({status:'pass',kind:'view-specific-adaptive-backend-router',directRequiredRatio:REQUIRED,directDefaultRatio:DEFAULT,deepSpeedMargin:MARGIN,deepProbeUnknownMax:UNKNOWN_MAX,cases:cases.length},null,2));
|
||||
46
tests/v24-baseline-idle-refinement-model.mjs
Normal file
46
tests/v24-baseline-idle-refinement-model.mjs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const root=new URL('../',import.meta.url);
|
||||
const script=await fs.readFile(new URL('script.js',root),'utf8');
|
||||
const kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
const sha=s=>crypto.createHash('sha256').update(s).digest('hex');
|
||||
const ctx={globalThis:{}};vm.runInNewContext(kernels,ctx);const G=ctx.globalThis.MANDEL_WEBGPU_KERNELS;
|
||||
const expected={
|
||||
DIRECT_F32_WGSL:'59301bf0c5ba3dda4a4055335b32d4a3f84a21991be30f8efc74deb5a5f11ccf',
|
||||
FAST_PERTURB_WGSL:'03b45713b1bbb7b56e18bd8fdf0f2d8161d7ed25adf02482b41c1038dc16e3b8',
|
||||
DEEP_PERTURB_WGSL:'2865a015d5c1a9b459085b7044e23563fcc5f0eec5362634249b6604a47e660a',
|
||||
COLOR_WGSL:'cccac7bfb69c64093c46c3468ff9d685d680ba3a88de0d1ea57168d91a067acf'
|
||||
};
|
||||
for(const [k,h] of Object.entries(expected))must(sha(G[k])===h,`${k} changed from v24.2.18 baseline`);
|
||||
|
||||
const fallback=script.match(/function renderFallback\(.*?\nasync function recolor/s)?.[0]?.replace(/\nasync function recolor$/,'');
|
||||
must(fallback,'fallback function not found');
|
||||
must(sha(fallback)==='689a6f56aedd416b5e512b17ab6a3ddebcf7c73494e0a8a600ce74a06d8073a2','fallback changed from v24.2.18 baseline');
|
||||
must(/function maxIter\(\)\{if\(!state\.adaptive\)return state\.baseIter;const z=zoomExp\(\),bonus=Math\.max\(0,Math\.floor\(70\*Math\.sqrt\(z\)\+15\*z\)\)/.test(script),'v24.2.18 maxIter policy changed');
|
||||
must(/scheduleIdleRefinement\(snap,iter,decision,token\);return;/.test(script),'fast idle refinement is not scheduled after primary completion');
|
||||
must(/presentFrame\(\{scaleX:1,scaleY:1,offsetX:0,offsetY:0\}\).*scheduleIdleRefinement\(snap,iter,decision,token\)/s.test(script),'accurate idle refinement is not scheduled after primary presentation');
|
||||
must(/ensureRefinePipelines\(\)/.test(script)&&/idleRefineTimer=setTimeout/.test(script),'refinement is not lazy/idle');
|
||||
const initBlock=script.slice(script.indexOf('async initPipelines(){'),script.indexOf('async ensureRefinePipelines(){'));
|
||||
must(!/LIKELY_QUEUE_WGSL|FAST_LIKELY_REFINE_WGSL|DEEP_LIKELY_REFINE_WGSL/.test(initBlock),'refinement shaders are compiled on primary startup path');
|
||||
must(/if\(\(\(fieldMeta\[out\]>>28u\)&3u\)!=FIELD_INTERIOR_LIKELY\)\{return;\}/.test(G.DEEP_LIKELY_REFINE_WGSL),'deep refinement can overwrite non-likely pixels');
|
||||
must(/setRefineDeepContext/.test(script)&&/this\.refineDeepCtx\.refsB/.test(script),'idle refinement replaces the primary Deep reference context');
|
||||
must(/async function paintIdleRefinement/.test(script)&&/async recolor\(token,iter=state\.fieldView\?\.iter\?\?maxIter\(\)\)/.test(script),'idle refinement does not preserve fieldView baseline color normalization');
|
||||
must(!/readMeta\(|readFrameMeta|copyBufferToBuffer\(f\.meta/.test(script.slice(script.indexOf('async function runIdleRefinement'),script.indexOf('function renderFallback'))),'idle refinement performs full meta readback');
|
||||
must(/REFINE_STATS_BYTES=16/.test(script),'queue readback is not constrained to 16-byte stats');
|
||||
must(/\.0006\*dt/.test(script)&&/\.012\*dt/.test(script),'automatic color speed is not 1/10 of v24.2.18');
|
||||
|
||||
const baseIter=z=>350+Math.floor(70*Math.sqrt(z)+15*z);
|
||||
const targetList=(z,base)=>{if(base>=8000)return[];const cap=Math.min(8000,Math.max(base*2,Math.round(1200+300*z))),raw=[Math.min(cap,base*2),Math.min(cap,base*4),cap],out=[];for(const v of raw){const n=Math.max(base+1,Math.floor(v));if(n>base&&(!out.length||n!==out.at(-1)))out.push(n)}return out};
|
||||
const batchPixels=(iter,budget)=>Math.max(64,Math.floor(Math.max(64,budget/Math.max(1,iter))/64)*64);
|
||||
const cssW=1920,cssH=1080,pixelBudget=1572864,dpr=Math.sqrt(pixelBudget/(cssW*cssH)),w=Math.round(cssW*dpr),h=Math.round(cssH*dpr),pixels=w*h;
|
||||
must(targetList(500,baseIter(500)).length===0,'idle refinement should stop once baseline iteration already exceeds 8000');
|
||||
const cases=[];
|
||||
for(const z of [6,8,12,20]){
|
||||
const base=baseIter(z),targets=targetList(z,base),row={zoomExp:z,baseIter:base,primaryWorstSampleIterations:pixels*base,targets,batches:{guardedIdle:[]}};
|
||||
for(const [name,budget] of [['guardedIdle',1800000]])for(const iter of targets){const px=batchPixels(iter,budget),work=px*iter;must(work<=budget,`${name} refinement batch exceeds budget at z${z}: ${work}>${budget}`);row.batches[name].push({iter,pixels:px,worstSampleIterations:work,budget})}
|
||||
cases.push(row);
|
||||
}
|
||||
console.log(JSON.stringify({status:'pass',check:'v24.2.18-baseline-plus-idle-refinement',baselineShaderHashes:expected,representativeDisplay:{css:[cssW,cssH],internal:[w,h],pixels},cases,qualityInvariant:'primary output is v24.2.18; idle refinement always uses guarded Deep, re-checks FIELD_INTERIOR_LIKELY, and never overwrites resolved pixels'},null,2));
|
||||
14
tests/v24-color-relative-model.mjs
Normal file
14
tests/v24-color-relative-model.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
const base=350,deltaCycle=0.001,relativeIteration=0.6;
|
||||
function effectiveCycle(cycle,iter){return cycle*base/Math.max(base,iter)}
|
||||
const changes=[350,3500,35000].map(iter=>relativeIteration*iter*(effectiveCycle(0.008+deltaCycle,iter)-effectiveCycle(0.008,iter)));
|
||||
const spread=Math.max(...changes)-Math.min(...changes);
|
||||
must(spread<1e-12,'zoom-relative cycle response is not invariant');
|
||||
const min=.001,max=.05,N=1000;
|
||||
const sliderToCycle=v=>min*Math.pow(max/min,v/N);
|
||||
const ratios=[100,500,900].map(v=>sliderToCycle(v+50)/sliderToCycle(v));
|
||||
must(Math.max(...ratios)-Math.min(...ratios)<1e-12,'log slider does not preserve equal multiplicative steps');
|
||||
must(/effectiveColorCycle\(iter/.test(script)&&/sliderToCycle/.test(script),'production relative color functions missing');
|
||||
console.log(JSON.stringify({status:'pass',check:'zoom-relative-color-cycle',phaseDelta:changes[0],sliderStepRatio:ratios[0]},null,2));
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/DIRECT_DS_WGSL/.test(kernels)&&/struct DS/.test(kernels)&&/centerReLo/.test(kernels),'DS-direct benchmark shader missing');
|
||||
must(/direct-ds-benchmark/.test(script)&&/this\.dsDirect=/.test(script),'DS-direct pipeline missing');
|
||||
must(/benchmarkDsDirect/.test(script)&&/renderTileMetaDsDirect/.test(script),'DS-direct benchmark methods missing');
|
||||
must(/CROSSOVER_DEPTHS=\[0,2,4,6,8,10,12,14,16,20,30,40\]/.test(script),'round-depth crossover suite missing');
|
||||
must(/runCrossoverBenchmark/.test(script)&&/referenceBuildMs/.test(script)&&/coldMedianMs/.test(script),'crossover benchmark timing/report missing');
|
||||
must(/CROSSOVER_SPEED_W=256/.test(script)&&/CROSSOVER_SPEED_H=144/.test(script)&&/crossoverSpeedTiles/.test(script),'bounded representative speed tiles missing');
|
||||
must(/fullMedianMs/.test(script)&&/msPerPixel/.test(script)&&/speedPixels/.test(script),'tile timing extrapolation missing');
|
||||
must(/CROSSOVER_GPU_TIMEOUT_MS=15000/.test(script)&&/benchmarkTimeout/.test(script)&&/timed out after/.test(script),'benchmark watchdog missing');
|
||||
must(/classMismatchRate/.test(script)&&/escapeIterationMismatchRate/.test(script),'GPU quality comparison missing');
|
||||
must(/id="backendBench"/.test(html)&&/id="benchmarkDialog"/.test(html)&&/id="benchmarkSave"/.test(html),'benchmark UI missing');
|
||||
const choose=(script.match(/function chooseBackend[\s\S]*?function deepNeeded/)||[''])[0];
|
||||
must(!/dsDirect|backend='ds'/.test(choose),'experimental DS-direct leaked into production router');
|
||||
console.log(JSON.stringify({status:'pass',check:'crossover-benchmark-contract',checks:11},null,2));
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const F=Math.fround;
|
||||
const U=2**-24;
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
|
||||
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
|
||||
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
|
||||
function bitsForDepth(z){return Math.max(256,Math.ceil(z*Math.log2(10))+300)}
|
||||
function snap(re,im,z){const span=`3.4e-${z}`,bits=bitsForDepth(z);return{bits,re:decFixed(re,bits),im:decFixed(im,bits),span:decFixed(span,bits),spanText:span}}
|
||||
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
|
||||
function log2FixedAt(v,b){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-b}
|
||||
function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:Math.pow(2,l-exp),exp}}
|
||||
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
|
||||
function f32Direct(cr,ci,limit){cr=F(cr);ci=F(ci);let zr=0,zi=0;for(let n=0;n<limit;n++){const a=F(zr*zr),b=F(zi*zi);zi=F(F(F(2*zr)*zi)+ci);zr=F(F(a-b)+cr);if(F(F(zr*zr)+F(zi*zi))>4)return n+1}return limit}
|
||||
// DS helpers
|
||||
function q(a,b){const x=F(a+b),e=F(b-F(x-a));return[x,e]}
|
||||
function sum(a,b){const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
|
||||
function prod(a,b){const x=F(a*b),ca=F(4097*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(4097*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
|
||||
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(a[1]+b[1])))}
|
||||
function neg(a){return[F(-a[0]),F(-a[1])]} function sub(a,b){return add(a,neg(b))}
|
||||
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(a[0]*b[1]));e=F(e+F(a[1]*b[0]));e=F(e+F(a[1]*b[1]));return q(t[0],e)}
|
||||
function scaleDS(a,b){const t=prod(a[0],b);return q(t[0],F(t[1]+F(a[1]*b)))}
|
||||
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
|
||||
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]} function cmulDS(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
|
||||
function mag2DS(a){return add(mul(a[0],a[0]),mul(a[1],a[1]))}
|
||||
function split(x){const h=F(x);return[h,F(x-h)]}
|
||||
function dsDirect(s,w,h,x,y,limit){const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),span=split(fixedNum(s.span,s.bits));const offx=(x+.5-.5*w)/w,offy=(.5*h-y-.5)/w;const cr=add(cre,scaleDS(span,F(offx))),ci=add(cim,scaleDS(span,F(offy)));let z=[[0,0],[0,0]];for(let n=0;n<limit;n++){z=cadd(cmulDS(z,z),[cr,ci]);if(cmp(mag2DS(z),[4,0])>0)return n+1}return limit}
|
||||
function adjacentUniq(s,w,h){const y=Math.floor(h/2),cx=Math.floor(w/2),f32s=new Set(),dss=new Set(),cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),sp=split(fixedNum(s.span,s.bits));for(let x=cx-10;x<=cx+10;x++){const p=pixel(s,w,h,x,y),rr=fixedNum(p[0],s.bits),ii=fixedNum(p[1],s.bits);f32s.add(F(rr)+','+F(ii));const ox=(x+.5-.5*w)/w,oy=(.5*h-y-.5)/w,cr=add(cre,scaleDS(sp,F(ox))),ci=add(cim,scaleDS(sp,F(oy)));dss.add(cr[0]+':'+cr[1]+','+ci[0]+':'+ci[1])}return{f32:f32s.size,ds:dss.size}}
|
||||
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<<BigInt(b);let zr=0n,zi=0n,refLen=limit;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;const mag=roundShift(zr*zr,b)+roundShift(zi*zi,b);if(mag>bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[2*refLen]=hr1;hi[2*refLen+1]=hi1;lo[2*refLen]=F(rr1-hr1);lo[2*refLen+1]=F(ri1-hi1);break}}return{hi,lo,refLen}}
|
||||
function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}
|
||||
function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))}
|
||||
function primary(ref,s,w,h,x,y,limit){const se0=spanME(s),se={mant:F(se0.mant),exp:se0.exp},dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w);let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);return ea<=1e-3?{kind:'bounded',n}:{kind:'unknown',n}}if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n};return{kind:'unknown',n}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
|
||||
function pow2DS(a,e){if(e<-126)return[0,0];if(e>126)return[F(8.507059e37),0];return[F(a[0]*2**e),F(a[1]*2**e)]}
|
||||
function cscale(a,b){return[scaleDS(a[0],b),scaleDS(a[1],b)]} function cpow2(a,e){return[pow2DS(a[0],e),pow2DS(a[1],e)]} function maxabsDS(a){return Math.max(Math.abs(F(a[0][0]+a[0][1])),Math.abs(F(a[1][0]+a[1][1])))}
|
||||
function correction(ref,s,w,h,x,y,limit){const se=spanME(s),mh=F(se.mant),sm=[mh,F(se.mant-mh)],iw=1/w,iwh=F(iw),inv=[iwh,F(iw-iwh)],ox=F(x+.5-.5*w),oy=F(.5*h-y-.5),dx=scaleDS(inv,ox),dy=scaleDS(inv,oy),d0=[mul(sm,dx),mul(sm,dy)];let d=d0,wv=[[0,0],[0,0]],e=se.exp,n=0,m=0,ops=0;while(true){if(n>=limit)return{kind:'bounded',n};if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,r=[[ref.hi[rp],ref.lo[rp]],[ref.hi[rp+1],ref.lo[rp+1]]],delta=cpow2(wv,e),z=cadd(r,delta),mg=mag2DS(z);if(cmp(mg,[4,0])>0)return{kind:'escaped',n};const dm=mag2DS(delta);if(m>0&&cmp(dm,[0,0])>0&&cmp(mg,dm)<0){if(se.exp-e<-96)return{kind:'unknown',n};wv=z;d=cpow2(d0,se.exp);e=0;m=0;continue}if(m>=ref.refLen)return{kind:'unknown',n};const linear=cscale(cmulDS(r,wv),2),sq=cpow2(cmulDS(wv,wv),e);wv=cadd(cadd(linear,sq),d);m++;n++;ops++;const mm=Math.max(maxabsDS(wv),maxabsDS(d));if(mm>=1e30||!Number.isFinite(mm))return{kind:'unknown',n};if(mm>65536){wv=cscale(wv,1/65536);d=cscale(d,1/65536);e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wv=cscale(wv,65536);d=cscale(d,65536);e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
|
||||
function adaptiveIter(z){return Math.max(350,350+Math.floor(70*Math.sqrt(z)+15*z))}
|
||||
const tracks=[
|
||||
{id:'cusp',re:'-0.75',im:'0'},
|
||||
{id:'seahorse',re:'-0.7453983606667815',im:'0.1125046349959942'}
|
||||
];
|
||||
const depths=[0,2,4,6,8,10,12,14,16,20,30,40];
|
||||
const SW=21,SH=15,FULL_W=1600,FULL_H=900;
|
||||
const rows=[];
|
||||
for(const tr of tracks){
|
||||
for(const z of depths){
|
||||
const s=snap(tr.re,tr.im,z);
|
||||
const limit=adaptiveIter(z);
|
||||
const ref=buildRef(s,limit);
|
||||
let guardMismatch=0,f32FE=0,f32FB=0,dsFE=0,dsFB=0,deepFE=0,deepFB=0,deepU=0,tested=0,f32IterMismatch=0,dsIterMismatch=0,deepIterMismatch=0;
|
||||
const f32Coords=new Set(), dsCoords=new Set();
|
||||
const t0=performance.now();
|
||||
for(let sy=0;sy<SH;sy++){
|
||||
const y=Math.round(sy*(FULL_H-1)/Math.max(1,SH-1));
|
||||
for(let sx=0;sx<SW;sx++){
|
||||
const x=Math.round(sx*(FULL_W-1)/Math.max(1,SW-1));
|
||||
const p=pixel(s,FULL_W,FULL_H,x,y);
|
||||
const g=pixel(s,FULL_W,FULL_H,x,y,s.bits+64);
|
||||
const o=oracle(p[0],p[1],s.bits,limit);
|
||||
const gg=oracle(g[0],g[1],s.bits+64,limit);
|
||||
if(o!==gg){guardMismatch++;continue;}
|
||||
tested++;
|
||||
const rr=fixedNum(p[0],s.bits),ii=fixedNum(p[1],s.bits);
|
||||
const fd=f32Direct(rr,ii,limit);
|
||||
const dd=dsDirect(s,FULL_W,FULL_H,x,y,limit);
|
||||
f32Coords.add(F(rr)+','+F(ii));
|
||||
{const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),sp=split(fixedNum(s.span,s.bits)),ox=(x+.5-.5*FULL_W)/FULL_W,oy=(.5*FULL_H-y-.5)/FULL_W,crd=add(cre,scaleDS(sp,F(ox))),cid=add(cim,scaleDS(sp,F(oy)));dsCoords.add(crd[0]+':'+crd[1]+','+cid[0]+':'+cid[1]);}
|
||||
const pr=primary(ref,s,FULL_W,FULL_H,x,y,limit);
|
||||
const dp=pr.kind==='unknown'?correction(ref,s,FULL_W,FULL_H,x,y,limit):pr;
|
||||
if(fd<limit&&o===limit)f32FE++;
|
||||
if(fd===limit&&o<limit)f32FB++;
|
||||
if(dd<limit&&o===limit)dsFE++;
|
||||
if(dd===limit&&o<limit)dsFB++;
|
||||
if(fd<limit&&o<limit&&fd!==o)f32IterMismatch++;
|
||||
if(dd<limit&&o<limit&&dd!==o)dsIterMismatch++;
|
||||
if(dp.kind==='unknown')deepU++;
|
||||
else {
|
||||
if(dp.kind==='escaped'&&o===limit)deepFE++;
|
||||
if(dp.kind==='bounded'&&o<limit)deepFB++;
|
||||
if(dp.kind==='escaped'&&o<limit&&dp.n!==o)deepIterMismatch++;
|
||||
}
|
||||
}
|
||||
}
|
||||
const adj=adjacentUniq(s,FULL_W,FULL_H);
|
||||
const row={
|
||||
track:tr.id,z,limit,tested,guardMismatch,adjacentUniqueX:adj,
|
||||
f32:{falseEscaped:f32FE,falseBounded:f32FB,escapeIterationMismatch:f32IterMismatch,uniqueCoords:f32Coords.size},
|
||||
ds:{falseEscaped:dsFE,falseBounded:dsFB,escapeIterationMismatch:dsIterMismatch,uniqueCoords:dsCoords.size},
|
||||
deep:{falseEscaped:deepFE,falseBounded:deepFB,unknown:deepU,escapeIterationMismatch:deepIterMismatch},
|
||||
modelMs:Math.round(performance.now()-t0)
|
||||
};
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
const seahorse=rows.filter(r=>r.track==='seahorse');
|
||||
const material=r=>((r.falseEscaped+r.falseBounded)/Math.max(1,r.tested))>0.01;
|
||||
const firstF32=seahorse.find(r=>material({...r.f32,tested:r.tested})||r.adjacentUniqueX.f32<11)?.z??null;
|
||||
const firstDs=seahorse.find(r=>material({...r.ds,tested:r.tested})||r.adjacentUniqueX.ds<11)?.z??null;
|
||||
const deepIssues=seahorse.filter(r=>r.deep.falseEscaped||r.deep.falseBounded||r.deep.unknown);
|
||||
const report={format:'mandelbrot-crossover-quality-v1',kind:'cpu-f32-model + BigInt P/P+64 oracle; not real GPU',viewport:[FULL_W,FULL_H],sampleGrid:[SW,SH],depths,tracks:tracks.map(t=>t.id),rows,summary:{firstMaterialF32DegradationDepth:firstF32,firstMaterialDsDegradationDepth:firstDs,deepIssueRows:deepIssues.map(r=>({z:r.z,...r.deep})),interpretation:'On the Seahorse track at a 1600x900 viewport, f32 direct becomes materially unsuitable around z6; DS direct remains a plausible bridge through roughly z8 and degrades by z10. Corrected deep is the best of the modeled backends, but one z8 false escape means it is not a membership certificate.'}};
|
||||
if(rows.some(r=>r.guardMismatch))throw new Error('P/P+64 oracle guard mismatch in crossover corpus');
|
||||
await fs.mkdir(new URL('../audit/',import.meta.url),{recursive:true});
|
||||
await fs.writeFile(new URL('../audit/v24-crossover-quality.json',import.meta.url),JSON.stringify(report,null,2));
|
||||
console.log(JSON.stringify({status:'pass',check:'crossover-quality-advisory',summary:report.summary},null,2));
|
||||
78
tests/v24-deep-sparse-queue-model.mjs
Normal file
78
tests/v24-deep-sparse-queue-model.mjs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import fs from 'node:fs/promises';
|
||||
|
||||
await import('../gpu-kernels.js');
|
||||
const G=globalThis.MANDEL_WEBGPU_KERNELS;
|
||||
const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
|
||||
must(!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('bucketState'),'production post-stats perturbation shader contains bucket state');
|
||||
must(!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('unknownQueue'),'production post-stats perturbation shader writes the UNKNOWN queue');
|
||||
must(!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('atomicAdd(&unresolved.remaining'),'production post-stats perturbation still performs per-UNKNOWN stats atomics');
|
||||
must(G.DEEP_BUCKET_HIST_STATS_WGSL.includes('localReasons'),'post-stats histogram lacks workgroup reason aggregation');
|
||||
must(G.DEEP_BUCKET_HIST_WGSL.includes('array<atomic<u32>,8>'),'bucket histogram lacks 8-bin workgroup aggregation');
|
||||
must(G.DEEP_BUCKET_HIST_WGSL.includes('(n*8u)/max(1u,p.maxIter)'),'bucket histogram does not use normalized UNKNOWN iteration');
|
||||
must(G.DEEP_BUCKET_PREFIX_WGSL.includes('atomicStore(&bucketState[8u+b],total)'),'bucket prefix does not initialize scatter cursors');
|
||||
must(G.DEEP_BUCKET_PREFIX_WGSL.includes('atomicStore(&bucketState[16u+b],total)'),'bucket prefix does not retain diagnostic offsets');
|
||||
must(G.DEEP_BUCKET_SCATTER_WGSL.includes('atomicAdd(&bucketState[8u+lane],c)'),'bucket scatter lacks workgroup range reservation');
|
||||
must(G.DEEP_BUCKET_SCATTER_WGSL.includes('atomicAdd(&localRanks[bucket],1u)'),'bucket scatter lacks workgroup-local rank allocation');
|
||||
must(G.DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)'),'queued correction is not a 1D 64-lane kernel');
|
||||
must(G.DEEP_CORRECT_QUEUE_WGSL.includes('let out=unknownQueue[qi]'),'queued correction does not consume queue indices');
|
||||
must(/this\.deepPostStats/.test(script)&&/this\.deepBucketHistStats/.test(script)&&/this\.deepBucketPrefix/.test(script)&&/this\.deepBucketScatter/.test(script)&&/this\.correctQueued/.test(script),'production post-stats/bucket pipelines missing');
|
||||
must(/encodeDeepPostStatsNumeric\(encoder,[\s\S]*encodeDeepBucketHistogramStats\(encoder,/.test(script),'production frame order is not post-stats primary -> histogram/stats');
|
||||
must(/correctUnknownFrame[\s\S]*encodeDeepBucketPrefix\(e,[\s\S]*encodeDeepBucketScatter\(e,[\s\S]*encodeQueuedDeepCorrection\(e,/.test(script),'production correction order is not prefix -> scatter -> indirect correction');
|
||||
must(!/correctUnknownFrame[\s\S]{0,1600}encodeDeepBucketHistogram\(e/.test(script),'production correction redundantly rescans the histogram');
|
||||
must((script.match(/dispatchWorkgroups\(Math\.ceil\(w\/64\),h\)/g)||[]).length>=2,'bucket histogram/scatter are not dispatched as 64-pixel row groups');
|
||||
must(/dispatchWorkgroupsIndirect\(indirect,0\)/.test(script),'production indirect correction dispatch missing');
|
||||
must(/encodeCorrectionNumeric/.test(script),'dense correction path for probes/exports was removed');
|
||||
|
||||
const FIELD_UNKNOWN=0,ITER_MASK=0x000fffff;
|
||||
const packUnknown=n=>(n&ITER_MASK)>>>0;
|
||||
const packKnown=n=>(((1<<28)|(n&ITER_MASK))>>>0);
|
||||
const bucketOf=(meta,maxIter)=>Math.min(7,Math.floor(((meta&ITER_MASK)*8)/Math.max(1,maxIter)));
|
||||
let seed=0x9e3779b9;
|
||||
const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/0x100000000};
|
||||
const shuffle=a=>{for(let i=a.length-1;i>0;i--){const j=Math.floor(rnd()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a};
|
||||
const cases=[];
|
||||
for(const [w,h,rate,maxIter] of [[1,1,1,350],[17,9,.01,413],[64,64,.05,521],[65,7,.2,613],[257,131,.2,721],[320,180,.8,721],[511,257,0,901]]){
|
||||
const n=w*h,meta=new Uint32Array(n);
|
||||
for(let i=0;i<n;i++){
|
||||
const it=Math.min(maxIter,Math.floor(rnd()*(maxIter+1)));
|
||||
meta[i]=rnd()<rate?packUnknown(it):packKnown(it);
|
||||
}
|
||||
const expected=Array.from({length:8},()=>[]);
|
||||
for(let i=0;i<n;i++)if((meta[i]>>>28)===FIELD_UNKNOWN)expected[bucketOf(meta[i],maxIter)].push(i);
|
||||
const counts=expected.map(x=>x.length),offsets=[];let total=0;
|
||||
for(const c of counts){offsets.push(total);total+=c}
|
||||
const cursors=[...offsets],queue=new Array(total),groups=[];
|
||||
for(let y=0;y<h;y++)for(let gx=0;gx<Math.ceil(w/64);gx++)groups.push([y,gx]);
|
||||
shuffle(groups);
|
||||
let globalReservations=0;
|
||||
for(const [y,gx] of groups){
|
||||
const local=Array.from({length:8},()=>[]);
|
||||
for(let lane=0;lane<64;lane++){
|
||||
const x=gx*64+lane;if(x>=w)continue;const out=y*w+x;
|
||||
if((meta[out]>>>28)===FIELD_UNKNOWN)local[bucketOf(meta[out],maxIter)].push(out);
|
||||
}
|
||||
for(let b=0;b<8;b++)if(local[b].length){
|
||||
globalReservations++;
|
||||
const base=cursors[b];cursors[b]+=local[b].length;
|
||||
const lanes=shuffle([...local[b]]); // workgroup atomic rank order is unspecified.
|
||||
for(let j=0;j<lanes.length;j++)queue[base+j]=lanes[j];
|
||||
}
|
||||
}
|
||||
must(total===expected.reduce((a,x)=>a+x.length,0),`total mismatch ${w}x${h}`);
|
||||
must(queue.length===total,`queue length mismatch ${w}x${h}`);
|
||||
must(new Set(queue).size===queue.length,`duplicate queue entry ${w}x${h}`);
|
||||
for(let b=0;b<8;b++){
|
||||
must(cursors[b]===offsets[b]+counts[b],`cursor mismatch bucket ${b} ${w}x${h}`);
|
||||
const got=queue.slice(offsets[b],offsets[b]+counts[b]).sort((a,c)=>a-c);
|
||||
const exp=[...expected[b]].sort((a,c)=>a-c);
|
||||
must(got.length===exp.length&&got.every((v,i)=>v===exp[i]),`bucket membership mismatch b${b} ${w}x${h}`);
|
||||
must(got.every(i=>bucketOf(meta[i],maxIter)===b),`bucket range contamination b${b} ${w}x${h}`);
|
||||
}
|
||||
const workgroups=Math.ceil(total/64),launched=workgroups*64,visited=[];
|
||||
for(let qi=0;qi<launched;qi++)if(qi<total)visited.push(queue[qi]);
|
||||
must(visited.length===total,`queued dispatch count mismatch ${w}x${h}`);
|
||||
cases.push({w,h,pixels:n,maxIter,unknown:total,rate:total/Math.max(1,n),counts,offsets,histogramWorkgroups:Math.ceil(w/64)*h,scatterWorkgroups:Math.ceil(w/64)*h,globalReservations,correctionWorkgroups:workgroups});
|
||||
}
|
||||
console.log(JSON.stringify({status:'pass',check:'deep-8-bucket-correction-queue-model',bucketCount:8,workgroupSize:64,cases},null,2));
|
||||
11
tests/v24-fast-precision-extension-model.mjs
Normal file
11
tests/v24-fast-precision-extension-model.mjs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
function f32Ulp(x){x=Math.abs(Math.fround(x));if(x===0)return 2**-149;if(x<2**-126)return 2**-149;return 2**(Math.floor(Math.log2(x))-23)}
|
||||
function ratio(center,span,w){return (span/w)/f32Ulp(Math.max(Math.abs(center),span*.75,2**-126))}
|
||||
must(ratio(-0.7436,3.4e-3,1600)>4,'shallow Direct should remain active');
|
||||
must(ratio(-0.7436,3.4e-5,1600)<4,'collapsed f32 coordinate regime should use fast extension');
|
||||
must(/fastReferenceBits\(snap\)/.test(script)&&/Math\.max\(96,Math\.min\(snap\.bits,need\)\)/.test(script),'adaptive reduced-precision reference policy missing');
|
||||
must(/FAST_PERTURB_WGSL/.test(kernels)&&/render_fast/.test(kernels),'fast perturbation shader missing');
|
||||
must(/fastExtended=!deep&&fastNeedsExtended/.test(script),'fast extension routing missing');
|
||||
console.log(JSON.stringify({status:'pass',check:'fast-precision-extension',shallowRatio:ratio(-0.7436,3.4e-3,1600),deepRatio:ratio(-0.7436,3.4e-5,1600)},null,2));
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/DIRECT_DS_GUARDED_WGSL/.test(kernels)&&/riskLimit:f32/.test(kernels)&&/riskQueue:array<u32>/.test(kernels)&&/atomicAdd\(&hybridStats\.enqueued,1u\)/.test(kernels),'guarded DS sensitivity queue shader missing');
|
||||
must(/DIRECT_FIXED96_QUEUE_ARGS_WGSL/.test(kernels)&&/dispatchCount/.test(kernels)&&/indirectArgs\.x=\(n\+63u\)\/64u/.test(kernels),'fixed96 queue indirect-args shader missing');
|
||||
must(/DIRECT_FIXED96_CORRECT_WGSL/.test(kernels)&&/struct FX96/.test(kernels)&&/fn mul32/.test(kernels)&&/Q8\.88/.test(kernels),'fixed96 correction shader missing');
|
||||
const a=kernels.indexOf('const DIRECT_FIXED96_CORRECT_WGSL'),b=kernels.indexOf('// Deep path',a),fx=kernels.slice(a,b);
|
||||
must(!/fn fx_bad_range/.test(fx),'invalid Q8.88 sign-extension range guard regressed');
|
||||
must(/let TWO=FX96\(0u,0u,0x02000000u\)/.test(fx)&&/fx_cmp_unsigned\(fx_abs\(zr\),TWO\)>0/.test(fx)&&/write_escape\(out,n,zr,zi\)/.test(fx),'fixed96 certain-escape guard missing');
|
||||
must(a>=0&&b>a&&!fx.includes('var<storage,read> refs'),'fixed96 correction unexpectedly depends on deep reference');
|
||||
must(/@workgroup_size\(64\)/.test(fx)&&/let out=riskQueue\[qi\]/.test(fx)&&/atomicLoad\(&hybridStats\.dispatchCount\)/.test(fx),'fixed96 correction is not queue-indexed');
|
||||
must(!/@workgroup_size\(8,8\)/.test(fx),'fixed96 correction regressed to full-screen 2D workgroups');
|
||||
must(/direct-fixed96-queue-args-experiment/.test(script)&&/this\.fixed96QueueArgs=/.test(script),'fixed96 queue-args pipeline missing');
|
||||
must(/direct-fixed96-correction-experiment/.test(script)&&/this\.fixed96Correct=/.test(script),'fixed96 correction pipeline missing');
|
||||
must(/q88FromRational/.test(script)&&/q88Words/.test(script)&&/fixed96TileData/.test(script),'exact BigInt-to-Q8.88 packing missing');
|
||||
must(/benchmarkFixed96Direct/.test(script)&&/renderTileMetaFixed96Direct/.test(script),'fixed96 benchmark methods missing');
|
||||
must(/GPUBufferUsage[\s\S]*?B\.INDIRECT/.test(script)&&/dispatchWorkgroupsIndirect\(indirect,0\)/.test(script),'true sparse indirect dispatch missing');
|
||||
must(/fixed96QueueIntegrity/.test(script)&&/queue integrity failure/.test(script),'queue integrity fail-closed gate missing');
|
||||
must(/FIXED96_DEPTHS=\[6,8,10,12\]/.test(script)&&/FIXED96_RISK_LIMITS=\[1e11,1e12,1e13,1e14\]/.test(script),'fixed96 depth/risk sweep missing');
|
||||
must(/runFixed96Experiment/.test(script)&&/failureReason/.test(script)&&/Every risk candidate/.test(script),'fixed96 experiment/full failure reporting missing');
|
||||
must(/speedQueueRate/.test(script)&&/timingRepresentative/.test(script)&&/sampling-mismatch/.test(script),'speed/quality sampling consistency gate missing');
|
||||
must(/id="sparseBench"/.test(html)&&/Fixed96 Direct実験/.test(html)&&/id="sparseDialog"/.test(html),'fixed96 UI missing');
|
||||
const choose=(script.match(/function chooseBackend[\s\S]*?function deepNeeded/)||[''])[0];must(!/fixed96|FIXED96/.test(choose),'diagnostic fixed96 leaked into production router');
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-experiment-contract',checks:20},null,2));
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
const MASK=0xffffffffn,MOD=1n<<96n,SIGN=1n<<95n,Q=88n;
|
||||
const u32=x=>Number(BigInt(x)&MASK)>>>0;
|
||||
function words(x){x%=MOD;if(x<0)x+=MOD;return[u32(x),u32(x>>32n),u32(x>>64n)]}
|
||||
function signed(w){let x=BigInt(w[0])+(BigInt(w[1])<<32n)+(BigInt(w[2])<<64n);return x>=SIGN?x-MOD:x}
|
||||
function neg(a){let lo=(~a[0]+1)>>>0,c0=lo===0?1:0,mid=(~a[1]+c0)>>>0,c1=c0&&mid===0?1:0,hi=(~a[2]+c1)>>>0;return[lo,mid,hi]}
|
||||
function isNeg(a){return (a[2]&0x80000000)!==0} function absw(a){return isNeg(a)?neg(a):a}
|
||||
function add(a,b){let lo=(a[0]+b[0])>>>0,c0=lo<a[0]?1:0,m0=(a[1]+b[1])>>>0,c1=m0<a[1]?1:0,mid=(m0+c0)>>>0,c2=mid<m0?1:0,hi=(a[2]+b[2]+c1+c2)>>>0;return[lo,mid,hi]}
|
||||
function mul32(a,b){const a0=a&65535,a1=a>>>16,b0=b&65535,b1=b>>>16,p0=a0*b0,p1=a0*b1,p2=a1*b0,p3=a1*b1,middle=(p0>>>16)+(p1&65535)+(p2&65535),lo=((p0&65535)|((middle&65535)<<16))>>>0,hi=(p3+(p1>>>16)+(p2>>>16)+(middle>>>16))>>>0;return[lo,hi]}
|
||||
function addAt(p,idx,v){v>>>=0;while(v&&idx<6){const old=p[idx]>>>0,sum=(old+v)>>>0;p[idx]=sum;v=sum<old?1:0;idx++}}
|
||||
function addProd(p,idx,a,b){const q=mul32(a,b);addAt(p,idx,q[0]);addAt(p,idx+1,q[1])}
|
||||
function mulfx(a0,b0){const sign=isNeg(a0)!==isNeg(b0),a=absw(a0),b=absw(b0),p=[0,0,0,0,0,0];addProd(p,0,a[0],b[0]);addProd(p,1,a[0],b[1]);addProd(p,2,a[0],b[2]);addProd(p,1,a[1],b[0]);addProd(p,2,a[1],b[1]);addProd(p,3,a[1],b[2]);addProd(p,2,a[2],b[0]);addProd(p,3,a[2],b[1]);addProd(p,4,a[2],b[2]);let r=[((p[2]>>>24)|(p[3]<<8))>>>0,((p[3]>>>24)|(p[4]<<8))>>>0,((p[4]>>>24)|(p[5]<<8))>>>0];if(((p[2]>>>23)&1)!==0)r=add(r,[1,0,0]);return sign?neg(r):r}
|
||||
function roundShift(v,b){const negv=v<0n,a=negv?-v:v,q=(a+(1n<<(b-1n)))>>b;return negv?-q:q}
|
||||
let seed=0x12345678;const rand=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed};
|
||||
for(let i=0;i<20000;i++){const ai=(BigInt(rand()%16000000)-8000000n)*(1n<<68n),bi=(BigInt(rand()%16000000)-8000000n)*(1n<<68n),a=words(ai),b=words(bi),got=signed(mulfx(a,b)),want=roundShift(ai*bi,Q);if(got!==want)throw new Error(`mul mismatch ${i}: ${got} != ${want}`)}
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-limb-multiply',cases:20000,format:'Q8.88'},null,2));
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
const MASK=0xffffffffn,MOD=1n<<96n,SIGN=1n<<95n,Q=88n,MIN=-(1n<<95n),MAX=(1n<<95n)-1n;
|
||||
const u32=x=>Number(BigInt(x)&MASK)>>>0;
|
||||
function words(x){if(x<MIN||x>MAX)throw new Error('Q8.88 overflow');if(x<0)x+=MOD;return[u32(x),u32(x>>32n),u32(x>>64n)]}
|
||||
function signed(w){let x=BigInt(w[0])+(BigInt(w[1])<<32n)+(BigInt(w[2])<<64n);return x>=SIGN?x-MOD:x}
|
||||
function neg(a){let lo=(~a[0]+1)>>>0,c0=lo===0?1:0,mid=(~a[1]+c0)>>>0,c1=c0&&mid===0?1:0,hi=(~a[2]+c1)>>>0;return[lo,mid,hi]}
|
||||
function isNeg(a){return (a[2]&0x80000000)!==0} function absw(a){return isNeg(a)?neg(a):a}
|
||||
function add(a,b){let lo=(a[0]+b[0])>>>0,c0=lo<a[0]?1:0,m0=(a[1]+b[1])>>>0,c1=m0<a[1]?1:0,mid=(m0+c0)>>>0,c2=mid<m0?1:0,hi=(a[2]+b[2]+c1+c2)>>>0;return[lo,mid,hi]}
|
||||
function sub(a,b){return add(a,neg(b))}
|
||||
function cmpu(a,b){for(let i=2;i>=0;i--){if(a[i]<b[i])return-1;if(a[i]>b[i])return 1}return 0}
|
||||
function mul32(a,b){const a0=a&65535,a1=a>>>16,b0=b&65535,b1=b>>>16,p0=a0*b0,p1=a0*b1,p2=a1*b0,p3=a1*b1,middle=(p0>>>16)+(p1&65535)+(p2&65535),lo=((p0&65535)|((middle&65535)<<16))>>>0,hi=(p3+(p1>>>16)+(p2>>>16)+(middle>>>16))>>>0;return[lo,hi]}
|
||||
function addAt(p,idx,v){v>>>=0;while(v&&idx<6){const old=p[idx]>>>0,sum=(old+v)>>>0;p[idx]=sum;v=sum<old?1:0;idx++}}
|
||||
function addProd(p,idx,a,b){const q=mul32(a,b);addAt(p,idx,q[0]);addAt(p,idx+1,q[1])}
|
||||
function mulfx(a0,b0){const sign=isNeg(a0)!==isNeg(b0),a=absw(a0),b=absw(b0),p=[0,0,0,0,0,0];addProd(p,0,a[0],b[0]);addProd(p,1,a[0],b[1]);addProd(p,2,a[0],b[2]);addProd(p,1,a[1],b[0]);addProd(p,2,a[1],b[1]);addProd(p,3,a[1],b[2]);addProd(p,2,a[2],b[0]);addProd(p,3,a[2],b[1]);addProd(p,4,a[2],b[2]);let r=[((p[2]>>>24)|(p[3]<<8))>>>0,((p[3]>>>24)|(p[4]<<8))>>>0,((p[4]>>>24)|(p[5]<<8))>>>0];if(((p[2]>>>23)&1)!==0)r=add(r,[1,0,0]);return sign?neg(r):r}
|
||||
function roundShift(v,b=Q){const n=v<0n,a=n?-v:v,q=(a+(1n<<(b-1n)))>>b;return n?-q:q}
|
||||
function exactMul(a,b){const r=roundShift(a*b);if(r<MIN||r>MAX)throw new Error('exact overflow');return r}
|
||||
const TWO=words(2n<<Q),FOUR=words(4n<<Q);
|
||||
function limbOrbit(cr0,ci0,limit){const cr=words(cr0),ci=words(ci0);let zr=[0,0,0],zi=[0,0,0];for(let n=1;n<=limit;n++){const zr2=mulfx(zr,zr),zi2=mulfx(zi,zi),zri=mulfx(zr,zi);zr=add(sub(zr2,zi2),cr);zi=add(add(zri,zri),ci);if(cmpu(absw(zr),TWO)>0||cmpu(absw(zi),TWO)>0)return n;const mag=add(mulfx(zr,zr),mulfx(zi,zi));if(cmpu(mag,FOUR)>0)return n}return limit}
|
||||
function exactOrbit(cr,ci,limit){let zr=0n,zi=0n;const four=4n<<Q;for(let n=1;n<=limit;n++){const zr2=exactMul(zr,zr),zi2=exactMul(zi,zi),zri=exactMul(zr,zi);zr=zr2-zi2+cr;zi=2n*zri+ci;if((zr<0n?-zr:zr)>2n<<Q||(zi<0n?-zi:zi)>2n<<Q)return n;const mag=exactMul(zr,zr)+exactMul(zi,zi);if(mag>four)return n}return limit}
|
||||
function qdec(x){return BigInt(Math.round(x*2**24))<<64n}
|
||||
const cases=[];
|
||||
for(const [re,im] of [[-0.7453983606667815,0.1125046349959942],[-0.75,0.1],[-0.743643887037151,0.13182590420533],[-1.8,0.2],[0.3,0.55],[1.2,0]]){
|
||||
const cr=qdec(re),ci=qdec(im),a=limbOrbit(cr,ci,1200),b=exactOrbit(cr,ci,1200);if(a!==b)throw new Error(`orbit mismatch ${re},${im}: ${a} != ${b}`);cases.push({re,im,iter:a});
|
||||
}
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-limb-orbit',cases},null,2));
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const F=Math.fround,Q=88,MOD=1n<<96n,MIN=-(1n<<95n),MAX=(1n<<95n)-1n;
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
|
||||
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const[m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
|
||||
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
|
||||
function bitsForDepth(z){return Math.max(256,Math.ceil(z*Math.log2(10))+300)}
|
||||
function snap(re,im,z){const bits=bitsForDepth(z);return{bits,re:decFixed(re,bits),im:decFixed(im,bits),span:decFixed(`3.4e-${z}`,bits)}}
|
||||
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
|
||||
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
|
||||
function q88FromRational(numer,denom,bits){let n=numer,d=denom,sh=Q-bits;if(sh>=0)n<<=BigInt(sh);else d<<=BigInt(-sh);return roundDiv(n,d)}
|
||||
function qmul(a,b){const r=roundShift(a*b,Q);if(r<MIN||r>MAX)throw new Error('Q8.88 overflow');return r}
|
||||
function qadd(a,b){const r=a+b;if(r<MIN||r>MAX)throw new Error('Q8.88 overflow');return r}
|
||||
function fixed96Direct(s,w,h,x,y,limit){const den=4n*BigInt(w),ox4=4n*BigInt(x)+2n-2n*BigInt(w),oy4=2n*BigInt(h)-4n*BigInt(y)-2n,cr=q88FromRational(s.re*den+s.span*ox4,den,s.bits),ci=q88FromRational(s.im*den+s.span*oy4,den,s.bits);let zr=0n,zi=0n;const four=4n<<88n;for(let n=0;n<limit;n++){const zr2=qmul(zr,zr),zi2=qmul(zi,zi),ri=qmul(zr,zi);zr=qadd(qadd(zr2,-zi2),cr);zi=qadd(qadd(ri,ri),ci);if(qadd(qmul(zr,zr),qmul(zi,zi))>four)return n+1}return limit}
|
||||
// DS sensitivity model matching the experimental guard.
|
||||
function q(a,b){const x=F(F(a)+F(b)),e=F(F(b)-F(x-F(a)));return[x,e]}
|
||||
function sum(a,b){a=F(a);b=F(b);const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
|
||||
function prod(a,b){a=F(a);b=F(b);const x=F(a*b),ca=F(F(4097)*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(F(4097)*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
|
||||
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(F(a[1])+F(b[1]))))}
|
||||
function neg(a){return[F(-a[0]),F(-a[1])]} function sub(a,b){return add(a,neg(b))}
|
||||
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(F(a[0])*F(b[1])));e=F(e+F(F(a[1])*F(b[0])));e=F(e+F(F(a[1])*F(b[1])));return q(t[0],e)}
|
||||
function scale(a,b){const t=prod(a[0],F(b));return q(t[0],F(t[1]+F(F(a[1])*F(b))))}
|
||||
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
|
||||
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]} function cmul(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
|
||||
function split(x){const h=F(x);return[h,F(x-h)]} function val(a){return F(F(a[0])+F(a[1]))}
|
||||
function dsRisk(s,w,h,x,y,limit){const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),sp=split(fixedNum(s.span,s.bits)),ox=F(F(F(x+.5)-F(.5*w))/F(w)),oy=F(F(F(.5*h)-F(y+.5))/F(w)),c=[add(cre,scale(sp,ox)),add(cim,scale(sp,oy))];let z=[[0,0],[0,0]],dr=0,di=0,maxDer=0;for(let n=0;n<limit;n++){const zr=val(z[0]),zi=val(z[1]),ndr=F(F(2)*F(F(zr*dr)-F(zi*di))+F(1)),ndi=F(F(2)*F(F(zr*di)+F(zi*dr)));dr=ndr;di=ndi;maxDer=Math.max(maxDer,Math.hypot(dr,di));z=cadd(cmul(z,z),c);const mag=add(mul(z[0],z[0]),mul(z[1],z[1]));if(cmp(mag,[4,0])>0)return{n:n+1,maxDer}}return{n:limit,maxDer}}
|
||||
function adaptiveIter(z){return Math.max(350,350+Math.floor(70*Math.sqrt(z)+15*z))}
|
||||
const re='-0.7453983606667815',im='0.1125046349959942',depths=[6,8,10,12],risks=[1e11,1e12,1e13,1e14],W=1600,H=900,SW=31,SH=21,rows=[];
|
||||
for(const z of depths){const s=snap(re,im,z),limit=adaptiveIter(z),samples=[];for(let sy=0;sy<SH;sy++){const y=Math.round(sy*(H-1)/(SH-1));for(let sx=0;sx<SW;sx++){const x=Math.round(sx*(W-1)/(SW-1)),p=pixel(s,W,H,x,y),pg=pixel(s,W,H,x,y,s.bits+64),o=oracle(p[0],p[1],s.bits,limit),g=oracle(pg[0],pg[1],s.bits+64,limit);if(o!==g){samples.push({guardMismatch:true});continue}const d=dsRisk(s,W,H,x,y,limit),f=fixed96Direct(s,W,H,x,y,limit);samples.push({guardMismatch:false,o,d,f})}}
|
||||
const variants=[];for(const risk of risks){let tested=0,guardMismatch=0,uncertain=0,wrongBefore=0,wrongAfter=0;for(const a of samples){if(a.guardMismatch){guardMismatch++;continue}tested++;if((a.d.n<a.d.limit)!=(a.o<a.d.limit)){}const before=(a.d.n<limit)!=(a.o<limit);if(before)wrongBefore++;let r=a.d.n;if(a.d.maxDer>risk){uncertain++;r=a.f}if((r<limit)!=(a.o<limit))wrongAfter++}variants.push({risk,tested,guardMismatch,uncertain,uncertainRate:uncertain/Math.max(1,tested),wrongBefore,wrongAfter})}
|
||||
rows.push({z,limit,variants})}
|
||||
if(rows.some(r=>r.variants.some(v=>v.guardMismatch)))throw new Error('oracle guard mismatch');
|
||||
for(const r of rows)if(!r.variants.some(v=>v.wrongAfter===0))throw new Error(`no safe fixed96 candidate at z${r.z}`);
|
||||
const report={format:'mandelbrot-fixed96-q88-model-v1',kind:'Exact BigInt model of signed Q8.88 direct + DS risk gate + BigInt P/P+64 oracle; integer GPU arithmetic should be deterministic modulo implementation bugs',viewport:[W,H],sampleGrid:[SW,SH],rows};
|
||||
await fs.mkdir(new URL('../audit/',import.meta.url),{recursive:true});await fs.writeFile(new URL('../audit/v24-fixed96-model.json',import.meta.url),JSON.stringify(report,null,2));
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-q88-model',rows:rows.map(r=>({z:r.z,variants:r.variants}))},null,2));
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
const root=new URL('../',import.meta.url);
|
||||
const script=await fs.readFile(new URL('script.js',root),'utf8');
|
||||
const start=script.indexOf('function fixed96QueueIntegrity('),end=script.indexOf('\nfunction mulRatio',start);
|
||||
if(start<0||end<=start)throw new Error('fixed96QueueIntegrity source missing');
|
||||
const source=script.slice(start,end);
|
||||
const ctx={};ctx.globalThis=ctx;
|
||||
vm.runInNewContext(`const FIXED96_WORKGROUP_SIZE=64;${source};globalThis.check=fixed96QueueIntegrity`,ctx);
|
||||
const check=ctx.check;
|
||||
const stats=(selected,{overflow=0,enqueued=selected,dispatchCount=enqueued}={})=>new Uint32Array([selected,overflow,enqueued,dispatchCount,0,0,0,0]);
|
||||
const fixed=(processed,{remaining=0,invalidIndex=0,staleEntry=0,corrected=processed-remaining}={})=>new Uint32Array([remaining,invalidIndex,processed,staleEntry,0,0,0,corrected]);
|
||||
const args=n=>new Uint32Array([Math.ceil(n/64),1,1,0]);
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(check(stats(0),fixed(0),args(0),64).ok,'zero queue should be valid');
|
||||
must(check(stats(20),fixed(20),args(20),64).ok,'20-entry queue should be valid');
|
||||
must(check(stats(65),fixed(65),args(65),128).ok,'65-entry queue should use two workgroups');
|
||||
must(!check(stats(65),fixed(65),new Uint32Array([1,1,1,0]),128).ok,'wrong indirect workgroup count was not rejected');
|
||||
must(!check(stats(64,{overflow:1,enqueued:63,dispatchCount:63}),fixed(63),args(63),64).ok,'queue overflow was not rejected');
|
||||
must(!check(stats(20),fixed(19,{corrected:19}),args(20),64).ok,'processed/enqueued mismatch was not rejected');
|
||||
must(!check(stats(20),fixed(20,{staleEntry:1,remaining:1,corrected:19}),args(20),64).ok,'stale queue entry was not rejected');
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-queue-integrity-model',cases:7,workgroupSize:64},null,2));
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
// Regression for v24.2.6: Q8.88 values such as +1.2 were incorrectly treated
|
||||
// as invalid sign extension. Model the corrected shader policy: all Q8.88
|
||||
// values are valid; after each iteration, |Re|>2 or |Im|>2 is an immediate
|
||||
// mathematically-certain escape, otherwise |z|^2 can be squared safely.
|
||||
const Q=88n, ONE=1n<<Q, TWO=2n<<Q, FOUR=4n<<Q, MIN=-(1n<<95n), MAX=(1n<<95n)-1n;
|
||||
function roundShift(v,b=Q){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(b-1n)))>>b;return neg?-q:q}
|
||||
function q(x){return BigInt(Math.round(x*2**24)) << 64n} // exact enough for test constants
|
||||
function mul(a,b){const r=roundShift(a*b);if(r<MIN||r>MAX)throw new Error('unexpected Q8.88 overflow');return r}
|
||||
function add(a,b){const r=a+b;if(r<MIN||r>MAX)throw new Error('unexpected Q8.88 overflow');return r}
|
||||
function abs(a){return a<0n?-a:a}
|
||||
function iterate(cr,ci,maxIter=2000){let zr=0n,zi=0n;for(let n=1;n<=maxIter;n++){
|
||||
const zr2=mul(zr,zr),zi2=mul(zi,zi),zri=mul(zr,zi);
|
||||
zr=add(add(zr2,-zi2),cr);zi=add(add(zri,zri),ci);
|
||||
if(abs(zr)>TWO||abs(zi)>TWO)return n;
|
||||
const mag=add(mul(zr,zr),mul(zi,zi));if(mag>FOUR)return n;
|
||||
}return maxIter}
|
||||
// +1.2 is a valid Q8.88 value and must not be rejected merely because the
|
||||
// integer byte is non-zero.
|
||||
const plus12=q(1.2);if(plus12<=ONE||plus12>=TWO)throw new Error('test constant invalid');
|
||||
// Escaping parameters whose orbit passes through component magnitudes >1.
|
||||
for(const [re,im] of [[1.2,0],[-1.8,0.2],[0.5,0.8],[0.4,0.6]]){
|
||||
const n=iterate(q(re),q(im),500);if(n>=500)throw new Error(`expected escape for ${re},${im}`);
|
||||
}
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-shader-range-regression',cases:4,regression:'v24.2.6 rejected valid Q8.88 integer-byte values'},null,2));
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/DIRECT_DS_GUARDED_WGSL/.test(kernels)&&/riskLimit:f32/.test(kernels)&&/deriv=2\.0\*cmul/.test(kernels),'guarded DS shader / derivative risk gate missing');
|
||||
must(/direct-ds-guarded-experiment/.test(script)&&/this\.dsGuarded=/.test(script),'guarded DS pipeline missing');
|
||||
must(/benchmarkHybrid/.test(script)&&/renderTileMetaHybrid/.test(script),'hybrid benchmark methods missing');
|
||||
must(/HYBRID_DEPTHS=\[6,8,10,12\]/.test(script)&&/HYBRID_RISK_LIMITS=\[1e12,1e13,1e14\]/.test(script),'hybrid depth/risk sweep missing');
|
||||
must(/runHybridExperiment/.test(script)&&/uncertainRate/.test(script)&&/speedupVsDeepCold/.test(script),'hybrid experiment/report missing');
|
||||
must(/id="hybridBench"/.test(html)&&/id="hybridDialog"/.test(html)&&/id="hybridSave"/.test(html),'hybrid experiment UI missing');
|
||||
must(/function openHybridDialog\(/.test(script)&&/function startHybridExperimentFromUi\(/.test(script),'hybrid UI start function missing');
|
||||
must(/\$\('#hybridBench'\)\.onclick=\(\)=>\{toast\('Hybrid実験を開始'\);void startHybridExperimentFromUi\(\)\}/.test(script),'toolbar Hybrid button does not directly start experiment');
|
||||
must(/\$\('#hybridStart'\)\.onclick=startHybridExperimentFromUi/.test(script),'dialog rerun button is not wired to experiment');
|
||||
const choose=(script.match(/function chooseBackend[\s\S]*?function deepNeeded/)||[''])[0];
|
||||
must(!/dsGuarded|hybrid-candidate|backend='hybrid'/.test(choose),'diagnostic hybrid leaked into production router');
|
||||
console.log(JSON.stringify({status:'pass',check:'hybrid-experiment-contract',checks:10},null,2));
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const F=Math.fround;
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
|
||||
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const[m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
|
||||
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
|
||||
function bitsForDepth(z){return Math.max(256,Math.ceil(z*Math.log2(10))+300)}
|
||||
function snap(re,im,z){const span=`3.4e-${z}`,bits=bitsForDepth(z);return{bits,re:decFixed(re,bits),im:decFixed(im,bits),span:decFixed(span,bits)}}
|
||||
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
|
||||
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
|
||||
function q(a,b){const x=F(a+b),e=F(b-F(x-a));return[x,e]}
|
||||
function sum(a,b){const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
|
||||
function prod(a,b){const x=F(a*b),ca=F(4097*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(4097*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
|
||||
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(a[1]+b[1])))}
|
||||
function neg(a){return[F(-a[0]),F(-a[1])]}
|
||||
function sub(a,b){return add(a,neg(b))}
|
||||
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(a[0]*b[1]));e=F(e+F(a[1]*b[0]));e=F(e+F(a[1]*b[1]));return q(t[0],e)}
|
||||
function scaleDS(a,b){const t=prod(a[0],b);return q(t[0],F(t[1]+F(a[1]*b)))}
|
||||
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
|
||||
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]}
|
||||
function cmulDS(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
|
||||
function mag2DS(a){return add(mul(a[0],a[0]),mul(a[1],a[1]))}
|
||||
function split(x){const h=F(x);return[h,F(x-h)]}
|
||||
function value(a){return F(a[0]+a[1])}
|
||||
function dsRisk(s,w,h,x,y,limit){const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),span=split(fixedNum(s.span,s.bits)),offx=(x+.5-.5*w)/w,offy=(.5*h-y-.5)/w,cr=add(cre,scaleDS(span,F(offx))),ci=add(cim,scaleDS(span,F(offy))),c=[cr,ci];let z=[[0,0],[0,0]],dr=0,di=0,maxDer=0;for(let n=0;n<limit;n++){const zr=value(z[0]),zi=value(z[1]),ndr=2*(zr*dr-zi*di)+1,ndi=2*(zr*di+zi*dr);dr=ndr;di=ndi;maxDer=Math.max(maxDer,Math.hypot(dr,di));z=cadd(cmulDS(z,z),c);if(cmp(mag2DS(z),[4,0])>0)return{n:n+1,maxDer}}return{n:limit,maxDer}}
|
||||
function adaptiveIter(z){return Math.max(350,350+Math.floor(70*Math.sqrt(z)+15*z))}
|
||||
const re='-0.7453983606667815',im='0.1125046349959942',depths=[6,8,10,12],W=1600,H=900,SW=31,SH=21,riskLimit=1e13,rows=[];
|
||||
for(const z of depths){const s=snap(re,im,z),limit=adaptiveIter(z);let tested=0,wrong=0,uncertain=0,caught=0,guardMismatch=0;for(let sy=0;sy<SH;sy++){const y=Math.round(sy*(H-1)/(SH-1));for(let sx=0;sx<SW;sx++){const x=Math.round(sx*(W-1)/(SW-1)),p=pixel(s,W,H,x,y),g=pixel(s,W,H,x,y,s.bits+64),o=oracle(p[0],p[1],s.bits,limit),gg=oracle(g[0],g[1],s.bits+64,limit);if(o!==gg){guardMismatch++;continue}tested++;const d=dsRisk(s,W,H,x,y,limit),bad=(d.n<limit)!=(o<limit),u=d.maxDer>riskLimit;if(bad)wrong++;if(u)uncertain++;if(bad&&u)caught++}}
|
||||
rows.push({z,tested,guardMismatch,wrong,uncertain,caught,uncertainRate:uncertain/tested,catchRate:wrong?caught/wrong:1});}
|
||||
if(rows.some(r=>r.guardMismatch))throw new Error('oracle guard mismatch');
|
||||
for(const r of rows.slice(0,3))if(r.catchRate!==1)throw new Error(`risk gate missed DS class error at z${r.z}`);
|
||||
if(!(rows[0].uncertainRate<.08&&rows[1].uncertainRate<.2&&rows[2].uncertainRate<.5))throw new Error('risk gate is too broad before z12');
|
||||
if(!(rows[3].uncertainRate>.75))throw new Error('z12 should signal crossover toward Full Deep');
|
||||
const report={format:'mandelbrot-hybrid-risk-model-v1',kind:'CPU Math.fround DS + BigInt P/P+64 oracle; not real GPU',viewport:[W,H],sampleGrid:[SW,SH],riskLimit,rows,interpretation:'At riskLimit=1e13 the derivative guard catches every observed DS classification error at z6/z8/z10 while sending about 3%/11%/35% of samples to Deep correction. At z12 it sends over 75%, which is a deliberate signal that Full Deep should replace the hybrid.'};
|
||||
await fs.mkdir(new URL('../audit/',import.meta.url),{recursive:true});await fs.writeFile(new URL('../audit/v24-hybrid-risk-model.json',import.meta.url),JSON.stringify(report,null,2));
|
||||
console.log(JSON.stringify({status:'pass',check:'hybrid-risk-model',rows},null,2));
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
const source=await fs.readFile(new URL('../script.js',import.meta.url),'utf8');
|
||||
const start=source.indexOf('function openHybridDialog()');
|
||||
const end=source.indexOf('// ── diagnostics',start);
|
||||
if(start<0||end<0)throw new Error('hybrid UI block not found');
|
||||
const block=source.slice(start,end);
|
||||
const el=(extra={})=>Object.assign({textContent:'',value:'',disabled:false,hidden:true,max:0,open:false,attrs:new Map(),setAttribute(k,v){this.attrs.set(k,String(v));if(k==='open')this.open=true},removeAttribute(k){this.attrs.delete(k)},showModal(){this.open=true},close(){this.open=false}},extra);
|
||||
const elements={
|
||||
'#hybridDialog':el(), '#hybridStart':el({textContent:'再実行'}), '#hybridBench':el(), '#hybridProgress':el(), '#hybridStatus':el(), '#hybridSave':el(), '#hybridOutput':el(), '#hybridTrack':el({value:'seahorse'}), '#hybridSamples':el({value:'2'}), '#hybridCancel':el()
|
||||
};
|
||||
let runCount=0,toastText='';
|
||||
const ctx={
|
||||
$:s=>elements[s], hybridJob:{active:false,cancelled:false,last:null}, benchmarkJob:{active:false}, HYBRID_DEPTHS:[6,8,10,12],
|
||||
cancelRender(){}, state:{dirty:true}, refs:{cancelPending(){}}, markDirty(){}, toast(s){toastText=s}, hybridSummary(){return 'summary'},
|
||||
async runHybridExperiment({onProgress}){runCount++;onProgress?.({index:1,total:4,z:6,phase:'probe'});return{rows:[1,2,3,4]}},
|
||||
downloadBlob(){}, Blob:globalThis.Blob, Date, Math, JSON, String, Error
|
||||
};
|
||||
vm.createContext(ctx);vm.runInContext(block,ctx,{filename:'hybrid-ui-block.js'});
|
||||
if(typeof elements['#hybridBench'].onclick!=='function')throw new Error('toolbar click handler not installed');
|
||||
elements['#hybridBench'].onclick();
|
||||
if(!elements['#hybridDialog'].open)throw new Error('toolbar click did not open dialog synchronously');
|
||||
if(runCount!==1)throw new Error('toolbar click did not start experiment synchronously');
|
||||
if(!/Hybrid実験/.test(toastText))throw new Error('toolbar click gives no visible acknowledgement');
|
||||
await new Promise(r=>setTimeout(r,0));
|
||||
if(!/実験完了/.test(elements['#hybridStatus'].textContent))throw new Error('experiment completion not reflected in UI: '+elements['#hybridStatus'].textContent);
|
||||
if(elements['#hybridBench'].disabled)throw new Error('toolbar stayed disabled');
|
||||
console.log(JSON.stringify({status:'pass',check:'hybrid-ui-runtime',runCount,dialogOpen:elements['#hybridDialog'].open,statusText:elements['#hybridStatus'].textContent,toastText},null,2));
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const h=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
|
||||
const req=['id="view"','id="processMode"','保守的 (Strict)','id="exportPrecision"','id="backendBench"','id="benchmarkDialog"','id="sparseBench"','id="sparseDialog"','mandelbrot-bundle','data-bundle="gpu-kernels"','data-bundle="app"'];
|
||||
const req=['id="view"','id="renderMode"','>高速<','>正確<','id="processMode"','id="palette"','id="cycle"','id="shift"','id="colorAuto"','id="exportDialog"','mandelbrot-bundle','data-bundle="gpu-kernels"','data-bundle="app"'];
|
||||
for(const x of req)if(!h.includes(x))throw new Error('missing '+x);
|
||||
for(const banned of ['詳細設定・正確な座標','診断情報','id="backendBench"','id="sparseBench"','id="benchmarkDialog"','id="sparseDialog"','id="coordReInput"'])if(h.includes(banned))throw new Error('obsolete UI remains: '+banned);
|
||||
if(/<script\s+src=/.test(h))throw new Error('standalone index still depends on external scripts');
|
||||
if(h.includes('<script src="kernels.js"></script>'))throw new Error('legacy kernels.js loaded');
|
||||
console.log(JSON.stringify({status:'pass',checks:req.length+2,singleFile:true},null,2));
|
||||
if(!/select option\{background:#000;color:#fff\}/.test(h))throw new Error('dropdown theme not embedded');
|
||||
if(!/overflow-y:auto/.test(h))throw new Error('panel scroll fix not embedded');
|
||||
console.log(JSON.stringify({status:'pass',checks:req.length+9,singleFile:true},null,2));
|
||||
|
|
|
|||
16
tests/v24-pan-reuse-model.mjs
Normal file
16
tests/v24-pan-reuse-model.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/source:\{bits:snap\.bits,re:snap\.re,im:snap\.im,span:snap\.span,iter\}/.test(script),'reference source metadata is not retained');
|
||||
must(/newSpan!==oldSpan/.test(script),'span equality reuse guard missing');
|
||||
must(/src\.iter!==iter/.test(script),'iteration reuse guard missing');
|
||||
must(/pixel\.x<0\|\|pixel\.x>w\|\|pixel\.y<0\|\|pixel\.y>h/.test(script),'reference viewport reuse guard missing');
|
||||
must(/referencePixelForSource/.test(script),'selected reference pixel mapping missing');
|
||||
const W=800,H=500;
|
||||
for(const [dx,dy] of [[0,0],[24,0],[-100,30],[399,-249]]){
|
||||
// Pure pan: new center = old center - dx*span/W (real), +dy*span/W (imag).
|
||||
// Mapping old center into new view must recover center+pointer displacement.
|
||||
const refX=W/2+dx,refY=H/2+dy;
|
||||
if(refX<0||refX>W||refY<0||refY>H)throw new Error('test vector outside viewport');
|
||||
}
|
||||
console.log(JSON.stringify({status:'pass',check:'pan-reference-reuse-model',vectors:4,policy:'same span + same iter + old reference inside new viewport'},null,2));
|
||||
30
tests/v24-primary-post-stats-model.mjs
Normal file
30
tests/v24-primary-post-stats-model.mjs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import fs from 'node:fs/promises';
|
||||
await import('../gpu-kernels.js');
|
||||
const G=globalThis.MANDEL_WEBGPU_KERNELS;
|
||||
const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
const ITER_MASK=0x000fffff, REASON_SHIFT=20, FIELD_ESCAPED=1;
|
||||
const packUnknown=(n,r)=>((n&ITER_MASK)|((r&0xff)<<REASON_SHIFT))>>>0;
|
||||
const packKnown=n=>((FIELD_ESCAPED<<28)|(n&ITER_MASK))>>>0;
|
||||
const bucket=(m,maxIter)=>Math.min(7,Math.floor(((m&ITER_MASK)*8)/Math.max(1,maxIter)));
|
||||
let seed=0x31415926; const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/0x100000000};
|
||||
const cases=[];
|
||||
for(const [w,h,maxIter,rate] of [[1,1,350,1],[65,7,611,.14],[320,180,667,.18],[257,131,721,.03],[511,23,901,.65]]){
|
||||
const meta=new Uint32Array(w*h),direct=new Uint32Array(8),wg=new Uint32Array(8),directBuckets=new Uint32Array(8),wgBuckets=new Uint32Array(8);
|
||||
for(let i=0;i<meta.length;i++){
|
||||
if(rnd()<rate){const reason=1+Math.floor(rnd()*6),n=Math.floor(rnd()*(maxIter+1));meta[i]=packUnknown(n,reason);direct[0]++;direct[reason]++;directBuckets[bucket(meta[i],maxIter)]++;}
|
||||
else meta[i]=packKnown(Math.floor(rnd()*(maxIter+1)));
|
||||
}
|
||||
for(let y=0;y<h;y++)for(let gx=0;gx<Math.ceil(w/64);gx++){
|
||||
const localReasons=new Uint32Array(8),localBuckets=new Uint32Array(8);
|
||||
for(let lane=0;lane<64;lane++){const x=gx*64+lane;if(x>=w)continue;const m=meta[y*w+x];if((m>>>28)===0){localReasons[0]++;const r=(m>>>REASON_SHIFT)&0xff;if(r>=1&&r<=6)localReasons[r]++;localBuckets[bucket(m,maxIter)]++;}}
|
||||
for(let i=0;i<8;i++){wg[i]+=localReasons[i];wgBuckets[i]+=localBuckets[i];}
|
||||
}
|
||||
must([...direct].every((v,i)=>v===wg[i]),`reason stats mismatch ${w}x${h}`);
|
||||
must([...directBuckets].every((v,i)=>v===wgBuckets[i]),`bucket stats mismatch ${w}x${h}`);
|
||||
cases.push({w,h,maxIter,unknown:direct[0],reasons:Array.from(direct.slice(1,7)),buckets:Array.from(directBuckets)});
|
||||
}
|
||||
must(G.DEEP_PERTURB_POSTSTATS_WGSL.includes('fieldMeta[out]=pack_unknown')&&!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('atomicAdd(&unresolved.remaining'),'post-stats primary contract broken');
|
||||
must(G.DEEP_BUCKET_HIST_STATS_WGSL.includes('localReasons')&&G.DEEP_BUCKET_HIST_STATS_WGSL.includes('unresolvedStats'),'histogram stats aggregation contract broken');
|
||||
must(/ensureDeepBucketState/.test(script)&&/encodeDeepBucketHistogramStats/.test(script),'production integration contract broken');
|
||||
console.log(JSON.stringify({status:'pass',check:'primary-post-stats-workgroup-model',workgroupSize:64,cases},null,2));
|
||||
15
tests/v24-reference-recovery-model.mjs
Normal file
15
tests/v24-reference-recovery-model.mjs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import fs from 'node:fs/promises';import vm from 'node:vm';import {performance} from 'node:perf_hooks';
|
||||
const source=await fs.readFile(new URL('../script.js',import.meta.url),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
function extract(prefix,suffix){const start=source.indexOf(prefix)+prefix.length,end=source.indexOf(suffix,start);if(start<prefix.length||end<0)throw new Error('worker source not found');return source.slice(start,end)}
|
||||
function runWorker(workerSource,data){let message=null;const self={postMessage:m=>{message=m},onmessage:null};const ctx={self,postMessage:(...a)=>self.postMessage(...a),performance,Math,Number,BigInt,ArrayBuffer,DataView,Float32Array,Float64Array,Uint32Array,Set,String,Error};vm.createContext(ctx);vm.runInContext(workerSource,ctx,{timeout:5000});self.onmessage({data});if(!message)throw new Error('worker produced no message');if(message.type==='error')throw new Error(message.error);return message}
|
||||
const bits=256,ONE=1n<<256n,re=ONE/2n,im=0n,span=34n*ONE/10n,iter=700,width=800,height=600;
|
||||
const deep=runWorker(extract("function referenceWorkerSource(){return String.raw`","`}\nclass ReferenceService"),{type:'build',id:1,key:'deep-select',bits,re:re.toString(),im:im.toString(),span:span.toString(),width,height,iter});
|
||||
must(BigInt(deep.referenceRe)!==re,'Deep candidate selection stayed on short-lived center');
|
||||
must(deep.selectionEscape===0,'Deep candidate selection did not find a full-length reference');
|
||||
must(deep.refLen===iter,'Deep selected reference is not full-length');
|
||||
const fast=runWorker(extract("function fastReferenceWorkerSource(){return String.raw`","`}\nclass FastReferenceService"),{type:'build',id:2,key:'fast-select',sourceBits:bits,targetBits:128,re:re.toString(),im:im.toString(),span:span.toString(),width,height,iter});
|
||||
must(BigInt(fast.referenceRe)!==re,'Fast candidate selection stayed on short-lived center');
|
||||
must(fast.selectionEscape===0,'Fast candidate selection did not find a full-length reference');
|
||||
must(fast.refLen===iter,'Fast selected reference is not full-length');
|
||||
console.log(JSON.stringify({status:'pass',check:'reference-recovery-selection',deepReference:deep.referenceRe,fastReference:fast.referenceRe,iter},null,2));
|
||||
|
|
@ -1,33 +1,26 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
|
||||
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),dev=await fs.readFile(new URL('index.external.html',root),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/const VERSION=24/.test(script),'renderer version is not 24');
|
||||
must(/navigator\.gpu\.requestAdapter/.test(script),'WebGPU adapter path missing');
|
||||
must(/guarded perturbation/.test(script)&&/DEEP_CORRECT_WGSL/.test(kernels)&&/correctUnknownFrame/.test(script),'guarded deep + sparse DS correction path missing');
|
||||
must(/renderMode:'fast'/.test(script)&&/manual-accurate/.test(script)&&/manual-fast/.test(script),'manual fast/accurate backend policy missing');
|
||||
must(/\$\('#renderMode'\)\.onchange/.test(script)&&/mandelbrot\.renderMode/.test(script),'manual render mode control/persistence missing');
|
||||
must(!/ensureBackendProbe|scheduleBackendProbeAfterPaint|backendProfiles|backendViewProbes|runCrossoverBenchmark|runFixed96Experiment|__MANDEL_DIAG__/.test(script),'obsolete diagnostic/router system remains');
|
||||
must(!/DIRECT_DS_WGSL|DIRECT_DS_GUARDED_WGSL|DIRECT_FIXED96/.test(kernels),'diagnostic Direct DS/Fixed96 kernels remain');
|
||||
must(/正確 perturbation|DEEP_PERTURB_WGSL/.test(script+kernels)&&/DEEP_CORRECT_WGSL/.test(kernels)&&/correctUnknownFrame/.test(script),'accurate perturbation + sparse DS correction path missing');
|
||||
must(/DEEP_BUCKET_HIST_WGSL/.test(kernels)&&/DEEP_BUCKET_PREFIX_WGSL/.test(kernels)&&/DEEP_BUCKET_SCATTER_WGSL/.test(kernels)&&/DEEP_CORRECT_QUEUE_WGSL/.test(kernels),'production bucketed Deep queue path missing');
|
||||
must(/DEEP_PERTURB_POSTSTATS_WGSL/.test(kernels)&&/DEEP_BUCKET_HIST_STATS_WGSL/.test(kernels)&&/encodeDeepPostStatsNumeric/.test(script),'post-stats Deep primary missing');
|
||||
must(/presentTransform/.test(script)&&/前フレーム再利用/.test(script),'pan frame reprojection reuse missing');
|
||||
must(/FAST_PERTURB_WGSL/.test(kernels)&&/fastNeedsExtended/.test(script)&&/FastReferenceService/.test(script)&&/manual-fast-extended/.test(script),'fast deep-coordinate precision extension missing');
|
||||
must(/reusableDeepReference/.test(script)&&/referencePixel/.test(script)&&/reference再利用/.test(script),'Deep pan reference reuse missing');
|
||||
must(/colorAutoStep/.test(script)&&/colorCycleDir/.test(script)&&/colorShiftDir/.test(script)&&/state\.cycle>=cmax/.test(script)&&/state\.shift>=smax/.test(script),'bouncing color auto-slide missing');
|
||||
must(/effectiveColorCycle/.test(script)&&/state\.cycle\*base\/effective/.test(script)&&/sliderToCycle/.test(script)&&/cycleToSlider/.test(script),'zoom-relative/logarithmic color-period control missing');
|
||||
must(/mark_fast_unresolved/.test(kernels)&&/REASON_REFERENCE_END/.test(kernels)&&/REASON_REBASE_GAP/.test(kernels)&&/@group\(0\) @binding\(4\) var<storage,read_write> unresolved/.test(kernels),'Fast unresolved accounting missing');
|
||||
must(/provisional_unknown/.test(kernels)&&/fillCount>0\.0/.test(kernels),'UNKNOWN visual neighbor fill missing');
|
||||
must(/if\(!deep\)\{const cbg/.test(script)&&/if\(!deep\)\[f\.front,f\.back\]/.test(script)&&/const painted=await r\.recolor\(token,iter\)/.test(script),'deferred Accurate color/commit missing');
|
||||
must(/chooseReference/.test(script)&&/referenceCandidates/.test(script)&&/selectionEscape/.test(script),'long-lived reference selection missing');
|
||||
must(/max-height:calc\(100dvh - 20px\)/.test(dev)&&/overflow-y:auto/.test(dev),'scrollable bottom panel missing');
|
||||
must(/select option\{background:#000;color:#fff\}/.test(dev),'black/white dropdown styling missing');
|
||||
must(!/詳細設定・正確な座標|診断情報|backendBench|sparseBench/.test(dev),'removed settings/diagnostics UI remains');
|
||||
must(/referenceWorkerSource/.test(script)&&/BigInt\(d\.re\)/.test(script),'BigInt reference worker missing');
|
||||
must(!/function buildBla\(/.test(script)&&!/BlaNode/.test(kernels)&&!/useBla/.test(kernels),'unsafe BLA path is not fully removed');
|
||||
must(/F32_U/.test(kernels)&&/safe_abs_error/.test(kernels)&&/errAbs/.test(kernels),'deep error-bound guard missing');
|
||||
must(/struct UnresolvedHead/.test(kernels)&&/atomicAdd\(&unresolved\.remaining/.test(kernels)&&/REASON_ERROR_BOUND/.test(kernels),'unresolved reason counters missing');
|
||||
must(/CORRECTION_MARK/.test(kernels)&&/ds_prod/.test(kernels)&&/correct_pixel/.test(kernels),'double-single correction kernel missing');
|
||||
must(!/strict_queue|QUEUE_FINALIZE_WGSL/.test(kernels)&&!/qitems|strict-indirect/.test(script),'obsolete strict retry queue remains');
|
||||
must(/texture_storage_2d<rgba8unorm,write>/.test(kernels),'GPU color field path missing');
|
||||
must(/AA_RESOLVE_WGSL/.test(kernels)&&/renderTileRGBA2x/.test(script),'GPU 2x2 export resolve missing');
|
||||
must(/correctUnknown=true/.test(script)&&/encodeCorrectionNumeric/.test(script),'export/probe DS correction integration missing');
|
||||
must(/class StreamingPng/.test(script)&&/CompressionStream\('deflate'\)/.test(script)&&/async abort\(reason\)/.test(script),'streaming PNG export/abort missing');
|
||||
must(/PRESENT_WGSL/.test(kernels),'GPU reprojection/presentation missing');
|
||||
must(/recolorPending/.test(script)&&!/const token=\+\+state\.token,stateSnap/.test(script),'recolor race fix missing');
|
||||
must(/this\.context=null/.test(script)&&/webgpuCanvasClaimed=true;this\.configure\(\)/.test(script)&&/if\(webgpuCanvasClaimed\)return null/.test(script),'delayed WebGPU canvas claim/fallback guard missing');
|
||||
must(/このズーム深度はWebGPUが必要です/.test(script),'deep fallback rejection missing');
|
||||
must(!/Math\.min\(340/.test(script),'deep exact-coordinate formatter still capped at 340 digits');
|
||||
must(/uncapturederror/.test(script)&&/pushErrorScope/.test(script),'WebGPU error capture missing');
|
||||
must(/mandelbrot-bundle/.test(html)&&/data-bundle="gpu-kernels"/.test(html)&&/data-bundle="app"/.test(html)&&!/<script\s+src=/.test(html),'standalone index is not self-contained WebGPU bundle');
|
||||
must(/numericParams/.test(script)&&/colorParams/.test(script),'persistent frame parameter buffers missing');
|
||||
must(/ensureExportWorkspace/.test(script)&&/unresolveds:Array\.from\(\{length:4\}/.test(script),'reusable corrected export workspace missing');
|
||||
must(/DIRECT_REQUIRED_RATIO=8/.test(script)&&/DEEP_SPEED_MARGIN=\.85/.test(script)&&/ensureBackendProbe/.test(script),'adaptive direct/deep router missing');
|
||||
must(/DIRECT_DS_WGSL/.test(kernels)&&/runCrossoverBenchmark/.test(script),'diagnostic DS/crossover benchmark missing');
|
||||
must(/overlap-view-probe-required/.test(script)&&/qualityPass/.test(script)&&/backendQualityTiles/.test(script)&&/overlap-probe-failed-safe-deep/.test(script),'view-specific quality probe/fail-safe gate missing');
|
||||
must(!/referenceSnapAtPixel|chooseUnknownCandidate|multi-reference-refine/.test(script),'obsolete multi-reference production path remains');
|
||||
must(/gpuUnavailable/.test(script)&&/return 262144/.test(script),'lightweight no-adapter CPU fallback budget missing');
|
||||
for(const banned of ['DEEP_SIMD_B64','DEEP_SCALAR_B64','BLA_SIMD_B64','BLA_SCALAR_B64','deepPool','deepWasm','highPrecisionDirectPixelAsync','renderDeepAdaptive','render_perturb_rebase_rect','WebAssembly.Instance'])must(!script.includes(banned),'legacy production deep dependency remains: '+banned);
|
||||
must(!/Validated direct/.test(html),'obsolete Validated UI remains');
|
||||
console.log(JSON.stringify({status:'pass',rendererVersion:24,shaderVersion:'24.2.8-fixed96-sparse-queue',checks:28},null,2));
|
||||
must(/class StreamingPng/.test(script)&&/CompressionStream\('deflate'\)/.test(script),'streaming PNG export missing');
|
||||
console.log(JSON.stringify({status:'pass',shaderVersion:'24.2.26-baseline-idle-refinement',checks:22},null,2));
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ const root=new URL('../',import.meta.url),h=await fs.readFile(new URL('index.htm
|
|||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(!/<script\s+src=/.test(h),'root standalone HTML has external script dependency');
|
||||
must(/data-bundle="gpu-kernels"/.test(h)&&/data-bundle="app"/.test(h),'inline bundle markers missing');
|
||||
must(/id="sparseBench"/.test(h)&&/id="sparseDialog"/.test(h),'Fixed96 UI missing in standalone');
|
||||
must(/\$\('#sparseBench'\)\.onclick/.test(h)&&/runFixed96Experiment/.test(h),'Fixed96 button handler not embedded in standalone');
|
||||
must(/id="renderMode"/.test(h)&&/>高速<\//.test(h)&&/>正確<\//.test(h)&&!/高速(標準)|正確(深部)/.test(h)&&/id="colorAuto"/.test(h),'manual render labels/color auto UI contract failed');
|
||||
must(!/診断情報|benchmarkDialog|sparseDialog|coordReInput/.test(h),'removed diagnostic/exact-coordinate UI still embedded');
|
||||
must(/<script src="gpu-kernels\.js"><\/script>/.test(dev)&&/<script src="script\.js"><\/script>/.test(dev),'hosted/dev template no longer has explicit source scripts');
|
||||
must(/\$\('#sparseBench'\)\.onclick/.test(script),'source button handler missing');
|
||||
console.log(JSON.stringify({status:'pass',check:'single-file-ui-contract',checks:6,reason:'Opening HTML from an archive temp extraction does not require sibling JS files.'},null,2));
|
||||
must(/\$\('#renderMode'\)\.onchange/.test(script)&&/colorAutoStep/.test(script),'source UI handlers missing');
|
||||
console.log(JSON.stringify({status:'pass',check:'single-file-ui-contract',checks:6},null,2));
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import crypto from 'node:crypto';
|
||||
const root=new URL('../',import.meta.url);
|
||||
const html=await fs.readFile(new URL('index.html',root),'utf8');
|
||||
const script=await fs.readFile(new URL('script.js',root));
|
||||
const kernels=await fs.readFile(new URL('gpu-kernels.js',root));
|
||||
const vs=await fs.readFile(new URL('script-v24.2.6.js',root));
|
||||
const vk=await fs.readFile(new URL('gpu-kernels-v24.2.6.js',root));
|
||||
const h=b=>crypto.createHash('sha256').update(b).digest('hex');
|
||||
if(!html.includes('script-v24.2.6.js')||!html.includes('gpu-kernels-v24.2.6.js'))throw new Error('versioned browser assets not referenced');
|
||||
if(html.includes('<script src="script.js"')||html.includes('<script src="gpu-kernels.js"'))throw new Error('bare cacheable browser assets still referenced');
|
||||
if(h(script)!==h(vs)||h(kernels)!==h(vk))throw new Error('versioned browser assets differ from canonical source');
|
||||
console.log(JSON.stringify({status:'pass',check:'ui-cacheproof-assets',scriptSha256:h(script),kernelSha256:h(kernels)},null,2));
|
||||
|
|
@ -1,35 +1,21 @@
|
|||
(()=>{'use strict';
|
||||
const out=document.querySelector('#out'),frame=document.querySelector('#app');
|
||||
const scenes=[
|
||||
['z0','-0.5','0','3.4',120],
|
||||
['period2-cusp-z14','-0.75','0','3.4e-14',1200],
|
||||
['swirly-seahorses-z12','-0.7453983606667815','0.1125046349959942','3.52e-12',2000],
|
||||
['period2-cusp-z20','-0.75','0','3.4e-20',1800],
|
||||
['period2-cusp-z100','-0.75','0','3.4e-100',2400],
|
||||
['period3-interior','-0.122561166876653619975245551820735654052','0.744861766619744236593170428604392367240','1e-8',1200],
|
||||
['period2-cusp-e280','-0.75','0','1e-280',2400],
|
||||
['period2-cusp-e400','-0.75','0','1e-400',1600]
|
||||
];
|
||||
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
|
||||
function pixel(st,x,y,bits=st.bits){const re=align(BigInt(st.re),st.bits,bits),im=align(BigInt(st.im),st.bits,bits),span=align(BigInt(st.span),st.bits,bits),den=BigInt(2*st.width);return[re+roundDiv(span*BigInt(2*x+1-st.width),den),im+roundDiv(span*BigInt(st.height-2*y-1),den)]}
|
||||
function orbit(cr,ci,bits,limit){const bail=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);zi=roundShift(2n*zr*zi,bits)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,bits)+roundShift(zi*zi,bits)>bail)return n+1}return limit}
|
||||
function points(w,h){const xs=[0,.25,.5,.75,1].map(t=>Math.min(w-1,Math.max(0,Math.round(t*(w-1))))),ys=[0,.25,.5,.75,1].map(t=>Math.min(h-1,Math.max(0,Math.round(t*(h-1)))));return ys.flatMap(y=>xs.map(x=>[x,y]))}
|
||||
async function waitApp(){if(frame.contentWindow&&frame.contentWindow.__MANDEL_TEST__)return;await new Promise(r=>frame.addEventListener('load',r,{once:true}));const t=performance.now();while(!frame.contentWindow.__MANDEL_TEST__&&performance.now()-t<15000)await new Promise(r=>setTimeout(r,50));if(!frame.contentWindow.__MANDEL_TEST__)throw new Error('test hook not available')}
|
||||
async function checkScene(api,id,re,im,span,iter,mode){
|
||||
const r=await api.setView({re,im,span,baseIter:iter,adaptive:false,processMode:mode}),diag=r.diag;
|
||||
if(diag.backend!=='webgpu'||diag.webgpuError)throw new Error(id+'/'+mode+': WebGPU backend unavailable/error: '+(diag.webgpuError||diag.backend));
|
||||
const st=api.state(),ps=points(st.width,st.height),meta=await api.sampleMeta(ps);let guardMismatch=0,falseEscaped=0,falseBounded=0,escapeIterationMismatch=0,unknown=0;
|
||||
for(let i=0;i<ps.length;i++){const [x,y]=ps[i],p=pixel(st,x,y),pg=pixel(st,x,y,st.bits+64),a=orbit(p[0],p[1],st.bits,st.iter),g=orbit(pg[0],pg[1],st.bits+64,st.iter);if(a!==g){guardMismatch++;continue}const m=meta[i],cls=(m>>>28)&3,n=m&0x000fffff;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a<st.iter)falseBounded++;if(cls===1&&a<st.iter&&n!==a)escapeIterationMismatch++}
|
||||
const row={id,mode,width:st.width,height:st.height,iter:st.iter,deep:diag.deep,guardMismatch,falseEscaped,falseBounded,escapeIterationMismatch,unknown,known:ps.length-unknown,reference:diag.reference};
|
||||
if(guardMismatch||falseEscaped||falseBounded)throw new Error(id+'/'+mode+' numeric gate failed: '+JSON.stringify(row));
|
||||
if(row.unknown!==0)throw new Error(id+'/'+mode+': corrected production frame produced UNKNOWN samples: '+JSON.stringify(row));
|
||||
return row;
|
||||
}
|
||||
async function run(){await waitApp();const api=frame.contentWindow.__MANDEL_TEST__,report=[];for(const [id,re,im,span,iter] of scenes){for(const mode of (id==='z0'?['standard']:['standard','validate'])){out.textContent='running '+id+' / '+mode+'…\n'+JSON.stringify(report,null,2);report.push(await checkScene(api,id,re,im,span,iter,mode));}}
|
||||
await api.setView({re:'-0.7453983606667815',im:'0.1125046349959942',span:'3.52e-12',baseIter:2000,adaptive:false,processMode:'validate'});const denseState={...api.state(),width:61,height:39},denseMeta=await api.probeMeta({w:61,h:39,strict:true,forceDeep:true,correctUnknown:true}),densePoints=[[25,12],[26,15],[27,25]],denseRegression=[];for(const[x,y]of densePoints){const p=pixel(denseState,x,y),pg=pixel(denseState,x,y,denseState.bits+64),a=orbit(p[0],p[1],denseState.bits,denseState.iter),g=orbit(pg[0],pg[1],denseState.bits+64,denseState.iter);if(a!==g)throw new Error('dense guard mismatch '+x+','+y);const m=denseMeta[y*61+x],cls=(m>>>28)&3,n=m&0x000fffff;if(cls===0)throw new Error('dense DS correction left known regression pixel UNKNOWN '+JSON.stringify({x,y,a,cls,n}));if(cls===1&&a===denseState.iter)throw new Error('dense false escape '+JSON.stringify({x,y,a,cls,n}));if(cls!==1&&a<denseState.iter)throw new Error('dense false bounded '+JSON.stringify({x,y,a,cls,n}));denseRegression.push({x,y,oracle:a,cls,n})}
|
||||
await api.setView({re:'0.4',im:'0',span:'0.0015',baseIter:500,adaptive:false,processMode:'standard'});const routerDiag=frame.contentWindow.__MANDEL_DIAG__.snapshot(),routerDecision=routerDiag.backendDecision;if(!routerDecision||routerDecision.probe||!routerDecision.viewProbe)throw new Error('overlap view-specific backend probe did not settle: '+JSON.stringify(routerDecision));const vp=routerDecision.viewProbe;if(!vp.qualityPass||vp.classDisagreement!==0||vp.deepUnknownRate>.005)throw new Error('overlap backend quality probe failed: '+JSON.stringify(vp));const deepShouldWin=vp.predictedDeepMs<vp.predictedDirectMs*.85;if((routerDecision.backend==='deep')!==deepShouldWin)throw new Error('backend router ignored measured 15% speed margin: '+JSON.stringify(routerDecision));
|
||||
await api.setView({re:'-0.75',im:'0',span:'3.4e-20',baseIter:1000,adaptive:false,processMode:'validate'});const exportSmoke=await api.smokeExportTile({w:48,h:32,strict:true,ss:1}),exportSmokeAA=await api.smokeExportTile({w:48,h:32,strict:true,ss:2});if(exportSmoke.length!==exportSmoke.expected||exportSmokeAA.length!==exportSmokeAA.expected)throw new Error('GPU export tile readback length mismatch');if(exportSmoke.unresolved!==0||exportSmokeAA.unresolved!==0)throw new Error('GPU export smoke left unresolved samples: '+JSON.stringify({exportSmoke,exportSmokeAA}));const fixed96Smoke=await api.runFixed96Experiment({track:'seahorse',depths:[8],riskLimits:[1e13],warmups:0,samples:1,qualityW:32,qualityH:24}),sr=fixed96Smoke.rows[0],sv=sr&&sr.variants&&sr.variants[0];if(!sv||!Number.isFinite(sv.fullMedianMs)||sv.remaining!==0||sv.queueIntegrity!==true||Math.abs((sv.dispatchRate||0)-(sv.uncertainRate||0))>1e-9)throw new Error('fixed96 experiment smoke failed: '+JSON.stringify(fixed96Smoke));const diag=frame.contentWindow.__MANDEL_DIAG__.snapshot();if(diag.uncapturedErrors&&diag.uncapturedErrors.length)throw new Error('uncaptured WebGPU errors: '+JSON.stringify(diag.uncapturedErrors));const result={status:'pass',kind:'real-webgpu-acceptance',date:new Date().toISOString(),shaderVersion:diag.shaderVersion,adapter:diag.adapter,limits:diag.limits,routerDecision,fixed96Smoke,exportSmoke,exportSmokeAA,denseRegression,report};out.textContent=JSON.stringify(result,null,2);document.title='PASS v24.2 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result}
|
||||
run().catch(e=>{const result={status:'fail',error:String(e&&e.stack||e)};out.textContent=JSON.stringify(result,null,2);document.title='FAIL v24.2.8 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result});
|
||||
function points(w,h){const xs=[.15,.35,.5,.65,.85].map(t=>Math.min(w-1,Math.max(0,Math.round(t*(w-1))))),ys=[.15,.35,.5,.65,.85].map(t=>Math.min(h-1,Math.max(0,Math.round(t*(h-1)))));return ys.flatMap(y=>xs.map(x=>[x,y]))}
|
||||
async function wait(ms){return new Promise(r=>setTimeout(r,ms))}
|
||||
async function waitApp(){if(frame.contentWindow&&frame.contentWindow.__MANDEL_TEST__)return;await new Promise(r=>frame.addEventListener('load',r,{once:true}));const t=performance.now();while(!frame.contentWindow.__MANDEL_TEST__&&performance.now()-t<15000)await wait(50);if(!frame.contentWindow.__MANDEL_TEST__)throw new Error('test hook not available')}
|
||||
async function numericDeep(api,id,re,im,span,iter){await api.setView({re,im,span,baseIter:iter,adaptive:false,processMode:'standard',renderMode:'accurate'});const st=api.state();if(st.renderMode!=='accurate'||!/正確 reference自動選択|正確 reference再利用/.test(st.engine))throw new Error('accurate mode did not select Deep: '+JSON.stringify(st));const ps=points(st.width,st.height),meta=await api.sampleMeta(ps);let guardMismatch=0,falseEscaped=0,falseBounded=0,unknown=0;for(let i=0;i<ps.length;i++){const[x,y]=ps[i],p=pixel(st,x,y),pg=pixel(st,x,y,st.bits+64),a=orbit(p[0],p[1],st.bits,st.iter),g=orbit(pg[0],pg[1],st.bits+64,st.iter);if(a!==g){guardMismatch++;continue}const m=meta[i],cls=(m>>>28)&3;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a<st.iter)falseBounded++}const row={id,guardMismatch,falseEscaped,falseBounded,unknown};if(guardMismatch||falseEscaped||falseBounded)throw new Error(id+' numeric gate failed: '+JSON.stringify(row));if(unknown!==0)throw new Error(id+': corrected production frame produced UNKNOWN: '+JSON.stringify(row));return row}
|
||||
async function run(){await waitApp();const api=frame.contentWindow.__MANDEL_TEST__,report=[];
|
||||
await api.setView({re:'-0.5',im:'0',span:'3.4',baseIter:350,adaptive:false,processMode:'standard',renderMode:'fast'});let st=api.state();if(st.renderMode!=='fast'||st.backend!=='direct')throw new Error('shallow fast mode did not select Direct: '+JSON.stringify(st));
|
||||
await api.setView({re:'-0.7453983606667815',im:'0.1125046349959942',span:'3.52e-8',baseIter:1200,adaptive:false,processMode:'standard',renderMode:'fast'});st=api.state();if(st.renderMode!=='fast'||st.backend!=='fast-extended'||!st.fastExtended||!/高速拡張/.test(st.engine))throw new Error('deep fast mode did not select precision extension: '+JSON.stringify(st));const fm=await api.sampleMeta(points(st.width,st.height));if(new Set(fm).size<2)throw new Error('fast precision extension collapsed sampled field');
|
||||
report.push(await numericDeep(api,'seahorse-z12','-0.7453983606667815','0.1125046349959942','3.52e-12',2000));
|
||||
const before=api.state();await api.panPixels(24,0);st=api.state();if(!st.referenceReused)throw new Error('pan did not reuse Deep reference: '+JSON.stringify({before,after:st}));
|
||||
const denseMeta=await api.probeMeta({w:61,h:39,strict:true,forceDeep:true,correctUnknown:true});if(denseMeta.some(m=>((m>>>28)&3)===0))throw new Error('forced corrected Deep probe left UNKNOWN');
|
||||
await api.setView({re:'-0.75',im:'0',span:'3.4e-20',baseIter:1000,adaptive:false,processMode:'standard',renderMode:'accurate'});const exportSmoke=await api.smokeExportTile({w:48,h:32,strict:true,ss:1}),exportSmokeAA=await api.smokeExportTile({w:48,h:32,strict:true,ss:2});if(exportSmoke.length!==exportSmoke.expected||exportSmokeAA.length!==exportSmokeAA.expected)throw new Error('GPU export tile readback length mismatch');if(exportSmoke.unresolved!==0||exportSmokeAA.unresolved!==0)throw new Error('GPU export smoke left unresolved samples: '+JSON.stringify({exportSmoke,exportSmokeAA}));
|
||||
const result={status:'pass',kind:'real-webgpu-acceptance',date:new Date().toISOString(),manualModes:true,fastPrecisionExtension:true,panReferenceReuse:true,exportSmoke,exportSmokeAA,report};out.textContent=JSON.stringify(result,null,2);document.title='PASS v24.2.26 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result}
|
||||
run().catch(e=>{const result={status:'fail',error:String(e&&e.stack||e)};out.textContent=JSON.stringify(result,null,2);document.title='FAIL v24.2.26 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result});
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue