201 lines
15 KiB
JavaScript
201 lines
15 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fsSync from 'node:fs';
|
|
import fs from 'node:fs/promises';
|
|
import vm from 'node:vm';
|
|
import {createHash} from 'node:crypto';
|
|
|
|
const prodUrl=new URL('../index.html',import.meta.url);
|
|
const prodHtml=await fs.readFile(prodUrl,'utf8');
|
|
const extract=html=>[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
|
|
const prodScripts=extract(prodHtml);
|
|
assert.equal(prodScripts.length,2);
|
|
for(const script of prodScripts)new vm.Script(script);
|
|
|
|
// Production is the single source of truth. No copied test HTML is allowed.
|
|
assert.equal(fsSync.existsSync(new URL('./index.test.html',import.meta.url)),false);
|
|
assert.doesNotMatch(prodHtml,/TEST_MODE|__MANDEL_TEST__|\bruntime\b|generationMetric|activeBatchLimit/);
|
|
assert.doesNotMatch(prodHtml,/truestate|returnfinishRenderClock/);
|
|
|
|
// Removed legacy/dead subsystems and stale diagnostics must not creep back.
|
|
for(const pattern of [
|
|
/DEEP_PERTURB/,/FAILURE_TILE_MAP_WGSL/,/DEEP_COMPACT_(?:INIT|CONTINUE)_WGSL/,
|
|
/referenceCandidates\(/,/refinePixelFrontierLegacy/,/refinePixelFrontierFinite/,
|
|
/pixelFrontierIterationFloor/,/prefetchFinalReference/,/readMetaAll/,/readFieldAll/,
|
|
/renderTileMeta/,/isOperationLimitMeta/,/readUnresolved\(/,
|
|
/numericRecoveryPending|numericRecoveryRunning|qualityFinalIter|qualityStage/,
|
|
/adaptiveProbeScorer|guidedReferenceScorer/,
|
|
/\bFIELD_INTERIOR_LIKELY\b/,/function fromDec\(/,/function decimalRequiredBits\(/,
|
|
/state\.(?:lastEngine|frontierRounds|frontierPixels|frontierEscaped|frontierIter|frontierMinimumIter|frontierConverged|gpuUnavailable|qualityIter|refineMs|lastRender|gpuStage)/,
|
|
/this\.(?:adapterInfo|compilation|uncapturedErrors)/,
|
|
/cpuFrameRgba/,/fast-coverage-repair/,/mode 2 repairs only untouched coverage holes/
|
|
]) assert.doesNotMatch(prodHtml,pattern,String(pattern));
|
|
|
|
// Pin the surviving kernels.
|
|
const kc={};vm.runInNewContext(prodScripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
|
|
assert.equal(Object.keys(kernels).length,20,'19 WGSL kernels + version');
|
|
const expected=JSON.parse(await fs.readFile(new URL('./kernel_hashes.json',import.meta.url),'utf8'));
|
|
const hashes=Object.fromEntries(Object.entries(kernels).map(([k,v])=>[k,createHash('sha256').update(v).digest('hex')]));
|
|
assert.deepEqual(hashes,expected);
|
|
assert.doesNotMatch(kernels.COLOR_WGSL,/reason\s*==\s*6u[^}]*interior_color/s);
|
|
assert.match(kernels.COLOR_WGSL,/Finite-budget survivors are unresolved membership/);
|
|
assert.match(kernels.COLOR_WGSL,/textureStore\(outTex,vec2<i32>\(gid\.xy\),vec4<f32>\(0\.0\)\)/);
|
|
assert.match(kernels.DIRECT_F32_WGSL,/candPeriod/);
|
|
assert.match(kernels.DIRECT_F32_WGSL,/FIELD_INTERIOR_HEURISTIC/);
|
|
assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/pack_meta\(n,FIELD_INTERIOR_HEURISTIC\)/);
|
|
assert.match(kernels.FAST_PERTURB_WGSL,/series_start/);
|
|
assert.match(prodHtml,/SERIES_APPROXIMATION_ENABLED=false/);
|
|
assert.match(prodHtml,/deferPrimaryColor=deferColor\|\|fastExtended/);
|
|
assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/unknownOnly==2u/);
|
|
assert.match(prodHtml,/knownInteriorViewportMayOverlap/);
|
|
assert.match(prodHtml,/numericHistoryScaleEligible/);
|
|
assert.match(prodHtml,/ensureExportAaSamples/);
|
|
assert.match(prodHtml,/exportWorkspaceDestroy\(\);exportJob\.active=false/);
|
|
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/pixelMap/);
|
|
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/states\[slot\]/);
|
|
|
|
function appContext(){
|
|
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}}};
|
|
let revoked=0;
|
|
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-${Math.random()}`,revokeObjectURL:()=>{revoked++}},
|
|
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=prodScripts[1].indexOf('// ── boot / teardown');
|
|
assert.ok(cut>0,'boot marker missing');
|
|
const code=prodScripts[1].slice(0,cut)+`\n`+
|
|
`globalThis.internal={state,refs,referenceScorer,fastRefs,precisionFallbackPool,ReferenceService,FastReferenceService,PrecisionFallbackService,WebGpuRenderer,chooseBackend,numericalFailureCount,pixelPrecisionBits,certifiedInteriorTiles,referenceWorkerSource,fastReferenceWorkerSource,precisionFallbackWorkerSource,adaptiveProbeChoice,adaptiveProbeSources,operationLimitReferenceEligible,failureGuidedReferenceEligible,tryOperationLimitGuidedReference,integerGridReuseShift,nearbyReferenceCandidates,refinePixelFrontier,fixedSourceForPixel,applyQuality,maxIter,buildExportMetadata,cancelRender,exportJob,renderFrame,cpuFallbackWorkerSource,recoverNumericalGuided};})();`;
|
|
vm.runInNewContext(code,context);
|
|
context.revoked=()=>revoked;
|
|
return context;
|
|
}
|
|
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',f='']=mant.split('.');
|
|
let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.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;
|
|
}
|
|
const c=appContext(),a=c.internal;
|
|
|
|
// Finite quality presets remain explicit.
|
|
for(const [q,budget] of [['fast',4096],['standard',16384],['high',32768]]){a.applyQuality(q);assert.equal(a.state.continuationBudget,budget)}
|
|
for(const old of ['fine','validate']){a.applyQuality(old);assert.equal(a.state.continuationBudget,32768)}
|
|
|
|
// Backend selection remains direct vs FAST only; primary deep routing is gone.
|
|
for(const [span,expectedBackend] of [['3.4','direct'],['0.00000000000034','fast-extended']]){
|
|
const snap={bits:256,re:fromDec('-0.743643887037151'),im:fromDec('0.13182590420533'),span:fromDec(span)};
|
|
assert.equal(a.chooseBackend(snap,800).backend,expectedBackend);
|
|
}
|
|
assert.doesNotMatch(a.chooseBackend.toString(),/deep/);
|
|
|
|
// Finite operation limit is not a numerical failure or a membership proof.
|
|
assert.equal(a.numericalFailureCount({reasons:{errorBound:1,escapeUncertain:1,referenceEnd:1,rebaseGap:1,range:1,operationLimit:99}}),5);
|
|
{
|
|
Object.assign(a.state,{continuationBudget:1024,token:7});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
|
|
const r={beginGpuOperationLimitContinuation:async()=>({active:5}),readUnresolvedStats:async()=>({total:5,reasons:{operationLimit:5}})};
|
|
const result=await a.refinePixelFrontier(r,snap,1024,7,{total:5,reasons:{operationLimit:5}});
|
|
assert.equal(result.finiteBudgetComplete,true);assert.equal(result.membershipCertified,false);
|
|
}
|
|
{
|
|
Object.assign(a.state,{continuationBudget:1024,token:8});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
|
|
const r={beginGpuOperationLimitContinuation:async()=>({active:0}),readUnresolvedStats:async()=>({total:0,reasons:{operationLimit:0}})};
|
|
const result=await a.refinePixelFrontier(r,snap,1024,8,{total:0,reasons:{operationLimit:0}});
|
|
assert.equal(result.membershipCertified,true);
|
|
}
|
|
{
|
|
Object.assign(a.state,{continuationBudget:1024,token:9});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
|
|
const r={beginGpuOperationLimitContinuation:async()=>({active:0}),readUnresolvedStats:async()=>({total:0,heuristicInterior:1,reasons:{operationLimit:0}})};
|
|
const result=await a.refinePixelFrontier(r,snap,1024,9,{total:0,heuristicInterior:1,reasons:{operationLimit:0}});
|
|
assert.equal(result.membershipCertified,false);
|
|
}
|
|
|
|
// The 12-point adaptive probe policy stays intact.
|
|
{
|
|
const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
|
|
assert.equal(a.adaptiveProbeSources(snap,800,600).length,12);
|
|
assert.equal(a.adaptiveProbeChoice(Array(12).fill(3000),3870,350).iter,3870);
|
|
assert.equal(a.adaptiveProbeChoice(Array(12).fill(7000),3870,350).iter,350);
|
|
}
|
|
|
|
// Operation-limit scoring is staged: first 12, optionally second 12, one build.
|
|
{
|
|
Object.assign(a.state,{token:20,bits:256});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')},stats={reasons:{operationLimit:1000}},current={refLen:100};
|
|
const r={sampleActiveIndices:async()=>Uint32Array.from({length:24},(_,i)=>i)};
|
|
const oldScore=a.referenceScorer.scoreFixedCandidates,oldBuild=a.referenceScorer.requestFixed;
|
|
let scores=0,builds=0;
|
|
a.referenceScorer.scoreFixedCandidates=async()=>{scores++;return{winner:0,bestScore:300,precisionBits:160,scoreMs:1}};
|
|
a.referenceScorer.requestFixed=async(_s,_i,_w,_h,src)=>{builds++;return{refLen:300,checkpointMismatch:false,source:src,buildMs:1}};
|
|
let got=await a.tryOperationLimitGuidedReference(r,{},snap,300,20,stats,current,new Set());
|
|
assert.equal(got.used,true);assert.equal(got.usedSecond,false);assert.equal(scores,1);assert.equal(builds,1);
|
|
scores=0;builds=0;
|
|
a.referenceScorer.scoreFixedCandidates=async()=>{scores++;return scores===1?{winner:0,bestScore:180,precisionBits:160,scoreMs:1}:{winner:0,bestScore:250,precisionBits:160,scoreMs:1}};
|
|
a.referenceScorer.requestFixed=async(_s,_i,_w,_h,src)=>{builds++;return{refLen:250,checkpointMismatch:false,source:src,buildMs:1}};
|
|
got=await a.tryOperationLimitGuidedReference(r,{},snap,300,20,stats,current,new Set());
|
|
assert.equal(got.used,true);assert.equal(got.usedSecond,true);assert.equal(scores,2);assert.equal(builds,1);
|
|
a.referenceScorer.scoreFixedCandidates=oldScore;a.referenceScorer.requestFixed=oldBuild;
|
|
}
|
|
|
|
// Exact certificates remain strict at cardioid/bulb boundaries.
|
|
{
|
|
const one=1n<<256n;
|
|
for(const [re,expectedInside] of [[0n,true],[-one,true],[one/4n,false],[one/4n-1n,true],[-5n*one/4n,false],[-5n*one/4n+1n,true]]){
|
|
assert.equal(a.certifiedInteriorTiles({bits:256,re,im:0n,span:0n},1,1).pixels===1,expectedInside,re.toString());
|
|
}
|
|
}
|
|
|
|
// Stable integer-grid history reuse remains exact.
|
|
{
|
|
const bits=256,span=(1n<<240n)+12345n,old={bits,re:0n,im:0n,span,w:800,h:600},next={bits,re:-(span*40n)/800n,im:0n,span,w:800,h:600};
|
|
const shift=a.integerGridReuseShift(old,next,800,600);assert.ok(shift);assert.equal(shift.pixels,456000);
|
|
assert.equal(a.integerGridReuseShift(old,{...next,re:-(span*81n)/1600n},800,600),null);
|
|
}
|
|
assert.match(a.nearbyReferenceCandidates.toString(),/deepContexts/,'GPU-resident guided refs participate in nearby reuse');
|
|
|
|
// Worker lifecycle: idle destroy still terminates/revokes resources.
|
|
for(const Service of [a.FastReferenceService,a.PrecisionFallbackService]){
|
|
const s=new Service();let terminated=0;s.worker={terminate(){terminated++}};s.url='blob:idle';s.destroy();assert.equal(terminated,1);assert.equal(s.worker,null);assert.equal(s.url,'');
|
|
}
|
|
|
|
// Correction lazy compile contains only the five actually used pipelines.
|
|
{
|
|
const src=a.WebGpuRenderer.prototype.ensureCorrectionPipelines.toString();
|
|
for(const name of ['DEEP_BUCKET_HIST_WGSL','DEEP_BUCKET_PREFIX_WGSL','DEEP_BUCKET_SCATTER_WGSL','DEEP_CORRECT_WGSL','DEEP_CORRECT_QUEUE_WGSL'])assert.match(src,new RegExp(name));
|
|
assert.doesNotMatch(src,/DEEP_PERTURB/);assert.equal((src.match(/this\.module\(/g)||[]).length,5);
|
|
}
|
|
|
|
// Render cancellation must cancel export and advance the generation token.
|
|
{
|
|
Object.assign(a.state,{token:40,rendering:true,recolorPending:true,deferNumericPublish:true,renderClock:null});a.exportJob.active=true;a.exportJob.cancelled=false;
|
|
a.cancelRender();assert.equal(a.exportJob.cancelled,true);assert.equal(a.state.token,41);assert.equal(a.state.rendering,false);
|
|
a.exportJob.active=false;
|
|
}
|
|
|
|
// Both export paths share one metadata schema builder.
|
|
{
|
|
Object.assign(a.state,{adaptive:true,baseIter:350,unknownReasons:{operationLimit:7},fieldView:{backend:'pixel-frontier-fast-extended',membershipCertified:false,frontier:{finiteBudgetComplete:true,policy:'explicit-gpu-resident-finite-budget'}}});
|
|
const snap={bits:256,re:1n,im:2n,span:3n},palette={id:0,cycle:.008,shift:.18,edgeAA:false};
|
|
const completed=a.buildExportMetadata({snap,iter:4096,w:800,h:600,palette,completedField:true,exportPipeline:'completed-numeric-field'});
|
|
const rerender=a.buildExportMetadata({snap,iter:4096,w:1600,h:1200,ss:2,palette:{...palette,edgeAA:true},fastExtended:true,strict:true,unresolvedSamples:3,exportPipeline:'tiles',exportTileSize:128,exportRingDepth:1});
|
|
for(const key of ['format','rendererVersion','backend','numericEngine','finiteBudgetComplete','membershipCertified','precisionPolicy','pixelContract','width','height','supersampling','numericSamples','unresolvedSamples','iterationPolicy','view','palette','exportPipeline','shaderVersion']){assert.ok(key in completed,key);assert.ok(key in rerender,key)}
|
|
assert.equal(completed.operationLimit,7);assert.equal(rerender.exportTileSize,128);assert.equal(rerender.exportRingDepth,1);assert.equal(rerender.membershipCertified,false);
|
|
}
|
|
|
|
// Run surviving CPU workers at tiny budgets.
|
|
function worker(source,message){let result;const wc={self:{},postMessage:r=>{result=r},performance};vm.runInNewContext(source,wc);wc.self.onmessage({data:message});assert.ok(result);assert.notEqual(result.type,'error',result.error);return result}
|
|
{
|
|
const scale=1n<<256n,message={type:'build',id:1,key:'tiny',bits:256,sourceBits:256,targetBits:128,re:'0',im:'0',span:'0',width:1,height:1,iter:64};
|
|
const guarded=worker(a.referenceWorkerSource(),message),fast=worker(a.fastReferenceWorkerSource(),message);
|
|
assert.equal(guarded.checkpointMismatch,false);assert.equal(guarded.refLen,64);assert.equal(fast.refLen,64);assert.deepEqual(Buffer.from(guarded.refs),Buffer.from(fast.refs));
|
|
assert.equal(fast.series.jump,0);assert.equal(fast.series.coeffs.length,8);
|
|
const escaped=worker(a.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:256,targetBits:128,re:(3n*scale).toString(),im:'0',span:'0',width:1,height:1,iter:64,terminalClass:-1,indices:Uint32Array.of(0).buffer});
|
|
assert.equal(new Uint8Array(escaped.accepted)[0],1);assert.equal(new Uint32Array(escaped.meta)[0]>>>28,1);
|
|
const cpu=worker(a.cpuFallbackWorkerSource(),{type:'render',id:3,w:4,h:4,y0:0,y1:4,maxIter:64,re:-.5,im:0,span:3.4,style:{palette:0,cycle:1,shift:0}});
|
|
assert.equal(cpu.type,'done');assert.equal(new Uint8ClampedArray(cpu.rgba).length,4*4*4);assert.ok(cpu.work>0);
|
|
}
|
|
|
|
assert.match(a.recoverNumericalGuided.toString(),/FAILURE_GUIDED_REF_MAX_PASSES/);
|
|
|
|
console.log('PASS: single-source finite viewer regression (production JS, 19 WGSL kernels, staged scoring, lifecycle, export metadata)');
|