307 lines
27 KiB
JavaScript
307 lines
27 KiB
JavaScript
|
|
// Connect to a separate Chromium/Edge instance with remote debugging enabled.
|
|||
|
|
// Usage: node tests/browser_smoke.mjs [port=9333] [reset|fast|mid|legacy|axis|black|ui|all]
|
|||
|
|
import assert from 'node:assert/strict';
|
|||
|
|
import {setTimeout as sleep} from 'node:timers/promises';
|
|||
|
|
|
|||
|
|
const port=Number(process.argv[2]||9333),selection=process.argv[3]||'all';
|
|||
|
|
const url=new URL('../index.html?test',import.meta.url).href;
|
|||
|
|
const response=await fetch(`http://127.0.0.1:${port}/json/list`);
|
|||
|
|
assert.ok(response.ok,'Remote debugging endpoint unavailable');
|
|||
|
|
const pages=(await response.json()).filter(t=>t.type==='page');
|
|||
|
|
assert.equal(pages.length,1,'Use one dedicated test tab');
|
|||
|
|
const target=pages[0],ws=new WebSocket(target.webSocketDebuggerUrl);
|
|||
|
|
await new Promise((resolve,reject)=>{ws.addEventListener('open',resolve,{once:true});ws.addEventListener('error',reject,{once:true})});
|
|||
|
|
let serial=0;const pending=new Map(),errors=[];
|
|||
|
|
ws.addEventListener('message',event=>{
|
|||
|
|
const m=JSON.parse(event.data);
|
|||
|
|
if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);
|
|||
|
|
const p=pending.get(m.id);if(!p)return;pending.delete(m.id);clearTimeout(p.timer);
|
|||
|
|
if(m.error)p.reject(new Error(JSON.stringify(m.error)));else p.resolve(m.result);
|
|||
|
|
});
|
|||
|
|
function command(method,params={}){
|
|||
|
|
const id=++serial;
|
|||
|
|
return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{pending.delete(id);reject(new Error(method+' timeout'))},240000);pending.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}))});
|
|||
|
|
}
|
|||
|
|
async function evaluate(expression){
|
|||
|
|
const result=await command('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});
|
|||
|
|
if(result.exceptionDetails)throw new Error(result.exceptionDetails.exception?.description||result.exceptionDetails.text);
|
|||
|
|
return result.result.value;
|
|||
|
|
}
|
|||
|
|
const cases=[
|
|||
|
|
{id:'reset',re:'-0.5',im:'0',span:'3.4',renderMode:'fast',backend:'direct'},
|
|||
|
|
{id:'fast',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',renderMode:'fast',backend:'fast-extended'},
|
|||
|
|
{id:'mid',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000001',renderMode:'fast',backend:'fast-extended'},
|
|||
|
|
{id:'legacy',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',renderMode:'accurate',backend:'fast-extended'},
|
|||
|
|
{id:'axis',re:'-0.75',im:'0',span:'0.02',renderMode:'fast',backend:'direct'},
|
|||
|
|
{id:'black',re:'-29466147000382485924219538765424656674342940730553342389512209954887705938978413587',im:'5179018789925933872641942859702187326563010594882200888329237785122225251258352',span:'250637324516887125119843167990565159991251418774166207381437546036409',bits:273,baseIter:350,adaptive:true,fixed:true,renderMode:'accurate',backend:'fast-extended'}
|
|||
|
|
];
|
|||
|
|
assert.ok(['all','kernels','cancel','bench','budget','ui'].includes(selection)||cases.some(c=>c.id===selection),'Unknown case');
|
|||
|
|
|
|||
|
|
// Execute the real production kernels on small, fully specified states.
|
|||
|
|
// This isolates terminal and queue behavior from render convergence heuristics.
|
|||
|
|
async function kernelCases(){
|
|||
|
|
const {renderer:r}=globalThis.__MANDEL_TEST__.kernelAccess();
|
|||
|
|
await r.ensureDeepActivePipelines();
|
|||
|
|
const d=r.device,B=GPUBufferUsage,created=[],reports=[];
|
|||
|
|
const check=(value,message)=>{if(!value)throw new Error(message)};
|
|||
|
|
const buffer=(size,usage)=>{const b=d.createBuffer({size:Math.max(4,size),usage});created.push(b);return b};
|
|||
|
|
async function read(b,bytes){const staging=buffer(bytes,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(b,0,staging,0,bytes);d.queue.submit([e.finish()]);await staging.mapAsync(GPUMapMode.READ);const out=staging.getMappedRange().slice(0);staging.unmap();return out}
|
|||
|
|
async function run({count=1,target=3,chunk=1,passes=3,orbit=[0,0,0,0],error=0,m=0,n=0}){
|
|||
|
|
const capacity=Math.max(1,count),params=new ArrayBuffer(96),p=new DataView(params);
|
|||
|
|
[capacity,1,capacity,1,0,0,150000,orbit.length-1,0,0,capacity,0].forEach((v,i)=>p.setUint32(i*4,v,true));
|
|||
|
|
p.setInt32(52,0,true);p.setUint32(84,target,true);p.setUint32(88,chunk,true);
|
|||
|
|
const pb=buffer(96,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pb,0,params);
|
|||
|
|
const refs=buffer(orbit.length*16,B.STORAGE|B.COPY_DST),refData=new Float32Array(orbit.length*4);orbit.forEach((v,i)=>refData[i*4]=v);d.queue.writeBuffer(refs,0,refData);
|
|||
|
|
const states=buffer(capacity*32,B.STORAGE|B.COPY_DST|B.COPY_SRC),initial=new ArrayBuffer(capacity*32),sv=new DataView(initial);
|
|||
|
|
for(let i=0;i<count;i++){sv.setUint32(i*32+20,n,true);sv.setUint32(i*32+24,m,true);sv.setFloat32(i*32+28,error,true)}d.queue.writeBuffer(states,0,initial);
|
|||
|
|
const queues=[0,1].map(()=>buffer(capacity*4,B.STORAGE|B.COPY_DST|B.COPY_SRC)),counts=[0,1].map(()=>buffer(4,B.STORAGE|B.COPY_DST|B.COPY_SRC)),indirect=buffer(12,B.STORAGE|B.INDIRECT|B.COPY_DST);
|
|||
|
|
if(count)d.queue.writeBuffer(queues[0],0,Uint32Array.from({length:count},(_,i)=>i));d.queue.writeBuffer(counts[0],0,Uint32Array.of(count));
|
|||
|
|
const meta=buffer(capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buffer(capacity*4,B.STORAGE|B.COPY_SRC);
|
|||
|
|
const e=d.createCommandEncoder();let input=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB:states,queues,counts,indirect,meta,smooth,input:0,passes,refsB:refs});d.queue.submit([e.finish()]);
|
|||
|
|
const active=new Uint32Array(await read(counts[input],4))[0],field=new Uint32Array(await read(meta,capacity*4));
|
|||
|
|
const live=active?Array.from(new Uint32Array(await read(queues[input],active*4))).sort((a,b)=>a-b):[];
|
|||
|
|
return{active,field,live,states:new DataView(await read(states,capacity*32)),async resume(nextTarget,nextPasses){p.setUint32(84,nextTarget,true);d.queue.writeBuffer(pb,0,params);const e=d.createCommandEncoder();input=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB:states,queues,counts,indirect,meta,smooth,input,passes:nextPasses,refsB:refs});d.queue.submit([e.finish()]);return{active:new Uint32Array(await read(counts[input],4))[0],field:new Uint32Array(await read(meta,capacity*4))}}};
|
|||
|
|
}
|
|||
|
|
try{
|
|||
|
|
let x=await run({orbit:[0,1,2,5]});check(x.active===0&&x.field[0]===(1<<28|3),'escape exactly at target');
|
|||
|
|
x=await run({target:1,n:1,m:1,passes:1,error:.002});check(x.active===0&&x.field[0]===(1<<20|1),'terminal error guard');
|
|||
|
|
x=await run({target:2,passes:2,orbit:[0]});check(x.active===0&&x.field[0]===(3<<20),'reference exhaustion');
|
|||
|
|
x=await run({target:1,passes:1,orbit:[0],m:1});check(x.active===0&&x.field[0]===(3<<20),'reference index guard');
|
|||
|
|
for(const count of [0,1,63,64,65])for(const passes of [1,2,3,4]){
|
|||
|
|
const target=passes;x=await run({count,target,passes,orbit:Array(10).fill(0)});
|
|||
|
|
check(x.active===count,'queue count');check(x.live.every((v,i)=>v===i),'queue membership');
|
|||
|
|
for(let i=0;i<count;i++){check(x.field[i]===(6<<20|target),'target metadata');check(x.states.getUint32(i*32+20,true)===target,'retained iteration')}
|
|||
|
|
const next=await x.resume(target+1,1);check(next.active===count,'target resubmission');
|
|||
|
|
for(let i=0;i<count;i++)check(next.field[i]===(6<<20|target+1),'next target metadata');
|
|||
|
|
}
|
|||
|
|
reports.push('target escape/error/reference bounds; 0/1/63/64/65 queues; 1–4 passes; retained states');
|
|||
|
|
await r.ensureCorrectionPipelines();await r.ensureCompactActivePipelines();
|
|||
|
|
{
|
|||
|
|
const width=17,height=5,n=width*height,initial=Uint32Array.from({length:n},(_,i)=>i%7===6?1<<28:(i%7)<<20|i);
|
|||
|
|
const p=new Uint32Array(24);p.set([9,3,width,height,3,1,100,0,0,2,width,width+3]);
|
|||
|
|
const pb=buffer(96,B.UNIFORM|B.COPY_DST),meta=buffer(n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),buckets=buffer(96,B.STORAGE|B.COPY_DST),stats=buffer(32,B.STORAGE|B.COPY_SRC|B.COPY_DST),queue=buffer(n*4,B.STORAGE|B.COPY_SRC),indirect=buffer(16,B.STORAGE|B.INDIRECT);
|
|||
|
|
d.queue.writeBuffer(pb,0,p);d.queue.writeBuffer(meta,0,initial);const e=d.createCommandEncoder();
|
|||
|
|
r.encodeDeepBucketHistogram(e,{pbuf:pb,meta,bucketState:buckets,w:9,h:3});r.encodeDeepBucketPrefix(e,{bucketState:buckets,queueStats:stats,indirect});r.encodeDeepBucketScatter(e,{pbuf:pb,meta,bucketState:buckets,queueStats:stats,queue,w:9,h:3});d.queue.submit([e.finish()]);
|
|||
|
|
const q=new Uint32Array(await read(stats,32)),expected=[];
|
|||
|
|
for(let y=1;y<4;y++)for(let x=3;x<12;x++){const i=y*width+x;if(i%7>=1&&i%7<=5)expected.push(i)}
|
|||
|
|
check(q[0]===expected.length&&q[1]===0&&q[2]===expected.length,'tile queue counts');
|
|||
|
|
const actual=Array.from(new Uint32Array(await read(queue,q[0]*4))).sort((a,b)=>a-b);check(JSON.stringify(actual)===JSON.stringify(expected),'tile queue exact membership');
|
|||
|
|
if(!r.precisionScatter)r.precisionScatter=await r.makeCompute(await r.module('precision-scatter-test',globalThis.MANDEL_WEBGPU_KERNELS.PRECISION_SCATTER_WGSL));
|
|||
|
|
const results=new Uint32Array(n*4);for(let i=0;i<n;i++)results.set([i,1<<28|7,0,i%2],i*4);
|
|||
|
|
const input=buffer(results.byteLength,B.STORAGE|B.COPY_DST),sm=buffer(n*4,B.STORAGE);d.queue.writeBuffer(input,0,results);
|
|||
|
|
const enc=d.createCommandEncoder(),bg=d.createBindGroup({layout:r.precisionScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:input}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:sm}}]}),pass=enc.beginComputePass();pass.setPipeline(r.precisionScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(2);pass.end();d.queue.submit([enc.finish()]);
|
|||
|
|
const applied=new Uint32Array(await read(meta,n*4));for(let i=0;i<n;i++)check(applied[i]===(i%2&&i%7>=1&&i%7<=5?(1<<28|7):initial[i]),'scatter preserves completed/operation-limit pixels');
|
|||
|
|
}
|
|||
|
|
{
|
|||
|
|
const pb=buffer(96,B.UNIFORM|B.COPY_DST),p=new Uint32Array(24);p.set([9,1,9,1,0,0,150000,4,0,3,9,0]);p[21]=3;p[22]=1;d.queue.writeBuffer(pb,0,p);
|
|||
|
|
const stateB=buffer(3*40,B.STORAGE|B.COPY_SRC),queues=[0,1].map(()=>buffer(12,B.STORAGE|B.COPY_SRC|B.COPY_DST)),counts=[0,1].map(()=>buffer(4,B.STORAGE|B.COPY_SRC|B.COPY_DST)),indirect=buffer(12,B.STORAGE|B.INDIRECT),refsB=buffer(5*16,B.STORAGE),meta=buffer(9*4,B.STORAGE|B.COPY_SRC),sm=buffer(9*4,B.STORAGE);
|
|||
|
|
d.queue.writeBuffer(queues[0],0,Uint32Array.of(8,2,5));d.queue.writeBuffer(counts[0],0,Uint32Array.of(3));
|
|||
|
|
const e=d.createCommandEncoder();r.encodeDeepActiveResumeInit(e,{pbuf:pb,stateB,queue:queues[0],count:3,pipeline:r.deepCompactInit});
|
|||
|
|
const output=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB,queues,counts,indirect,meta,smooth:sm,input:0,passes:3,refsB,pipeline:r.deepCompactContinue});d.queue.submit([e.finish()]);
|
|||
|
|
const field=new Uint32Array(await read(meta,36)),states=new DataView(await read(stateB,120));
|
|||
|
|
for(let i=0;i<9;i++)check(field[i]===([8,2,5].includes(i)?6<<20|3:0),'compact pixel mapping');
|
|||
|
|
for(let i=0;i<3;i++){check(states.getUint32(i*40+32,true)===[8,2,5][i],'compact slot identity');check(states.getUint32(i*40+20,true)===3,'compact state retained')}
|
|||
|
|
check(new Uint32Array(await read(counts[output],4))[0]===3,'compact queue retained');
|
|||
|
|
}
|
|||
|
|
reports.push('tile failure queue exact membership; scatter ownership; compact slot identity');
|
|||
|
|
{
|
|||
|
|
const width=17,height=5,n=width*height,whole=buffer(n*4,B.STORAGE|B.COPY_SRC),pieces=buffer(n*4,B.STORAGE|B.COPY_SRC),sm=buffer(n*4,B.STORAGE);
|
|||
|
|
const unit=1n<<256n,snap={bits:256,re:-unit/2n,im:0n,span:34n*unit/10n},e=d.createCommandEncoder();
|
|||
|
|
const encode=(meta,x,y,w,h)=>{const pb=buffer(96,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pb,0,r.directParams(w,h,width,height,x,y,100,snap,.5,.5,0,width,y*width+x));r.encodeDirectNumeric(e,{pbuf:pb,meta,smooth:sm,w,h})};
|
|||
|
|
encode(whole,0,0,width,height);
|
|||
|
|
for(let y=0;y<height;y+=2)for(let x=0;x<width;x+=6)encode(pieces,x,y,Math.min(6,width-x),Math.min(2,height-y));
|
|||
|
|
d.queue.submit([e.finish()]);const expected=new Uint32Array(await read(whole,n*4)),actual=new Uint32Array(await read(pieces,n*4));
|
|||
|
|
check(expected.every((v,i)=>v!==0&&v===actual[i]),'direct tiles reproduce whole-frame coordinates');
|
|||
|
|
}
|
|||
|
|
reports.push('direct tile coordinates and full pixel coverage');
|
|||
|
|
return reports;
|
|||
|
|
}finally{await d.queue.onSubmittedWorkDone();for(const b of created)b.destroy()}
|
|||
|
|
}
|
|||
|
|
// Read the actual texture selected by presentation, including animation during
|
|||
|
|
// numeric work. UI text alone cannot prove that colors are changing.
|
|||
|
|
async function displayedColorHash(){
|
|||
|
|
const r=globalThis.__MANDEL_TEST__.kernelAccess().renderer,f=r.frame,d=r.device;
|
|||
|
|
const texture=r.colorSourceVisible?r.colorSource.texture:f.front;
|
|||
|
|
const pitch=Math.ceil(f.w*4/256)*256,b=d.createBuffer({size:pitch*f.h,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ});
|
|||
|
|
try{
|
|||
|
|
const e=d.createCommandEncoder();e.copyTextureToBuffer({texture},{buffer:b,bytesPerRow:pitch},[f.w,f.h]);d.queue.submit([e.finish()]);
|
|||
|
|
await b.mapAsync(GPUMapMode.READ);const data=new Uint8Array(b.getMappedRange()),pixels=new Uint8Array(f.n*4);
|
|||
|
|
for(let y=0;y<f.h;y++)pixels.set(data.subarray(y*pitch,y*pitch+f.w*4),y*f.w*4);
|
|||
|
|
const digest=await crypto.subtle.digest('SHA-256',pixels);b.unmap();return Array.from(new Uint8Array(digest),x=>x.toString(16).padStart(2,'0')).join('');
|
|||
|
|
}finally{b.destroy()}
|
|||
|
|
}
|
|||
|
|
try{
|
|||
|
|
await command('Runtime.enable');
|
|||
|
|
await command('Emulation.setDeviceMetricsOverride',{width:320,height:240,deviceScaleFactor:1,mobile:false});
|
|||
|
|
await command('Page.navigate',{url});
|
|||
|
|
for(let i=0;i<100;i++){if(await evaluate('!!globalThis.__MANDEL_TEST__'))break;await sleep(100)}
|
|||
|
|
const ready=await evaluate('globalThis.__MANDEL_TEST__.ensureGpuReady()');
|
|||
|
|
assert.equal(ready.ready,true,JSON.stringify(ready));
|
|||
|
|
if(selection==='ui'||selection==='all'){
|
|||
|
|
const initial=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
|||
|
|
assert.equal(initial.renderClock.status,'complete');
|
|||
|
|
assert.ok(initial.lastRender>=initial.previewMs+initial.refineMs);
|
|||
|
|
if(initial.previewMs)assert.ok(initial.lastRender>=initial.previewMs+initial.refineMs+100,'include refinement delay');
|
|||
|
|
assert.equal(await evaluate('!!document.querySelector("#renderMode")'),false);
|
|||
|
|
const original=await evaluate('__MANDEL_TEST__.fieldHashes()');
|
|||
|
|
const originalColor=await evaluate(`(${displayedColorHash.toString()})()`);
|
|||
|
|
await evaluate('document.querySelector("#colorAuto").click()');
|
|||
|
|
await sleep(350);
|
|||
|
|
assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),originalColor,'animation changes GPU pixels');
|
|||
|
|
assert.deepEqual(await evaluate('__MANDEL_TEST__.fieldHashes()'),original,'animation preserves numeric data');
|
|||
|
|
const idle=await evaluate('__MANDEL_TEST__.state()');
|
|||
|
|
assert.equal(idle.cycle,initial.cycle);assert.equal(idle.lastRender,initial.lastRender);
|
|||
|
|
await evaluate('document.querySelector("#colorAuto").click()');
|
|||
|
|
await sleep(150);
|
|||
|
|
const stopped=await evaluate('__MANDEL_TEST__.state().shift');await sleep(150);
|
|||
|
|
assert.equal(await evaluate('__MANDEL_TEST__.state().shift'),stopped);
|
|||
|
|
assert.equal(await evaluate('!!__MANDEL_TEST__.kernelAccess().renderer.colorSource'),false,'release animation memory');
|
|||
|
|
await evaluate('document.querySelector("#colorAuto").click()');
|
|||
|
|
const ongoing=evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false})})`);
|
|||
|
|
let active=false;
|
|||
|
|
for(let i=0;i<1000;i++){
|
|||
|
|
const state=await evaluate('__MANDEL_TEST__.state()');
|
|||
|
|
if(state.rendering&&state.provisional&&state.engine.includes('数値補修')){active=true;break}
|
|||
|
|
await sleep(20);
|
|||
|
|
}
|
|||
|
|
assert.ok(active,'animate during expensive repair');
|
|||
|
|
const before=await evaluate(`(${displayedColorHash.toString()})()`);
|
|||
|
|
await sleep(350);
|
|||
|
|
assert.equal(await evaluate('__MANDEL_TEST__.state().rendering'),true);
|
|||
|
|
assert.equal(await evaluate('!!__MANDEL_TEST__.kernelAccess().renderer.colorSourceVisible'),true);
|
|||
|
|
assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),before,'color changes while numeric work is running');
|
|||
|
|
assert.match(await evaluate('document.querySelector("#render").textContent'),/^描画中… /);
|
|||
|
|
// A manual change made while rendering must be drained at completion.
|
|||
|
|
await evaluate('document.querySelector("#colorAuto").click();const shift=document.querySelector("#shift");shift.value="0.65";shift.dispatchEvent(new Event("input",{bubbles:true}))');
|
|||
|
|
await ongoing;const final=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
|||
|
|
for(let i=0;i<100;i++){if(await evaluate('!__MANDEL_TEST__.state().recolorPending&&!__MANDEL_TEST__.state().recoloring'))break;await sleep(20)}
|
|||
|
|
assert.equal(final.renderClock.status,'complete');assert.ok(final.lastRender>2000);
|
|||
|
|
assert.ok(final.lastRender>=final.refineMs);
|
|||
|
|
const timing=await evaluate('({label:document.querySelector("#render").textContent,now:performance.now(),state:__MANDEL_TEST__.state(),front:__MANDEL_TEST__.kernelAccess().renderer.frontColorKey})');
|
|||
|
|
assert.equal(timing.label,(final.lastRender/1000).toFixed(2)+' s');
|
|||
|
|
assert.ok(timing.now-final.renderClock.startedAt-final.lastRender<1000,'time is current without starting another render');
|
|||
|
|
assert.equal(Number(timing.front.split(':')[2]),0.65);assert.equal(timing.state.recolorPending,false);
|
|||
|
|
const hash=await evaluate('__MANDEL_TEST__.fieldHashes()');
|
|||
|
|
const view={...cases[1],bits:448,baseIter:512,adaptive:false};
|
|||
|
|
await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify(view)})`);await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
|||
|
|
assert.deepEqual(await evaluate('__MANDEL_TEST__.fieldHashes()'),hash,'animated and ordinary renders agree');
|
|||
|
|
// Old preferences and shared URLs migrate to the sole supported renderer.
|
|||
|
|
const legacyUrl=await evaluate('localStorage.setItem("mandelbrot.renderMode","accurate");const u=new URL(location.href),p=new URLSearchParams(u.hash.slice(1));p.set("rm","accurate");u.hash=p.toString();u.href');
|
|||
|
|
await command('Page.navigate',{url:legacyUrl});await command('Page.reload');await sleep(150);
|
|||
|
|
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
|||
|
|
assert.equal(await evaluate('__MANDEL_TEST__.state().renderMode'),'fast');
|
|||
|
|
assert.equal(await evaluate('localStorage.getItem("mandelbrot.renderMode")'),null);
|
|||
|
|
console.log(JSON.stringify({ui:'PASS: animation idle/during repair, stop/manual color, numeric equality, total time, legacy migration',renderMs:final.lastRender}));
|
|||
|
|
}
|
|||
|
|
if(selection==='kernels'||selection==='all'){
|
|||
|
|
await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()');
|
|||
|
|
console.log(JSON.stringify({kernels:await evaluate(`(${kernelCases.toString()})()`)}));
|
|||
|
|
}
|
|||
|
|
for(const scenario of cases.filter(c=>selection==='all'||c.id===selection)){
|
|||
|
|
const small=['axis','black'].includes(scenario.id);
|
|||
|
|
await command('Emulation.setDeviceMetricsOverride',{width:small?64:320,height:small?48:240,deviceScaleFactor:1,mobile:false});
|
|||
|
|
const view={bits:448,baseIter:512,adaptive:false,processMode:'standard',...scenario};
|
|||
|
|
const result=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__;await t.setView(${JSON.stringify(view)});const state=await t.waitForNumericComplete({timeout:180000});return {state,hashes:await t.fieldHashes(),diagnostics:t.gpuDiagnostics()}})()`);
|
|||
|
|
assert.equal(result.state.numericalFailures,0);
|
|||
|
|
assert.equal(result.state.frontierConverged,true);
|
|||
|
|
assert.equal(result.state.historyReady,true);
|
|||
|
|
assert.ok(result.state.backend.endsWith(scenario.backend),result.state.backend);
|
|||
|
|
assert.ok(result.state.frontierMaxDispatchWork<=4000000);
|
|||
|
|
assert.equal(result.diagnostics.lossReason,'');
|
|||
|
|
assert.deepEqual(result.diagnostics.uncapturedErrors,[]);
|
|||
|
|
assert.equal(result.diagnostics.compilation.flatMap(c=>c.messages).filter(m=>m.type==='error').length,0);
|
|||
|
|
console.log(JSON.stringify({case:scenario.id,backend:result.state.backend,iter:result.state.effectiveIter,ms:result.state.lastRender,hashes:result.hashes}));
|
|||
|
|
if(scenario.id!=='reset'){
|
|||
|
|
const n=result.hashes.pixels,indices=Array.from(new Set([0,n-1,Math.floor(n/2),...Array.from({length:9},(_,i)=>(i*7919+127)%n)]));
|
|||
|
|
const samples=await evaluate(`globalThis.__MANDEL_TEST__.independentSamples(${JSON.stringify(indices)})`);
|
|||
|
|
let maxSmoothError=0;
|
|||
|
|
for(const p of samples){
|
|||
|
|
assert.equal(p.accepted,1,'independent sample inconclusive');
|
|||
|
|
assert.equal((p.packed>>>28)===1,(p.oracle>>>28)===1,JSON.stringify(p));
|
|||
|
|
if((p.oracle>>>28)===1){
|
|||
|
|
assert.equal(p.packed&0xfffff,p.oracle&0xfffff,JSON.stringify(p));
|
|||
|
|
// WGSL reconstructs magnitude and evaluates log2 in f32. Compare
|
|||
|
|
// smoothing in representable units, separately from exact escape n.
|
|||
|
|
const ulp=2**(Math.floor(Math.log2(Math.max(1,Math.abs(p.oracleSmooth))))-23);
|
|||
|
|
const error=Math.abs(p.smoothed-p.oracleSmooth);maxSmoothError=Math.max(maxSmoothError,error);
|
|||
|
|
// Keep ULP checks for the established FAST/axis samples. The new
|
|||
|
|
// cap case and former Deep-only black case use approximate smoothing
|
|||
|
|
// in the unified renderer; classification and escape n remain exact
|
|||
|
|
// checks, and their smoothing deviations are explicitly reported.
|
|||
|
|
if(!['mid','black'].includes(scenario.id))assert.ok(error<=8*ulp,JSON.stringify(p));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
console.log(JSON.stringify({case:scenario.id,independentSamples:samples.length,maxSmoothError}));
|
|||
|
|
if(scenario.id==='mid'){
|
|||
|
|
const cap=await evaluate('(async()=>{const r=__MANDEL_TEST__.kernelAccess().renderer,field=await r.readFieldAll();let below=0,remaining=0;for(const p of field.meta){if((p>>>28)===0){remaining++;if((p>>>20)!==6||(p&0xfffff)!==150000)below++}}return {below,remaining,png:await __MANDEL_TEST__.pngRoundTrip()}})()');
|
|||
|
|
assert.equal(result.state.effectiveIter,150000);assert.equal(cap.below,0);assert.ok(cap.remaining>0);assert.equal(cap.png.mismatches,0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if(scenario.id==='reset'){
|
|||
|
|
// Returning to the same view must reproduce both numerical buffers.
|
|||
|
|
await evaluate('(async()=>{const t=globalThis.__MANDEL_TEST__;await t.panPixels(8,0);await t.waitForNumericComplete();await t.panPixels(-8,0);await t.waitForNumericComplete()})()');
|
|||
|
|
assert.deepEqual(await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()'),result.hashes);
|
|||
|
|
const exports=await evaluate('(async()=>{const t=globalThis.__MANDEL_TEST__;return [await t.smokeExportTile({w:32,h:24}),await t.smokeExportTile({w:32,h:24,ss:2})]})()');
|
|||
|
|
for(const item of exports){assert.equal(item.length,item.expected);assert.ok(item.checksum)}
|
|||
|
|
const png=await evaluate('globalThis.__MANDEL_TEST__.pngRoundTrip()');
|
|||
|
|
assert.equal(png.mismatches,0);assert.equal(png.width,320);assert.equal(png.height,240);
|
|||
|
|
console.log(JSON.stringify({png}));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if(selection==='all'||selection==='budget'){
|
|||
|
|
await command('Emulation.setDeviceMetricsOverride',{width:64,height:48,deviceScaleFactor:1,mobile:false});
|
|||
|
|
for(const baseIter of [512,4096]){
|
|||
|
|
const view={...cases[3],bits:448,baseIter,adaptive:false,continuationBudget:4096};
|
|||
|
|
const result=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__;await t.setView(${JSON.stringify(view)});const state=await t.waitForNumericComplete();const r=t.kernelAccess().renderer,field=await r.readFieldAll();let below=0,above=0;for(const p of field.meta){if((p>>>28)===0&&(p>>>20)===6&&(p&0xfffff)!==4096)below++;if((p&0xfffff)>4096)above++}return {state,below,above,png:await t.pngRoundTrip()}})()`);
|
|||
|
|
assert.equal(result.state.effectiveIter,4096);assert.equal(result.state.continuationBudget,4096);assert.equal(result.below,0);assert.equal(result.above,0);assert.equal(result.png.mismatches,0);assert.equal(result.png.iter,4096);
|
|||
|
|
}
|
|||
|
|
console.log('PASS: explicit finite budget, all retained pixels reach target, PNG uses that budget');
|
|||
|
|
}
|
|||
|
|
if(selection==='all'||selection==='cancel'){
|
|||
|
|
await command('Emulation.setDeviceMetricsOverride',{width:320,height:240,deviceScaleFactor:1,mobile:false});
|
|||
|
|
const normal={...cases[0],bits:448,baseIter:512,adaptive:false};
|
|||
|
|
await evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify(normal)})`);
|
|||
|
|
const expected=await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()');
|
|||
|
|
const ongoing=evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false})})`).then(value=>({value}),error=>({error:String(error)}));
|
|||
|
|
let during=false;
|
|||
|
|
for(let i=0;i<1000;i++){
|
|||
|
|
const s=await evaluate('globalThis.__MANDEL_TEST__.state()');
|
|||
|
|
if(s.provisional&&(s.engine.includes('数値補修')||s.gpuStage==='precision scatter')){during=true;break}
|
|||
|
|
await sleep(20);
|
|||
|
|
}
|
|||
|
|
assert.ok(during,'exercise cancellation during CPU repair, not after completion');
|
|||
|
|
const provisionalPng=await evaluate('globalThis.__MANDEL_TEST__.pngRoundTrip().then(()=>false,()=>true)');
|
|||
|
|
assert.equal(provisionalPng,true,'unfinished field must not enter complete PNG path');
|
|||
|
|
await evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify(normal)})`);
|
|||
|
|
await ongoing;
|
|||
|
|
await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()');
|
|||
|
|
assert.deepEqual(await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()'),expected);
|
|||
|
|
assert.deepEqual((await evaluate('globalThis.__MANDEL_TEST__.gpuDiagnostics()')).uncapturedErrors,[]);
|
|||
|
|
console.log('PASS: cancel during CPU repair, preserve newest frame, reject provisional PNG');
|
|||
|
|
}
|
|||
|
|
assert.deepEqual(errors,[]);
|
|||
|
|
if(selection==='bench'){
|
|||
|
|
await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()');
|
|||
|
|
for(const scenario of cases.filter(c=>['reset','fast'].includes(c.id))){
|
|||
|
|
let expected;
|
|||
|
|
for(let run=0;run<4;run++){
|
|||
|
|
const view={bits:448,baseIter:512,adaptive:false,processMode:'standard',...scenario};
|
|||
|
|
const record=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__,before=t.efficiency();await t.setView(${JSON.stringify(view)});const s=await t.waitForNumericComplete(),after=t.efficiency(),metrics={};for(const k of Object.keys(after))metrics[k]=k==='activeGpuBytes'?after[k]:after[k]-before[k];return {ms:s.lastRender,iter:s.effectiveIter,numericalFailures:s.numericalFailures,operationLimit:s.unknownReasons.operationLimit,firstDisplayMs:s.generationMetrics.at(-1)?.firstDisplayMs,metrics,hashes:await t.fieldHashes()}})()`);
|
|||
|
|
if(expected)assert.deepEqual(record.hashes,expected,'repeated view changed numeric output');expected=record.hashes;
|
|||
|
|
console.log(JSON.stringify({benchmark:scenario.id,run,warmup:run===0,...record}));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Ordinary visits should neither expose diagnostics nor invoke legacy gates.
|
|||
|
|
await command('Page.navigate',{url:new URL('../index.html',import.meta.url).href});
|
|||
|
|
for(let i=0;i<100;i++){if(await evaluate("!!globalThis.MANDEL_WEBGPU_KERNELS && !!document.querySelector('#engine')"))break;await sleep(100)}
|
|||
|
|
assert.equal(await evaluate("typeof globalThis.__MANDEL_TEST__"),'undefined');
|
|||
|
|
console.log(`PASS: ${selection}; normal startup exposes no diagnostics`);
|
|||
|
|
}catch(error){
|
|||
|
|
try{console.error(JSON.stringify(await evaluate('({state:globalThis.__MANDEL_TEST__?.state(),gpu:globalThis.__MANDEL_TEST__?.gpuDiagnostics()})')))}catch{}
|
|||
|
|
throw error;
|
|||
|
|
}finally{
|
|||
|
|
try{await command('Page.navigate',{url:'about:blank'})}catch{}
|
|||
|
|
for(const p of pending.values())clearTimeout(p.timer);
|
|||
|
|
ws.close();
|
|||
|
|
}
|