publish
This commit is contained in:
parent
01204dfa2f
commit
f5fc9b76ce
73 changed files with 32628 additions and 376 deletions
34
tests/README.md
Normal file
34
tests/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Regression tests
|
||||
|
||||
通常の回帰確認:
|
||||
|
||||
```bash
|
||||
node tests/regression.mjs
|
||||
```
|
||||
|
||||
`regression.mjs` は **production `index.html` を唯一のsource of truthとして直接読み込みます**。恒久的なtest用HTMLコピーはありません。
|
||||
|
||||
高速スイートで固定する項目:
|
||||
|
||||
- production JavaScript構文
|
||||
- 19本の現行WGSL kernel SHA-256
|
||||
- direct / FAST routing と有限continuation budget
|
||||
- operation-limitが数値failureやmembership証明として扱われないこと
|
||||
- 12点adaptive initialIter probe
|
||||
- 12→必要時+12 operation-limit scoring(winner 1本だけbuild)
|
||||
- exact interior certificate / integer-grid numeric history reuse
|
||||
- guided/deep GPU-resident referenceのnearby reuse参加
|
||||
- idle Workerの確実なterminate/revoke
|
||||
- correction lazy compileが実使用5 pipelineだけであること
|
||||
- cancelRenderのexport取消・generation更新
|
||||
- export metadata共通builder
|
||||
- reference / FAST reference / precision fallback Workerの小規模数値回帰
|
||||
- 削除済みlegacy/dead subsystem・write-only stateがbundleへ再混入しないこと
|
||||
|
||||
実ブラウザ/WebGPU E2Eが必要な場合のみ:
|
||||
|
||||
```bash
|
||||
node tests/browser_smoke.mjs 9333
|
||||
```
|
||||
|
||||
`browser_smoke.mjs` はproduction `index.html`から一時的な診断HTMLを生成し、終了時に削除します。テスト用アプリ本体のコピーは保持しません。
|
||||
68
tests/browser_smoke.mjs
Normal file
68
tests/browser_smoke.mjs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Optional real-browser WebGPU smoke test.
|
||||
// Starts from the production index.html and injects a temporary test-only API.
|
||||
// Usage: node tests/browser_smoke.mjs [port=9333]
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import {fileURLToPath,pathToFileURL} from 'node:url';
|
||||
import {setTimeout as sleep} from 'node:timers/promises';
|
||||
|
||||
const port=Number(process.argv[2]||9333);
|
||||
const prodUrl=new URL('../index.html',import.meta.url),generatedUrl=new URL('./.browser-smoke.generated.html',import.meta.url);
|
||||
const prodHtml=await fs.readFile(prodUrl,'utf8');
|
||||
const marker='// ── boot / teardown';
|
||||
assert.equal(prodHtml.includes(marker),true,'boot marker missing');
|
||||
const hook=String.raw`
|
||||
function testDecimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
|
||||
function testFromDec(s,bits){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';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}
|
||||
async function waitForTestState(predicate,label,timeout=120000){const start=performance.now();for(;;){if(state.drawState==='ERROR')throw new Error(state.gpuError||'render failed');if(predicate())return true;if(performance.now()-start>=timeout)throw new Error(label+' timeout');if(state.dirty&&!state.rendering)schedule();await new Promise(resolve=>setTimeout(resolve,20))}}
|
||||
globalThis.__MANDEL_TEST__={
|
||||
async ensureGpuReady(){const r=renderer||await initRenderer();return{ready:!!r,navigatorGpu:!!navigator.gpu,renderer:!!renderer,gpuInitFailed:state.gpuInitFailed,gpuError:state.gpuError}},
|
||||
async setView({re,im,span,bits,baseIter=350,adaptive=false,continuationBudget=0,quality=null,fixed=false}){const ready=await this.ensureGpuReady();if(!ready.ready)throw new Error('WebGPU initialization failed: '+JSON.stringify(ready));cancelRender();state.bits=bits||Math.max(256,testDecimalRequiredBits(re),testDecimalRequiredBits(im),testDecimalRequiredBits(span));state.re=fixed?BigInt(re):testFromDec(re,state.bits);state.im=fixed?BigInt(im):testFromDec(im,state.bits);state.span=fixed?BigInt(span):testFromDec(span,state.bits);state.baseIter=baseIter;state.adaptive=adaptive;state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(continuationBudget)));state.quality='standard';if(quality)applyQuality(quality);else if(!state.continuationBudget)applyQuality('standard');ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return this.state()},
|
||||
async waitForFinal({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&state.fieldView?.complete===true&&state.renderClock?.status==='complete','final',timeout);return this.state()},
|
||||
async waitForNumericComplete({timeout=120000}={}){const result=await this.waitForFinal({timeout});if(result.numericalFailures)throw new Error('numeric recovery ended with '+result.numericalFailures+' unresolved failures');return result},
|
||||
async sampleMeta(points){if(!renderer?.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);return Array.from(await renderer.readMeta(idx))},
|
||||
state:()=>{const reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter(),effectiveIter:state.fieldView?.iter??maxIter(),continuationBudget:state.continuationBudget,quality:state.quality,rendering:state.rendering,renderClock:state.renderClock?{...state.renderClock}:null,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,finiteBudgetComplete:!!state.fieldView?.frontier?.finiteBudgetComplete,membershipCertified:!!state.fieldView?.membershipCertified,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version}},
|
||||
async panPixels(dx,dy){pan(dx,dy);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test pan timeout');return this.state()},
|
||||
async smokeExportTile({w=48,h=32,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),fastExtended=fastNeedsExtended(snap,w),fastContext=fastExtended?await fastRefs.request(snap,iter,w,h):null,args={snap,iter,fastContext,fullW:w,fullH:h,tileX:0,tileY:0,w,h},result=ss===2?await renderer.renderTileRGBA2x(args):await renderer.renderTileRGBA({...args,sampleX:.5,sampleY:.5,edgeAA:false}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,ss,unresolved:result.unresolved||0}}
|
||||
};
|
||||
`;
|
||||
const generatedHtml=prodHtml.replace(marker,hook+'\n'+marker);
|
||||
await fs.writeFile(generatedUrl,generatedHtml);
|
||||
const viewerUrl=pathToFileURL(fileURLToPath(generatedUrl)).href;
|
||||
|
||||
let ws=null;
|
||||
try{
|
||||
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');
|
||||
ws=new WebSocket(pages[0].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(),exceptions=[];
|
||||
ws.addEventListener('message',event=>{
|
||||
const m=JSON.parse(event.data);
|
||||
if(m.method==='Runtime.exceptionThrown')exceptions.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'))},180000);pending.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}))})}
|
||||
async function evaluate(expression){const r=await command('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(r.exceptionDetails)throw new Error(r.exceptionDetails.exception?.description||r.exceptionDetails.text);return r.result.value}
|
||||
await command('Runtime.enable');await command('Page.enable');await command('Page.navigate',{url:viewerUrl});
|
||||
for(let i=0;i<200;i++){if(await evaluate('typeof globalThis.__MANDEL_TEST__ === "object"'))break;if(i===199)throw new Error('__MANDEL_TEST__ timeout');await sleep(50)}
|
||||
const ready=await evaluate('__MANDEL_TEST__.ensureGpuReady()');assert.equal(ready.ready,true,JSON.stringify(ready));
|
||||
const reset=await evaluate(`__MANDEL_TEST__.setView({re:'-0.5',im:'0',span:'3.4',bits:256,baseIter:350,adaptive:true,quality:'fast'})`);
|
||||
assert.equal(reset.continuationBudget,4096);assert.equal(reset.numericalFailures,0);
|
||||
const finalReset=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');assert.equal(finalReset.finiteBudgetComplete,true);
|
||||
const deep=await evaluate(`__MANDEL_TEST__.setView({re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',bits:256,baseIter:350,adaptive:true,quality:'standard'})`);
|
||||
assert.equal(deep.continuationBudget,16384);assert.equal(deep.fastExtended,true);
|
||||
const finalDeep=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');assert.equal(finalDeep.numericalFailures,0);
|
||||
const samples=await evaluate(`__MANDEL_TEST__.sampleMeta([[0,0],[Math.floor(innerWidth/2),Math.floor(innerHeight/2)]])`);assert.equal(samples.length,2);
|
||||
const ss1=await evaluate('__MANDEL_TEST__.smokeExportTile({w:24,h:16,ss:1})');assert.equal(ss1.length,ss1.expected);assert.ok(ss1.checksum>0);
|
||||
const ss2=await evaluate('__MANDEL_TEST__.smokeExportTile({w:16,h:12,ss:2})');assert.equal(ss2.length,ss2.expected);assert.ok(ss2.checksum>0);
|
||||
const panned=await evaluate('__MANDEL_TEST__.panPixels(24,0)');assert.equal(panned.numericalFailures,0);
|
||||
assert.deepEqual(exceptions,[]);
|
||||
console.log('PASS: optional Chromium WebGPU smoke (generated from production source)');
|
||||
}finally{
|
||||
ws?.close();
|
||||
await fs.rm(generatedUrl,{force:true});
|
||||
}
|
||||
22
tests/kernel_hashes.json
Normal file
22
tests/kernel_hashes.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"version": "7980409b2607c884489f4551f410a57de46b1361a061ac49b27dc0a2259d3e02",
|
||||
"INTERIOR_MASK_WGSL": "00e8a502c23fe3468abab23365bc13cae3e35ecf401dcfe6e9a2f92f810d1eeb",
|
||||
"PRECISION_SCATTER_WGSL": "428ff66314ed0d69bc3bef83d5d0adc554bd89eb4a7137a4a8910f2d1016b03a",
|
||||
"DIRECT_F32_WGSL": "d695042d016fb7060923184db20df42e480af4b223714c5eaca195a8b9b2f18b",
|
||||
"ACTIVE_PREPARE_WGSL": "27bb3af8310bd0784888f2a648896e1fcf5872b4a9a3ade883ebe95a4868fa6e",
|
||||
"OPERATION_LIMIT_QUEUE_WGSL": "ca83fa6e239a4b429431d65b79f3c2d1b7db840885dbf7c60aa8faa8f07c9026",
|
||||
"FAST_PERTURB_WGSL": "c0dcbc5139c2e57e5cee6340024c841d379ffe57d7035cb3dc2a89c8cac50f65",
|
||||
"FAST_PERTURB_POSTSTATS_WGSL": "92d99eaaa928da7920b60700c0ba5520fbf86bb783373ed7545e3d8c2dcffd45",
|
||||
"DEEP_ACTIVE_RESUME_INIT_WGSL": "fc50c00738705568a76bafc10e092e2500977387858839b5f46c83cb870822cf",
|
||||
"DEEP_ACTIVE_CONTINUE_WGSL": "a78d21e31220b3740a0ad33506594d4c79bed1c0d95a1662c79621d471aefcaf",
|
||||
"DEEP_BUCKET_HIST_WGSL": "b4237a51bd5200cc9c95efe086123c22a1f0a27fd82dcde5786c4628b2b50f9d",
|
||||
"DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da",
|
||||
"DEEP_BUCKET_SCATTER_WGSL": "9f3c75251ddd34d5a0e1ed4088f2801fd675010c73121d9b86393a9ab55e1389",
|
||||
"DEEP_CORRECT_WGSL": "48f01249578b582fc7f207e44875ba8a18fc4f9c48300209648da85e50ac5d90",
|
||||
"DEEP_CORRECT_QUEUE_WGSL": "b15fce3347f0b82689f4a0d0a712bda2dfa58a808e6a6972f49929c1782da616",
|
||||
"UNKNOWN_STATS_WGSL": "c145312a819014dd3b2710cfa1c8f1d6ca4210d9c0a8f1f661c713013c69f8d2",
|
||||
"SYMMETRY_COPY_WGSL": "658e1cf2b9f40d8e759abe2269c041f582a80727cb24e2f48a405cb8676d70e1",
|
||||
"COLOR_WGSL": "21c5485e14905305124450c7909ac355ff5aa17b330e35d68517524498813323",
|
||||
"AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342",
|
||||
"PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc"
|
||||
}
|
||||
201
tests/regression.mjs
Normal file
201
tests/regression.mjs
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
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)');
|
||||
Loading…
Add table
Add a link
Reference in a new issue