e
This commit is contained in:
parent
de177e8896
commit
8a2b1f6d4b
64 changed files with 10760 additions and 757 deletions
|
|
@ -1,10 +1,12 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const s=await fs.readFile(new URL('./webgpu-acceptance.js',import.meta.url),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/row\.known<12/.test(s),'swirly minimum-known gate missing');
|
||||
must(/row\.unknown!==0/.test(s),'stable-scene UNKNOWN=0 gate missing');
|
||||
must(/corrected production frame produced UNKNOWN/.test(s),'corrected screen UNKNOWN=0 gate missing');
|
||||
must(/forceDeep:true,correctUnknown:true/.test(s),'forced corrected deep dense probe missing');
|
||||
must(/dense DS correction left known regression pixel UNKNOWN/.test(s),'dense DS recovery gate missing');
|
||||
must(/falseEscaped/.test(s)&&/falseBounded/.test(s)&&/guardMismatch/.test(s),'membership/reference gates missing');
|
||||
must(/dense false escape/.test(s)&&/dense false bounded/.test(s),'dense regression gates missing');
|
||||
must(/exportSmoke\.unresolved!==0/.test(s)&&/exportSmokeAA\.unresolved!==0/.test(s),'export unresolved gate missing');
|
||||
must(/backend router ignored measured 15% speed margin/.test(s)&&/viewProbe/.test(s),'view-specific router acceptance gate missing');
|
||||
must(/exportSmoke\.unresolved!==0/.test(s)&&/exportSmokeAA\.unresolved!==0/.test(s),'corrected export unresolved gate missing');
|
||||
must(/uncapturedErrors/.test(s),'uncaptured WebGPU error gate missing');
|
||||
console.log(JSON.stringify({status:'pass',checks:6,stableUnknownMax:0,swirlyKnownMin:12},null,2));
|
||||
must(/runFixed96Experiment/.test(s)&&/fixed96 experiment smoke failed/.test(s)&&/queueIntegrity/.test(s)&&/dispatchRate/.test(s),'fixed96 sparse-queue real-GPU smoke gate missing');
|
||||
console.log(JSON.stringify({status:'pass',checks:8,correctedUnknownMax:0,routerSpeedMargin:.15,fixed96Smoke:true},null,2));
|
||||
|
|
|
|||
10
tests/v24-backend-policy.mjs
Normal file
10
tests/v24-backend-policy.mjs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
const REQUIRED=8,DEFAULT=128,MARGIN=.85,UNKNOWN_MAX=.005;
|
||||
function choose(ratio,viewProbe=null){const eligible=Number.isFinite(ratio)&&ratio>=REQUIRED;if(!eligible)return'deep';if(ratio>=DEFAULT)return'direct';if(!viewProbe)return'probe';if(!viewProbe.qualityPass||viewProbe.classDisagreement!==0||viewProbe.deepUnknownRate>UNKNOWN_MAX)return'deep';if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*MARGIN)return'deep';return'direct'}
|
||||
const good=(d,u=0)=>({qualityPass:true,classDisagreement:0,deepUnknownRate:u,predictedDirectMs:100,predictedDeepMs:d});
|
||||
const cases=[
|
||||
[0.5,null,'deep'],[7.999,null,'deep'],[8,null,'probe'],[64,null,'probe'],[128,null,'direct'],
|
||||
[32,good(80),'deep'],[32,good(86),'direct'],[32,good(60,.01),'deep'],
|
||||
[32,{...good(60),qualityPass:false},'deep'],[32,{...good(60),classDisagreement:1},'deep']
|
||||
];
|
||||
for(const [ratio,p,want] of cases){const got=choose(ratio,p);if(got!==want)throw new Error(JSON.stringify({ratio,p,want,got}))}
|
||||
console.log(JSON.stringify({status:'pass',kind:'view-specific-adaptive-backend-router',directRequiredRatio:REQUIRED,directDefaultRatio:DEFAULT,deepSpeedMargin:MARGIN,deepProbeUnknownMax:UNKNOWN_MAX,cases:cases.length},null,2));
|
||||
|
|
@ -6,9 +6,9 @@ function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),
|
|||
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}function log2FixedAt(v,b){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-b}function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:F(Math.pow(2,l-exp)),exp}}
|
||||
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||
function direct(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
|
||||
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b);let zr=0n,zi=0n;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr}return{hi,lo,refLen:limit}}
|
||||
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<<BigInt(b);let zr=0n,zi=0n,refLen=limit;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;const mag=roundShift(zr*zr,b)+roundShift(zi*zi,b);if(mag>bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[refLen*2]=hr1;hi[refLen*2+1]=hi1;lo[refLen*2]=F(rr1-hr1);lo[refLen*2+1]=F(ri1-hi1);break}}return{hi,lo,refLen}}
|
||||
function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))}
|
||||
function perturb(ref,s,w,h,x,y,limit,strict=false){const se=spanME(s),dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w),U=2**-24;let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=64*U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+64*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30),threshold=strict?1e-4:1e-3;return ea<=threshold?{kind:'bounded',n,err:ea}:{kind:'unknown',n,reason:'error-bound',err:ea}}if(m>ref.refLen)return{kind:'unknown',n,reason:'ref'};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+64*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n,err:ea};return{kind:'unknown',n,reason:'escape-uncertain',err:ea}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n,reason:'rebase-gap'};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=64*U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n,reason:'ref-end'};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=64*U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n,reason:'range'};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n,reason:'guard'}}}
|
||||
function perturb(ref,s,w,h,x,y,limit,strict=false){const se=spanME(s),dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w),U=2**-24;let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=1*U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+1*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30),threshold=strict?1e-4:1e-3;return ea<=threshold?{kind:'bounded',n,err:ea}:{kind:'unknown',n,reason:'error-bound',err:ea}}if(m>ref.refLen)return{kind:'unknown',n,reason:'ref'};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+1*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n,err:ea};return{kind:'unknown',n,reason:'escape-uncertain',err:ea}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n,reason:'rebase-gap'};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=1*U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n,reason:'ref-end'};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=1*U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n,reason:'range'};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n,reason:'guard'}}}
|
||||
const cfg=[['period2-cusp-z14',1200],['swirly-seahorses-z12',2000],['period2-cusp-z20',1800],['period2-cusp-z100',2400],['period3-interior',1200],['period2-cusp-e280',2400],['period2-cusp-e400',1600]],w=17,h=11,pts=[];
|
||||
for(let y=0;y<h;y++)for(let x=0;x<w;x++)pts.push([x,y]);
|
||||
const report=[];
|
||||
|
|
|
|||
16
tests/v24-crossover-contract.mjs
Normal file
16
tests/v24-crossover-contract.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/DIRECT_DS_WGSL/.test(kernels)&&/struct DS/.test(kernels)&&/centerReLo/.test(kernels),'DS-direct benchmark shader missing');
|
||||
must(/direct-ds-benchmark/.test(script)&&/this\.dsDirect=/.test(script),'DS-direct pipeline missing');
|
||||
must(/benchmarkDsDirect/.test(script)&&/renderTileMetaDsDirect/.test(script),'DS-direct benchmark methods missing');
|
||||
must(/CROSSOVER_DEPTHS=\[0,2,4,6,8,10,12,14,16,20,30,40\]/.test(script),'round-depth crossover suite missing');
|
||||
must(/runCrossoverBenchmark/.test(script)&&/referenceBuildMs/.test(script)&&/coldMedianMs/.test(script),'crossover benchmark timing/report missing');
|
||||
must(/CROSSOVER_SPEED_W=256/.test(script)&&/CROSSOVER_SPEED_H=144/.test(script)&&/crossoverSpeedTiles/.test(script),'bounded representative speed tiles missing');
|
||||
must(/fullMedianMs/.test(script)&&/msPerPixel/.test(script)&&/speedPixels/.test(script),'tile timing extrapolation missing');
|
||||
must(/CROSSOVER_GPU_TIMEOUT_MS=15000/.test(script)&&/benchmarkTimeout/.test(script)&&/timed out after/.test(script),'benchmark watchdog missing');
|
||||
must(/classMismatchRate/.test(script)&&/escapeIterationMismatchRate/.test(script),'GPU quality comparison missing');
|
||||
must(/id="backendBench"/.test(html)&&/id="benchmarkDialog"/.test(html)&&/id="benchmarkSave"/.test(html),'benchmark UI missing');
|
||||
const choose=(script.match(/function chooseBackend[\s\S]*?function deepNeeded/)||[''])[0];
|
||||
must(!/dsDirect|backend='ds'/.test(choose),'experimental DS-direct leaked into production router');
|
||||
console.log(JSON.stringify({status:'pass',check:'crossover-benchmark-contract',checks:11},null,2));
|
||||
106
tests/v24-crossover-quality.mjs
Normal file
106
tests/v24-crossover-quality.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const F=Math.fround;
|
||||
const U=2**-24;
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
|
||||
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
|
||||
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
|
||||
function bitsForDepth(z){return Math.max(256,Math.ceil(z*Math.log2(10))+300)}
|
||||
function snap(re,im,z){const span=`3.4e-${z}`,bits=bitsForDepth(z);return{bits,re:decFixed(re,bits),im:decFixed(im,bits),span:decFixed(span,bits),spanText:span}}
|
||||
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
|
||||
function log2FixedAt(v,b){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-b}
|
||||
function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:Math.pow(2,l-exp),exp}}
|
||||
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
|
||||
function f32Direct(cr,ci,limit){cr=F(cr);ci=F(ci);let zr=0,zi=0;for(let n=0;n<limit;n++){const a=F(zr*zr),b=F(zi*zi);zi=F(F(F(2*zr)*zi)+ci);zr=F(F(a-b)+cr);if(F(F(zr*zr)+F(zi*zi))>4)return n+1}return limit}
|
||||
// DS helpers
|
||||
function q(a,b){const x=F(a+b),e=F(b-F(x-a));return[x,e]}
|
||||
function sum(a,b){const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
|
||||
function prod(a,b){const x=F(a*b),ca=F(4097*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(4097*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
|
||||
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(a[1]+b[1])))}
|
||||
function neg(a){return[F(-a[0]),F(-a[1])]} function sub(a,b){return add(a,neg(b))}
|
||||
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(a[0]*b[1]));e=F(e+F(a[1]*b[0]));e=F(e+F(a[1]*b[1]));return q(t[0],e)}
|
||||
function scaleDS(a,b){const t=prod(a[0],b);return q(t[0],F(t[1]+F(a[1]*b)))}
|
||||
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
|
||||
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]} function cmulDS(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
|
||||
function mag2DS(a){return add(mul(a[0],a[0]),mul(a[1],a[1]))}
|
||||
function split(x){const h=F(x);return[h,F(x-h)]}
|
||||
function dsDirect(s,w,h,x,y,limit){const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),span=split(fixedNum(s.span,s.bits));const offx=(x+.5-.5*w)/w,offy=(.5*h-y-.5)/w;const cr=add(cre,scaleDS(span,F(offx))),ci=add(cim,scaleDS(span,F(offy)));let z=[[0,0],[0,0]];for(let n=0;n<limit;n++){z=cadd(cmulDS(z,z),[cr,ci]);if(cmp(mag2DS(z),[4,0])>0)return n+1}return limit}
|
||||
function adjacentUniq(s,w,h){const y=Math.floor(h/2),cx=Math.floor(w/2),f32s=new Set(),dss=new Set(),cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),sp=split(fixedNum(s.span,s.bits));for(let x=cx-10;x<=cx+10;x++){const p=pixel(s,w,h,x,y),rr=fixedNum(p[0],s.bits),ii=fixedNum(p[1],s.bits);f32s.add(F(rr)+','+F(ii));const ox=(x+.5-.5*w)/w,oy=(.5*h-y-.5)/w,cr=add(cre,scaleDS(sp,F(ox))),ci=add(cim,scaleDS(sp,F(oy)));dss.add(cr[0]+':'+cr[1]+','+ci[0]+':'+ci[1])}return{f32:f32s.size,ds:dss.size}}
|
||||
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<<BigInt(b);let zr=0n,zi=0n,refLen=limit;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;const mag=roundShift(zr*zr,b)+roundShift(zi*zi,b);if(mag>bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[2*refLen]=hr1;hi[2*refLen+1]=hi1;lo[2*refLen]=F(rr1-hr1);lo[2*refLen+1]=F(ri1-hi1);break}}return{hi,lo,refLen}}
|
||||
function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}
|
||||
function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))}
|
||||
function primary(ref,s,w,h,x,y,limit){const se0=spanME(s),se={mant:F(se0.mant),exp:se0.exp},dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w);let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);return ea<=1e-3?{kind:'bounded',n}:{kind:'unknown',n}}if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n};return{kind:'unknown',n}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
|
||||
function pow2DS(a,e){if(e<-126)return[0,0];if(e>126)return[F(8.507059e37),0];return[F(a[0]*2**e),F(a[1]*2**e)]}
|
||||
function cscale(a,b){return[scaleDS(a[0],b),scaleDS(a[1],b)]} function cpow2(a,e){return[pow2DS(a[0],e),pow2DS(a[1],e)]} function maxabsDS(a){return Math.max(Math.abs(F(a[0][0]+a[0][1])),Math.abs(F(a[1][0]+a[1][1])))}
|
||||
function correction(ref,s,w,h,x,y,limit){const se=spanME(s),mh=F(se.mant),sm=[mh,F(se.mant-mh)],iw=1/w,iwh=F(iw),inv=[iwh,F(iw-iwh)],ox=F(x+.5-.5*w),oy=F(.5*h-y-.5),dx=scaleDS(inv,ox),dy=scaleDS(inv,oy),d0=[mul(sm,dx),mul(sm,dy)];let d=d0,wv=[[0,0],[0,0]],e=se.exp,n=0,m=0,ops=0;while(true){if(n>=limit)return{kind:'bounded',n};if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,r=[[ref.hi[rp],ref.lo[rp]],[ref.hi[rp+1],ref.lo[rp+1]]],delta=cpow2(wv,e),z=cadd(r,delta),mg=mag2DS(z);if(cmp(mg,[4,0])>0)return{kind:'escaped',n};const dm=mag2DS(delta);if(m>0&&cmp(dm,[0,0])>0&&cmp(mg,dm)<0){if(se.exp-e<-96)return{kind:'unknown',n};wv=z;d=cpow2(d0,se.exp);e=0;m=0;continue}if(m>=ref.refLen)return{kind:'unknown',n};const linear=cscale(cmulDS(r,wv),2),sq=cpow2(cmulDS(wv,wv),e);wv=cadd(cadd(linear,sq),d);m++;n++;ops++;const mm=Math.max(maxabsDS(wv),maxabsDS(d));if(mm>=1e30||!Number.isFinite(mm))return{kind:'unknown',n};if(mm>65536){wv=cscale(wv,1/65536);d=cscale(d,1/65536);e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wv=cscale(wv,65536);d=cscale(d,65536);e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
|
||||
function adaptiveIter(z){return Math.max(350,350+Math.floor(70*Math.sqrt(z)+15*z))}
|
||||
const tracks=[
|
||||
{id:'cusp',re:'-0.75',im:'0'},
|
||||
{id:'seahorse',re:'-0.7453983606667815',im:'0.1125046349959942'}
|
||||
];
|
||||
const depths=[0,2,4,6,8,10,12,14,16,20,30,40];
|
||||
const SW=21,SH=15,FULL_W=1600,FULL_H=900;
|
||||
const rows=[];
|
||||
for(const tr of tracks){
|
||||
for(const z of depths){
|
||||
const s=snap(tr.re,tr.im,z);
|
||||
const limit=adaptiveIter(z);
|
||||
const ref=buildRef(s,limit);
|
||||
let guardMismatch=0,f32FE=0,f32FB=0,dsFE=0,dsFB=0,deepFE=0,deepFB=0,deepU=0,tested=0,f32IterMismatch=0,dsIterMismatch=0,deepIterMismatch=0;
|
||||
const f32Coords=new Set(), dsCoords=new Set();
|
||||
const t0=performance.now();
|
||||
for(let sy=0;sy<SH;sy++){
|
||||
const y=Math.round(sy*(FULL_H-1)/Math.max(1,SH-1));
|
||||
for(let sx=0;sx<SW;sx++){
|
||||
const x=Math.round(sx*(FULL_W-1)/Math.max(1,SW-1));
|
||||
const p=pixel(s,FULL_W,FULL_H,x,y);
|
||||
const g=pixel(s,FULL_W,FULL_H,x,y,s.bits+64);
|
||||
const o=oracle(p[0],p[1],s.bits,limit);
|
||||
const gg=oracle(g[0],g[1],s.bits+64,limit);
|
||||
if(o!==gg){guardMismatch++;continue;}
|
||||
tested++;
|
||||
const rr=fixedNum(p[0],s.bits),ii=fixedNum(p[1],s.bits);
|
||||
const fd=f32Direct(rr,ii,limit);
|
||||
const dd=dsDirect(s,FULL_W,FULL_H,x,y,limit);
|
||||
f32Coords.add(F(rr)+','+F(ii));
|
||||
{const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),sp=split(fixedNum(s.span,s.bits)),ox=(x+.5-.5*FULL_W)/FULL_W,oy=(.5*FULL_H-y-.5)/FULL_W,crd=add(cre,scaleDS(sp,F(ox))),cid=add(cim,scaleDS(sp,F(oy)));dsCoords.add(crd[0]+':'+crd[1]+','+cid[0]+':'+cid[1]);}
|
||||
const pr=primary(ref,s,FULL_W,FULL_H,x,y,limit);
|
||||
const dp=pr.kind==='unknown'?correction(ref,s,FULL_W,FULL_H,x,y,limit):pr;
|
||||
if(fd<limit&&o===limit)f32FE++;
|
||||
if(fd===limit&&o<limit)f32FB++;
|
||||
if(dd<limit&&o===limit)dsFE++;
|
||||
if(dd===limit&&o<limit)dsFB++;
|
||||
if(fd<limit&&o<limit&&fd!==o)f32IterMismatch++;
|
||||
if(dd<limit&&o<limit&&dd!==o)dsIterMismatch++;
|
||||
if(dp.kind==='unknown')deepU++;
|
||||
else {
|
||||
if(dp.kind==='escaped'&&o===limit)deepFE++;
|
||||
if(dp.kind==='bounded'&&o<limit)deepFB++;
|
||||
if(dp.kind==='escaped'&&o<limit&&dp.n!==o)deepIterMismatch++;
|
||||
}
|
||||
}
|
||||
}
|
||||
const adj=adjacentUniq(s,FULL_W,FULL_H);
|
||||
const row={
|
||||
track:tr.id,z,limit,tested,guardMismatch,adjacentUniqueX:adj,
|
||||
f32:{falseEscaped:f32FE,falseBounded:f32FB,escapeIterationMismatch:f32IterMismatch,uniqueCoords:f32Coords.size},
|
||||
ds:{falseEscaped:dsFE,falseBounded:dsFB,escapeIterationMismatch:dsIterMismatch,uniqueCoords:dsCoords.size},
|
||||
deep:{falseEscaped:deepFE,falseBounded:deepFB,unknown:deepU,escapeIterationMismatch:deepIterMismatch},
|
||||
modelMs:Math.round(performance.now()-t0)
|
||||
};
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
const seahorse=rows.filter(r=>r.track==='seahorse');
|
||||
const material=r=>((r.falseEscaped+r.falseBounded)/Math.max(1,r.tested))>0.01;
|
||||
const firstF32=seahorse.find(r=>material({...r.f32,tested:r.tested})||r.adjacentUniqueX.f32<11)?.z??null;
|
||||
const firstDs=seahorse.find(r=>material({...r.ds,tested:r.tested})||r.adjacentUniqueX.ds<11)?.z??null;
|
||||
const deepIssues=seahorse.filter(r=>r.deep.falseEscaped||r.deep.falseBounded||r.deep.unknown);
|
||||
const report={format:'mandelbrot-crossover-quality-v1',kind:'cpu-f32-model + BigInt P/P+64 oracle; not real GPU',viewport:[FULL_W,FULL_H],sampleGrid:[SW,SH],depths,tracks:tracks.map(t=>t.id),rows,summary:{firstMaterialF32DegradationDepth:firstF32,firstMaterialDsDegradationDepth:firstDs,deepIssueRows:deepIssues.map(r=>({z:r.z,...r.deep})),interpretation:'On the Seahorse track at a 1600x900 viewport, f32 direct becomes materially unsuitable around z6; DS direct remains a plausible bridge through roughly z8 and degrades by z10. Corrected deep is the best of the modeled backends, but one z8 false escape means it is not a membership certificate.'}};
|
||||
if(rows.some(r=>r.guardMismatch))throw new Error('P/P+64 oracle guard mismatch in crossover corpus');
|
||||
await fs.mkdir(new URL('../audit/',import.meta.url),{recursive:true});
|
||||
await fs.writeFile(new URL('../audit/v24-crossover-quality.json',import.meta.url),JSON.stringify(report,null,2));
|
||||
console.log(JSON.stringify({status:'pass',check:'crossover-quality-advisory',summary:report.summary},null,2));
|
||||
24
tests/v24-fixed96-contract.mjs
Normal file
24
tests/v24-fixed96-contract.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/DIRECT_DS_GUARDED_WGSL/.test(kernels)&&/riskLimit:f32/.test(kernels)&&/riskQueue:array<u32>/.test(kernels)&&/atomicAdd\(&hybridStats\.enqueued,1u\)/.test(kernels),'guarded DS sensitivity queue shader missing');
|
||||
must(/DIRECT_FIXED96_QUEUE_ARGS_WGSL/.test(kernels)&&/dispatchCount/.test(kernels)&&/indirectArgs\.x=\(n\+63u\)\/64u/.test(kernels),'fixed96 queue indirect-args shader missing');
|
||||
must(/DIRECT_FIXED96_CORRECT_WGSL/.test(kernels)&&/struct FX96/.test(kernels)&&/fn mul32/.test(kernels)&&/Q8\.88/.test(kernels),'fixed96 correction shader missing');
|
||||
const a=kernels.indexOf('const DIRECT_FIXED96_CORRECT_WGSL'),b=kernels.indexOf('// Deep path',a),fx=kernels.slice(a,b);
|
||||
must(!/fn fx_bad_range/.test(fx),'invalid Q8.88 sign-extension range guard regressed');
|
||||
must(/let TWO=FX96\(0u,0u,0x02000000u\)/.test(fx)&&/fx_cmp_unsigned\(fx_abs\(zr\),TWO\)>0/.test(fx)&&/write_escape\(out,n,zr,zi\)/.test(fx),'fixed96 certain-escape guard missing');
|
||||
must(a>=0&&b>a&&!fx.includes('var<storage,read> refs'),'fixed96 correction unexpectedly depends on deep reference');
|
||||
must(/@workgroup_size\(64\)/.test(fx)&&/let out=riskQueue\[qi\]/.test(fx)&&/atomicLoad\(&hybridStats\.dispatchCount\)/.test(fx),'fixed96 correction is not queue-indexed');
|
||||
must(!/@workgroup_size\(8,8\)/.test(fx),'fixed96 correction regressed to full-screen 2D workgroups');
|
||||
must(/direct-fixed96-queue-args-experiment/.test(script)&&/this\.fixed96QueueArgs=/.test(script),'fixed96 queue-args pipeline missing');
|
||||
must(/direct-fixed96-correction-experiment/.test(script)&&/this\.fixed96Correct=/.test(script),'fixed96 correction pipeline missing');
|
||||
must(/q88FromRational/.test(script)&&/q88Words/.test(script)&&/fixed96TileData/.test(script),'exact BigInt-to-Q8.88 packing missing');
|
||||
must(/benchmarkFixed96Direct/.test(script)&&/renderTileMetaFixed96Direct/.test(script),'fixed96 benchmark methods missing');
|
||||
must(/GPUBufferUsage[\s\S]*?B\.INDIRECT/.test(script)&&/dispatchWorkgroupsIndirect\(indirect,0\)/.test(script),'true sparse indirect dispatch missing');
|
||||
must(/fixed96QueueIntegrity/.test(script)&&/queue integrity failure/.test(script),'queue integrity fail-closed gate missing');
|
||||
must(/FIXED96_DEPTHS=\[6,8,10,12\]/.test(script)&&/FIXED96_RISK_LIMITS=\[1e11,1e12,1e13,1e14\]/.test(script),'fixed96 depth/risk sweep missing');
|
||||
must(/runFixed96Experiment/.test(script)&&/failureReason/.test(script)&&/Every risk candidate/.test(script),'fixed96 experiment/full failure reporting missing');
|
||||
must(/speedQueueRate/.test(script)&&/timingRepresentative/.test(script)&&/sampling-mismatch/.test(script),'speed/quality sampling consistency gate missing');
|
||||
must(/id="sparseBench"/.test(html)&&/Fixed96 Direct実験/.test(html)&&/id="sparseDialog"/.test(html),'fixed96 UI missing');
|
||||
const choose=(script.match(/function chooseBackend[\s\S]*?function deepNeeded/)||[''])[0];must(!/fixed96|FIXED96/.test(choose),'diagnostic fixed96 leaked into production router');
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-experiment-contract',checks:20},null,2));
|
||||
15
tests/v24-fixed96-limb-model.mjs
Normal file
15
tests/v24-fixed96-limb-model.mjs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
const MASK=0xffffffffn,MOD=1n<<96n,SIGN=1n<<95n,Q=88n;
|
||||
const u32=x=>Number(BigInt(x)&MASK)>>>0;
|
||||
function words(x){x%=MOD;if(x<0)x+=MOD;return[u32(x),u32(x>>32n),u32(x>>64n)]}
|
||||
function signed(w){let x=BigInt(w[0])+(BigInt(w[1])<<32n)+(BigInt(w[2])<<64n);return x>=SIGN?x-MOD:x}
|
||||
function neg(a){let lo=(~a[0]+1)>>>0,c0=lo===0?1:0,mid=(~a[1]+c0)>>>0,c1=c0&&mid===0?1:0,hi=(~a[2]+c1)>>>0;return[lo,mid,hi]}
|
||||
function isNeg(a){return (a[2]&0x80000000)!==0} function absw(a){return isNeg(a)?neg(a):a}
|
||||
function add(a,b){let lo=(a[0]+b[0])>>>0,c0=lo<a[0]?1:0,m0=(a[1]+b[1])>>>0,c1=m0<a[1]?1:0,mid=(m0+c0)>>>0,c2=mid<m0?1:0,hi=(a[2]+b[2]+c1+c2)>>>0;return[lo,mid,hi]}
|
||||
function mul32(a,b){const a0=a&65535,a1=a>>>16,b0=b&65535,b1=b>>>16,p0=a0*b0,p1=a0*b1,p2=a1*b0,p3=a1*b1,middle=(p0>>>16)+(p1&65535)+(p2&65535),lo=((p0&65535)|((middle&65535)<<16))>>>0,hi=(p3+(p1>>>16)+(p2>>>16)+(middle>>>16))>>>0;return[lo,hi]}
|
||||
function addAt(p,idx,v){v>>>=0;while(v&&idx<6){const old=p[idx]>>>0,sum=(old+v)>>>0;p[idx]=sum;v=sum<old?1:0;idx++}}
|
||||
function addProd(p,idx,a,b){const q=mul32(a,b);addAt(p,idx,q[0]);addAt(p,idx+1,q[1])}
|
||||
function mulfx(a0,b0){const sign=isNeg(a0)!==isNeg(b0),a=absw(a0),b=absw(b0),p=[0,0,0,0,0,0];addProd(p,0,a[0],b[0]);addProd(p,1,a[0],b[1]);addProd(p,2,a[0],b[2]);addProd(p,1,a[1],b[0]);addProd(p,2,a[1],b[1]);addProd(p,3,a[1],b[2]);addProd(p,2,a[2],b[0]);addProd(p,3,a[2],b[1]);addProd(p,4,a[2],b[2]);let r=[((p[2]>>>24)|(p[3]<<8))>>>0,((p[3]>>>24)|(p[4]<<8))>>>0,((p[4]>>>24)|(p[5]<<8))>>>0];if(((p[2]>>>23)&1)!==0)r=add(r,[1,0,0]);return sign?neg(r):r}
|
||||
function roundShift(v,b){const negv=v<0n,a=negv?-v:v,q=(a+(1n<<(b-1n)))>>b;return negv?-q:q}
|
||||
let seed=0x12345678;const rand=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed};
|
||||
for(let i=0;i<20000;i++){const ai=(BigInt(rand()%16000000)-8000000n)*(1n<<68n),bi=(BigInt(rand()%16000000)-8000000n)*(1n<<68n),a=words(ai),b=words(bi),got=signed(mulfx(a,b)),want=roundShift(ai*bi,Q);if(got!==want)throw new Error(`mul mismatch ${i}: ${got} != ${want}`)}
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-limb-multiply',cases:20000,format:'Q8.88'},null,2));
|
||||
24
tests/v24-fixed96-limb-orbit-model.mjs
Normal file
24
tests/v24-fixed96-limb-orbit-model.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
const MASK=0xffffffffn,MOD=1n<<96n,SIGN=1n<<95n,Q=88n,MIN=-(1n<<95n),MAX=(1n<<95n)-1n;
|
||||
const u32=x=>Number(BigInt(x)&MASK)>>>0;
|
||||
function words(x){if(x<MIN||x>MAX)throw new Error('Q8.88 overflow');if(x<0)x+=MOD;return[u32(x),u32(x>>32n),u32(x>>64n)]}
|
||||
function signed(w){let x=BigInt(w[0])+(BigInt(w[1])<<32n)+(BigInt(w[2])<<64n);return x>=SIGN?x-MOD:x}
|
||||
function neg(a){let lo=(~a[0]+1)>>>0,c0=lo===0?1:0,mid=(~a[1]+c0)>>>0,c1=c0&&mid===0?1:0,hi=(~a[2]+c1)>>>0;return[lo,mid,hi]}
|
||||
function isNeg(a){return (a[2]&0x80000000)!==0} function absw(a){return isNeg(a)?neg(a):a}
|
||||
function add(a,b){let lo=(a[0]+b[0])>>>0,c0=lo<a[0]?1:0,m0=(a[1]+b[1])>>>0,c1=m0<a[1]?1:0,mid=(m0+c0)>>>0,c2=mid<m0?1:0,hi=(a[2]+b[2]+c1+c2)>>>0;return[lo,mid,hi]}
|
||||
function sub(a,b){return add(a,neg(b))}
|
||||
function cmpu(a,b){for(let i=2;i>=0;i--){if(a[i]<b[i])return-1;if(a[i]>b[i])return 1}return 0}
|
||||
function mul32(a,b){const a0=a&65535,a1=a>>>16,b0=b&65535,b1=b>>>16,p0=a0*b0,p1=a0*b1,p2=a1*b0,p3=a1*b1,middle=(p0>>>16)+(p1&65535)+(p2&65535),lo=((p0&65535)|((middle&65535)<<16))>>>0,hi=(p3+(p1>>>16)+(p2>>>16)+(middle>>>16))>>>0;return[lo,hi]}
|
||||
function addAt(p,idx,v){v>>>=0;while(v&&idx<6){const old=p[idx]>>>0,sum=(old+v)>>>0;p[idx]=sum;v=sum<old?1:0;idx++}}
|
||||
function addProd(p,idx,a,b){const q=mul32(a,b);addAt(p,idx,q[0]);addAt(p,idx+1,q[1])}
|
||||
function mulfx(a0,b0){const sign=isNeg(a0)!==isNeg(b0),a=absw(a0),b=absw(b0),p=[0,0,0,0,0,0];addProd(p,0,a[0],b[0]);addProd(p,1,a[0],b[1]);addProd(p,2,a[0],b[2]);addProd(p,1,a[1],b[0]);addProd(p,2,a[1],b[1]);addProd(p,3,a[1],b[2]);addProd(p,2,a[2],b[0]);addProd(p,3,a[2],b[1]);addProd(p,4,a[2],b[2]);let r=[((p[2]>>>24)|(p[3]<<8))>>>0,((p[3]>>>24)|(p[4]<<8))>>>0,((p[4]>>>24)|(p[5]<<8))>>>0];if(((p[2]>>>23)&1)!==0)r=add(r,[1,0,0]);return sign?neg(r):r}
|
||||
function roundShift(v,b=Q){const n=v<0n,a=n?-v:v,q=(a+(1n<<(b-1n)))>>b;return n?-q:q}
|
||||
function exactMul(a,b){const r=roundShift(a*b);if(r<MIN||r>MAX)throw new Error('exact overflow');return r}
|
||||
const TWO=words(2n<<Q),FOUR=words(4n<<Q);
|
||||
function limbOrbit(cr0,ci0,limit){const cr=words(cr0),ci=words(ci0);let zr=[0,0,0],zi=[0,0,0];for(let n=1;n<=limit;n++){const zr2=mulfx(zr,zr),zi2=mulfx(zi,zi),zri=mulfx(zr,zi);zr=add(sub(zr2,zi2),cr);zi=add(add(zri,zri),ci);if(cmpu(absw(zr),TWO)>0||cmpu(absw(zi),TWO)>0)return n;const mag=add(mulfx(zr,zr),mulfx(zi,zi));if(cmpu(mag,FOUR)>0)return n}return limit}
|
||||
function exactOrbit(cr,ci,limit){let zr=0n,zi=0n;const four=4n<<Q;for(let n=1;n<=limit;n++){const zr2=exactMul(zr,zr),zi2=exactMul(zi,zi),zri=exactMul(zr,zi);zr=zr2-zi2+cr;zi=2n*zri+ci;if((zr<0n?-zr:zr)>2n<<Q||(zi<0n?-zi:zi)>2n<<Q)return n;const mag=exactMul(zr,zr)+exactMul(zi,zi);if(mag>four)return n}return limit}
|
||||
function qdec(x){return BigInt(Math.round(x*2**24))<<64n}
|
||||
const cases=[];
|
||||
for(const [re,im] of [[-0.7453983606667815,0.1125046349959942],[-0.75,0.1],[-0.743643887037151,0.13182590420533],[-1.8,0.2],[0.3,0.55],[1.2,0]]){
|
||||
const cr=qdec(re),ci=qdec(im),a=limbOrbit(cr,ci,1200),b=exactOrbit(cr,ci,1200);if(a!==b)throw new Error(`orbit mismatch ${re},${im}: ${a} != ${b}`);cases.push({re,im,iter:a});
|
||||
}
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-limb-orbit',cases},null,2));
|
||||
39
tests/v24-fixed96-model.mjs
Normal file
39
tests/v24-fixed96-model.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const F=Math.fround,Q=88,MOD=1n<<96n,MIN=-(1n<<95n),MAX=(1n<<95n)-1n;
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
|
||||
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const[m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
|
||||
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
|
||||
function bitsForDepth(z){return Math.max(256,Math.ceil(z*Math.log2(10))+300)}
|
||||
function snap(re,im,z){const bits=bitsForDepth(z);return{bits,re:decFixed(re,bits),im:decFixed(im,bits),span:decFixed(`3.4e-${z}`,bits)}}
|
||||
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
|
||||
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
|
||||
function q88FromRational(numer,denom,bits){let n=numer,d=denom,sh=Q-bits;if(sh>=0)n<<=BigInt(sh);else d<<=BigInt(-sh);return roundDiv(n,d)}
|
||||
function qmul(a,b){const r=roundShift(a*b,Q);if(r<MIN||r>MAX)throw new Error('Q8.88 overflow');return r}
|
||||
function qadd(a,b){const r=a+b;if(r<MIN||r>MAX)throw new Error('Q8.88 overflow');return r}
|
||||
function fixed96Direct(s,w,h,x,y,limit){const den=4n*BigInt(w),ox4=4n*BigInt(x)+2n-2n*BigInt(w),oy4=2n*BigInt(h)-4n*BigInt(y)-2n,cr=q88FromRational(s.re*den+s.span*ox4,den,s.bits),ci=q88FromRational(s.im*den+s.span*oy4,den,s.bits);let zr=0n,zi=0n;const four=4n<<88n;for(let n=0;n<limit;n++){const zr2=qmul(zr,zr),zi2=qmul(zi,zi),ri=qmul(zr,zi);zr=qadd(qadd(zr2,-zi2),cr);zi=qadd(qadd(ri,ri),ci);if(qadd(qmul(zr,zr),qmul(zi,zi))>four)return n+1}return limit}
|
||||
// DS sensitivity model matching the experimental guard.
|
||||
function q(a,b){const x=F(F(a)+F(b)),e=F(F(b)-F(x-F(a)));return[x,e]}
|
||||
function sum(a,b){a=F(a);b=F(b);const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
|
||||
function prod(a,b){a=F(a);b=F(b);const x=F(a*b),ca=F(F(4097)*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(F(4097)*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
|
||||
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(F(a[1])+F(b[1]))))}
|
||||
function neg(a){return[F(-a[0]),F(-a[1])]} function sub(a,b){return add(a,neg(b))}
|
||||
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(F(a[0])*F(b[1])));e=F(e+F(F(a[1])*F(b[0])));e=F(e+F(F(a[1])*F(b[1])));return q(t[0],e)}
|
||||
function scale(a,b){const t=prod(a[0],F(b));return q(t[0],F(t[1]+F(F(a[1])*F(b))))}
|
||||
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
|
||||
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]} function cmul(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
|
||||
function split(x){const h=F(x);return[h,F(x-h)]} function val(a){return F(F(a[0])+F(a[1]))}
|
||||
function dsRisk(s,w,h,x,y,limit){const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),sp=split(fixedNum(s.span,s.bits)),ox=F(F(F(x+.5)-F(.5*w))/F(w)),oy=F(F(F(.5*h)-F(y+.5))/F(w)),c=[add(cre,scale(sp,ox)),add(cim,scale(sp,oy))];let z=[[0,0],[0,0]],dr=0,di=0,maxDer=0;for(let n=0;n<limit;n++){const zr=val(z[0]),zi=val(z[1]),ndr=F(F(2)*F(F(zr*dr)-F(zi*di))+F(1)),ndi=F(F(2)*F(F(zr*di)+F(zi*dr)));dr=ndr;di=ndi;maxDer=Math.max(maxDer,Math.hypot(dr,di));z=cadd(cmul(z,z),c);const mag=add(mul(z[0],z[0]),mul(z[1],z[1]));if(cmp(mag,[4,0])>0)return{n:n+1,maxDer}}return{n:limit,maxDer}}
|
||||
function adaptiveIter(z){return Math.max(350,350+Math.floor(70*Math.sqrt(z)+15*z))}
|
||||
const re='-0.7453983606667815',im='0.1125046349959942',depths=[6,8,10,12],risks=[1e11,1e12,1e13,1e14],W=1600,H=900,SW=31,SH=21,rows=[];
|
||||
for(const z of depths){const s=snap(re,im,z),limit=adaptiveIter(z),samples=[];for(let sy=0;sy<SH;sy++){const y=Math.round(sy*(H-1)/(SH-1));for(let sx=0;sx<SW;sx++){const x=Math.round(sx*(W-1)/(SW-1)),p=pixel(s,W,H,x,y),pg=pixel(s,W,H,x,y,s.bits+64),o=oracle(p[0],p[1],s.bits,limit),g=oracle(pg[0],pg[1],s.bits+64,limit);if(o!==g){samples.push({guardMismatch:true});continue}const d=dsRisk(s,W,H,x,y,limit),f=fixed96Direct(s,W,H,x,y,limit);samples.push({guardMismatch:false,o,d,f})}}
|
||||
const variants=[];for(const risk of risks){let tested=0,guardMismatch=0,uncertain=0,wrongBefore=0,wrongAfter=0;for(const a of samples){if(a.guardMismatch){guardMismatch++;continue}tested++;if((a.d.n<a.d.limit)!=(a.o<a.d.limit)){}const before=(a.d.n<limit)!=(a.o<limit);if(before)wrongBefore++;let r=a.d.n;if(a.d.maxDer>risk){uncertain++;r=a.f}if((r<limit)!=(a.o<limit))wrongAfter++}variants.push({risk,tested,guardMismatch,uncertain,uncertainRate:uncertain/Math.max(1,tested),wrongBefore,wrongAfter})}
|
||||
rows.push({z,limit,variants})}
|
||||
if(rows.some(r=>r.variants.some(v=>v.guardMismatch)))throw new Error('oracle guard mismatch');
|
||||
for(const r of rows)if(!r.variants.some(v=>v.wrongAfter===0))throw new Error(`no safe fixed96 candidate at z${r.z}`);
|
||||
const report={format:'mandelbrot-fixed96-q88-model-v1',kind:'Exact BigInt model of signed Q8.88 direct + DS risk gate + BigInt P/P+64 oracle; integer GPU arithmetic should be deterministic modulo implementation bugs',viewport:[W,H],sampleGrid:[SW,SH],rows};
|
||||
await fs.mkdir(new URL('../audit/',import.meta.url),{recursive:true});await fs.writeFile(new URL('../audit/v24-fixed96-model.json',import.meta.url),JSON.stringify(report,null,2));
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-q88-model',rows:rows.map(r=>({z:r.z,variants:r.variants}))},null,2));
|
||||
22
tests/v24-fixed96-queue-model.mjs
Normal file
22
tests/v24-fixed96-queue-model.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
const root=new URL('../',import.meta.url);
|
||||
const script=await fs.readFile(new URL('script.js',root),'utf8');
|
||||
const start=script.indexOf('function fixed96QueueIntegrity('),end=script.indexOf('\nfunction mulRatio',start);
|
||||
if(start<0||end<=start)throw new Error('fixed96QueueIntegrity source missing');
|
||||
const source=script.slice(start,end);
|
||||
const ctx={};ctx.globalThis=ctx;
|
||||
vm.runInNewContext(`const FIXED96_WORKGROUP_SIZE=64;${source};globalThis.check=fixed96QueueIntegrity`,ctx);
|
||||
const check=ctx.check;
|
||||
const stats=(selected,{overflow=0,enqueued=selected,dispatchCount=enqueued}={})=>new Uint32Array([selected,overflow,enqueued,dispatchCount,0,0,0,0]);
|
||||
const fixed=(processed,{remaining=0,invalidIndex=0,staleEntry=0,corrected=processed-remaining}={})=>new Uint32Array([remaining,invalidIndex,processed,staleEntry,0,0,0,corrected]);
|
||||
const args=n=>new Uint32Array([Math.ceil(n/64),1,1,0]);
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(check(stats(0),fixed(0),args(0),64).ok,'zero queue should be valid');
|
||||
must(check(stats(20),fixed(20),args(20),64).ok,'20-entry queue should be valid');
|
||||
must(check(stats(65),fixed(65),args(65),128).ok,'65-entry queue should use two workgroups');
|
||||
must(!check(stats(65),fixed(65),new Uint32Array([1,1,1,0]),128).ok,'wrong indirect workgroup count was not rejected');
|
||||
must(!check(stats(64,{overflow:1,enqueued:63,dispatchCount:63}),fixed(63),args(63),64).ok,'queue overflow was not rejected');
|
||||
must(!check(stats(20),fixed(19,{corrected:19}),args(20),64).ok,'processed/enqueued mismatch was not rejected');
|
||||
must(!check(stats(20),fixed(20,{staleEntry:1,remaining:1,corrected:19}),args(20),64).ok,'stale queue entry was not rejected');
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-queue-integrity-model',cases:7,workgroupSize:64},null,2));
|
||||
24
tests/v24-fixed96-shader-range-model.mjs
Normal file
24
tests/v24-fixed96-shader-range-model.mjs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// Regression for v24.2.6: Q8.88 values such as +1.2 were incorrectly treated
|
||||
// as invalid sign extension. Model the corrected shader policy: all Q8.88
|
||||
// values are valid; after each iteration, |Re|>2 or |Im|>2 is an immediate
|
||||
// mathematically-certain escape, otherwise |z|^2 can be squared safely.
|
||||
const Q=88n, ONE=1n<<Q, TWO=2n<<Q, FOUR=4n<<Q, MIN=-(1n<<95n), MAX=(1n<<95n)-1n;
|
||||
function roundShift(v,b=Q){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(b-1n)))>>b;return neg?-q:q}
|
||||
function q(x){return BigInt(Math.round(x*2**24)) << 64n} // exact enough for test constants
|
||||
function mul(a,b){const r=roundShift(a*b);if(r<MIN||r>MAX)throw new Error('unexpected Q8.88 overflow');return r}
|
||||
function add(a,b){const r=a+b;if(r<MIN||r>MAX)throw new Error('unexpected Q8.88 overflow');return r}
|
||||
function abs(a){return a<0n?-a:a}
|
||||
function iterate(cr,ci,maxIter=2000){let zr=0n,zi=0n;for(let n=1;n<=maxIter;n++){
|
||||
const zr2=mul(zr,zr),zi2=mul(zi,zi),zri=mul(zr,zi);
|
||||
zr=add(add(zr2,-zi2),cr);zi=add(add(zri,zri),ci);
|
||||
if(abs(zr)>TWO||abs(zi)>TWO)return n;
|
||||
const mag=add(mul(zr,zr),mul(zi,zi));if(mag>FOUR)return n;
|
||||
}return maxIter}
|
||||
// +1.2 is a valid Q8.88 value and must not be rejected merely because the
|
||||
// integer byte is non-zero.
|
||||
const plus12=q(1.2);if(plus12<=ONE||plus12>=TWO)throw new Error('test constant invalid');
|
||||
// Escaping parameters whose orbit passes through component magnitudes >1.
|
||||
for(const [re,im] of [[1.2,0],[-1.8,0.2],[0.5,0.8],[0.4,0.6]]){
|
||||
const n=iterate(q(re),q(im),500);if(n>=500)throw new Error(`expected escape for ${re},${im}`);
|
||||
}
|
||||
console.log(JSON.stringify({status:'pass',check:'fixed96-shader-range-regression',cases:4,regression:'v24.2.6 rejected valid Q8.88 integer-byte values'},null,2));
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const must=(x,m)=>{if(!x)throw new Error(m)};
|
||||
for(const [W,H] of [[17,11],[1024,768],[16384,9216]])for(const tile of [64,512])for(const sx of [.25,.5,.75])for(const sy of [.25,.5,.75])for(const [tx,ty,lx,ly] of [[0,0,0,0],[tile,0,3,5],[tile*2,tile,7,11]]){if(tx+lx>=W||ty+ly>=H)continue;const gx=tx+lx+sx,gy=ty+ly+sy,fullX=(gx-.5*W)/W,fullY=(.5*H-gy)/W,tiledX=((tx+lx+sx)-.5*W)/W,tiledY=(.5*H-(ty+ly+sy))/W;must(Object.is(fullX,tiledX)&&Object.is(fullY,tiledY),'tile mapping seam')}
|
||||
const maxFramePixels=8*1048576,fieldBytes=maxFramePixels*4;must(fieldBytes<=128*1024*1024,'default storage binding budget exceeded');
|
||||
const oldQueueBytes=maxFramePixels*4,newCounterBytes=16;must(newCounterBytes<oldQueueBytes/100000,'unresolved queue was not reduced to a counter');
|
||||
const oldQueueBytes=maxFramePixels*4,newCounterBytes=32;must(newCounterBytes<oldQueueBytes/100000,'unresolved queue was not reduced to a counter');
|
||||
for(const [cw,ch] of [[390,844],[1920,1080],[1080,1920]]){const aspect=ch/cw,w0=16384,h0=Math.round(w0*aspect),h=Math.min(16384,h0),w=h0>16384?Math.max(64,Math.round(h/aspect)):w0;must(w<=16384&&h<=16384,'export dimension clamp failed')}
|
||||
console.log(JSON.stringify({status:'pass',pixelContract:'centered',exportTile:512,strictRetryPass:false,unresolvedCounterBytes:newCounterBytes,maxExportSide:16384},null,2));
|
||||
|
|
|
|||
15
tests/v24-hybrid-contract.mjs
Normal file
15
tests/v24-hybrid-contract.mjs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/DIRECT_DS_GUARDED_WGSL/.test(kernels)&&/riskLimit:f32/.test(kernels)&&/deriv=2\.0\*cmul/.test(kernels),'guarded DS shader / derivative risk gate missing');
|
||||
must(/direct-ds-guarded-experiment/.test(script)&&/this\.dsGuarded=/.test(script),'guarded DS pipeline missing');
|
||||
must(/benchmarkHybrid/.test(script)&&/renderTileMetaHybrid/.test(script),'hybrid benchmark methods missing');
|
||||
must(/HYBRID_DEPTHS=\[6,8,10,12\]/.test(script)&&/HYBRID_RISK_LIMITS=\[1e12,1e13,1e14\]/.test(script),'hybrid depth/risk sweep missing');
|
||||
must(/runHybridExperiment/.test(script)&&/uncertainRate/.test(script)&&/speedupVsDeepCold/.test(script),'hybrid experiment/report missing');
|
||||
must(/id="hybridBench"/.test(html)&&/id="hybridDialog"/.test(html)&&/id="hybridSave"/.test(html),'hybrid experiment UI missing');
|
||||
must(/function openHybridDialog\(/.test(script)&&/function startHybridExperimentFromUi\(/.test(script),'hybrid UI start function missing');
|
||||
must(/\$\('#hybridBench'\)\.onclick=\(\)=>\{toast\('Hybrid実験を開始'\);void startHybridExperimentFromUi\(\)\}/.test(script),'toolbar Hybrid button does not directly start experiment');
|
||||
must(/\$\('#hybridStart'\)\.onclick=startHybridExperimentFromUi/.test(script),'dialog rerun button is not wired to experiment');
|
||||
const choose=(script.match(/function chooseBackend[\s\S]*?function deepNeeded/)||[''])[0];
|
||||
must(!/dsGuarded|hybrid-candidate|backend='hybrid'/.test(choose),'diagnostic hybrid leaked into production router');
|
||||
console.log(JSON.stringify({status:'pass',check:'hybrid-experiment-contract',checks:10},null,2));
|
||||
39
tests/v24-hybrid-risk-model.mjs
Normal file
39
tests/v24-hybrid-risk-model.mjs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const F=Math.fround;
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
|
||||
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const[m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
|
||||
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
|
||||
function bitsForDepth(z){return Math.max(256,Math.ceil(z*Math.log2(10))+300)}
|
||||
function snap(re,im,z){const span=`3.4e-${z}`,bits=bitsForDepth(z);return{bits,re:decFixed(re,bits),im:decFixed(im,bits),span:decFixed(span,bits)}}
|
||||
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
|
||||
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
|
||||
function q(a,b){const x=F(a+b),e=F(b-F(x-a));return[x,e]}
|
||||
function sum(a,b){const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
|
||||
function prod(a,b){const x=F(a*b),ca=F(4097*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(4097*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
|
||||
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(a[1]+b[1])))}
|
||||
function neg(a){return[F(-a[0]),F(-a[1])]}
|
||||
function sub(a,b){return add(a,neg(b))}
|
||||
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(a[0]*b[1]));e=F(e+F(a[1]*b[0]));e=F(e+F(a[1]*b[1]));return q(t[0],e)}
|
||||
function scaleDS(a,b){const t=prod(a[0],b);return q(t[0],F(t[1]+F(a[1]*b)))}
|
||||
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
|
||||
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]}
|
||||
function cmulDS(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
|
||||
function mag2DS(a){return add(mul(a[0],a[0]),mul(a[1],a[1]))}
|
||||
function split(x){const h=F(x);return[h,F(x-h)]}
|
||||
function value(a){return F(a[0]+a[1])}
|
||||
function dsRisk(s,w,h,x,y,limit){const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),span=split(fixedNum(s.span,s.bits)),offx=(x+.5-.5*w)/w,offy=(.5*h-y-.5)/w,cr=add(cre,scaleDS(span,F(offx))),ci=add(cim,scaleDS(span,F(offy))),c=[cr,ci];let z=[[0,0],[0,0]],dr=0,di=0,maxDer=0;for(let n=0;n<limit;n++){const zr=value(z[0]),zi=value(z[1]),ndr=2*(zr*dr-zi*di)+1,ndi=2*(zr*di+zi*dr);dr=ndr;di=ndi;maxDer=Math.max(maxDer,Math.hypot(dr,di));z=cadd(cmulDS(z,z),c);if(cmp(mag2DS(z),[4,0])>0)return{n:n+1,maxDer}}return{n:limit,maxDer}}
|
||||
function adaptiveIter(z){return Math.max(350,350+Math.floor(70*Math.sqrt(z)+15*z))}
|
||||
const re='-0.7453983606667815',im='0.1125046349959942',depths=[6,8,10,12],W=1600,H=900,SW=31,SH=21,riskLimit=1e13,rows=[];
|
||||
for(const z of depths){const s=snap(re,im,z),limit=adaptiveIter(z);let tested=0,wrong=0,uncertain=0,caught=0,guardMismatch=0;for(let sy=0;sy<SH;sy++){const y=Math.round(sy*(H-1)/(SH-1));for(let sx=0;sx<SW;sx++){const x=Math.round(sx*(W-1)/(SW-1)),p=pixel(s,W,H,x,y),g=pixel(s,W,H,x,y,s.bits+64),o=oracle(p[0],p[1],s.bits,limit),gg=oracle(g[0],g[1],s.bits+64,limit);if(o!==gg){guardMismatch++;continue}tested++;const d=dsRisk(s,W,H,x,y,limit),bad=(d.n<limit)!=(o<limit),u=d.maxDer>riskLimit;if(bad)wrong++;if(u)uncertain++;if(bad&&u)caught++}}
|
||||
rows.push({z,tested,guardMismatch,wrong,uncertain,caught,uncertainRate:uncertain/tested,catchRate:wrong?caught/wrong:1});}
|
||||
if(rows.some(r=>r.guardMismatch))throw new Error('oracle guard mismatch');
|
||||
for(const r of rows.slice(0,3))if(r.catchRate!==1)throw new Error(`risk gate missed DS class error at z${r.z}`);
|
||||
if(!(rows[0].uncertainRate<.08&&rows[1].uncertainRate<.2&&rows[2].uncertainRate<.5))throw new Error('risk gate is too broad before z12');
|
||||
if(!(rows[3].uncertainRate>.75))throw new Error('z12 should signal crossover toward Full Deep');
|
||||
const report={format:'mandelbrot-hybrid-risk-model-v1',kind:'CPU Math.fround DS + BigInt P/P+64 oracle; not real GPU',viewport:[W,H],sampleGrid:[SW,SH],riskLimit,rows,interpretation:'At riskLimit=1e13 the derivative guard catches every observed DS classification error at z6/z8/z10 while sending about 3%/11%/35% of samples to Deep correction. At z12 it sends over 75%, which is a deliberate signal that Full Deep should replace the hybrid.'};
|
||||
await fs.mkdir(new URL('../audit/',import.meta.url),{recursive:true});await fs.writeFile(new URL('../audit/v24-hybrid-risk-model.json',import.meta.url),JSON.stringify(report,null,2));
|
||||
console.log(JSON.stringify({status:'pass',check:'hybrid-risk-model',rows},null,2));
|
||||
28
tests/v24-hybrid-ui-runtime.mjs
Normal file
28
tests/v24-hybrid-ui-runtime.mjs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
const source=await fs.readFile(new URL('../script.js',import.meta.url),'utf8');
|
||||
const start=source.indexOf('function openHybridDialog()');
|
||||
const end=source.indexOf('// ── diagnostics',start);
|
||||
if(start<0||end<0)throw new Error('hybrid UI block not found');
|
||||
const block=source.slice(start,end);
|
||||
const el=(extra={})=>Object.assign({textContent:'',value:'',disabled:false,hidden:true,max:0,open:false,attrs:new Map(),setAttribute(k,v){this.attrs.set(k,String(v));if(k==='open')this.open=true},removeAttribute(k){this.attrs.delete(k)},showModal(){this.open=true},close(){this.open=false}},extra);
|
||||
const elements={
|
||||
'#hybridDialog':el(), '#hybridStart':el({textContent:'再実行'}), '#hybridBench':el(), '#hybridProgress':el(), '#hybridStatus':el(), '#hybridSave':el(), '#hybridOutput':el(), '#hybridTrack':el({value:'seahorse'}), '#hybridSamples':el({value:'2'}), '#hybridCancel':el()
|
||||
};
|
||||
let runCount=0,toastText='';
|
||||
const ctx={
|
||||
$:s=>elements[s], hybridJob:{active:false,cancelled:false,last:null}, benchmarkJob:{active:false}, HYBRID_DEPTHS:[6,8,10,12],
|
||||
cancelRender(){}, state:{dirty:true}, refs:{cancelPending(){}}, markDirty(){}, toast(s){toastText=s}, hybridSummary(){return 'summary'},
|
||||
async runHybridExperiment({onProgress}){runCount++;onProgress?.({index:1,total:4,z:6,phase:'probe'});return{rows:[1,2,3,4]}},
|
||||
downloadBlob(){}, Blob:globalThis.Blob, Date, Math, JSON, String, Error
|
||||
};
|
||||
vm.createContext(ctx);vm.runInContext(block,ctx,{filename:'hybrid-ui-block.js'});
|
||||
if(typeof elements['#hybridBench'].onclick!=='function')throw new Error('toolbar click handler not installed');
|
||||
elements['#hybridBench'].onclick();
|
||||
if(!elements['#hybridDialog'].open)throw new Error('toolbar click did not open dialog synchronously');
|
||||
if(runCount!==1)throw new Error('toolbar click did not start experiment synchronously');
|
||||
if(!/Hybrid実験/.test(toastText))throw new Error('toolbar click gives no visible acknowledgement');
|
||||
await new Promise(r=>setTimeout(r,0));
|
||||
if(!/実験完了/.test(elements['#hybridStatus'].textContent))throw new Error('experiment completion not reflected in UI: '+elements['#hybridStatus'].textContent);
|
||||
if(elements['#hybridBench'].disabled)throw new Error('toolbar stayed disabled');
|
||||
console.log(JSON.stringify({status:'pass',check:'hybrid-ui-runtime',runCount,dialogOpen:elements['#hybridDialog'].open,statusText:elements['#hybridStatus'].textContent,toastText},null,2));
|
||||
|
|
@ -1 +1,7 @@
|
|||
import fs from 'node:fs/promises';const h=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');const req=['id="view"','id="processMode"','保守的 (Strict)','id="exportPrecision"','gpu-kernels.js','script.js'];for(const x of req)if(!h.includes(x))throw new Error('missing '+x);if(h.includes('<script src="kernels.js"></script>'))throw new Error('legacy kernels.js loaded');console.log(JSON.stringify({status:'pass',checks:req.length+1},null,2));
|
||||
import fs from 'node:fs/promises';
|
||||
const h=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
|
||||
const req=['id="view"','id="processMode"','保守的 (Strict)','id="exportPrecision"','id="backendBench"','id="benchmarkDialog"','id="sparseBench"','id="sparseDialog"','mandelbrot-bundle','data-bundle="gpu-kernels"','data-bundle="app"'];
|
||||
for(const x of req)if(!h.includes(x))throw new Error('missing '+x);
|
||||
if(/<script\s+src=/.test(h))throw new Error('standalone index still depends on external scripts');
|
||||
if(h.includes('<script src="kernels.js"></script>'))throw new Error('legacy kernels.js loaded');
|
||||
console.log(JSON.stringify({status:'pass',checks:req.length+2,singleFile:true},null,2));
|
||||
|
|
|
|||
|
|
@ -3,14 +3,16 @@ const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('scri
|
|||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(/const VERSION=24/.test(script),'renderer version is not 24');
|
||||
must(/navigator\.gpu\.requestAdapter/.test(script),'WebGPU adapter path missing');
|
||||
must(/guarded rescaled perturbation/.test(script),'guarded deep WebGPU path missing');
|
||||
must(/guarded perturbation/.test(script)&&/DEEP_CORRECT_WGSL/.test(kernels)&&/correctUnknownFrame/.test(script),'guarded deep + sparse DS correction path missing');
|
||||
must(/referenceWorkerSource/.test(script)&&/BigInt\(d\.re\)/.test(script),'BigInt reference worker missing');
|
||||
must(!/function buildBla\(/.test(script)&&!/BlaNode/.test(kernels)&&!/useBla/.test(kernels),'unsafe BLA path is not fully quarantined');
|
||||
must(!/function buildBla\(/.test(script)&&!/BlaNode/.test(kernels)&&!/useBla/.test(kernels),'unsafe BLA path is not fully removed');
|
||||
must(/F32_U/.test(kernels)&&/safe_abs_error/.test(kernels)&&/errAbs/.test(kernels),'deep error-bound guard missing');
|
||||
must(/struct UnresolvedHead/.test(kernels)&&/atomicAdd\(&unresolved\.remaining/.test(kernels),'unresolved counter missing');
|
||||
must(!/strict_queue|QUEUE_FINALIZE_WGSL/.test(kernels)&&!/dispatchWorkgroupsIndirect|qitems|strict-indirect/.test(script),'redundant strict retry queue remains');
|
||||
must(/struct UnresolvedHead/.test(kernels)&&/atomicAdd\(&unresolved\.remaining/.test(kernels)&&/REASON_ERROR_BOUND/.test(kernels),'unresolved reason counters missing');
|
||||
must(/CORRECTION_MARK/.test(kernels)&&/ds_prod/.test(kernels)&&/correct_pixel/.test(kernels),'double-single correction kernel missing');
|
||||
must(!/strict_queue|QUEUE_FINALIZE_WGSL/.test(kernels)&&!/qitems|strict-indirect/.test(script),'obsolete strict retry queue remains');
|
||||
must(/texture_storage_2d<rgba8unorm,write>/.test(kernels),'GPU color field path missing');
|
||||
must(/AA_RESOLVE_WGSL/.test(kernels)&&/renderTileRGBA2x/.test(script),'GPU 2x2 export resolve missing');
|
||||
must(/correctUnknown=true/.test(script)&&/encodeCorrectionNumeric/.test(script),'export/probe DS correction integration missing');
|
||||
must(/class StreamingPng/.test(script)&&/CompressionStream\('deflate'\)/.test(script)&&/async abort\(reason\)/.test(script),'streaming PNG export/abort missing');
|
||||
must(/PRESENT_WGSL/.test(kernels),'GPU reprojection/presentation missing');
|
||||
must(/recolorPending/.test(script)&&!/const token=\+\+state\.token,stateSnap/.test(script),'recolor race fix missing');
|
||||
|
|
@ -18,15 +20,14 @@ must(/this\.context=null/.test(script)&&/webgpuCanvasClaimed=true;this\.configur
|
|||
must(/このズーム深度はWebGPUが必要です/.test(script),'deep fallback rejection missing');
|
||||
must(!/Math\.min\(340/.test(script),'deep exact-coordinate formatter still capped at 340 digits');
|
||||
must(/uncapturederror/.test(script)&&/pushErrorScope/.test(script),'WebGPU error capture missing');
|
||||
must(/gpu-kernels\.js/.test(html)&&!/src="kernels\.js"/.test(html),'index is not WebGPU-only');
|
||||
must(/mandelbrot-bundle/.test(html)&&/data-bundle="gpu-kernels"/.test(html)&&/data-bundle="app"/.test(html)&&!/<script\s+src=/.test(html),'standalone index is not self-contained WebGPU bundle');
|
||||
must(/numericParams/.test(script)&&/colorParams/.test(script),'persistent frame parameter buffers missing');
|
||||
must(/ensureExportWorkspace/.test(script)&&/exportWorkspaceDestroy/.test(script)&&/export tile exceeds reusable workspace/.test(script),'reusable export GPU workspace missing');
|
||||
must(/if\(!adapter\)adapter=await navigator\.gpu\.requestAdapter\(\)/.test(script),'adapter fallback request missing');
|
||||
must(/gpuInitFailed/.test(script)&&/WebGPU初期化失敗/.test(script),'shader/pipeline failure guard missing');
|
||||
must(/if\(state\.gpuInitFailed\)return null/.test(script),'failed shader initialization can be retried on every render');
|
||||
must(/if\(!r\)\{if\(state\.gpuInitFailed\)/.test(script),'shader failure still enters CPU fallback');
|
||||
must(/return 524288/.test(script)&&/1572864/.test(script),'responsive screen pixel budgets missing');
|
||||
must(/ensureExportWorkspace/.test(script)&&/unresolveds:Array\.from\(\{length:4\}/.test(script),'reusable corrected export workspace missing');
|
||||
must(/DIRECT_REQUIRED_RATIO=8/.test(script)&&/DEEP_SPEED_MARGIN=\.85/.test(script)&&/ensureBackendProbe/.test(script),'adaptive direct/deep router missing');
|
||||
must(/DIRECT_DS_WGSL/.test(kernels)&&/runCrossoverBenchmark/.test(script),'diagnostic DS/crossover benchmark missing');
|
||||
must(/overlap-view-probe-required/.test(script)&&/qualityPass/.test(script)&&/backendQualityTiles/.test(script)&&/overlap-probe-failed-safe-deep/.test(script),'view-specific quality probe/fail-safe gate missing');
|
||||
must(!/referenceSnapAtPixel|chooseUnknownCandidate|multi-reference-refine/.test(script),'obsolete multi-reference production path remains');
|
||||
must(/gpuUnavailable/.test(script)&&/return 262144/.test(script),'lightweight no-adapter CPU fallback budget missing');
|
||||
for(const banned of ['DEEP_SIMD_B64','DEEP_SCALAR_B64','BLA_SIMD_B64','BLA_SCALAR_B64','deepPool','deepWasm','highPrecisionDirectPixelAsync','renderDeepAdaptive','render_perturb_rebase_rect','WebAssembly.Instance'])must(!script.includes(banned),'legacy production deep dependency remains: '+banned);
|
||||
must(!/Validated direct/.test(html),'obsolete Validated UI remains');
|
||||
console.log(JSON.stringify({status:'pass',rendererVersion:24,shaderVersion:'24.1.3',checks:28},null,2));
|
||||
console.log(JSON.stringify({status:'pass',rendererVersion:24,shaderVersion:'24.2.8-fixed96-sparse-queue',checks:28},null,2));
|
||||
|
|
|
|||
45
tests/v24-sparse-correction-model.mjs
Normal file
45
tests/v24-sparse-correction-model.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const corpus=JSON.parse(await fs.readFile(new URL('./scenes.json',import.meta.url),'utf8'));
|
||||
const sc=corpus.scenes.find(s=>s.id==='swirly-seahorses-z12');
|
||||
if(!sc)throw new Error('swirly scene missing');
|
||||
const F=Math.fround,U=2**-24;
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
|
||||
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
|
||||
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
|
||||
function bitsForSpan(t){const s=String(t).toLowerCase(),a=s.split('e'),m=Math.abs(Number(a[0])),e=a[1]?Number(a[1]):0,l2=Math.log2(m)+e*Math.log2(10);return Math.max(256,Math.ceil(-l2)+240)}
|
||||
function snap(scene){const bits=bitsForSpan(scene.span);return{bits,re:decFixed(scene.re,bits),im:decFixed(scene.im,bits),span:decFixed(scene.span,bits)}}
|
||||
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
|
||||
function log2FixedAt(v,b){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-b}
|
||||
function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:Math.pow(2,l-exp),exp}}
|
||||
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||
function direct(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
|
||||
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<<BigInt(b);let zr=0n,zi=0n,refLen=limit;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;const mag=roundShift(zr*zr,b)+roundShift(zi*zi,b);if(mag>bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[2*refLen]=hr1;hi[2*refLen+1]=hi1;lo[2*refLen]=F(rr1-hr1);lo[2*refLen+1]=F(ri1-hi1);break}}return{hi,lo,refLen}}
|
||||
function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}
|
||||
function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))}
|
||||
function primary(ref,s,w,h,x,y,limit){const se0=spanME(s),se={mant:F(se0.mant),exp:se0.exp},dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w);let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);return ea<=1e-3?{kind:'bounded',n}:{kind:'unknown',n}}if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n};return{kind:'unknown',n}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
|
||||
function q(a,b){const x=F(a+b),e=F(b-F(x-a));return[x,e]}
|
||||
function sum(a,b){const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
|
||||
function prod(a,b){const x=F(a*b),ca=F(4097*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(4097*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
|
||||
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(a[1]+b[1])))}
|
||||
function neg(a){return[F(-a[0]),F(-a[1])]}
|
||||
function sub(a,b){return add(a,neg(b))}
|
||||
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(a[0]*b[1]));e=F(e+F(a[1]*b[0]));e=F(e+F(a[1]*b[1]));return q(t[0],e)}
|
||||
function scaleDS(a,b){const t=prod(a[0],b);return q(t[0],F(t[1]+F(a[1]*b)))}
|
||||
function pow2DS(a,e){if(e<-126)return[0,0];if(e>126)return[F(8.507059e37),0];return[F(a[0]*2**e),F(a[1]*2**e)]}
|
||||
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
|
||||
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]}
|
||||
function cmulDS(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
|
||||
function cscale(a,b){return[scaleDS(a[0],b),scaleDS(a[1],b)]}
|
||||
function cpow2(a,e){return[pow2DS(a[0],e),pow2DS(a[1],e)]}
|
||||
function mag2(a){return add(mul(a[0],a[0]),mul(a[1],a[1]))}
|
||||
function val(a){return F(a[0]+a[1])}
|
||||
function maxabsDS(a){return Math.max(Math.abs(val(a[0])),Math.abs(val(a[1])))}
|
||||
function correction(ref,s,w,h,x,y,limit){const se=spanME(s),mh=F(se.mant),sm=[mh,F(se.mant-mh)],iw=1/w,iwh=F(iw),inv=[iwh,F(iw-iwh)],ox=F(x+.5-.5*w),oy=F(.5*h-y-.5),dx=scaleDS(inv,ox),dy=scaleDS(inv,oy),d0=[mul(sm,dx),mul(sm,dy)];let d=d0,wv=[[0,0],[0,0]],e=se.exp,n=0,m=0,ops=0;while(true){if(n>=limit)return{kind:'bounded',n};if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,r=[[ref.hi[rp],ref.lo[rp]],[ref.hi[rp+1],ref.lo[rp+1]]],delta=cpow2(wv,e),z=cadd(r,delta),mg=mag2(z);if(cmp(mg,[4,0])>0)return{kind:'escaped',n};const dm=mag2(delta);if(m>0&&cmp(dm,[0,0])>0&&cmp(mg,dm)<0){if(se.exp-e<-96)return{kind:'unknown',n};wv=z;d=cpow2(d0,se.exp);e=0;m=0;continue}if(m>=ref.refLen)return{kind:'unknown',n};const linear=cscale(cmulDS(r,wv),2),sq=cpow2(cmulDS(wv,wv),e);wv=cadd(cadd(linear,sq),d);m++;n++;ops++;const mm=Math.max(maxabsDS(wv),maxabsDS(d));if(mm>=1e30||!Number.isFinite(mm))return{kind:'unknown',n};if(mm>65536){wv=cscale(wv,1/65536);d=cscale(d,1/65536);e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wv=cscale(wv,65536);d=cscale(d,65536);e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
|
||||
const s=snap(sc),w=61,h=39,limit=2000,ref=buildRef(s,limit);let guardMismatch=0,primaryUnknown=0,primaryFalseEscaped=0,primaryFalseBounded=0,corrected=0,remaining=0,correctionFalseEscaped=0,correctionFalseBounded=0,countMismatch=0,maxCountDiff=0;
|
||||
for(let y=0;y<h;y++)for(let x=0;x<w;x++){const p=pixel(s,w,h,x,y),g=pixel(s,w,h,x,y,s.bits+64),oracle=direct(p[0],p[1],s.bits,limit),guard=direct(g[0],g[1],s.bits+64,limit);if(oracle!==guard){guardMismatch++;continue}const a=primary(ref,s,w,h,x,y,limit);if(a.kind==='escaped'&&oracle===limit)primaryFalseEscaped++;if(a.kind==='bounded'&&oracle<limit)primaryFalseBounded++;if(a.kind!=='unknown')continue;primaryUnknown++;const c=correction(ref,s,w,h,x,y,limit);if(c.kind==='unknown'){remaining++;continue}corrected++;if(c.kind==='escaped'&&oracle===limit)correctionFalseEscaped++;if(c.kind==='bounded'&&oracle<limit)correctionFalseBounded++;if(c.kind==='escaped'&&oracle<limit&&c.n!==oracle){countMismatch++;maxCountDiff=Math.max(maxCountDiff,Math.abs(c.n-oracle))}}
|
||||
const report={status:'pass',kind:'sparse-double-single-correction-cpu-model-not-real-gpu',scene:sc.id,grid:[w,h],limit,referenceLength:ref.refLen,guardMismatch,primary:{unknown:primaryUnknown,falseEscaped:primaryFalseEscaped,falseBounded:primaryFalseBounded},correction:{corrected,remaining,falseEscaped:correctionFalseEscaped,falseBounded:correctionFalseBounded,escapeIterationMismatch:countMismatch,maxEscapeIterationDifference:maxCountDiff},note:'DS correction is a visual-quality recovery pass; escape iteration equality is not certified.'};
|
||||
if(guardMismatch||primaryFalseEscaped||primaryFalseBounded||correctionFalseEscaped||correctionFalseBounded||remaining)throw new Error(JSON.stringify(report));
|
||||
console.log(JSON.stringify(report,null,2));
|
||||
10
tests/v24-standalone-ui-contract.mjs
Normal file
10
tests/v24-standalone-ui-contract.mjs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import fs from 'node:fs/promises';
|
||||
const root=new URL('../',import.meta.url),h=await fs.readFile(new URL('index.html',root),'utf8'),dev=await fs.readFile(new URL('index.external.html',root),'utf8'),script=await fs.readFile(new URL('script.js',root),'utf8');
|
||||
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
|
||||
must(!/<script\s+src=/.test(h),'root standalone HTML has external script dependency');
|
||||
must(/data-bundle="gpu-kernels"/.test(h)&&/data-bundle="app"/.test(h),'inline bundle markers missing');
|
||||
must(/id="sparseBench"/.test(h)&&/id="sparseDialog"/.test(h),'Fixed96 UI missing in standalone');
|
||||
must(/\$\('#sparseBench'\)\.onclick/.test(h)&&/runFixed96Experiment/.test(h),'Fixed96 button handler not embedded in standalone');
|
||||
must(/<script src="gpu-kernels\.js"><\/script>/.test(dev)&&/<script src="script\.js"><\/script>/.test(dev),'hosted/dev template no longer has explicit source scripts');
|
||||
must(/\$\('#sparseBench'\)\.onclick/.test(script),'source button handler missing');
|
||||
console.log(JSON.stringify({status:'pass',check:'single-file-ui-contract',checks:6,reason:'Opening HTML from an archive temp extraction does not require sibling JS files.'},null,2));
|
||||
13
tests/v24-ui-cacheproof.mjs
Normal file
13
tests/v24-ui-cacheproof.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import crypto from 'node:crypto';
|
||||
const root=new URL('../',import.meta.url);
|
||||
const html=await fs.readFile(new URL('index.html',root),'utf8');
|
||||
const script=await fs.readFile(new URL('script.js',root));
|
||||
const kernels=await fs.readFile(new URL('gpu-kernels.js',root));
|
||||
const vs=await fs.readFile(new URL('script-v24.2.6.js',root));
|
||||
const vk=await fs.readFile(new URL('gpu-kernels-v24.2.6.js',root));
|
||||
const h=b=>crypto.createHash('sha256').update(b).digest('hex');
|
||||
if(!html.includes('script-v24.2.6.js')||!html.includes('gpu-kernels-v24.2.6.js'))throw new Error('versioned browser assets not referenced');
|
||||
if(html.includes('<script src="script.js"')||html.includes('<script src="gpu-kernels.js"'))throw new Error('bare cacheable browser assets still referenced');
|
||||
if(h(script)!==h(vs)||h(kernels)!==h(vk))throw new Error('versioned browser assets differ from canonical source');
|
||||
console.log(JSON.stringify({status:'pass',check:'ui-cacheproof-assets',scriptSha256:h(script),kernelSha256:h(kernels)},null,2));
|
||||
|
|
@ -21,15 +21,15 @@ async function checkScene(api,id,re,im,span,iter,mode){
|
|||
const r=await api.setView({re,im,span,baseIter:iter,adaptive:false,processMode:mode}),diag=r.diag;
|
||||
if(diag.backend!=='webgpu'||diag.webgpuError)throw new Error(id+'/'+mode+': WebGPU backend unavailable/error: '+(diag.webgpuError||diag.backend));
|
||||
const st=api.state(),ps=points(st.width,st.height),meta=await api.sampleMeta(ps);let guardMismatch=0,falseEscaped=0,falseBounded=0,escapeIterationMismatch=0,unknown=0;
|
||||
for(let i=0;i<ps.length;i++){const [x,y]=ps[i],p=pixel(st,x,y),pg=pixel(st,x,y,st.bits+64),a=orbit(p[0],p[1],st.bits,st.iter),g=orbit(pg[0],pg[1],st.bits+64,st.iter);if(a!==g){guardMismatch++;continue}const m=meta[i],cls=(m>>>28)&3,n=m&0x0fffffff;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a<st.iter)falseBounded++;if(cls===1&&a<st.iter&&n!==a)escapeIterationMismatch++}
|
||||
for(let i=0;i<ps.length;i++){const [x,y]=ps[i],p=pixel(st,x,y),pg=pixel(st,x,y,st.bits+64),a=orbit(p[0],p[1],st.bits,st.iter),g=orbit(pg[0],pg[1],st.bits+64,st.iter);if(a!==g){guardMismatch++;continue}const m=meta[i],cls=(m>>>28)&3,n=m&0x000fffff;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a<st.iter)falseBounded++;if(cls===1&&a<st.iter&&n!==a)escapeIterationMismatch++}
|
||||
const row={id,mode,width:st.width,height:st.height,iter:st.iter,deep:diag.deep,guardMismatch,falseEscaped,falseBounded,escapeIterationMismatch,unknown,known:ps.length-unknown,reference:diag.reference};
|
||||
if(guardMismatch||falseEscaped||falseBounded)throw new Error(id+'/'+mode+' numeric gate failed: '+JSON.stringify(row));
|
||||
if(id==='swirly-seahorses-z12'){if(row.known<12)throw new Error(id+'/'+mode+': excessive UNKNOWN rate: '+JSON.stringify(row));}
|
||||
else if(row.unknown!==0)throw new Error(id+'/'+mode+': stable corpus scene produced UNKNOWN samples: '+JSON.stringify(row));
|
||||
if(row.unknown!==0)throw new Error(id+'/'+mode+': corrected production frame produced UNKNOWN samples: '+JSON.stringify(row));
|
||||
return row;
|
||||
}
|
||||
async function run(){await waitApp();const api=frame.contentWindow.__MANDEL_TEST__,report=[];for(const [id,re,im,span,iter] of scenes){for(const mode of (id==='z0'?['standard']:['standard','validate'])){out.textContent='running '+id+' / '+mode+'…\n'+JSON.stringify(report,null,2);report.push(await checkScene(api,id,re,im,span,iter,mode));}}
|
||||
await api.setView({re:'-0.7453983606667815',im:'0.1125046349959942',span:'3.52e-12',baseIter:2000,adaptive:false,processMode:'validate'});const denseState={...api.state(),width:61,height:39},denseMeta=await api.probeMeta({w:61,h:39,strict:true}),densePoints=[[25,12],[26,15],[27,25]],denseRegression=[];for(const[x,y]of densePoints){const p=pixel(denseState,x,y),pg=pixel(denseState,x,y,denseState.bits+64),a=orbit(p[0],p[1],denseState.bits,denseState.iter),g=orbit(pg[0],pg[1],denseState.bits+64,denseState.iter);if(a!==g)throw new Error('dense guard mismatch '+x+','+y);const m=denseMeta[y*61+x],cls=(m>>>28)&3,n=m&0x0fffffff;if(cls===1&&a===denseState.iter)throw new Error('dense false escape '+JSON.stringify({x,y,a,cls,n}));if(cls!==0&&cls!==1&&a<denseState.iter)throw new Error('dense false bounded '+JSON.stringify({x,y,a,cls,n}));denseRegression.push({x,y,oracle:a,cls,n})}
|
||||
await api.setView({re:'-0.75',im:'0',span:'3.4e-20',baseIter:1000,adaptive:false,processMode:'validate'});const exportSmoke=await api.smokeExportTile({w:48,h:32,strict:true,ss:1}),exportSmokeAA=await api.smokeExportTile({w:48,h:32,strict:true,ss:2});if(exportSmoke.length!==exportSmoke.expected||exportSmokeAA.length!==exportSmokeAA.expected)throw new Error('GPU export tile readback length mismatch');if(exportSmoke.unresolved!==0||exportSmokeAA.unresolved!==0)throw new Error('GPU export smoke left unresolved samples: '+JSON.stringify({exportSmoke,exportSmokeAA}));const diag=frame.contentWindow.__MANDEL_DIAG__.snapshot();if(diag.uncapturedErrors&&diag.uncapturedErrors.length)throw new Error('uncaptured WebGPU errors: '+JSON.stringify(diag.uncapturedErrors));const result={status:'pass',kind:'real-webgpu-acceptance',date:new Date().toISOString(),shaderVersion:diag.shaderVersion,adapter:diag.adapter,limits:diag.limits,exportSmoke,exportSmokeAA,denseRegression,report};out.textContent=JSON.stringify(result,null,2);document.title='PASS v24.1 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result}
|
||||
run().catch(e=>{const result={status:'fail',error:String(e&&e.stack||e)};out.textContent=JSON.stringify(result,null,2);document.title='FAIL v24.1 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result});
|
||||
await api.setView({re:'-0.7453983606667815',im:'0.1125046349959942',span:'3.52e-12',baseIter:2000,adaptive:false,processMode:'validate'});const denseState={...api.state(),width:61,height:39},denseMeta=await api.probeMeta({w:61,h:39,strict:true,forceDeep:true,correctUnknown:true}),densePoints=[[25,12],[26,15],[27,25]],denseRegression=[];for(const[x,y]of densePoints){const p=pixel(denseState,x,y),pg=pixel(denseState,x,y,denseState.bits+64),a=orbit(p[0],p[1],denseState.bits,denseState.iter),g=orbit(pg[0],pg[1],denseState.bits+64,denseState.iter);if(a!==g)throw new Error('dense guard mismatch '+x+','+y);const m=denseMeta[y*61+x],cls=(m>>>28)&3,n=m&0x000fffff;if(cls===0)throw new Error('dense DS correction left known regression pixel UNKNOWN '+JSON.stringify({x,y,a,cls,n}));if(cls===1&&a===denseState.iter)throw new Error('dense false escape '+JSON.stringify({x,y,a,cls,n}));if(cls!==1&&a<denseState.iter)throw new Error('dense false bounded '+JSON.stringify({x,y,a,cls,n}));denseRegression.push({x,y,oracle:a,cls,n})}
|
||||
await api.setView({re:'0.4',im:'0',span:'0.0015',baseIter:500,adaptive:false,processMode:'standard'});const routerDiag=frame.contentWindow.__MANDEL_DIAG__.snapshot(),routerDecision=routerDiag.backendDecision;if(!routerDecision||routerDecision.probe||!routerDecision.viewProbe)throw new Error('overlap view-specific backend probe did not settle: '+JSON.stringify(routerDecision));const vp=routerDecision.viewProbe;if(!vp.qualityPass||vp.classDisagreement!==0||vp.deepUnknownRate>.005)throw new Error('overlap backend quality probe failed: '+JSON.stringify(vp));const deepShouldWin=vp.predictedDeepMs<vp.predictedDirectMs*.85;if((routerDecision.backend==='deep')!==deepShouldWin)throw new Error('backend router ignored measured 15% speed margin: '+JSON.stringify(routerDecision));
|
||||
await api.setView({re:'-0.75',im:'0',span:'3.4e-20',baseIter:1000,adaptive:false,processMode:'validate'});const exportSmoke=await api.smokeExportTile({w:48,h:32,strict:true,ss:1}),exportSmokeAA=await api.smokeExportTile({w:48,h:32,strict:true,ss:2});if(exportSmoke.length!==exportSmoke.expected||exportSmokeAA.length!==exportSmokeAA.expected)throw new Error('GPU export tile readback length mismatch');if(exportSmoke.unresolved!==0||exportSmokeAA.unresolved!==0)throw new Error('GPU export smoke left unresolved samples: '+JSON.stringify({exportSmoke,exportSmokeAA}));const fixed96Smoke=await api.runFixed96Experiment({track:'seahorse',depths:[8],riskLimits:[1e13],warmups:0,samples:1,qualityW:32,qualityH:24}),sr=fixed96Smoke.rows[0],sv=sr&&sr.variants&&sr.variants[0];if(!sv||!Number.isFinite(sv.fullMedianMs)||sv.remaining!==0||sv.queueIntegrity!==true||Math.abs((sv.dispatchRate||0)-(sv.uncertainRate||0))>1e-9)throw new Error('fixed96 experiment smoke failed: '+JSON.stringify(fixed96Smoke));const diag=frame.contentWindow.__MANDEL_DIAG__.snapshot();if(diag.uncapturedErrors&&diag.uncapturedErrors.length)throw new Error('uncaptured WebGPU errors: '+JSON.stringify(diag.uncapturedErrors));const result={status:'pass',kind:'real-webgpu-acceptance',date:new Date().toISOString(),shaderVersion:diag.shaderVersion,adapter:diag.adapter,limits:diag.limits,routerDecision,fixed96Smoke,exportSmoke,exportSmokeAA,denseRegression,report};out.textContent=JSON.stringify(result,null,2);document.title='PASS v24.2 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result}
|
||||
run().catch(e=>{const result={status:'fail',error:String(e&&e.stack||e)};out.textContent=JSON.stringify(result,null,2);document.title='FAIL v24.2.8 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result});
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue