This commit is contained in:
33333-33333 2026-09-29 23:45:12 +09:00
commit f5fc9b76ce
73 changed files with 32628 additions and 376 deletions

View file

@ -0,0 +1,57 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
import {performance} from 'node:perf_hooks';
import {createHash} from 'node:crypto';
const root=new URL('../../',import.meta.url);
const html=await fs.readFile(new URL('index.html',root),'utf8');
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
const element={width:1024,height:768,clientWidth:1024,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:this.clientWidth,height:this.clientHeight}}};
const app={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:8,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:1024,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
const cut=scripts[1].indexOf('// ── boot / teardown');
vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.abInternal={state,canvas,snapshot,maxIter,pixelPrecisionBits,adaptiveProbeSources,adaptiveProbeChoice,referenceWorkerSource,certifiedInteriorTiles,integerGridReuseShift,WebGpuRenderer,fixedNum};})();`,app);
const I=app.abInternal;
const corpus=JSON.parse(await fs.readFile(new URL('experiments/benchmark-corpus.json',root),'utf8')).views;
const median=a=>{const b=[...a].sort((x,y)=>x-y),m=b.length>>1;return b.length%2?b[m]:(b[m-1]+b[m])/2};
const mean=a=>a.reduce((s,x)=>s+x,0)/Math.max(1,a.length);
function fromDec(s,bits=256){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [mant,es='0']=s.toLowerCase().split('e'),exp=parseInt(es,10)||0,[i='0',fr='']=mant.split('.');let digits=(i+fr).replace(/^0+(?=\d)/,'')||'0',places=fr.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
function snapOf(v,bits=256){return{bits,re:fromDec(v.re,bits),im:fromDec(v.im,bits),span:fromDec(v.span,bits)}}
function setView(v,w=1024,h=768){I.canvas.width=w;I.canvas.height=h;I.canvas.clientWidth=w;I.canvas.clientHeight=h;const s=snapOf(v);Object.assign(I.state,s);I.state.adaptive=true;I.state.baseIter=350;I.state.continuationBudget=16384;return s}
// A/B 1: strict direct path: current A maintains Brent cycle state even though strict disables classification.
const f=Math.fround;
function analyticF32(cr,ci){cr=f(cr);ci=f(ci);const y2=f(ci*ci),x=f(cr-.25),q=f(f(x*x)+y2),lhs=f(q*f(q+x)),rhs=f(.25*y2),margin=f(16*5.960464477539063e-8*f(f(Math.abs(lhs)+Math.abs(rhs))+1));if(lhs<f(rhs-margin))return true;const x2=f(cr+1),bulb=f(f(x2*x2)+y2),bm=f(16*5.960464477539063e-8*f(f(Math.abs(bulb)+.0625)+1));return bulb<f(.0625-bm)}
function coordF32(view,x,y,w,h){const scale=f(f(view.span)/f(w));return[f(f(view.re)+f(f(f(x+.5)-f(.5*w))*scale)),f(f(view.im)+f(f(f(.5*h)-f(y+.5))*scale))]}
function strictA(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0),n=0,cycleR=f(0),cycleI=f(0),cyclePower=1,cycleLam=0;while(n<maxIter){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;if(f(f(zr*zr)+f(zi*zi))>4)return n;cycleLam++;if(cycleLam>=cyclePower){cycleR=zr;cycleI=zi;cyclePower=Math.min(65536,cyclePower*2);cycleLam=0}}return maxIter}
function strictB(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0),n=0;while(n<maxIter){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;if(f(f(zr*zr)+f(zi*zi))>4)return n}return maxIter}
function benchStrict(){const w=64,h=40,maxIter=4096,rows=[];for(const v of corpus){const pts=[];for(let y=0;y<h;y++)for(let x=0;x<w;x++)pts.push(coordF32(v,x,y,w,h));const run=fn=>{let sum=0;const t0=performance.now();for(const p of pts)sum+=fn(p[0],p[1],maxIter);return{ms:performance.now()-t0,sum}};run(strictA);run(strictB);const A=[],B=[];for(let k=0;k<5;k++){A.push(run(strictA));B.push(run(strictB))}if(A.at(-1).sum!==B.at(-1).sum)throw new Error('strict A/B result mismatch '+v.name);const a=median(A.map(x=>x.ms)),b=median(B.map(x=>x.ms));rows.push({view:v.name,aMs:a,bMs:b,speedup:a/b,changePct:(b/a-1)*100,iterationChecksum:A.at(-1).sum})}return rows}
// A/B 2: exact interior proof with a conservative cheap viewport bounding-box gate.
// Main cardioid is inside x[-3/4,1/4], |y|<3sqrt(3)/8<2/3. Period-2 bulb is inside x[-5/4,-3/4], |y|<=1/4.
function maybeOverlapsKnownInterior(v,w,h){const re=Number(v.re),im=Number(v.im),span=Number(v.span),halfX=span/2,halfY=span*h/(2*w),xl=re-halfX,xh=re+halfX,yl=im-halfY,yh=im+halfY;const hit=(ax,bx,ay,by)=>!(xh<ax||xl>bx||yh<ay||yl>by);return hit(-.75,.375,-2/3,2/3)||hit(-1.25,-.75,-.25,.25)}
function benchInteriorGate(){const sizes=[[1024,768],[2048,2048]],rows=[];for(const [w,h] of sizes){for(const v of corpus){const snap=setView(v,w,h);const runsA=[],runsB=[];let aRes,bRes;for(let k=0;k<7;k++){let t=performance.now();aRes=I.certifiedInteriorTiles(snap,w,h,32,null);runsA.push(performance.now()-t);t=performance.now();bRes=maybeOverlapsKnownInterior(v,w,h)?I.certifiedInteriorTiles(snap,w,h,32,null):{pixels:0};runsB.push(performance.now()-t)}if(aRes.pixels!==bRes.pixels)throw new Error('interior gate false negative '+v.name);rows.push({view:v.name,w,h,gate:maybeOverlapsKnownInterior(v,w,h),certified:aRes.pixels,aMs:median(runsA),bMs:median(runsB),savedMs:median(runsA)-median(runsB)})}}return rows}
// A/B 3: adaptive probe vs always base iteration. Fixed corpus + deterministic exploratory survey.
function scorerVM(){let result=null;const c={performance,postMessage:m=>{result=m},BigInt,Float32Array,Float64Array,ArrayBuffer,DataView,Math,Number,String,Map,Set,Uint32Array};c.self=c;vm.runInNewContext(I.referenceWorkerSource(),c);return{score(snap,iter,w,h,candidates){result=null;const targetBits=Math.max(I.pixelPrecisionBits(snap,w,40)+32,...candidates.map(x=>x.precisionBits||0));c.self.onmessage({data:{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,candidates:candidates.map(x=>({re:x.re.toString(),im:x.im.toString()}))}});if(result?.type==='error')throw new Error(result.error);return result}}}
function probeOne(v,sc,w=1024,h=768){const snap=setView(v,w,h),iter=I.maxIter(),sources=I.adaptiveProbeSources(snap,w,h),t0=performance.now(),res=sc.score(snap,iter,w,h,sources),wall=performance.now()-t0,choice=I.adaptiveProbeChoice(res.escapes,iter,I.state.baseIter),baseWork=choice.work.find(x=>x.initial===I.state.baseIter)?.total??null,chosenWork=choice.work.find(x=>x.initial===choice.iter)?.total??null;return{iter,chosen:choice.iter,probeMs:wall,baseWork,chosenWork,workPenaltyAlwaysBase:chosenWork?baseWork/chosenWork:1,escapes:res.escapes}}
function exploratoryViews(){let seed=0x12345678;const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/4294967296};const anchors=[[-.75,0],[-.743643887, .131825904],[-.1011,.9563],[-1.25066,.02012],[-.16,1.0405],[-.1225611669,.7448617666],[-.5,0]];const spans=[2,1,.4,.1,.03,.01,.003,.001];const out=[];for(let i=0;i<48;i++){const a=anchors[i%anchors.length],sp=spans[(i*3)%spans.length],j=sp*.35;out.push({name:'survey-'+i,re:a[0]+(rnd()-.5)*j,im:a[1]+(rnd()-.5)*j,span:sp})}return out}
function benchProbe(){const sc=scorerVM(),fixed=corpus.map(v=>({view:v.name,...probeOne(v,sc)})),survey=[];for(const v of exploratoryViews())survey.push({view:v.name,...v,...probeOne(v,sc,640,480)});return{fixed,survey,nonBase:survey.filter(x=>x.chosen!==350)}}
// A/B 4: FAST full-screen repair. Tiled post-stats pass covers all compute pixels; symmetry covers the other half.
function coverageCase(w,h,iter,mirror){const r=Object.create(I.WebGpuRenderer.prototype),shape=r.numericTileShape(w,iter),tileW=shape.width,baseRows=r.initialNumericRows(w,iter),seen=new Uint8Array(w*h),computeH=mirror?Math.ceil(h/2):h;let rows=baseRows,y=0,step=0;while(y<computeH){const th=Math.min(rows,computeH-y);for(let x=0;x<w;x+=tileW){const tw=Math.min(tileW,w-x);for(let yy=0;yy<th;yy++)seen.fill(1,(y+yy)*w+x,(y+yy)*w+x+tw)}y+=th;step++;if(step%3===0)rows=Math.max(1,Math.min(computeH-y||1,Math.ceil(rows*.73)));else if(step%4===0)rows=Math.max(1,Math.min(computeH-y||1,rows+1))}if(mirror){for(let yy=0;yy<Math.floor(h/2);yy++){const my=h-1-yy;for(let x=0;x<w;x++)if(seen[yy*w+x])seen[my*w+x]=1}}let holes=0;for(const v of seen)if(!v)holes++;return{w,h,iter,mirror,tileW,initialRows:baseRows,holes,repairThreads:Math.ceil(w/8)*Math.ceil(h/8)*64,minimumMetaReadBytes:w*h*4}}
function benchCoverage(){const cases=[];for(const [w,h] of [[965,543],[1365,768],[1024,768],[2731,1536],[2048,2048],[513,511]])for(const iter of [350,4096,16384])for(const mirror of [false,true])cases.push(coverageCase(w,h,iter,mirror));if(cases.some(x=>x.holes))throw new Error('coverage hole found');return cases}
// A/B 5: numeric history exact-grid reuse hit survey and conditional-residency policy.
function historyPanSurvey(){const configs=[{name:'1920x1080-fast',cssW:1920,cssH:1080,w:965,h:543},{name:'1920x1080-standard',cssW:1920,cssH:1080,w:1365,h:768},{name:'1920x1080-high',cssW:1920,cssH:1080,w:2731,h:1536},{name:'1024x768-standard',cssW:1024,cssH:768,w:1024,h:768},{name:'1024x768-dpr2',cssW:1024,cssH:768,w:2048,h:1536}],rows=[];const base=snapOf(corpus[0]);for(const c of configs){let hits=0,total=0,reused=0;const old={...base,w:c.w,h:c.h};for(let dx=-500;dx<=500;dx++){if(dx===0)continue;const re=old.re-old.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(c.cssW*1e6)),next={...old,re};const sh=I.integerGridReuseShift(old,next,c.w,c.h);total++;if(sh){hits++;reused+=sh.pixels}}const g=(a,b)=>b?g(b,a%b):Math.abs(a),den=c.cssW/g(c.w,c.cssW),policyEligible=den<=8;rows.push({...c,hits,total,hitRate:hits/total,meanReuseRatio:hits?reused/hits/(c.w*c.h):0,reducedScaleDenominator:den,policyEligible,aResidentBytes:8*c.w*c.h,bResidentBytes:policyEligible?8*c.w*c.h:0})}return rows}
// A/B 6: continuation replay surrogate (direct arithmetic): A restarts survivors from zero; B resumes saved z.
function analytic64(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<.25*y2)return true;const x2=cr+1;return x2*x2+y2<.0625}
function continueFrom(cr,ci,maxIter,zr=0,zi=0,n=0){let work=0;for(;n<maxIter;n++){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;work++;if(zr*zr+zi*zi>4)return{escaped:true,n:n+1,zr,zi,work}}return{escaped:false,n,zr,zi,work}}
function continuationReplay(){const w=64,h=40,base=350,target=4096,rows=[];for(const v of corpus){let workA=0,workB=0,survivors=0,mismatch=0;for(let y=0;y<h;y++)for(let x=0;x<w;x++){const scale=v.span/w,cr=v.re+(x+.5-w/2)*scale,ci=v.im+(h/2-y-.5)*scale;if(analytic64(cr,ci))continue;const p=continueFrom(cr,ci,base);workA+=p.work;workB+=p.work;if(!p.escaped){survivors++;const a=continueFrom(cr,ci,target);const b=continueFrom(cr,ci,target,p.zr,p.zi,p.n);workA+=a.work;workB+=b.work;if(a.escaped!==b.escaped||a.n!==b.n)mismatch++}}rows.push({view:v.name,survivors,workA,workB,workReduction:1-workB/Math.max(1,workA),mismatch})}return rows}
function memoryTraffic(){const presets={fast:524288,standard:1048576,high:4194304};const rows=[];for(const [preset,n] of Object.entries(presets)){rows.push({preset,pixels:n,recolorNumericCopyA:8*n,recolorNumericCopyB:0,cpuRgbaRetainedA:4*n,cpuRgbaRetainedB:0,idleColorSnapshotA:12*n,idleColorSnapshotB:0,unknownPrepassMetaReadA:4*n,unknownPrepassMetaReadB:0,fastRepairMetaReadA:4*n,fastRepairMetaReadB:0})}const export512PerSlot=(512*512)*(4+4+4*4+4+4),activeStdFull=1048576*(32+4+4+4)+Math.ceil(1048576/32)*4+24,activeHighFull=4194304*(32+4+4+4)+Math.ceil(4194304/32)*4+24;return{rows,export:{retainedA:3*export512PerSlot,retainedB:0,reallocationSlotsB:3},deep:{standardFullA:activeStdFull,highFullA:activeHighFull,releaseAfterEasyB:0}}}
const results={date:new Date().toISOString(),source:{sha256:createHash('sha256').update(html).digest('hex'),webGpuTiming:'unavailable in container; Chromium exposes no usable WebGPU adapter'},strictCycle:benchStrict(),interiorGate:benchInteriorGate(),adaptiveProbe:benchProbe(),fastCoverage:benchCoverage(),numericHistory:historyPanSurvey(),continuationReplay:continuationReplay(),memoryTraffic:memoryTraffic()};
console.log(JSON.stringify(results,null,2));
if(process.argv[2])await fs.writeFile(process.argv[2],JSON.stringify(results,null,2)+'\n');

View file

@ -0,0 +1,8 @@
import fs from 'node:fs/promises';import vm from 'node:vm';import {performance} from 'node:perf_hooks';
const html=await fs.readFile(new URL('../../index.html',import.meta.url),'utf8'),scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]),kc={};vm.runInNewContext(scripts[0],kc);const element={width:256,height:192,clientWidth:256,clientHeight:192,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},getBoundingClientRect(){return{left:0,top:0,width:256,height:192}}};const app={MANDEL_WEBGPU_KERNELS:kc.MANDEL_WEBGPU_KERNELS,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:8,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:256,innerHeight:192,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};const cut=scripts[1].indexOf('// ── boot / teardown');vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.x={certifiedInteriorTiles};})();`,app);const C=app.x;
function fromDec(s,bits=256){s=String(s);let neg=s[0]=='-';if(neg)s=s.slice(1);const [m,e0='0']=s.toLowerCase().split('e'),e=+e0||0,[i='0',f='']=m.split('.');let d=(i+f).replace(/^0+(?=\d)/,'')||'0',p=f.length-e;if(p<0){d+='0'.repeat(-p);p=0}let den=10n**BigInt(p),v=(BigInt(d)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
function gate(v,w,h){const re=v.re,im=v.im,span=v.span,unit=1n<<BigInt(v.bits),bw=BigInt(w),bh=BigInt(h),xLo=2n*re-span,xHi=2n*re+span,xDen=2n*unit,yLo=2n*bw*im-span*bh,yHi=2n*bw*im+span*bh,yDen=2n*bw*unit,axis=(lo,hi,den,lp,lq,hp,hq)=>hi*BigInt(lq)>=BigInt(lp)*den&&lo*BigInt(hq)<=BigInt(hp)*den;return axis(xLo,xHi,xDen,-3,4,3,8)&&axis(yLo,yHi,yDen,-2,3,2,3)||axis(xLo,xHi,xDen,-5,4,-3,4)&&axis(yLo,yHi,yDen,-1,4,1,4)}
let seed=0x9e3779b9;const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/2**32};let gateFalse=0,falseNeg=0,totalProof=0;for(let i=0;i<240;i++){const span=10**(-4+rnd()*4.7),v={bits:256,re:fromDec(-2+rnd()*3),im:fromDec(-1.2+rnd()*2.4),span:fromDec(span)},g=gate(v,256,192),p=C.certifiedInteriorTiles(v,256,192,32,null).pixels;if(!g)gateFalse++;if(!g&&p){falseNeg++;totalProof+=p}}
// FAST semantic coverage with random seeded/proven/unknown metadata. 0 means untouched hole; any invocation writes nonzero unless a nonzero seed/proof is intentionally retained.
let fastFalse=0,fastCases=0;for(let c=0;c<200;c++){const w=1+Math.floor(rnd()*700),h=1+Math.floor(rnd()*500),mirror=rnd()<.5,computeH=mirror?Math.ceil(h/2):h,meta=new Uint32Array(w*h);for(let i=0;i<meta.length;i++){const q=rnd();meta[i]=q<.1?(3<<28):q<.2?(1<<28)|5:q<.3?123:0}const tileW=1+Math.floor(rnd()*Math.min(w,200)),rows=1+Math.floor(rnd()*Math.min(computeH,80));for(let y=0;y<computeH;y+=rows)for(let x=0;x<w;x+=tileW){const tw=Math.min(tileW,w-x),th=Math.min(rows,computeH-y);for(let yy=0;yy<th;yy++)for(let xx=0;xx<tw;xx++){const i=(y+yy)*w+x+xx,cls=meta[i]>>>28;if(cls===3)continue;if(cls!==0)continue;meta[i]=(1<<28)|1}}if(mirror)for(let y=0;y<Math.floor(h/2);y++)for(let x=0;x<w;x++)meta[(h-1-y)*w+x]=meta[y*w+x]||1;let holes=0;for(const m of meta)if(m===0)holes++;if(holes)fastFalse++;fastCases++}
console.log(JSON.stringify({interiorGateRandom:{cases:240,gateFalse,falseNeg,totalProofInFalseNeg:totalProof},fastCoverageSemantic:{cases:fastCases,casesWithHoles:fastFalse}},null,2));

View file

@ -0,0 +1,13 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
for (const file of process.argv.slice(2)) {
const html=await fs.readFile(file,'utf8');
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
if(scripts.length!==2) throw new Error(file+': expected 2 scripts, got '+scripts.length);
for(const s of scripts)new vm.Script(s);
const kc={}; vm.runInNewContext(scripts[0],kc);
const kernels=kc.MANDEL_WEBGPU_KERNELS;
if(!kernels||Object.keys(kernels).length!==20)throw new Error(file+': kernel bundle malformed');
for(const k of ['DIRECT_F32_WGSL','FAST_PERTURB_WGSL','UNKNOWN_STATS_WGSL','COLOR_WGSL'])if(typeof kernels[k]!=='string')throw new Error(file+': missing '+k);
console.log('PASS',file.split('/').at(-1),'JS syntax, 19 WGSL kernels + version');
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
{
"purpose": "Fixed surrogate corpus for evaluating proposed Mandelbrot viewer improvements without changing production index.html.",
"views": [
{"name":"overview","re":-0.5,"im":0,"span":3.4},
{"name":"period3-bulb","re":-0.1225611668766536,"im":0.7448617666197442,"span":0.10},
{"name":"period3-core","re":-0.1225611668766536,"im":0.7448617666197442,"span":0.025},
{"name":"seahorse-boundary","re":-0.743643887037151,"im":0.13182590420533,"span":0.004},
{"name":"seahorse-deep","re":-0.743643887037151,"im":0.13182590420533,"span":0.000002},
{"name":"exterior","re":0.7,"im":0.2,"span":1.2}
],
"notes": [
"CPU tests are surrogates for algorithmic work and classification behavior, not WebGPU frame-time benchmarks.",
"Deep-zoom tests that depend on arbitrary precision are represented by perturbation/series locality tests rather than full production-equivalent rendering.",
"Do not change these views when comparing future revisions; add new named views instead."
]
}

View file

@ -0,0 +1,40 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
import {performance} from 'node:perf_hooks';
const html=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
const element={width:1024,height:768,clientWidth:1024,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:this.clientWidth,height:this.clientHeight}}};
const app={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:8,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:1024,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
const cut=scripts[1].indexOf('// ── boot / teardown');
vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.auditInternal={state,canvas,snapshot,maxIter,zoomExp,pixelPrecisionBits,adaptiveProbeSources,adaptiveProbeChoice,referenceWorkerSource,certifiedInteriorTiles,integerGridReuseShift,fmtFixed,updateStats};})();`,app);
const I=app.auditInternal;
const corpus=JSON.parse(await fs.readFile(new URL('./benchmark-corpus.json',import.meta.url),'utf8')).views;
const median=a=>{const b=[...a].sort((x,y)=>x-y),m=b.length>>1;return b.length%2?b[m]:(b[m-1]+b[m])/2};
function fromDec(s,bits=256){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [mant,es='0']=s.toLowerCase().split('e'),exp=parseInt(es,10)||0,[i='0',fr='']=mant.split('.');let digits=(i+fr).replace(/^0+(?=\d)/,'')||'0',places=fr.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
function snapOf(v,bits=256){return{bits,re:fromDec(v.re,bits),im:fromDec(v.im,bits),span:fromDec(v.span,bits)}}
function setView(v,w=1024,h=768){I.canvas.width=w;I.canvas.height=h;I.canvas.clientWidth=w;I.canvas.clientHeight=h;const s=snapOf(v);Object.assign(I.state,s);return s}
function benchInteriorProof(){const sizes=[[1024,768],[2048,2048]],rows=[];for(const [w,h] of sizes){for(const v of corpus){const snap=setView(v,w,h);const times=[];let r;for(let k=0;k<7;k++){const t0=performance.now();r=I.certifiedInteriorTiles(snap,w,h,32,null);times.push(performance.now()-t0)}rows.push({view:v.name,w,h,pixels:w*h,certified:r.pixels,certifiedRatio:r.pixels/(w*h),medianMs:median(times),times})}}return rows}
function runScoreWorker(snap,iter,w,h,candidates){let result;const c={performance,postMessage:m=>{result=m},BigInt,Float32Array,Float64Array,ArrayBuffer,DataView,Math,Number,String,Map,Set,Uint32Array};c.self=c;vm.runInNewContext(I.referenceWorkerSource(),c);const targetBits=Math.max(I.pixelPrecisionBits(snap,w,40)+32,...candidates.map(x=>x.precisionBits||0));c.self.onmessage({data:{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,candidates:candidates.map(x=>({re:x.re.toString(),im:x.im.toString()}))}});if(result?.type==='error')throw new Error(result.error);return result}
function benchAdaptiveProbe(){const w=1024,h=768,rows=[];for(const v of corpus){const snap=setView(v,w,h),adaptiveIter=I.maxIter(),candidates=I.adaptiveProbeSources(snap,w,h);const times=[];let res;for(let k=0;k<3;k++){const t0=performance.now();res=runScoreWorker(snap,adaptiveIter,w,h,candidates);times.push(performance.now()-t0)}const choice=I.adaptiveProbeChoice(res.escapes,adaptiveIter,I.state.baseIter);rows.push({view:v.name,adaptiveIter,chosenInitialIter:choice.iter,targetBits:I.pixelPrecisionBits(snap,w,40)+32,samples:candidates.length,scoreReportedMs:res.scoreMs,wallMedianMs:median(times),escapeSamples:res.escapes,workChoices:choice.work})}return rows}
const f=Math.fround;
function analyticF32(cr,ci){cr=f(cr);ci=f(ci);const y2=f(ci*ci),x=f(cr-.25),q=f(f(x*x)+y2),lhs=f(q*f(q+x)),rhs=f(.25*y2),margin=f(16*5.960464477539063e-8*f(f(Math.abs(lhs)+Math.abs(rhs))+1));if(lhs<f(rhs-margin))return true;const x2=f(cr+1),bulb=f(f(x2*x2)+y2),bm=f(16*5.960464477539063e-8*f(f(Math.abs(bulb)+.0625)+1));return bulb<f(.0625-bm)}
function coordF32(view,x,y,w,h){const scale=f(f(view.span)/f(w)),cr=f(f(view.re)+f(f(f(x+.5)-f(.5*w))*scale)),ci=f(f(view.im)+f(f(f(.5*h)-f(y+.5))*scale));return[cr,ci]}
function cmul(a,b){return[f(f(a[0]*b[0])-f(a[1]*b[1])),f(f(a[0]*b[1])+f(a[1]*b[0]))]}
function directBase(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0);for(let n=1;n<=maxIter;n++){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);if(f(f(zr*zr)+f(zi*zi))>4)return n}return maxIter}
function directPeriodic(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0),n=0,cycleR=f(0),cycleI=f(0),cyclePower=1,cycleLam=0,candPeriod=0,candAge=0,candR=f(0),candI=f(0),candMul=[f(1),f(0)],candRepeats=0;while(n<maxIter){const prev=[zr,zi],zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;if(f(f(zr*zr)+f(zi*zi))>4)return n;if(candPeriod>0){candMul=cmul(candMul,[f(2*prev[0]),f(2*prev[1])]);candAge++;if(candAge>=candPeriod){const tol=f(4*5.960464477539063e-8*f(1+Math.max(Math.abs(zr),Math.abs(zi)))),close=Math.max(Math.abs(f(zr-candR)),Math.abs(f(zi-candI)))<=tol,attracting=f(f(candMul[0]*candMul[0])+f(candMul[1]*candMul[1]))<.9025;if(close&&attracting){candRepeats++;if(candRepeats>=2)return n;candR=zr;candI=zi;candAge=0;candMul=[f(1),f(0)]}else{candPeriod=0;candRepeats=0}}}if(candPeriod===0&&n>=64){const tol=f(4*5.960464477539063e-8*f(1+Math.max(Math.abs(zr),Math.abs(zi))));if(Math.max(Math.abs(f(zr-cycleR)),Math.abs(f(zi-cycleI)))<=tol){const period=cycleLam+1;if(period<=2048){candPeriod=period;candAge=0;candR=zr;candI=zi;candMul=[f(1),f(0)];candRepeats=0}}}cycleLam++;if(cycleLam>=cyclePower){cycleR=zr;cycleI=zi;cyclePower=Math.min(65536,cyclePower*2);cycleLam=0}}return maxIter}
function benchPeriodicityRuntime(){const w=64,h=40,maxIter=4096,rows=[];for(const v of corpus){const pts=[];for(let y=0;y<h;y++)for(let x=0;x<w;x++)pts.push(coordF32(v,x,y,w,h));const run=fn=>{let sum=0;const t0=performance.now();for(const [cr,ci] of pts)sum+=fn(cr,ci,maxIter);return{ms:performance.now()-t0,sum}};run(directBase);run(directPeriodic);const b=[],p=[];for(let k=0;k<4;k++){b.push(run(directBase));p.push(run(directPeriodic))}rows.push({view:v.name,baseMedianMs:median(b.map(x=>x.ms)),periodicMedianMs:median(p.map(x=>x.ms)),runtimeRatio:median(p.map(x=>x.ms))/median(b.map(x=>x.ms)),baseIterationSum:b.at(-1).sum,periodicIterationSum:p.at(-1).sum,iterationReduction:1-p.at(-1).sum/Math.max(1,b.at(-1).sum)})}return rows}
function memoryAudit(){const presets={fast:524288,standard:1048576,high:4194304},rows=[];for(const [preset,n] of Object.entries(presets)){rows.push({preset,pixels:n,baseFrameBytes:28*n,numericHistoryBytes:8*n,colorSourceBytes:12*n,deepCorrectionQueueBytes:4*n,cpuFallbackRgbaBytes:4*n,colorAutoNumericHistoryCopyBytesPerSecond:8*n*20,provisionalSnapshotCopyBytesPerSecondAt8Hz:8*n*8})}const export512PerSlot=(512*512)*(4+4+4*4+4+4); // meta,smooth,4 samples,resolve,read
const export256PerSlot=(256*256)*(4+4+4*4+4+4);
return{rows,export:{tile512PerSlotBytes:export512PerSlot,tile512Depth3Bytes:3*export512PerSlot,tile256PerSlotBytes:export256PerSlot,tile256Depth2Bytes:2*export256PerSlot,nonAaUnusedSampleTextureBytesPer512Slot:4*512*512*4}}}
function gridReusePanAudit(){const rows=[];for(const dpr of [1,1.25,1.5,2])for(const cssW of [800,1024,1365]){const w=Math.round(cssW*dpr),h=Math.round(w*.75),old={...snapOf(corpus[0]),w,h};for(const dx of [1,3,10,37,100]){const denom=BigInt(Math.round(cssW*1e6)),re=old.re-old.span*BigInt(Math.round(dx*1e6))/denom,newv={...old,re};const sh=I.integerGridReuseShift(old,newv,w,h);rows.push({dpr,cssW,w,dx,reused:!!sh,sx:sh?.sx??null,pixels:sh?.pixels??0,ratio:sh?sh.pixels/(w*h):0})}}return rows}
const results={date:new Date().toISOString(),sourceSha256:null,interiorProof:benchInteriorProof(),adaptiveProbe:benchAdaptiveProbe(),periodicityRuntimeSurrogate:benchPeriodicityRuntime(),memory:memoryAudit(),gridReusePan:gridReusePanAudit(),staticFindings:{certifyInteriorRunsUnusedUnknownStats:/encodeUnknownStats\(e,\{meta:f\.meta,unresolved:f\.unresolved,n:f\.n\}\);\s*return await this\.submitStage\(e,historySeed\?/.test(html),numericHistoryAlwaysAllocated:/historyMeta:buf\(d,n\*4/.test(html)&&/historySmooth:buf\(d,n\*4/.test(html),commitHistoryAlwaysCommitsNumeric:/this\.commitNumericHistory\(view,state\.fieldView\?\.iter/.test(html),colorAuto20Hz:/state\.rendering\?100:50/.test(html),colorSourceSnapshotCopiesNumeric:/copyBufferToBuffer\(f\.meta,0,source\.meta,0,f\.n\*4\)/.test(html)&&/copyBufferToBuffer\(f\.smooth,0,source\.smooth,0,f\.n\*4\)/.test(html),cpuFallbackRgbaUnused:(html.match(/cpuFrameRgba/g)||[]).length===2,exportWorkspacePersists:!/exportWorkspaceDestroy\(\)/.test(html.slice(html.indexOf('async function runExport'),html.indexOf("$('png').onclick"))),fastCoverageRepair:/fast-coverage-repair/.test(html)&&/this\.encodeFastNumeric\(e,\{pbuf:f\.numericParams/.test(html)}};
console.log(JSON.stringify(results,null,2));
if(process.argv[2])await fs.writeFile(process.argv[2],JSON.stringify(results,null,2)+'\n');

View file

@ -0,0 +1,46 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
import os from 'node:os';
import {Worker} from 'node:worker_threads';
import {performance} from 'node:perf_hooks';
const html=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}};
const app={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
const cut=scripts[1].indexOf('// ── boot / teardown');
vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.internal={fastReferenceWorkerSource,cpuFallbackWorkerSource,chooseBackend};})();`,app);
const internal=app.internal;
const corpus=JSON.parse(await fs.readFile(new URL('./benchmark-corpus.json',import.meta.url),'utf8')).views;
const median=a=>{const b=[...a].sort((x,y)=>x-y),m=b.length>>1;return b.length%2?b[m]:(b[m-1]+b[m])/2};
const f=Math.fround;
function analyticF32(cr,ci){cr=f(cr);ci=f(ci);const y2=f(ci*ci),x=f(cr-.25),q=f(f(x*x)+y2),lhs=f(q*f(q+x)),rhs=f(.25*y2),margin=f(16*5.960464477539063e-8*f(f(Math.abs(lhs)+Math.abs(rhs))+1));if(lhs<f(rhs-margin))return true;const x2=f(cr+1),bulb=f(f(x2*x2)+y2),bm=f(16*5.960464477539063e-8*f(f(Math.abs(bulb)+.0625)+1));return bulb<f(.0625-bm)}
function coordF32(view,x,y,w,h){const scale=f(f(view.span)/f(w)),cr=f(f(view.re)+f(f(f(x+.5)-f(.5*w))*scale)),ci=f(f(view.im)+f(f(f(.5*h)-f(y+.5))*scale));return[cr,ci]}
function cMulF(a,b){return[f(f(a[0]*b[0])-f(a[1]*b[1])),f(f(a[0]*b[1])+f(a[1]*b[0]))]}
function iterateProd(cr,ci,maxIter,periodic){if(analyticF32(cr,ci))return{cls:'analytic',work:0,iter:0};let zr=f(0),zi=f(0),n=0,cycleR=f(0),cycleI=f(0),power=1,lam=0,candPeriod=0,candAge=0,candR=0,candI=0,candMul=[1,0],candRepeats=0;while(n<maxIter){const prev=[zr,zi],zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(f(2*zr)*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;const mag=f(f(zr*zr)+f(zi*zi));if(mag>4)return{cls:'escaped',work:n,iter:n};if(periodic&&candPeriod>0){candMul=cMulF(candMul,[f(2*prev[0]),f(2*prev[1])]);candAge++;if(candAge>=candPeriod){const tol=f(4*5.960464477539063e-8*f(1+Math.max(Math.abs(zr),Math.abs(zi)))),close=Math.max(Math.abs(f(zr-candR)),Math.abs(f(zi-candI)))<=tol,attracting=f(f(candMul[0]*candMul[0])+f(candMul[1]*candMul[1]))<.9025;if(close&&attracting){candRepeats++;if(candRepeats>=2)return{cls:'heuristic',work:n,iter:n};candR=zr;candI=zi;candAge=0;candMul=[1,0]}else{candPeriod=0;candRepeats=0}}}if(periodic&&candPeriod===0&&n>=64){const tol=f(4*5.960464477539063e-8*f(1+Math.max(Math.abs(zr),Math.abs(zi))));if(Math.max(Math.abs(f(zr-cycleR)),Math.abs(f(zi-cycleI)))<=tol){const period=lam+1;if(period<=2048){candPeriod=period;candAge=0;candR=zr;candI=zi;candMul=[1,0];candRepeats=0}}}lam++;if(lam>=power){cycleR=zr;cycleI=zi;power=Math.min(65536,power*2);lam=0}}return{cls:'limit',work:maxIter,iter:maxIter}}
function iterateOracle(cr,ci,maxIter){let zr=0,zi=0;for(let n=1;n<=maxIter;n++){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;if(zr*zr+zi*zi>4)return n}return 0}
function periodicityBench(){const w=96,h=64,maxIter=8192,oracleIter=65536,rows=[];for(const view of corpus){const snap={bits:256,re:fromDec(String(view.re)),im:fromDec(String(view.im)),span:fromDec(String(view.span))},backend=internal.chooseBackend(snap,800).backend;let baseWork=0,heurWork=0,candidates=0,falseCandidates=0,baseLimit=0,heurLimit=0;const t0=performance.now();for(let y=0;y<h;y++)for(let x=0;x<w;x++){const [cr,ci]=coordF32(view,x,y,w,h),b=iterateProd(cr,ci,maxIter,false),q=iterateProd(cr,ci,maxIter,true);baseWork+=b.work;heurWork+=q.work;if(b.cls==='limit')baseLimit++;if(q.cls==='limit')heurLimit++;if(q.cls==='heuristic'){candidates++;if(iterateOracle(cr,ci,oracleIter)>q.iter)falseCandidates++}}rows.push({view:view.name,productionBackend:backend,pixels:w*h,baseWork,heuristicWork:heurWork,workReduction:backend==='direct'?1-heurWork/Math.max(1,baseWork):null,candidates:backend==='direct'?candidates:0,falseCandidates:backend==='direct'?falseCandidates:0,baseLimit,heuristicLimit:backend==='direct'?heurLimit:baseLimit,diagnosticIfForcedDirect:backend==='direct'?null:{workReduction:1-heurWork/Math.max(1,baseWork),candidates,falseCandidates},ms:performance.now()-t0})}return{w,h,maxIter,oracleIter,rows,note:'Production DIRECT_F32 rule with two attracting-cycle confirmations. FAST/perturbation periodic early-stop is intentionally disabled.'}}
function fromDec(s,bits=256){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [mant,es='0']=s.toLowerCase().split('e'),exp=parseInt(es,10)||0,[i='0',fr='']=mant.split('.');let digits=(i+fr).replace(/^0+(?=\d)/,'')||'0',places=fr.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
function workerBuildFast(span){let result;const c={performance,postMessage:m=>{result=m},BigInt,Float32Array,Float64Array,ArrayBuffer,DataView,Math,Number,String,Map,Set,Uint32Array};c.self=c;vm.runInNewContext(internal.fastReferenceWorkerSource(),c);c.self.onmessage({data:{type:'build',id:1,key:'bench',re:fromDec('-0.743643887037151').toString(),im:fromDec('0.13182590420533').toString(),span:fromDec(span).toString(),sourceBits:256,targetBits:320,width:800,height:600,iter:1200}});if(result?.type==='error')throw new Error(result.error);return result}
function cMul(a,b){return[a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]]}
function refsAt(buf,n){const d=new DataView(buf),o=n*16;return[d.getFloat32(o,true)+d.getFloat32(o+8,true),d.getFloat32(o+4,true)+d.getFloat32(o+12,true)]}
function evalSeries(coeffs,dc){let sum=[0,0],pow=[1,0];for(let k=0;k<4;k++){pow=cMul(pow,dc);const a=[coeffs[k*2],coeffs[k*2+1]],t=cMul(a,pow);sum=[sum[0]+t[0],sum[1]+t[1]]}return sum}
function directDelta(refs,jump,dc){let d=[0,0];for(let n=0;n<jump;n++){const z=refsAt(refs,n),lin=cMul([2*z[0],2*z[1]],d),sq=cMul(d,d);d=[lin[0]+sq[0]+dc[0],lin[1]+sq[1]+dc[1]]}return d}
function seriesBench(){const spans=['1e-5','1e-7','1e-9','1e-11'],rows=[];for(const spanText of spans){const r=workerBuildFast(spanText),span=Number(spanText),samples=[[-.5,-.375],[-.5,.375],[.5,-.375],[.5,.375],[0,-.375],[0,.375],[-.5,0],[.5,0]];let maxAbsError=0,maxRelError=0;for(const [x,y] of samples){const dc=[x*span,y*span],p=evalSeries(r.series.coeffs,dc),e=directDelta(r.refs,r.series.jump,dc),ae=Math.hypot(p[0]-e[0],p[1]-e[1]),re=ae/Math.max(1e-30,Math.hypot(e[0],e[1]));maxAbsError=Math.max(maxAbsError,ae);maxRelError=Math.max(maxRelError,re)}rows.push({span:Number(spanText),jump:r.series.jump,errorLog2:r.series.errorLog2,maxAbsError,maxRelativeError:maxRelError,refLen:r.refLen,buildMs:r.buildMs})}return{rows,note:'Production fast-reference worker output. Error compares packed 4th-order series against direct perturbation over 8 screen-domain deltas.'}}
function sparseMemoryBench(){const presets={fast:524288,standard:1048576,high:4194304},rows=[];for(const [preset,n] of Object.entries(presets))for(const activeRatio of [1,.25,.1,.01]){const oldBytes=44*n,capacity=Math.min(n,Math.max(Math.ceil(n*activeRatio),Math.ceil(n*activeRatio*1.25)+256)),newBytes=44*capacity+4*Math.ceil(n/32),reduction=1-newBytes/oldBytes;rows.push({preset,pixels:n,activeRatio,capacity,oldBytes,newBytes,reduction})}return{rows,note:'Production allocation: 32 B state + 2x4 B queues + 4 B pixel map per active slot, plus 1-bit membership bitmap. Capacity adds 25% + 256 headroom.'}}
// Same perturbation locality surrogate as the first evaluation; production now requests up to 3 failure-guided references instead of one.
function analytic(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;return q*(q+x)<.25*y2||(cr+1)*(cr+1)+y2<.0625}
function iterate(cr,ci,maxIter){if(analytic(cr,ci))return{cls:'analytic',iter:0};let zr=0,zi=0;for(let n=1;n<=maxIter;n++){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;if(zr*zr+zi*zi>4)return{cls:'escaped',iter:n}}return{cls:'limit',iter:maxIter}}
function coord(view,x,y,w,h){const scale=view.span/w;return[view.re+(x+.5-w/2)*scale,view.im+(h/2-y-.5)*scale]}
function mMul(ar,ai,br,bi){return[ar*br-ai*bi,ar*bi+ai*br]}
function mFMul(ar,ai,br,bi){return[f(f(ar*br)-f(ai*bi)),f(f(ar*bi)+f(ai*br))]}
function buildReference(cr,ci,maxIter){const re=new Float64Array(maxIter+1),im=new Float64Array(maxIter+1),hiRe=new Float32Array(maxIter+1),hiIm=new Float32Array(maxIter+1),loRe=new Float32Array(maxIter+1),loIm=new Float32Array(maxIter+1);let zr=0,zi=0,valid=maxIter;for(let n=0;n<=maxIter;n++){re[n]=zr;im[n]=zi;hiRe[n]=f(zr);hiIm[n]=f(zi);loRe[n]=f(zr-hiRe[n]);loIm[n]=f(zi-hiIm[n]);if(n===maxIter)break;const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;if(!Number.isFinite(zr+zi)||Math.hypot(zr,zi)>1e18){valid=n;break}}return{cr,ci,re,im,hiRe,hiIm,loRe,loIm,valid}}
function mPerturbEscape(cRe,cIm,ref,maxIter){if(ref.valid<maxIter)return{cls:'reference-end',iter:ref.valid};let wr=f(0),wi=f(0),dr=f(cRe-ref.cr),di=f(cIm-ref.ci);for(let n=0;n<maxIter;n++){const zr=ref.hiRe[n]+ref.loRe[n]+wr,zi=ref.hiIm[n]+ref.loIm[n]+wi,mag=zr*zr+zi*zi;if(mag>4)return{cls:'escaped',iter:n};const [lr,li]=mFMul(ref.hiRe[n],ref.hiIm[n],wr,wi),[lr2,li2]=mFMul(ref.loRe[n],ref.loIm[n],wr,wi),[sr,si]=mFMul(wr,wi,wr,wi);wr=f(f(2*f(lr+lr2))+sr+dr);wi=f(f(2*f(li+li2))+si+di)}const n=maxIter,zr=ref.hiRe[n]+ref.loRe[n]+wr,zi=ref.hiIm[n]+ref.loIm[n]+wi;return zr*zr+zi*zi>4?{cls:'escaped',iter:n}:{cls:'limit',iter:maxIter}}
function perturbGrid(view,w,h,maxIter,grid){const refs=[],seen=new Set(),add=(x,y)=>{const key=`${x.toFixed(6)}:${y.toFixed(6)}`;if(seen.has(key))return;seen.add(key);const [cr,ci]=coord(view,x,y,w,h);refs.push({...buildReference(cr,ci,maxIter),x,y})};add((w-1)/2,(h-1)/2);if(grid>1)for(let gy=0;gy<grid;gy++)for(let gx=0;gx<grid;gx++)add((gx+.5)*w/grid-.5,(gy+.5)*h/grid-.5);let mismatchClass=0,mismatchIter=0,refEnd=0,total=0;for(let y=0;y<h;y++)for(let x=0;x<w;x++){const [cr,ci]=coord(view,x,y,w,h),truth=iterate(cr,ci,maxIter);if(truth.cls==='analytic')continue;let ref=null,best=Infinity;for(const r of refs){if(r.valid<maxIter)continue;const d=(x-r.x)**2+(y-r.y)**2;if(d<best){best=d;ref=r}}if(!ref){refEnd++;continue}const p=mPerturbEscape(cr,ci,ref,maxIter);total++;const tc=truth.cls==='escaped'?'escaped':'limit';if(p.cls!==tc)mismatchClass++;else if(tc==='escaped'&&Math.abs(p.iter-truth.iter)>1)mismatchIter++}return{grid,references:refs.length,total,refEnd,mismatchClass,mismatchIter,mismatchRate:mismatchClass/Math.max(1,total)}}
function multiReferenceBench(){const w=64,h=40,maxIter=1200,views=[{name:'seahorse-1e-5',re:-.743643887037151,im:.13182590420533,span:1e-5},{name:'seahorse-1e-7',re:-.743643887037151,im:.13182590420533,span:1e-7}],rows=[];for(const v of views)for(const grid of [1,2,3])rows.push({view:v.name,...perturbGrid(v,w,h,maxIter,grid)});return{maxGuidedPasses:(html.match(/FAILURE_GUIDED_REF_MAX_PASSES=(\d+)/)||[])[1]|0,w,h,maxIter,rows,note:'CPU f32 perturbation locality surrogate. Production selects up to three additional references from failure clusters instead of fixed grids.'}}
function makeNodeWorkerSource(){return `const {parentPort}=require('node:worker_threads');globalThis.self=globalThis;globalThis.postMessage=(m,t)=>parentPort.postMessage(m,t);${internal.cpuFallbackWorkerSource()}\nparentPort.on('message',d=>self.onmessage({data:d}));`}
async function cpuFallbackBench(){const source=makeNodeWorkerSource(),w=640,h=360,iter=350,threads=Math.min(4,Math.max(2,os.cpus().length));const payload=(y0,y1,id=1)=>({type:'render',id,w,h,y0,y1,maxIter:iter,re:-.5,im:0,span:3.4,style:{palette:0,cycle:1.0,shift:0}});async function run(count){const workers=Array.from({length:count},()=>new Worker(source,{eval:true}));const once=()=>Promise.all(workers.map((wk,i)=>new Promise((resolve,reject)=>{wk.once('message',resolve);wk.once('error',reject);wk.postMessage(payload(Math.floor(h*i/count),Math.floor(h*(i+1)/count),i+1))})));await once();const times=[];let parts;for(let k=0;k<7;k++){const t0=performance.now();parts=await once();times.push(performance.now()-t0)}await Promise.all(workers.map(w=>w.terminate()));return{medianMs:median(times),times,work:parts.reduce((s,p)=>s+p.work,0),escaped:parts.reduce((s,p)=>s+p.escaped,0),operationLimit:parts.reduce((s,p)=>s+p.operationLimit,0),heuristicInterior:parts.reduce((s,p)=>s+p.heuristicInterior,0)}}const one=await run(1),multi=await run(threads);return{w,h,iter,threads,one,multi,speedup:one.medianMs/multi.medianMs,workMatch:one.work===multi.work,escapedMatch:one.escaped===multi.escaped,operationLimitMatch:one.operationLimit===multi.operationLimit,heuristicMatch:one.heuristicInterior===multi.heuristicInterior,note:'Production CPU fallback Worker source executed under node:worker_threads shim.'}}
const results={date:new Date().toISOString(),periodicity:periodicityBench(),series:seriesBench(),sparseActiveState:sparseMemoryBench(),multiReference:multiReferenceBench(),cpuFallback:await cpuFallbackBench(),sourceAudit:{heuristicClass:/FIELD_INTERIOR_HEURISTIC:u32=2u/.test(html),strictDisablesPeriodicity:/candPeriod==0u && n>=64u/.test(html)&&!/cycleReady/.test(kernels.FAST_PERTURB_WGSL),seriesBinding:/binding\(5\).*series:SeriesData/s.test(html),compactPixelMap:/deepActivePixel/.test(html)&&/pixelMap/.test(kernels.DEEP_ACTIVE_CONTINUE_WGSL),multiReferencePasses:/FAILURE_GUIDED_REF_MAX_PASSES=3/.test(html),cpuFallback:/class CpuFallbackRenderer/.test(html)},environment:{node:process.version,cpus:os.cpus().length,platform:process.platform,arch:process.arch}};
console.log(JSON.stringify(results,null,2));
if(process.argv[2])await fs.writeFile(process.argv[2],JSON.stringify(results,null,2)+'\n');

View file

@ -0,0 +1,47 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
import {createHash} from 'node:crypto';
const beforeUrl=new URL('../../docs/reported-view-bugfix/index.before.html',import.meta.url);
const afterUrl=new URL('../../index.html',import.meta.url);
const view={
bits:273,
re:'-15958919693715511752069626580195447581643016686947107473855747766272418724092005269',
im:'3884297345146583343999140717464554120497735725078805729405733239132420430333375719',
span:'35679807704506851074208413824041445358585910697267232113437882789120793',
width:1108,height:1582,iter:1536,
};
function extract(html){return [...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1])}
function appInternal(html){
const scripts=extract(html);const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
const element={width:view.width,height:view.height,clientWidth:view.width,clientHeight:view.height,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:view.width,height:view.height}}};
const context={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>`blob:test`,revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:view.width,innerHeight:view.height,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
const cut=scripts[1].indexOf('// ── boot / teardown');
const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={fastReferenceWorkerSource,pixelPrecisionBits};})();`;
vm.runInNewContext(code,context);return context.internal;
}
function runWorker(source,message){let result;const wc={self:{},postMessage:r=>{result=r},performance};vm.runInNewContext(source,wc);wc.self.onmessage({data:message});if(!result)throw new Error('worker returned nothing');if(result.type==='error')throw new Error(result.error);return result}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function fixedLog2Abs(v,bits){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-bits}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
function spanMantExp(span,bits){const l=fixedLog2Abs(span,bits),exp=Math.floor(l);return{mant:Math.pow(2,l-exp),exp}}
function complexMul(ar,ai,br,bi){return [Math.fround(Math.fround(ar*br)-Math.fround(ai*bi)),Math.fround(Math.fround(ar*bi)+Math.fround(ai*br))]}
function nonFiniteSeriesStarts(series,refX,refY,spanMant,width,height,cols=32,rows=32){
if(!series?.jump)return 0;let bad=0;const c=series.coeffs.map(Math.fround);
for(let jy=0;jy<rows;jy++)for(let ix=0;ix<cols;ix++){
const gx=(ix+.5)*width/cols,gy=(jy+.5)*height/rows,dx=Math.fround((gx-refX)/width),dy=Math.fround((refY-gy)/width),d=[Math.fround(spanMant*dx),Math.fround(spanMant*dy)];
const d2=complexMul(d[0],d[1],d[0],d[1]),d4=complexMul(d2[0],d2[1],d2[0],d2[1]);
const t4=complexMul(c[6],c[7],d4[0],d4[1]);
if(!Number.isFinite(t4[0])||!Number.isFinite(t4[1]))bad++;
}
return bad;
}
async function analyze(path){
const html=await fs.readFile(path,'utf8'),a=appInternal(html);const snap={bits:view.bits,re:BigInt(view.re),im:BigInt(view.im),span:BigInt(view.span)};const targetBits=a.pixelPrecisionBits(snap,view.width,40);
const result=runWorker(a.fastReferenceWorkerSource(),{type:'build',id:1,key:'reported-view',sourceBits:view.bits,targetBits,re:view.re,im:view.im,span:view.span,width:view.width,height:view.height,iter:view.iter});
const refRe=BigInt(result.referenceRe),refIm=BigInt(result.referenceIm),span=BigInt(view.span),refX=view.width*.5+fixedRatio(refRe-BigInt(view.re),span)*view.width,refY=view.height*.5-fixedRatio(refIm-BigInt(view.im),span)*view.width,se=spanMantExp(span,view.bits);
return {sha256:createHash('sha256').update(html).digest('hex'),targetBits,refLen:result.refLen,selectionEscape:result.selectionEscape,referencePixel:{x:refX,y:refY},series:result.series,nonFiniteSeriesStartSamples:nonFiniteSeriesStarts(result.series,refX,refY,se.mant,view.width,view.height),seriesSampleCount:32*32};
}
const before=await analyze(beforeUrl),after=await analyze(afterUrl);const output={view,before,after,checks:{baselineSeriesActive:before.series.jump>0,baselineHasNonFiniteSeriesStarts:before.nonFiniteSeriesStartSamples>0,patchedSeriesDisabled:after.series.jump===0&&after.nonFiniteSeriesStartSamples===0}};
console.log(JSON.stringify(output,null,2));