diff --git a/dist/hosted/_headers b/dist/hosted/_headers deleted file mode 100644 index b7d7893..0000000 --- a/dist/hosted/_headers +++ /dev/null @@ -1,3 +0,0 @@ -/* - X-Content-Type-Options: nosniff - Referrer-Policy: no-referrer diff --git a/dist/hosted/gpu-kernels.js b/dist/hosted/gpu-kernels.js deleted file mode 100644 index 4f73ff2..0000000 --- a/dist/hosted/gpu-kernels.js +++ /dev/null @@ -1,778 +0,0 @@ -(()=>{'use strict'; -const COMMON=String.raw` -const FIELD_UNKNOWN:u32=0u; -const FIELD_ESCAPED:u32=1u; -const FIELD_INTERIOR_LIKELY:u32=2u; -const FIELD_INTERIOR_PROVEN:u32=3u; -const ITER_MASK:u32=0x000fffffu; -const REASON_SHIFT:u32=20u; -const REASON_MASK:u32=0x0ff00000u; -const REASON_NONE:u32=0u; -const REASON_ERROR_BOUND:u32=1u; -const REASON_ESCAPE_UNCERTAIN:u32=2u; -const REASON_REFERENCE_END:u32=3u; -const REASON_REBASE_GAP:u32=4u; -const REASON_RANGE:u32=5u; -const REASON_OPERATION_LIMIT:u32=6u; - -fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); } -fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); } -fn cmul(a:vec2, b:vec2)->vec2{ - return vec2(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x); -} -fn maxabs(v:vec2)->f32 { return max(abs(v.x),abs(v.y)); } -const F32_U:f32=5.960464477539063e-8; -fn pow2_safe(e:i32)->f32 { - if(e < -126){ return 0.0; } - if(e > 126){ return 8.507059e37; } - return ldexp(1.0,e); -} -fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2, delta:vec2)->f32{ - let propagated=abs(errScaled*pow2_safe(scaleExp)); - let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30); - return propagated+reconstruction; -} -fn scaled_to_f32(v:vec2, e:i32)->vec2{ - if(e < -126){ return vec2(0.0); } - if(e > 126){ return vec2(8.507059e37); } - return ldexp(v,vec2(e)); -} -fn smooth_escape(n:u32, mag2:f32)->f32{ - let u=log2(max(4.0000005,mag2)); - return f32(n)+1.0-log2(max(1.0e-20,0.5*u)); -} -`; - -const DIRECT_F32_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, strict:u32, - centerRe:f32, centerIm:f32, span:f32, sampleX:f32, - sampleY:f32, _p0:f32, _p1:f32, _p2:f32, -}; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var fieldSmooth:array; -fn analytic(cr:f32,ci:f32)->bool{ - let y2=ci*ci; let x=cr-0.25; let q=x*x+y2; - let lhs=q*(q+x); let rhs=0.25*y2; - let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0); - if(lhs){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=gid.y*p.tileW+gid.x; - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - let scale=p.span/f32(p.fullW); - let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale; - let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale; - if(analytic(cr,ci)){ - fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return; - } - var zr=0.0; var zi=0.0; var n=0u; - loop{ - if(n>=p.maxIter){break;} - let zr2=zr*zr; let zi2=zi*zi; - zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u; - let mag=zr*zr+zi*zi; - if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;} - } - fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0; -} -`; - - -// Fast-mode precision extension. This keeps the interactive path independent -// of the verified Deep backend: a reduced-precision reference orbit is built -// quickly on the CPU worker and this lightweight perturbation kernel preserves -// sub-f32 pixel offsets once absolute Direct coordinates begin to collapse. -const FAST_PERTURB_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct RefPoint{ hi:vec2, lo:vec2 }; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var refs:array; -@group(0) @binding(2) var fieldMeta:array; -@group(0) @binding(3) var fieldSmooth:array; -@group(0) @binding(4) var unresolved:array>; -fn mark_fast_unresolved(out:u32,n:u32,reason:u32){ - fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u); - if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);} -} - -fn render_fast(out:u32,gx:f32,gy:f32){ - let dx=(gx-p.refPixelX)/f32(p.fullW); - let dy=(p.refPixelY-gy)/f32(p.fullW); - var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); - var w=vec2(0.0); - var scaleExp=p.spanExp; - var n=0u; var m=0u; - loop{ - if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;} - let rp=refs[min(m,p.refLen)]; - let delta=scaled_to_f32(w,scaleExp); - let z=rp.hi+(rp.lo+delta); - let mag=dot(z,z); - if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} - - // Rebase the perturbation when it grows beyond the reference value. This - // keeps the fast path usable across pans without any verified correction. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); - scaleExp=0; m=0u; - continue; - } - if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;} - let r=rp; - let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); - let sq=cmul(w,w)*pow2_safe(scaleExp); - w=linear+sq+d; m+=1u; n+=1u; - let mm=max(maxabs(w),maxabs(d)); - if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;} - if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;} - else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;} - if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;} - } -} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - render_fast(out,gx,gy); -} -`; - - -const DEEP_PERTURB_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct RefPoint{ hi:vec2, lo:vec2 }; -struct UnresolvedHead{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var refs:array; -@group(0) @binding(2) var fieldMeta:array; -@group(0) @binding(3) var fieldSmooth:array; -@group(0) @binding(4) var unresolved:UnresolvedHead; - -fn mark_unresolved(out:u32,n:u32,reason:u32){ - fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0; - atomicAdd(&unresolved.remaining,1u); - if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);} - else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);} - else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);} - else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);} - else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);} - else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);} -} -fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ - let dx=(gx-p.refPixelX)/f32(p.fullW); - let dy=(p.refPixelY-gy)/f32(p.fullW); - // dc = d * 2^scaleExp. Keep d and w in one shared scale. - var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); - var w=vec2(0.0); - var scaleExp=p.spanExp; - var n=0u; var m=0u; - var errScaled=1.0*F32_U*maxabs(d); - loop{ - if(n>=p.maxIter){ - let rpEnd=refs[min(m,p.refLen)]; - let deltaEnd=scaled_to_f32(w,scaleExp); - let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd); - let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd); - let limit=select(1.0e-3,1.0e-4,strictMode); - if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);} - return; - } - let rp=refs[m]; - let delta=scaled_to_f32(w,scaleExp); - let z=rp.hi+(rp.lo+delta); - let mag=dot(z,z); - if(mag>4.0){ - let errAbs=safe_abs_error(errScaled,scaleExp,z,delta); - if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} - mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return; - } - - // Rebase only when dc remains numerically representable in the new scale. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u; - errScaled+=1.0*F32_U*maxabs(d); - continue; - } - - if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;} - let r=rp; - let refAbs=maxabs(r.hi)+maxabs(r.lo); - let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp)); - let gain=2.0*refAbs+2.0*wAbs*p2; - let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30); - errScaled=gain*errScaled+roundErr; - let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); - // delta^2 / 2^scaleExp = w^2 * 2^scaleExp - let sq=cmul(w,w)*pow2_safe(scaleExp); - w=linear+sq+d; m+=1u; n+=1u; - if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;} - let mm=max(maxabs(w),maxabs(d)); - if(mm>65536.0){ - w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16; - }else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){ - w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16; - } - if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;} - } -} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; - render_pixel(out,gx,gy,p.strict!=0u); -} -`; - - -// Idle refinement is compiled as a separate guarded-Deep module so the -// production Fast/Deep main shaders remain byte-for-byte identical to v24.2.18. -const DEEP_LIKELY_REFINE_WGSL=DEEP_PERTURB_WGSL.replace( - '@group(0) @binding(4) var unresolved:UnresolvedHead;', - '@group(0) @binding(4) var unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var refineQueue:array;\n@group(1) @binding(1) var refineBatch:RefineBatch;' -)+String.raw` -@compute @workgroup_size(64) -fn refine_likely(@builtin(global_invocation_id) gid:vec3){ - let slot=refineBatch.base+gid.x; - if(slot>=refineBatch.end){return;} - let out=refineQueue[slot]; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;} - let x=out%p.fullW; let y=out/p.fullW; - render_pixel(out,f32(x)+p.sampleX,f32(y)+p.sampleY,p.strict!=0u); -} -`; - -// Production Deep sparse path: keep the long-running perturbation kernel -// untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets. -// Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS -// correction kernel sees less workgroup-level iteration divergence. -// -// Bucket state layout (24 u32 atomics): -// [0..7] histogram counts -// [8..15] scatter cursors (initialized from prefix offsets) -// [16..23] immutable prefix offsets for queue accounting -const DEEP_BUCKET_HIST_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var bucketState:array>; -var localCounts:array,8>; -fn deep_bucket(packed:u32)->u32{ - let n=packed&ITER_MASK; - return min(7u,(n*8u)/max(1u,p.maxIter)); -} -@compute @workgroup_size(64) -fn main( - @builtin(local_invocation_id) lid3:vec3, - @builtin(workgroup_id) wid:vec3 -){ - let lane=lid3.x; - if(lane<8u){atomicStore(&localCounts[lane],0u);} - workgroupBarrier(); - let lx=wid.x*64u+lane; - let ly=wid.y; - if(lx>28u)&3u)==FIELD_UNKNOWN){ - atomicAdd(&localCounts[deep_bucket(packed)],1u); - } - } - workgroupBarrier(); - if(lane<8u){ - let c=atomicLoad(&localCounts[lane]); - if(c>0u){atomicAdd(&bucketState[lane],c);} - } -} -`; - -// Convert the 8-bin histogram into contiguous queue ranges, initialize each -// scatter cursor, and produce the correction indirect-dispatch arguments. -const DEEP_BUCKET_PREFIX_WGSL=String.raw` -struct SparseQueueStats{ - selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, -}; -struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 }; -@group(0) @binding(0) var bucketState:array>; -@group(0) @binding(1) var sparseQueueStats:SparseQueueStats; -@group(0) @binding(2) var indirectArgs:IndirectArgs; -@compute @workgroup_size(1) -fn main(){ - var total=0u; - var b=0u; - loop{ - if(b>=8u){break;} - let c=atomicLoad(&bucketState[b]); - atomicStore(&bucketState[8u+b],total); - atomicStore(&bucketState[16u+b],total); - total+=c; - b+=1u; - } - atomicStore(&sparseQueueStats.selected,total); - atomicStore(&sparseQueueStats.overflow,0u); - atomicStore(&sparseQueueStats.enqueued,total); - atomicStore(&sparseQueueStats.dispatchCount,total); - indirectArgs.x=(total+63u)/64u; - indirectArgs.y=1u; - indirectArgs.z=1u; - indirectArgs._pad=0u; -} -`; - -// Scatter UNKNOWN indices into the precomputed bucket ranges. Within each -// 64-lane workgroup, workgroup-memory atomics allocate local ranks; each -// non-empty bucket reserves one global subrange, so global atomics scale with -// non-empty (workgroup,bucket) pairs rather than with UNKNOWN pixels. -const DEEP_BUCKET_SCATTER_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct SparseQueueStats{ - selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, -}; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var bucketState:array>; -@group(0) @binding(3) var sparseQueueStats:SparseQueueStats; -@group(0) @binding(4) var unknownQueue:array; -var localCounts:array,8>; -var localRanks:array,8>; -var groupBase:array; -fn deep_bucket(packed:u32)->u32{ - let n=packed&ITER_MASK; - return min(7u,(n*8u)/max(1u,p.maxIter)); -} -@compute @workgroup_size(64) -fn main( - @builtin(local_invocation_id) lid3:vec3, - @builtin(workgroup_id) wid:vec3 -){ - let lane=lid3.x; - if(lane<8u){ - atomicStore(&localCounts[lane],0u); - atomicStore(&localRanks[lane],0u); - groupBase[lane]=0u; - } - workgroupBarrier(); - - let lx=wid.x*64u+lane; - let ly=wid.y; - var out=0u; - var hit=0u; - var bucket=0u; - if(lx>28u)&3u)==FIELD_UNKNOWN){ - hit=1u; - bucket=deep_bucket(packed); - atomicAdd(&localCounts[bucket],1u); - } - } - workgroupBarrier(); - - if(lane<8u){ - let c=atomicLoad(&localCounts[lane]); - if(c>0u){groupBase[lane]=atomicAdd(&bucketState[8u+lane],c);} - } - workgroupBarrier(); - - if(hit!=0u){ - let rank=atomicAdd(&localRanks[bucket],1u); - let qi=groupBase[bucket]+rank; - let capacity=p.tileW*p.tileH; - if(qi, lo:vec2 }; -struct CorrectionStats{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -struct DS{ h:f32, l:f32 }; -struct CDS{ r:DS, i:DS }; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var refs:array; -@group(0) @binding(2) var fieldMeta:array; -@group(0) @binding(3) var fieldSmooth:array; -@group(0) @binding(4) var stats:CorrectionStats; - -fn count_remaining(out:u32){ - let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu; - atomicAdd(&stats.remaining,1u); - if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);} - else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);} - else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);} - else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);} - else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);} - else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);} -} -fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){ - fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u); -} - -fn ds_quick(a:f32,b:f32)->DS{ - let q=a+b; - let e=b-(q-a); - return DS(q,e); -} -fn ds_sum(a:f32,b:f32)->DS{ - let q=a+b; - let bb=q-a; - let e=(a-(q-bb))+(b-bb); - return DS(q,e); -} -fn ds_prod(a:f32,b:f32)->DS{ - let q=a*b; - let ca=4097.0*a; - let ah=ca-(ca-a); - let al=a-ah; - let cb=4097.0*b; - let bh=cb-(cb-b); - let bl=b-bh; - var e=ah*bh-q; - e=e+ah*bl; - e=e+al*bh; - e=e+al*bl; - return DS(q,e); -} -fn ds_add(a:DS,b:DS)->DS{ - let q=ds_sum(a.h,b.h); - return ds_quick(q.h,q.l+(a.l+b.l)); -} -fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);} -fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));} -fn ds_mul(a:DS,b:DS)->DS{ - let q=ds_prod(a.h,b.h); - var e=q.l+a.h*b.l; - e=e+a.l*b.h; - e=e+a.l*b.l; - return ds_quick(q.h,e); -} -fn ds_scale(a:DS,b:f32)->DS{ - let q=ds_prod(a.h,b); - return ds_quick(q.h,q.l+a.l*b); -} -fn ds_pow2(a:DS,e:i32)->DS{ - if(e < -126){return DS(0.0,0.0);} - if(e > 126){return DS(8.507059e37,0.0);} - return DS(ldexp(a.h,e),ldexp(a.l,e)); -} -fn ds_cmp(a:DS,b:DS)->i32{ - if(a.hb.h){return 1;} - if(a.lb.l){return 1;} return 0; -} -fn ds_value(a:DS)->f32{return a.h+a.l;} -fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));} -fn cds_mul(a:CDS,b:CDS)->CDS{ - let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i)); - let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r)); - return CDS(rr,ii); -} -fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));} -fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));} -fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));} -fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));} -fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;} - let rp=refs[m]; - let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y)); - let delta=cds_pow2(w,scaleExp); - let z=cds_add(r,delta); - let mag=cds_mag2(z); - if(ds_cmp(mag,DS(4.0,0.0))>0){ - accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag)))); - return; - } - let dmag=cds_mag2(delta); - if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){ - if(p.spanExp-scaleExp < -96){count_remaining(out);return;} - w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u; - continue; - } - if(m>=p.refLen){count_remaining(out);return;} - let linear=cds_scale(cds_mul(r,w),2.0); - let sq=cds_pow2(cds_mul(w,w),scaleExp); - w=cds_add(cds_add(linear,sq),d); - m+=1u; n+=1u; - let mm=max(cds_maxabs(w),cds_maxabs(d)); - if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;} - if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;} - else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;} - if(scaleExp>126){count_remaining(out);return;} - } -} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - correct_pixel(out,gx,gy); -} -`; - - -// Production sparse correction variant. It shares the complete DS arithmetic -// with DEEP_CORRECT_WGSL, but consumes only indices emitted by the queued deep -// perturbation pass and launches via dispatchWorkgroupsIndirect. -const DEEP_CORRECT_QUEUE_WGSL=DEEP_CORRECT_WGSL - .replace( -`struct CorrectionStats{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -};`, -`struct CorrectionStats{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -struct SparseQueueStats{ - selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, -};`) - .replace( -`@group(0) @binding(4) var stats:CorrectionStats;`, -`@group(0) @binding(4) var stats:CorrectionStats; -@group(0) @binding(5) var sparseQueueStats:SparseQueueStats; -@group(0) @binding(6) var unknownQueue:array;`) - .replace( -`@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - correct_pixel(out,gx,gy); -}`, -`@compute @workgroup_size(64) -fn main(@builtin(global_invocation_id) gid:vec3){ - let qi=gid.x; - let queued=atomicLoad(&sparseQueueStats.dispatchCount); - if(qi>=queued){return;} - let out=unknownQueue[qi]; - atomicAdd(&sparseQueueStats.processed,1u); - if(out=p.tileW||ly>=p.tileH){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;} - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;} - let gx=f32(p.tileX+lx)+p.sampleX; - let gy=f32(p.tileY+ly)+p.sampleY; - correct_pixel(out,gx,gy); -}`); - -if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)')){ - throw new Error('failed to derive queued deep correction shader'); -} - -const LIKELY_QUEUE_WGSL=COMMON+String.raw` -struct QueueParams{width:u32,height:u32,mode:u32,sparseStride:u32}; -struct QueueStats{count:atomic,overflow:atomic,_p0:atomic,_p1:atomic}; -@group(0) @binding(0) var p:QueueParams; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var queue:array; -@group(0) @binding(3) var stats:QueueStats; -fn is_frontier(x:u32,y:u32,r:i32)->bool{let xi=i32(x);let yi=i32(y);var oy=-1;loop{if(oy>1){break;}var ox=-1;loop{if(ox>1){break;}if(ox!=0||oy!=0){let xx=xi+ox*r;let yy=yi+oy*r;if(xx>=0&&yy>=0&&xx>28u)&3u;if(cls==FIELD_ESCAPED||cls==FIELD_UNKNOWN){return true;}}}ox+=1;}oy+=1;}return false;} -@compute @workgroup_size(256) -fn main(@builtin(global_invocation_id) gid:vec3){let i=gid.x;let total=p.width*p.height;if(i>=total){return;}if(((fieldMeta[i]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}let x=i%p.width;let y=i/p.width;var selected=false;if(p.mode==0u){selected=true;}else{let radius:i32=select(2,select(3,4,p.mode>=3u),p.mode>=2u);selected=is_frontier(x,y,radius);if(!selected&&p.sparseStride>0u){selected=(x%p.sparseStride==0u)&&(y%p.sparseStride==0u);}}if(!selected){return;}let slot=atomicAdd(&stats.count,1u);if(slot p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var fieldSmooth:array; -@group(0) @binding(3) var outTex:texture_storage_2d; -fn hsv(h:f32,s:f32,v:f32)->vec3{ - let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f)); - if(i==0){return vec3(v,t,pp);} if(i==1){return vec3(q,v,pp);} if(i==2){return vec3(pp,v,t);} if(i==3){return vec3(pp,q,v);} if(i==4){return vec3(t,pp,v);} return vec3(v,pp,q); -} -fn current_palette(t0:f32)->vec3{ - let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5); - if(t<0.11){return mix(vec3(4,10,27),vec3(12,53,79),smoothstep(0.0,0.11,t))/255.0;} - if(t<0.25){return mix(vec3(12,53,79),vec3(31,156,184),smoothstep(0.11,0.25,t))/255.0;} - if(t<0.38){return mix(vec3(31,156,184),vec3(91,226,234),smoothstep(0.25,0.38,t))/255.0;} - if(t<0.50){return mix(vec3(91,226,234),vec3(66,53,151),smoothstep(0.38,0.50,t))/255.0;} - if(t<0.62){return mix(vec3(66,53,151),vec3(139,49,170),smoothstep(0.50,0.62,t))/255.0;} - if(t<0.73){return mix(vec3(139,49,170),vec3(232,72,145),smoothstep(0.62,0.73,t))/255.0;} - if(t<0.84){return mix(vec3(232,72,145),vec3(255,137,64),smoothstep(0.73,0.84,t))/255.0;} - if(t<0.93){return mix(vec3(255,137,64),vec3(255,211,99),smoothstep(0.84,0.93,t))/255.0;} - return mix(vec3(255,211,99),vec3(255,250,223),smoothstep(0.93,1.0,t))/255.0; -} -fn palette_color(phase:f32)->vec3{ - if(p.palette==1u){return hsv(phase,0.92,1.0);}if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3(g);}return current_palette(phase); -} -fn escaped_color(m:u32,sm:f32)->vec3{ - let phase=fract(p.shift+sm*p.cycle);let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.34+0.66*pow(edge,0.38);let floorc=select(vec3(2,5,15)/255.0,vec3(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv); -} -fn provisional_unknown(m:u32)->vec3{ - let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3(6,10,22)/255.0,vec3(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52); -} -fn base_color(i:u32)->vec3{ - let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3(0.0);}return escaped_color(m,fieldSmooth[i]); -} -fn linearize(c:vec3)->vec3{return pow(c,vec3(2.2));} -fn delinearize(c:vec3)->vec3{return pow(max(c,vec3(0.0)),vec3(1.0/2.2));} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.width||gid.y>=p.height){return;}let i=gid.y*p.width+gid.x;let m=fieldMeta[i];let cls=(m>>28u)&3u;var c=base_color(i);let x=i32(gid.x);let y=i32(gid.y); - if(cls==FIELD_UNKNOWN){ - var fill=vec3(0.0);var fillCount=0.0; - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];if(((mj>>28u)&3u)!=FIELD_UNKNOWN){fill+=linearize(base_color(j));fillCount+=1.0;}}} - if(fillCount>0.0){c=delinearize(fill/fillCount);} - } - if(p.edgeAA!=0u){ - var boundary=false;var sum=linearize(c);var cnt=1.0; - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}} - if(boundary){c=delinearize(sum/cnt);} - } - textureStore(outTex,vec2(gid.xy),vec4(c,1.0)); -} -` - - -const AA_RESOLVE_WGSL=String.raw` -@group(0) @binding(0) var a:texture_2d; -@group(0) @binding(1) var b:texture_2d; -@group(0) @binding(2) var c:texture_2d; -@group(0) @binding(3) var d:texture_2d; -@group(0) @binding(4) var outTex:texture_storage_2d; -fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);} -fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);} -fn lin3(v:vec3)->vec3{return vec3(to_linear(v.x),to_linear(v.y),to_linear(v.z));} -fn srgb3(v:vec3)->vec3{return vec3(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;} - let q=vec2(gid.xy); - let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb); - textureStore(outTex,q,vec4(srgb3(sum*0.25),1.0)); -} -`; - -const PRESENT_WGSL=String.raw` -struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32}; -@group(0) @binding(0) var samp:sampler; -@group(0) @binding(1) var tex:texture_2d; -@group(0) @binding(2) var p:Params; -struct VSOut{@builtin(position) pos:vec4,@location(0) uv:vec2}; -@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{ - var pos=array,3>(vec2(-1.0,-1.0),vec2(3.0,-1.0),vec2(-1.0,3.0)); - var uv=array,3>(vec2(0.0,1.0),vec2(2.0,1.0),vec2(0.0,-1.0)); - var o:VSOut;o.pos=vec4(pos[i],0.0,1.0);o.uv=uv[i];return o; -} -@fragment fn fs(in:VSOut)->@location(0) vec4{ - let uv=vec2(0.5)+(in.uv-vec2(0.5))*vec2(p.scaleX,p.scaleY)+vec2(p.offsetX,p.offsetY); - if(any(uv(0.0))||any(uv>vec2(1.0))){return vec4(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0); -} -`; - - -// v24.2.14 production screen path: defer UNKNOWN statistics out of the long -// perturbation kernel. The existing bucket histogram pass already scans the -// same field before sparse correction, so it can aggregate remaining/reason -// counters per workgroup without per-UNKNOWN global atomics in the primary. -// The counted primary is retained for export and verification paths. -const DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u);\n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}'); - -const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL - .replace('@group(0) @binding(2) var bucketState:array>;', '@group(0) @binding(2) var bucketState:array>;\n@group(0) @binding(3) var unresolvedStats:array>;') - .replace('var localCounts:array,8>;', 'var localCounts:array,8>;\nvar localReasons:array,8>;') - .replace('if(lane<8u){atomicStore(&localCounts[lane],0u);}', 'if(lane<8u){atomicStore(&localCounts[lane],0u);atomicStore(&localReasons[lane],0u);}') - .replace('atomicAdd(&localCounts[deep_bucket(packed)],1u);', `atomicAdd(&localCounts[deep_bucket(packed)],1u); - atomicAdd(&localReasons[0],1u); - let reason=(packed&REASON_MASK)>>REASON_SHIFT; - if(reason>=1u && reason<=6u){atomicAdd(&localReasons[reason],1u);}`) - .replace('if(c>0u){atomicAdd(&bucketState[lane],c);}', `if(c>0u){atomicAdd(&bucketState[lane],c);} - let r=atomicLoad(&localReasons[lane]); - if(r>0u){atomicAdd(&unresolvedStats[lane],r);}`); -if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('v24.2.14 post-stats shader derivation failed'); - -globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({ - version:'24.2.26-baseline-idle-refinement',DIRECT_F32_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_LIKELY_REFINE_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,LIKELY_QUEUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL -}); -})(); diff --git a/dist/hosted/index.html b/dist/hosted/index.html deleted file mode 100644 index 1b9bf1e..0000000 --- a/dist/hosted/index.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - -Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement - - - - -
MANDELBROT DEEP ZOOM
中心
倍率  表示幅
計算 起動中… 描画
-
起動中
-
-
-
-
-
-
-
0.0080
-
.18
-
-
-
起動中
-
ドラッグ中は直前フレームを再投影して再利用。ホイール / ピンチでズーム、HキーでUI表示を切り替え。
-
- -
- -

高解像度 PNG 出力

-
- - - - -
- -
-
-
- - - - diff --git a/dist/hosted/script.js b/dist/hosted/script.js deleted file mode 100644 index 865cb8f..0000000 --- a/dist/hosted/script.js +++ /dev/null @@ -1,371 +0,0 @@ -(()=>{'use strict'; -const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません'); -const $=s=>document.querySelector(s),canvas=$('#view'); -const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n; -const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000; -const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20; -const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity']; -const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96,REFINE_QUEUE_PARAM_BYTES=16,REFINE_STATS_BYTES=16,REFINE_BATCH_BYTES=16; -const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorCycleDir:1,colorShiftDir:1,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,refinementRunning:false,refinementStage:0,refinementQueue:0,refinedIter:0}; -let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,idleRefineTimer=0,lastWrittenHash='',navigationHash=''; -const viewHistory=[];let viewHistoryIndex=-1; -const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,idleRefineBatches:0,idleRefineQueued:0}; - -// ── exact fixed-point view state ───────────────────────────────────────── -function one(bits=state.bits){return 1n<0?v<>BigInt(-d)} -function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x} -function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits} -function log2Fixed(v){return log2FixedAt(v,state.bits)} -function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v} -function f32ToFixed(x,bits){ - x=Math.fround(x);if(!Number.isFinite(x))throw new Error('non-finite f32 split component');if(x===0)return 0n; - const ab=new ArrayBuffer(4),dv=new DataView(ab);dv.setFloat32(0,x,false);const u=dv.getUint32(0,false),neg=(u>>>31)!==0,eb=(u>>>23)&255,frac=u&0x7fffff,m=BigInt(eb===0?frac:(0x800000|frac)),e=(eb===0?-149:eb-127-23),sh=bits+e; - let q=sh>=0?m<>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} -function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} -function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}} -function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))} -function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e} -function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}} -function f32Ulp(x){x=Math.abs(Math.fround(x));if(!Number.isFinite(x))return Infinity;if(x===0)return Math.pow(2,-149);if(x=4)} -function chooseBackend(snap=snapshot(),w=canvas.width){const deep=state.renderMode==='accurate',fastExtended=!deep&&fastNeedsExtended(snap,w);return{backend:deep?'deep':fastExtended?'fast-extended':'direct',deep,fastExtended,probe:false,reason:deep?'manual-accurate':fastExtended?'manual-fast-extended':'manual-fast'}} -function deepNeeded(){return state.renderMode==='accurate'} -function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,renderMode:state.renderMode}} -function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')} -function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()} -function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,renderMode:v.renderMode||state.renderMode});ensurePrecision();syncControls();saveHash(false);markDirty()} - -// ── iteration / quality policy ─────────────────────────────────────────── -function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))} -function effectiveColorCycle(iter=maxIter()){const base=Math.max(1,state.baseIter),effective=Math.max(base,Number(iter)||base);return state.cycle*base/effective} -function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))} -function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)} -function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)} -function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}} - -// ── high precision reference worker ───────────────────────────────────── -function referenceWorkerSource(){return String.raw` -'use strict'; -const MAX_REF=150001,MAX_LEVELS=20; -function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length} -function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q} -function fixedNum(v,b){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)),n=top*Math.pow(2,sh-b);return neg?-n:n} -function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}} -function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)return n+1}return 0} -function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]} -function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;kbest.score){best={...c,escape,score}}if(escape===0)break}return best} -function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<BAIL)escape=n+1}return{mismatch,checked}} -function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf} -self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceBits=d.bits,sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),limit=Math.min(MAX_REF-1,d.iter),bits=sourceBits+64,centerRef=orbit(bits,sourceRe<<64n,sourceIm<<64n,limit),chosen=chooseReference(sourceBits,sourceRe,sourceIm,sourceSpan,w,h,limit,centerRef.escape),re=chosen.re<<64n,im=chosen.im<<64n,ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,limit),v=verify(sourceBits,chosen.re,chosen.im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} -`} -class ReferenceService{ - constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16} - ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1,fresh=false){const key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,'guarded-perturb-v24.2.26'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} - cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} - destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} -} -const refs=new ReferenceService(); - -function fastReferenceBits(snap){const need=Math.ceil(Math.max(0,-log2FixedAt(snap.span,snap.bits)))+48;return Math.max(96,Math.min(snap.bits,need))} -function fastReferenceWorkerSource(){return String.raw` -'use strict'; -function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length} -function roundShift(v,b){if(b<=0)return v;const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q} -function fixedNum(v,b){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)),n=top*Math.pow(2,sh-b);return neg?-n:n} -function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}} -function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)return n+1}return 0} -function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]} -function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;kbest.score){best={...c,escape,score}}if(escape===0)break}return best} -function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf} -self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),bits=d.targetBits,centerRe=requant(sourceRe,d.sourceBits,bits),centerIm=requant(sourceIm,d.sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,d.iter),chosen=chooseReference(d.sourceBits,sourceRe,sourceIm,sourceSpan,w,h,d.iter,centerRef.escape),re=requant(chosen.re,d.sourceBits,bits),im=requant(chosen.im,d.sourceBits,bits),ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,d.iter),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} -`} -class FastReferenceService{ - constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=12} - ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([fastReferenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'fast reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.2.26'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} - cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} - destroy(){this.cancelPending('destroyed');this.cache.clear()} -} -const fastRefs=new FastReferenceService(); - -// ── WebGPU renderer ────────────────────────────────────────────────────── -function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})} -function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}} -function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a} -class WebGpuRenderer{ - constructor(adapter,device){ - this.adapter=adapter;this.device=device; - const ai=adapter.info||{}; - this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''}; - this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)}; - this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat(); - this.frame=null;this.deepCtx=null;this.fastCtx=null;this.refineDeepCtx=null;this.exportWs=null;this.refinePipelinePromise=null;this.likelyQueue=null;this.deepLikelyRefine=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'}); - device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)}); - this.ready=this.initPipelines(); - device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()}); - } - configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})} - async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m} - async initPipelines(){ - this.device.pushErrorScope?.('validation'); - try{ - const [dm,fm,xm,xhm,xpm,xsm,zm,zqm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]); - this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}}); - this.fast=this.device.createComputePipeline({layout:'auto',compute:{module:fm,entryPoint:'main'}}); - this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}}); - const [xpsm,xhsm]=await Promise.all([this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL),this.module('deep-bucket-histogram-stats-production',G.DEEP_BUCKET_HIST_STATS_WGSL)]); - this.deepPostStats=this.device.createComputePipeline({layout:'auto',compute:{module:xpsm,entryPoint:'main'}}); - this.deepBucketHist=this.device.createComputePipeline({layout:'auto',compute:{module:xhm,entryPoint:'main'}}); - this.deepBucketHistStats=this.device.createComputePipeline({layout:'auto',compute:{module:xhsm,entryPoint:'main'}}); - this.deepBucketPrefix=this.device.createComputePipeline({layout:'auto',compute:{module:xpm,entryPoint:'main'}}); - this.deepBucketScatter=this.device.createComputePipeline({layout:'auto',compute:{module:xsm,entryPoint:'main'}}); - this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}}); - this.correctQueued=this.device.createComputePipeline({layout:'auto',compute:{module:zqm,entryPoint:'main'}}); - this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}}); - this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}}); - this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}}); - this.context=canvas.getContext('webgpu'); - if(!this.context)throw new Error('WebGPU canvas contextを取得できません'); - webgpuCanvasClaimed=true;this.configure(); - }finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}} - } - async ensureRefinePipelines(){if(this.likelyQueue&&this.deepLikelyRefine)return;if(this.refinePipelinePromise)return this.refinePipelinePromise;this.refinePipelinePromise=(async()=>{const [lqm,drm]=await Promise.all([this.module('likely-refine-queue',G.LIKELY_QUEUE_WGSL),this.module('deep-likely-refine',G.DEEP_LIKELY_REFINE_WGSL)]);this.likelyQueue=this.device.createComputePipeline({layout:'auto',compute:{module:lqm,entryPoint:'main'}});this.deepLikelyRefine=this.device.createComputePipeline({layout:'auto',compute:{module:drm,entryPoint:'refine_likely'}})})().finally(()=>{this.refinePipelinePromise=null});return this.refinePipelinePromise} - frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','refineQueue','refineStats','refineQueueParams','refineBatchParams','refineRead','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null} - ensureFrame(w,h){ - const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage; - this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,refineQueue:null,refineStats:null,refineQueueParams:null,refineBatchParams:null,refineRead:null,numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame; - } - ensureDeepBucketState(f=this.frame){if(!f)return null;if(!f.deepBucketState){const d=this.device,B=GPUBufferUsage;f.deepBucketState=buf(d,DEEP_BUCKET_STATE_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-bucket-state')}return f} - ensureDeepQueueWorkspace(f=this.frame){if(!f)return null;this.ensureDeepBucketState(f);if(f.deepQueue&&f.deepQueueStats&&f.deepIndirect)return f;const d=this.device,B=GPUBufferUsage;f.deepQueueStats=buf(d,SPARSE_QUEUE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue-stats');f.deepQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue');f.deepIndirect=buf(d,SPARSE_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'deep-correction-indirect');return f} - ensureRefineWorkspace(f=this.frame){if(!f)return null;if(f.refineQueue&&f.refineStats&&f.refineQueueParams&&f.refineBatchParams&&f.refineRead)return f;const d=this.device,B=GPUBufferUsage;f.refineQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-queue');f.refineStats=buf(d,REFINE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-stats');f.refineQueueParams=buf(d,REFINE_QUEUE_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-queue-params');f.refineBatchParams=buf(d,REFINE_BATCH_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-batch-params');f.refineRead=buf(d,REFINE_STATS_BYTES,B.COPY_DST|B.MAP_READ,'likely-refine-readback');return f} - ensureExportWorkspace(){ - if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S; - this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs; - } - exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null} - setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}} - destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null} - setRefineDeepContext(ctx){if(this.refineDeepCtx&&this.refineDeepCtx.key===ctx.key)return;this.destroyRefineDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'idle-refinement-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.refineDeepCtx={...ctx,refsB}} - destroyRefineDeepContext(){if(this.refineDeepCtx)destroy(this.refineDeepCtx.refsB);this.refineDeepCtx=null} - setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}} - destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null} - directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})} - deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,0,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - colorParamsData(w,h,iter=state.fieldView?.iter??maxIter()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,effectiveColorCycle(iter),true);d.setFloat32(20,state.shift,true)})} - encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,pipeline=this.deepPostStats,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.fast.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fast);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHist.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHist);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHistStats.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeDeepBucketPrefix(encoder,{bucketState,queueStats,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketPrefix.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:bucketState}},{binding:1,resource:{buffer:queueStats}},{binding:2,resource:{buffer:indirect}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketPrefix);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end()} - encodeDeepBucketScatter(encoder,{pbuf,meta,bucketState,queueStats,queue,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeQueuedDeepCorrection(encoder,{pbuf,meta,smooth,unresolved,queueStats,queue,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.correctQueued.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{binding:5,resource:{buffer:queueStats}},{binding:6,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.correctQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()} - encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})} - async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null){ - await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep){this.setDeepContext(deepContext);this.ensureDeepBucketState(f)}else if(fastExtended){this.setFastContext(fastContext)}d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep)d.queue.writeBuffer(f.deepBucketState,0,new Uint32Array(DEEP_BUCKET_STATE_BYTES/4)); - const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;const params=deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,refX,refY):fastExtended?this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap);d.queue.writeBuffer(f.numericParams,0,params); - d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'}); - if(deep){this.encodeDeepPostStatsNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeDeepBucketHistogramStats(encoder,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,unresolved:f.unresolved,w:f.w,h:f.h});} - else if(fastExtended){this.encodeFastNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});} - else{this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});} - if(!deep){const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();} - d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;if(!deep)[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true; - } - async buildLikelyRefineQueue(token,mode=1,sparseStride=4){await this.ready;await this.ensureRefinePipelines();if(token!==state.token)return{count:0,overflow:0,cancelled:true};const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;d.queue.writeBuffer(f.refineStats,0,new Uint32Array(REFINE_STATS_BYTES/4));d.queue.writeBuffer(f.refineQueueParams,0,new Uint32Array([f.w,f.h,mode>>>0,sparseStride>>>0]));const e=d.createCommandEncoder({label:'likely-refine-queue-build'}),bg=d.createBindGroup({layout:this.likelyQueue.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.refineQueueParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.refineQueue}},{binding:3,resource:{buffer:f.refineStats}}]}),pass=e.beginComputePass();pass.setPipeline(this.likelyQueue);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.n/256));pass.end();e.copyBufferToBuffer(f.refineStats,0,f.refineRead,0,REFINE_STATS_BYTES);d.queue.submit([e.finish()]);await f.refineRead.mapAsync(GPUMapMode.READ);const a=new Uint32Array(f.refineRead.getMappedRange().slice(0));f.refineRead.unmap();if(token!==state.token)return{count:0,overflow:0,cancelled:true};return{count:Math.min(f.n,a[0]||0),overflow:a[1]||0,cancelled:false}} - async refineLikelyBatch({snap,iter,deepContext,token,base,end,forceStrict=false}){await this.ready;await this.ensureRefinePipelines();if(token!==state.token||base>=end)return false;const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setRefineDeepContext(deepContext);const ref=referencePixelForSource(deepContext?.source,snap,f.w,f.h);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,ref.x,ref.y,0,f.w,0));d.queue.writeBuffer(f.refineBatchParams,0,new Uint32Array([base>>>0,end>>>0,0,0]));const pipeline=this.deepLikelyRefine,bg0=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.refineDeepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),bg1=d.createBindGroup({layout:pipeline.getBindGroupLayout(1),entries:[{binding:0,resource:{buffer:f.refineQueue}},{binding:1,resource:{buffer:f.refineBatchParams}}]}),e=d.createCommandEncoder({label:'likely-idle-refine'}),pass=e.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg0);pass.setBindGroup(1,bg1);pass.dispatchWorkgroups(Math.ceil((end-base)/64));pass.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.idleRefineBatches++;return true} - async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true} - presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}} - presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])} - async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4)); - if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})} - const ca=this.colorParamsData(w,h,iter),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end(); - const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h,iter);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca); - for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()} - const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end(); - const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y=Math.max(32,Math.floor(n*.0005));return true} -function referencePixelForSource(src,snap,w,h){if(!src)return{x:w*.5,y:h*.5};const b=Math.max(snap.bits,src.bits),span=align(snap.span,snap.bits,b),dr=align(src.re,src.bits,b)-align(snap.re,snap.bits,b),di=align(src.im,src.bits,b)-align(snap.im,snap.bits,b);return{x:w*.5+fixedRatio(dr,span)*w,y:h*.5-fixedRatio(di,span)*w}} -function reusableDeepReference(snap,iter,w,h){const ctx=renderer&&renderer.deepCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} -function reusableFastReference(snap,iter,w,h){const ctx=renderer&&renderer.fastCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} - -// ── GPU startup / rendering orchestration ──────────────────────────────── -async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise} -function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx} -function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span} -function cancelIdleRefinement(){if(idleRefineTimer){clearTimeout(idleRefineTimer);idleRefineTimer=0}state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0} -function idleRefinementTargets(baseIter){if(baseIter>=8000)return[];const z=zoomExp(),mode=state.processMode,mul=mode==='power'?.75:mode==='fine'?1.25:mode==='validate'?1.4:1,cap=Math.min(8000,Math.max(baseIter*2,Math.round((1200+300*z)*mul))),raw=mode==='power'?[Math.min(cap,baseIter*2)]:[Math.min(cap,baseIter*2),Math.min(cap,baseIter*4),cap],out=[];for(const v of raw){const n=Math.max(baseIter+1,Math.floor(v));if(n>baseIter&&(!out.length||n!==out[out.length-1]))out.push(n)}return out} -function idleRefinementBatchBudget(){const m=state.processMode;if(m==='power')return 900000;if(m==='fine')return 2800000;if(m==='validate')return 1800000;return 1800000} -function idleRefinementQueuePolicy(stage){if(stage<=0)return{mode:1,stride:4};if(stage===1)return{mode:2,stride:8};return{mode:3,stride:16}} -function idleRefinementSliceMs(){return state.processMode==='power'?100:state.processMode==='fine'?220:160} -function idleRefinementPauseMs(){return state.processMode==='power'?160:80} -function canIdleRefine(token,snap,baseIter){return token===state.token&&renderer&&!state.rendering&&!state.pointerActive&&!state.wheelActive&&sameSnapshot(snap,snapshot())&&state.fieldView&&state.fieldView.iter===baseIter&&state.fieldView.w===canvas.width&&state.fieldView.h===canvas.height} -const sleep=ms=>new Promise(r=>setTimeout(r,ms)); -async function paintIdleRefinement(token,snap,baseIter){while(state.recoloring&&canIdleRefine(token,snap,baseIter))await sleep(12);if(!canIdleRefine(token,snap,baseIter))return false;await recolor();while((state.recoloring||state.recolorPending)&&canIdleRefine(token,snap,baseIter))await sleep(8);return canIdleRefine(token,snap,baseIter)} -function scheduleIdleRefinement(snap,baseIter,decision,token){cancelIdleRefinement();if(!renderer||(!decision.deep&&!decision.fastExtended))return;const targets=idleRefinementTargets(baseIter);if(!targets.length)return;idleRefineTimer=setTimeout(()=>{idleRefineTimer=0;runIdleRefinement(snap,baseIter,decision,token,targets)},320)} -async function runIdleRefinement(snap,baseIter,decision,token,targets){if(!canIdleRefine(token,snap,baseIter))return;state.refinementRunning=true;state.refinedIter=baseIter;updateStats();try{for(let stage=0;stage=idleRefinementSliceMs()){const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;updateStats();lastPaint=performance.now();await sleep(idleRefinementPauseMs())}}const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;state.fieldView.refinedIter=targetIter;updateStats();await sleep(idleRefinementPauseMs())}}catch(e){if(token===state.token){console.warn('idle refinement stopped:',e);state.gpuError=String(e&&e.message||e)}}finally{if(token===state.token){state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0;updateStats()}}} - -function cancelRender(){cancelIdleRefinement();state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled')} -function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()} -function schedule(){if(!raf)raf=requestAnimationFrame(loop)} -async function renderFrame(){ - const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height); - state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats(); - try{ - const r=renderer||await initRenderer();if(token!==state.token)return; - if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return} - const deep=decision.deep,fastExtended=decision.fastExtended;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false; - if(deep){const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height);if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 正確 reference再利用'}else{state.lastEngine='WebGPU · 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · 正確 perturbation'}} - else if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 高速拡張 reference再利用'}else{state.lastEngine='WebGPU · 高速拡張準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height);state.lastEngine='WebGPU · 高速拡張'}} - else state.lastEngine='WebGPU · 高速'; - const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate',referencePixel,fastExtended,fastCtx);if(!ok)return; - if(!deep){ - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false; - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0}); - if(fastExtended){const stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons} - const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token);return; - } - // Accurate mode keeps the previous committed frame visible while the new - // primary field is checked/corrected. The incomplete primary texture lives - // only in frame.back and is never presented to the viewer. - let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats(); - const n=canvas.width*canvas.height; - if(shouldApplySparseCorrection(state.unresolved,n)){ - state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats(); - const corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return;state.correctionPasses=1; - stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected; - } - const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return; - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · '+(referenceReused?'正確 reference再利用':'正確 reference自動選択')+(state.correctionPasses?' + DS correction':''); - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token); - }catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)} -} -function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · 正確モード非対応';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*effectiveColorCycle(iter)+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false}); -canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}}); -canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}}); -function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer); - -// ── URL / controls ─────────────────────────────────────────────────────── -function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');p.set('rm',state.renderMode);const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}} -function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';if(p.has('rm'))state.renderMode=p.get('rm')==='accurate'?'accurate':'fast';ensurePrecision();return true}catch{return false}} -function syncHistoryButtons(){} -function syncControls(){$('#renderMode').value=state.renderMode;$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()} -function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)} -function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')} -$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()}; -$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()}; -$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}}; -let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0; -function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色アニメ停止':'色を自動変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')} -function stopColorAuto(){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton()} -function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;const cmin=.001,cmax=.05,smin=0,smax=1;state.cycle+=state.colorCycleDir*.0006*dt;state.shift+=state.colorShiftDir*.012*dt;if(state.cycle>=cmax){state.cycle=cmax;state.colorCycleDir=-1}else if(state.cycle<=cmin){state.cycle=cmin;state.colorCycleDir=1}if(state.shift>=smax){state.shift=smax;state.colorShiftDir=-1}else if(state.shift<=smin){state.shift=smin;state.colorShiftDir=1}$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;recolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)} -$('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf)colorAutoRaf=requestAnimationFrame(colorAutoStep);else if(!state.colorAuto)stopColorAuto()}; -$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()}; -$('#renderMode').onchange=e=>{state.renderMode=e.target.value==='accurate'?'accurate':'fast';try{localStorage.setItem('mandelbrot.renderMode',state.renderMode)}catch{}markDirty()}; -$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}}; -addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}}); -addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}}); - -// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ────── -const exportJob={active:false,cancelled:false}; -function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)} -const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})(); -function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0} -function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out} -class StreamingPng{ - constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()} - async rows(filteredRows){await this.writer.write(filteredRows)} - async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})} - async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}} -} -function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}} -async function runExport(){ - if(exportJob.active)return; - const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return} - const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),fastExtended=!deep&&fastNeedsExtended(snap,w),strict=$('#exportPrecision').value==='strict'; - let ctx=null,fastCtx=null; - if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}else if(fastExtended){$('#exportStatus').textContent='高速拡張参照を準備中…';fastCtx=await fastRefs.request(snap,iter,w,h)} - const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1; - exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true; - let done=0,unresolvedSamples=0; - try{ - for(let y=0;y{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png'); - -function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.deep?'WebGPU 正確':decision.fastExtended?'WebGPU 高速':'WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'正確描画の補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.refinementRunning)status+=' · 補足精密化 '+state.refinementStage+' ('+state.refinedIter+' iter, 残 '+state.refinementQueue+')';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status} - -globalThis.__MANDEL_TEST__={ - async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard',renderMode='fast'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.renderMode=renderMode==='accurate'?'accurate':'fast';state.hq=false;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{width:canvas.width,height:canvas.height,state:this.state()}}, - async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)}, - state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter(),renderMode:state.renderMode,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused}), - 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 probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded():!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))}, - async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),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,deep,strict,ss,unresolved:result.unresolved||0}} -}; - -// ── boot / teardown ────────────────────────────────────────────────────── -addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();refs.destroy();fastRefs.destroy();if(renderer)renderer.destroy()},{once:true}); -try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}state.renderMode=localStorage.getItem('mandelbrot.renderMode')==='accurate'?'accurate':'fast'}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule(); -})(); diff --git a/dist/standalone/index.html b/dist/standalone/index.html deleted file mode 100644 index ff9a2e2..0000000 --- a/dist/standalone/index.html +++ /dev/null @@ -1,1216 +0,0 @@ - - - - - - - -Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement - - - - -
MANDELBROT DEEP ZOOM
中心
倍率  表示幅
計算 起動中… 描画
-
起動中
-
-
-
-
-
-
-
0.0080
-
.18
-
-
-
起動中
-
ドラッグ中は直前フレームを再投影して再利用。ホイール / ピンチでズーム、HキーでUI表示を切り替え。
-
- -
- -

高解像度 PNG 出力

-
- - - - -
- -
-
-
- - - - diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md deleted file mode 100644 index f333761..0000000 --- a/docs/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# CHANGELOG - -## v24.2.26 - -- v24.2.23〜v24.2.24 のLOD / frontier全画面再計算系を撤回。 -- v24.2.18 の Direct / Fast / Deep / Color WGSL を完全復元。 -- v24.2.18 の `maxIter` と fallback を復元。 -- 一次描画後だけ起動する GPU-only `INTERIOR_LIKELY` queue を追加。 -- 補足精密化は高速モードでも guarded Deep で実行。 -- 精密化シェーダーは一次表示後に遅延コンパイル。 -- queue候補は境界近傍 + 疎なscreen-space seedに限定。 -- CPUへ戻す精密化情報は16 byte統計のみ。全meta readbackなし。 -- 標準精密化batchを最大1.8M sample-iterationsに制限。 -- 自動色変化速度を v24.2.18 の1/10へ変更。 -- baseline一致・batch予算・品質単調性の専用テストを追加。 diff --git a/docs/README.md b/docs/README.md index eada060..9fe55a6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,117 +1,31 @@ -# Mandelbrot Deep Zoom v24.2.26 +# Mandelbrot WebGPU v24.2.55 ## 目的 +v24.2.54で確認された、FAST深部描画の矩形状/穴状の黒欠損と、数値補修を最終表示前に待つことによるレイテンシ増大を修正する。 -v24.2.23〜v24.2.24 系で発生した「品質低下・速度低下」を撤回し、**v24.2.18 の一次描画を基準線として復元**した版です。 +## 黒穴の原因 +COLOR passでは数値UNKNOWNを透明としていたが、PRESENT passが履歴補完を `scale <= 1.035 / offset <= 0.022` に制限していた。そのため通常のズームでも履歴補完が無効になり、透明UNKNOWNがcanvasの黒背景へ落ちた。 -この版の合格条件は次の4点です。 +さらに、その不完全front textureを完成historyとしてcommitする経路があった。局所reference補修が予算内で全UNKNOWNを回収できない場合、タイル境界に沿った黒欠損が完成画像へ固定され得た。 -1. 一次表示は v24.2.18 より遅くしない。 -2. 補足精密化を停止しても v24.2.18 より品質を下げない。 -3. 通常描画ループで fieldMeta 全体を GPU→CPU readback しない。 -4. 黒い `INTERIOR_LIKELY` の追加判定は一次表示後・アイドル時だけ行う。 +## v24.2.55の契約 +1. 数値UNKNOWNを含むfrontはstable historyへ昇格しない。 +2. 通常のパン/ズーム範囲では、数値UNKNOWNを直前のstable frameから再投影して埋める。 +3. FASTの数値補修は一次画像のpublishをブロックせず、同一viewのbackground taskとして継続する。 +4. background補修が完了して数値UNKNOWNが0になった時点で初めてhistoryを更新する。 +5. tiled FAST開始時にmeta/smoothをzero sentinelへ初期化し、最終color前にreason=0の未書込pixelだけbaseline FASTで再計算する。 -## 一次描画 +`REASON_OPERATION_LIMIT` は数値故障ではなく、maxIterまで未脱出だったpixelなので従来どおり黒表示する。今回の矩形状欠損対策は主に数値UNKNOWN/未書込pixelを対象とする。 -一次描画の以下のWGSLは v24.2.18 と **SHA-256まで一致**しています。 +## FASTレイテンシ +production BLAのCPU側64点shadow計算を廃止した。BLA workerはtable構築だけを行い、採用判定は既存の64点GPU probeへ一本化した。 -- `DIRECT_F32_WGSL` -- `FAST_PERTURB_WGSL` -- `DEEP_PERTURB_WGSL` -- `COLOR_WGSL` +classic Seahorse Valley相当 (654x690, span 3.4e-13, 2900 iter) のNode/V8 workerモデル: +- 旧 CPU validate 64点: 約446.9 ms +- 新 table build only: 約60.0 ms +- 同期prep削減: 約386.9 ms -`maxIter()` も v24.2.18 と同じです。 +BLAが不利なviewでは本体FAST計算時間そのものは残る。今回の変更では、数値補修Deep/multi-referenceを一次表示のクリティカルパスから外すことで、黒穴を出さずに操作から画像表示までの待ち時間を短縮する。 -```text -baseIter + floor(70*sqrt(zoomExp) + 15*zoomExp) -``` - -1920×1080 / 標準負荷の代表条件では内部描画は約 1672×941 = 1,573,352 px です。 - -| zoom | 一次 iter | 一次描画の最悪 sample-iterations | -|---:|---:|---:| -| 10^6 | 611 | 0.961 B | -| 10^8 | 667 | 1.049 B | -| 10^12 | 772 | 1.215 B | -| 10^20 | 963 | 1.515 B | - -これは v24.2.18 と同じ計算量です。補足精密化用WGSLも起動時にはコンパイルせず、一次描画完了から320ms後に初めて遅延コンパイルします。 - -## アイドル補足精密化 - -一次描画後にだけ `FIELD_INTERIOR_LIKELY` を追加判定します。 - -### GPU-only queue - -全 `fieldMeta` をCPUへ戻しません。GPU上で候補インデックスqueueを作り、CPUへ戻すのは **16 byte のqueue統計だけ**です。 - -候補は次のように絞ります。 - -- stage 1: 境界近傍 + 4px間隔 seed -- stage 2: 境界近傍 + 8px間隔 seed -- stage 3: 境界近傍 + 16px間隔 seed - -境界判定は各候補につき最大8点だけ参照します。広い真内部を毎回全面再計算する方式にはしていません。1920×1080/標準の代表内部解像度では、純粋な疎seedだけなら stage 1/2/3 はそれぞれ約98,648 / 24,662 / 6,195画素です。10^8 の場合、疎seed部分の合計は約0.220B sample-iterationsで、一次描画最悪値約1.049Bの約21%です。境界画素はこれに追加されますが、batch上限と休止でGPU占有を制限します。 - -### 数値エンジン - -高速モードの一次表示は従来のFast経路ですが、**アイドル精密化は高速モードでも guarded Deep を使用**します。精密化用reference bufferは通常描画の `deepCtx / fastCtx` と分離しているため、精密化後もv24.2.18のpan reference再利用を壊しません。 - -これにより、一次描画で `INTERIOR_LIKELY` だった点だけを保守的に再評価します。精密化シェーダーは実行直前にも対象がまだ `INTERIOR_LIKELY` か確認するため、既存の `ESCAPED` / `INTERIOR_PROVEN` を上書きしません。 - -### 反復ターゲット - -標準モードでは概ね次の3段階です。 - -| zoom | 一次 | stage 1 | stage 2 | stage 3 | -|---:|---:|---:|---:|---:| -| 10^6 | 611 | 1222 | 2444 | 3000 | -| 10^8 | 667 | 1334 | 2668 | 3600 | -| 10^12 | 772 | 1544 | 3088 | 4800 | -| 10^20 | 963 | 1926 | 3852 | 7200 | - -### 1 GPU batch の上限 - -標準モードの補足精密化は **1.8 million sample-iterations / batch 以下**に丸めます。 - -例: `10^8` - -- 1334 iter: 1344 pixels / batch → 1,792,896 sample-iterations -- 2668 iter: 640 pixels / batch → 1,707,520 -- 3600 iter: 448 pixels / batch → 1,612,800 - -ユーザー操作が入ると `state.token` が変わり、次batchへ進む前に中断します。また一定時間ごとにGPUを休ませます。 - -## 品質の単調性 - -補足精密化前の画像は v24.2.18 と同じです。 - -精密化は `FIELD_INTERIOR_LIKELY` のみを対象とし、既に確定した画素には触れません。さらに再着色時も一次描画の `baseIter` を色周期正規化に使うため、精密化によって既存の外部色が一斉に変化することを避けています。 - -## 色操作 - -v24.2.18 のズーム相対色周期と対数スライダーを維持しています。`色を自動変化` の速度のみ、要求どおり v24.2.18 の **1/10** にしています。 - -## fallback - -JavaScript fallback は v24.2.18 と SHA-256一致です。疑似等高線・縞模様は追加していません。 - -## 検査 - -```bash -npm test -npm run build -``` - -`tests/v24-baseline-idle-refinement-model.mjs` が以下を検査します。 - -- 一次WGSL 4本の v24.2.18 SHA-256一致 -- fallback の v24.2.18 SHA-256一致 -- `maxIter()` の基準線維持 -- 精密化WGSLの遅延コンパイル -- 全meta readback不使用 -- 16 byte queue統計readback -- 既存確定画素を上書きしない契約 -- 各倍率で1batchが予算を超えないこと - -実GPUのfpsやdevice-lost有無は、このコンテナでWebGPUが初期化できない場合は保証できません。静的・CPUモデル結果と実機測定は区別します。 +## 数値コア +v24.2.54から変更したproduction WGSLはFAST_PERTURB_WGSLのみ。通常 `unknownOnly=0` の計算式・perturbation recurrenceは変更していない。追加分はcoverage repair用のearly guardである。 diff --git a/docs/VALIDATION_REPORT.md b/docs/VALIDATION_REPORT.md index a45f6c0..253c22d 100644 --- a/docs/VALIDATION_REPORT.md +++ b/docs/VALIDATION_REPORT.md @@ -1,101 +1,75 @@ -# v24.2.26 検査報告 +# Validation Report — v24.2.55 -## 結論 +## 1. JavaScript / bundle syntax +- kernel bundle: PASS +- application bundle: PASS -v24.2.26 は v24.2.18 を一次描画の基準線として復元し、黒い `INTERIOR_LIKELY` の追加判定だけを一次表示後のアイドルGPU処理として分離した。 +## 2. production kernel差分 +v24.2.54とのSHA-256比較。 -一次描画の品質・数値計算量を変えないことを、ソースハッシュ・式・静的契約で確認した。実GPU上のfps/device-lostは、この実行環境でWebGPU/EGLを初期化できなかったため未測定である。 +変更: +- `version` +- `FAST_PERTURB_WGSL` -## 一次描画の同一性 +不変: +- Direct +- Accurate seed / DS direct +- Deep perturbation +- Deep correction +- COLOR +- PRESENT shader本体 +- BLA candidate / production frame kernels -v24.2.18 と SHA-256 が一致する本番WGSL: +FAST_PERTURBの通常recurrenceは不変。`unknownOnly=2` 時だけzero-sentinel pixelを再計算するearly guardを追加。 -| Shader | SHA-256 | -|---|---| -| DIRECT_F32_WGSL | `59301bf0c5ba3dda4a4055335b32d4a3f84a21991be30f8efc74deb5a5f11ccf` | -| FAST_PERTURB_WGSL | `03b45713b1bbb7b56e18bd8fdf0f2d8161d7ed25adf02482b41c1038dc16e3b8` | -| DEEP_PERTURB_WGSL | `2865a015d5c1a9b459085b7044e23563fcc5f0eec5362634249b6604a47e660a` | -| COLOR_WGSL | `cccac7bfb69c64093c46c3468ff9d685d680ba3a88de0d1ea57168d91a067acf` | +## 3. coverage contract +PASS: +- tiled numeric開始時にmeta/smoothをzero clear +- color前にbaseline FAST coverage repair +- coverage repairはreason=0 UNKNOWNだけをiteration +- 既計算pixelはearly return -JavaScript fallback も v24.2.18 と一致: +これにより、strip/tile未書込が発生してもrectangular zero-metadata holeをそのままpublishしない。 -`689a6f56aedd416b5e512b17ab6a3ddebcf7c73494e0a8a600ce74a06d8073a2` +## 4. temporal fill / history +PASS: +- history利用範囲をstable reprojectionと同じ `2.25 / 0.45` へ統一 +- numeric recovery pending/running時はstable historyへcommitしない +- recolor経路も `numericFrameComplete()` を満たさない限りhistoryへcommitしない -`maxIter()` は v24.2.18 と同じ: +## 5. background numeric recovery +PASS (static/control-flow): +- FAST primary終了後に数値failureがあれば60ms後にbackground recoveryを予約 +- local multi-referenceを先行 +- 残存時だけfull Deep + DS + local recovery +- view tokenが変われば結果をpublishしない +- recovery中のrecolorはqueueへ退避 -`baseIter + floor(70*sqrt(zoomExp) + 15*zoomExp)` +## 6. BLA production prep +classic Seahorse Valley相当のworker-model実測: +- 旧64-point CPU validation: 446.9 ms +- 新build-only: 60.0 ms +- 同期prep削減: 386.9 ms -補足精密化用WGSLは本番pipeline初期化ではコンパイルしない。一次表示完了後、320msのアイドル遅延後に必要な場合だけ遅延コンパイルする。 +BLA table buildそのものはほぼ同時間 (58.4 vs 59.4 ms)。削減分は描画前CPU shadow sample計算。 -## 代表計算量 +## 7. BLA safety regression +既存289-point Seahorse test、safety=1/64: +- classification mismatch: 0 +- first escape mismatch: 0 -1920×1080 CSS / 標準負荷では内部描画を約1672×941 = 1,573,352画素とする。 +productionではCPU per-view validationを外したが、64-point GPU probeとBLA pixel fallback契約は維持。 -| zoom | 一次iter | 一次最悪 sample-iterations | -|---:|---:|---:| -| 10^6 | 611 | 961,318,072 | -| 10^8 | 667 | 1,049,425,784 | -| 10^12 | 772 | 1,214,627,744 | -| 10^20 | 963 | 1,515,137,976 | +## 8. browser smoke +Chromiumでlocalhostからロード: +- page JavaScript error: 0 +- kernelVersion: `24.2.55-no-black-hole-async-recovery` -これはv24.2.18と同一。 +このコンテナでは当該起動条件でWebGPU adapterを取得できなかったため、canvasを含む実端末フレーム時間は未測定。 -標準モードのアイドル補足精密化は1バッチ1,800,000 sample-iterations以下に制限する。 +## 9. release layout +- executable HTML: `index.html` の1本のみ +- `index.baseline-*.html`: なし -| zoom | refine iter | pixels/batch | 最悪 sample-iterations | -|---:|---:|---:|---:| -| 10^6 | 1222 | 1472 | 1,798,784 | -| 10^6 | 2444 | 704 | 1,720,576 | -| 10^6 | 3000 | 576 | 1,728,000 | -| 10^8 | 1334 | 1344 | 1,792,896 | -| 10^8 | 2668 | 640 | 1,707,520 | -| 10^8 | 3600 | 448 | 1,612,800 | -| 10^12 | 1544 | 1152 | 1,778,688 | -| 10^12 | 3088 | 576 | 1,778,688 | -| 10^12 | 4800 | 320 | 1,536,000 | -| 10^20 | 1926 | 896 | 1,725,696 | -| 10^20 | 3852 | 448 | 1,725,696 | -| 10^20 | 7200 | 192 | 1,382,400 | - -10^8 の最初のrefine batchは一次描画最悪値の約0.171%。1バッチを巨大dispatchにしないことを優先している。 - -## 候補量モデル - -代表解像度で境界判定以外の疎seedだけを数えると: - -- stride 4: 約98,648画素 -- stride 8: 約24,662画素 -- stride 16: 約6,195画素 - -10^8 でこの疎seed全部を3段階処理した場合は約0.220B sample-iterationsで、一次描画最悪値1.049Bの約20.9%。実際には境界候補も加わるが、処理は小batchに分割し、一定時間ごとに休止し、ユーザー操作で中止する。 - -## 品質契約 - -- 一次画像はv24.2.18と同じ本番シェーダーで生成する。 -- refinement対象は `FIELD_INTERIOR_LIKELY` のみ。 -- refinement shaderは実行時にも対象classを再確認する。 -- `FIELD_ESCAPED` / `FIELD_INTERIOR_PROVEN` は上書きしない。 -- 高速モードのidle refinementもguarded Deepを使う。 -- refinement用reference bufferを通常描画の `deepCtx / fastCtx` と分離し、pan reference reuseを壊さない。 -- 再着色は一次描画の色周期スケールを維持し、既存の外部色を一斉に動かさない。 -- JavaScript fallbackに疑似等高線を追加しない。 - -## 自動テスト - -最終ソースに対して以下を実行し、すべてPASS: - -```text -npm test -> RC=0 -npm run build -> RC=0 -npm test -> RC=0 (build後再検査) -node --check script.js -> PASS -node --check gpu-kernels.js -> PASS -``` - -専用モデル `tests/v24-baseline-idle-refinement-model.mjs` もPASSし、上記ハッシュ、batch上限、readback契約、品質単調性を検査した。 - -## 実WebGPU検査の制約 - -このコンテナのChromiumではGPUプロセスがEGL/ANGLE初期化に失敗したため、実WebGPUのfps・device lost試験は完了できなかった。観測した主な失敗は `EGL_NOT_INITIALIZED` / `SwANGLE failed` 系である。 - -したがって「実機でv24.2.18より何%速い」という主張はしない。保証しているのは一次描画経路の構造的な基準線復元と、idle refinementのbatch仕事量上限である。 +## 残る性能課題 +BLA probeがOFFになるSeahorse/Misiurewicz型viewではbaseline FASTのpixel-iteration量が依然支配的。v24.2.55はこのケースのCPU prepと同期補修待ちを削るが、primary perturbation本体を別アルゴリズムで高速化する変更ではない。 diff --git a/gpu-kernels.js b/gpu-kernels.js deleted file mode 100644 index 4f73ff2..0000000 --- a/gpu-kernels.js +++ /dev/null @@ -1,778 +0,0 @@ -(()=>{'use strict'; -const COMMON=String.raw` -const FIELD_UNKNOWN:u32=0u; -const FIELD_ESCAPED:u32=1u; -const FIELD_INTERIOR_LIKELY:u32=2u; -const FIELD_INTERIOR_PROVEN:u32=3u; -const ITER_MASK:u32=0x000fffffu; -const REASON_SHIFT:u32=20u; -const REASON_MASK:u32=0x0ff00000u; -const REASON_NONE:u32=0u; -const REASON_ERROR_BOUND:u32=1u; -const REASON_ESCAPE_UNCERTAIN:u32=2u; -const REASON_REFERENCE_END:u32=3u; -const REASON_REBASE_GAP:u32=4u; -const REASON_RANGE:u32=5u; -const REASON_OPERATION_LIMIT:u32=6u; - -fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); } -fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); } -fn cmul(a:vec2, b:vec2)->vec2{ - return vec2(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x); -} -fn maxabs(v:vec2)->f32 { return max(abs(v.x),abs(v.y)); } -const F32_U:f32=5.960464477539063e-8; -fn pow2_safe(e:i32)->f32 { - if(e < -126){ return 0.0; } - if(e > 126){ return 8.507059e37; } - return ldexp(1.0,e); -} -fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2, delta:vec2)->f32{ - let propagated=abs(errScaled*pow2_safe(scaleExp)); - let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30); - return propagated+reconstruction; -} -fn scaled_to_f32(v:vec2, e:i32)->vec2{ - if(e < -126){ return vec2(0.0); } - if(e > 126){ return vec2(8.507059e37); } - return ldexp(v,vec2(e)); -} -fn smooth_escape(n:u32, mag2:f32)->f32{ - let u=log2(max(4.0000005,mag2)); - return f32(n)+1.0-log2(max(1.0e-20,0.5*u)); -} -`; - -const DIRECT_F32_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, strict:u32, - centerRe:f32, centerIm:f32, span:f32, sampleX:f32, - sampleY:f32, _p0:f32, _p1:f32, _p2:f32, -}; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var fieldSmooth:array; -fn analytic(cr:f32,ci:f32)->bool{ - let y2=ci*ci; let x=cr-0.25; let q=x*x+y2; - let lhs=q*(q+x); let rhs=0.25*y2; - let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0); - if(lhs){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=gid.y*p.tileW+gid.x; - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - let scale=p.span/f32(p.fullW); - let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale; - let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale; - if(analytic(cr,ci)){ - fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return; - } - var zr=0.0; var zi=0.0; var n=0u; - loop{ - if(n>=p.maxIter){break;} - let zr2=zr*zr; let zi2=zi*zi; - zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u; - let mag=zr*zr+zi*zi; - if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;} - } - fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0; -} -`; - - -// Fast-mode precision extension. This keeps the interactive path independent -// of the verified Deep backend: a reduced-precision reference orbit is built -// quickly on the CPU worker and this lightweight perturbation kernel preserves -// sub-f32 pixel offsets once absolute Direct coordinates begin to collapse. -const FAST_PERTURB_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct RefPoint{ hi:vec2, lo:vec2 }; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var refs:array; -@group(0) @binding(2) var fieldMeta:array; -@group(0) @binding(3) var fieldSmooth:array; -@group(0) @binding(4) var unresolved:array>; -fn mark_fast_unresolved(out:u32,n:u32,reason:u32){ - fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u); - if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);} -} - -fn render_fast(out:u32,gx:f32,gy:f32){ - let dx=(gx-p.refPixelX)/f32(p.fullW); - let dy=(p.refPixelY-gy)/f32(p.fullW); - var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); - var w=vec2(0.0); - var scaleExp=p.spanExp; - var n=0u; var m=0u; - loop{ - if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;} - let rp=refs[min(m,p.refLen)]; - let delta=scaled_to_f32(w,scaleExp); - let z=rp.hi+(rp.lo+delta); - let mag=dot(z,z); - if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} - - // Rebase the perturbation when it grows beyond the reference value. This - // keeps the fast path usable across pans without any verified correction. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); - scaleExp=0; m=0u; - continue; - } - if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;} - let r=rp; - let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); - let sq=cmul(w,w)*pow2_safe(scaleExp); - w=linear+sq+d; m+=1u; n+=1u; - let mm=max(maxabs(w),maxabs(d)); - if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;} - if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;} - else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;} - if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;} - } -} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - render_fast(out,gx,gy); -} -`; - - -const DEEP_PERTURB_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct RefPoint{ hi:vec2, lo:vec2 }; -struct UnresolvedHead{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var refs:array; -@group(0) @binding(2) var fieldMeta:array; -@group(0) @binding(3) var fieldSmooth:array; -@group(0) @binding(4) var unresolved:UnresolvedHead; - -fn mark_unresolved(out:u32,n:u32,reason:u32){ - fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0; - atomicAdd(&unresolved.remaining,1u); - if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);} - else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);} - else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);} - else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);} - else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);} - else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);} -} -fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ - let dx=(gx-p.refPixelX)/f32(p.fullW); - let dy=(p.refPixelY-gy)/f32(p.fullW); - // dc = d * 2^scaleExp. Keep d and w in one shared scale. - var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); - var w=vec2(0.0); - var scaleExp=p.spanExp; - var n=0u; var m=0u; - var errScaled=1.0*F32_U*maxabs(d); - loop{ - if(n>=p.maxIter){ - let rpEnd=refs[min(m,p.refLen)]; - let deltaEnd=scaled_to_f32(w,scaleExp); - let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd); - let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd); - let limit=select(1.0e-3,1.0e-4,strictMode); - if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);} - return; - } - let rp=refs[m]; - let delta=scaled_to_f32(w,scaleExp); - let z=rp.hi+(rp.lo+delta); - let mag=dot(z,z); - if(mag>4.0){ - let errAbs=safe_abs_error(errScaled,scaleExp,z,delta); - if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} - mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return; - } - - // Rebase only when dc remains numerically representable in the new scale. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u; - errScaled+=1.0*F32_U*maxabs(d); - continue; - } - - if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;} - let r=rp; - let refAbs=maxabs(r.hi)+maxabs(r.lo); - let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp)); - let gain=2.0*refAbs+2.0*wAbs*p2; - let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30); - errScaled=gain*errScaled+roundErr; - let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); - // delta^2 / 2^scaleExp = w^2 * 2^scaleExp - let sq=cmul(w,w)*pow2_safe(scaleExp); - w=linear+sq+d; m+=1u; n+=1u; - if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;} - let mm=max(maxabs(w),maxabs(d)); - if(mm>65536.0){ - w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16; - }else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){ - w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16; - } - if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;} - } -} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; - render_pixel(out,gx,gy,p.strict!=0u); -} -`; - - -// Idle refinement is compiled as a separate guarded-Deep module so the -// production Fast/Deep main shaders remain byte-for-byte identical to v24.2.18. -const DEEP_LIKELY_REFINE_WGSL=DEEP_PERTURB_WGSL.replace( - '@group(0) @binding(4) var unresolved:UnresolvedHead;', - '@group(0) @binding(4) var unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var refineQueue:array;\n@group(1) @binding(1) var refineBatch:RefineBatch;' -)+String.raw` -@compute @workgroup_size(64) -fn refine_likely(@builtin(global_invocation_id) gid:vec3){ - let slot=refineBatch.base+gid.x; - if(slot>=refineBatch.end){return;} - let out=refineQueue[slot]; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;} - let x=out%p.fullW; let y=out/p.fullW; - render_pixel(out,f32(x)+p.sampleX,f32(y)+p.sampleY,p.strict!=0u); -} -`; - -// Production Deep sparse path: keep the long-running perturbation kernel -// untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets. -// Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS -// correction kernel sees less workgroup-level iteration divergence. -// -// Bucket state layout (24 u32 atomics): -// [0..7] histogram counts -// [8..15] scatter cursors (initialized from prefix offsets) -// [16..23] immutable prefix offsets for queue accounting -const DEEP_BUCKET_HIST_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var bucketState:array>; -var localCounts:array,8>; -fn deep_bucket(packed:u32)->u32{ - let n=packed&ITER_MASK; - return min(7u,(n*8u)/max(1u,p.maxIter)); -} -@compute @workgroup_size(64) -fn main( - @builtin(local_invocation_id) lid3:vec3, - @builtin(workgroup_id) wid:vec3 -){ - let lane=lid3.x; - if(lane<8u){atomicStore(&localCounts[lane],0u);} - workgroupBarrier(); - let lx=wid.x*64u+lane; - let ly=wid.y; - if(lx>28u)&3u)==FIELD_UNKNOWN){ - atomicAdd(&localCounts[deep_bucket(packed)],1u); - } - } - workgroupBarrier(); - if(lane<8u){ - let c=atomicLoad(&localCounts[lane]); - if(c>0u){atomicAdd(&bucketState[lane],c);} - } -} -`; - -// Convert the 8-bin histogram into contiguous queue ranges, initialize each -// scatter cursor, and produce the correction indirect-dispatch arguments. -const DEEP_BUCKET_PREFIX_WGSL=String.raw` -struct SparseQueueStats{ - selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, -}; -struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 }; -@group(0) @binding(0) var bucketState:array>; -@group(0) @binding(1) var sparseQueueStats:SparseQueueStats; -@group(0) @binding(2) var indirectArgs:IndirectArgs; -@compute @workgroup_size(1) -fn main(){ - var total=0u; - var b=0u; - loop{ - if(b>=8u){break;} - let c=atomicLoad(&bucketState[b]); - atomicStore(&bucketState[8u+b],total); - atomicStore(&bucketState[16u+b],total); - total+=c; - b+=1u; - } - atomicStore(&sparseQueueStats.selected,total); - atomicStore(&sparseQueueStats.overflow,0u); - atomicStore(&sparseQueueStats.enqueued,total); - atomicStore(&sparseQueueStats.dispatchCount,total); - indirectArgs.x=(total+63u)/64u; - indirectArgs.y=1u; - indirectArgs.z=1u; - indirectArgs._pad=0u; -} -`; - -// Scatter UNKNOWN indices into the precomputed bucket ranges. Within each -// 64-lane workgroup, workgroup-memory atomics allocate local ranks; each -// non-empty bucket reserves one global subrange, so global atomics scale with -// non-empty (workgroup,bucket) pairs rather than with UNKNOWN pixels. -const DEEP_BUCKET_SCATTER_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct SparseQueueStats{ - selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, -}; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var bucketState:array>; -@group(0) @binding(3) var sparseQueueStats:SparseQueueStats; -@group(0) @binding(4) var unknownQueue:array; -var localCounts:array,8>; -var localRanks:array,8>; -var groupBase:array; -fn deep_bucket(packed:u32)->u32{ - let n=packed&ITER_MASK; - return min(7u,(n*8u)/max(1u,p.maxIter)); -} -@compute @workgroup_size(64) -fn main( - @builtin(local_invocation_id) lid3:vec3, - @builtin(workgroup_id) wid:vec3 -){ - let lane=lid3.x; - if(lane<8u){ - atomicStore(&localCounts[lane],0u); - atomicStore(&localRanks[lane],0u); - groupBase[lane]=0u; - } - workgroupBarrier(); - - let lx=wid.x*64u+lane; - let ly=wid.y; - var out=0u; - var hit=0u; - var bucket=0u; - if(lx>28u)&3u)==FIELD_UNKNOWN){ - hit=1u; - bucket=deep_bucket(packed); - atomicAdd(&localCounts[bucket],1u); - } - } - workgroupBarrier(); - - if(lane<8u){ - let c=atomicLoad(&localCounts[lane]); - if(c>0u){groupBase[lane]=atomicAdd(&bucketState[8u+lane],c);} - } - workgroupBarrier(); - - if(hit!=0u){ - let rank=atomicAdd(&localRanks[bucket],1u); - let qi=groupBase[bucket]+rank; - let capacity=p.tileW*p.tileH; - if(qi, lo:vec2 }; -struct CorrectionStats{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -struct DS{ h:f32, l:f32 }; -struct CDS{ r:DS, i:DS }; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var refs:array; -@group(0) @binding(2) var fieldMeta:array; -@group(0) @binding(3) var fieldSmooth:array; -@group(0) @binding(4) var stats:CorrectionStats; - -fn count_remaining(out:u32){ - let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu; - atomicAdd(&stats.remaining,1u); - if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);} - else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);} - else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);} - else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);} - else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);} - else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);} -} -fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){ - fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u); -} - -fn ds_quick(a:f32,b:f32)->DS{ - let q=a+b; - let e=b-(q-a); - return DS(q,e); -} -fn ds_sum(a:f32,b:f32)->DS{ - let q=a+b; - let bb=q-a; - let e=(a-(q-bb))+(b-bb); - return DS(q,e); -} -fn ds_prod(a:f32,b:f32)->DS{ - let q=a*b; - let ca=4097.0*a; - let ah=ca-(ca-a); - let al=a-ah; - let cb=4097.0*b; - let bh=cb-(cb-b); - let bl=b-bh; - var e=ah*bh-q; - e=e+ah*bl; - e=e+al*bh; - e=e+al*bl; - return DS(q,e); -} -fn ds_add(a:DS,b:DS)->DS{ - let q=ds_sum(a.h,b.h); - return ds_quick(q.h,q.l+(a.l+b.l)); -} -fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);} -fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));} -fn ds_mul(a:DS,b:DS)->DS{ - let q=ds_prod(a.h,b.h); - var e=q.l+a.h*b.l; - e=e+a.l*b.h; - e=e+a.l*b.l; - return ds_quick(q.h,e); -} -fn ds_scale(a:DS,b:f32)->DS{ - let q=ds_prod(a.h,b); - return ds_quick(q.h,q.l+a.l*b); -} -fn ds_pow2(a:DS,e:i32)->DS{ - if(e < -126){return DS(0.0,0.0);} - if(e > 126){return DS(8.507059e37,0.0);} - return DS(ldexp(a.h,e),ldexp(a.l,e)); -} -fn ds_cmp(a:DS,b:DS)->i32{ - if(a.hb.h){return 1;} - if(a.lb.l){return 1;} return 0; -} -fn ds_value(a:DS)->f32{return a.h+a.l;} -fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));} -fn cds_mul(a:CDS,b:CDS)->CDS{ - let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i)); - let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r)); - return CDS(rr,ii); -} -fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));} -fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));} -fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));} -fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));} -fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;} - let rp=refs[m]; - let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y)); - let delta=cds_pow2(w,scaleExp); - let z=cds_add(r,delta); - let mag=cds_mag2(z); - if(ds_cmp(mag,DS(4.0,0.0))>0){ - accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag)))); - return; - } - let dmag=cds_mag2(delta); - if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){ - if(p.spanExp-scaleExp < -96){count_remaining(out);return;} - w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u; - continue; - } - if(m>=p.refLen){count_remaining(out);return;} - let linear=cds_scale(cds_mul(r,w),2.0); - let sq=cds_pow2(cds_mul(w,w),scaleExp); - w=cds_add(cds_add(linear,sq),d); - m+=1u; n+=1u; - let mm=max(cds_maxabs(w),cds_maxabs(d)); - if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;} - if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;} - else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;} - if(scaleExp>126){count_remaining(out);return;} - } -} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - correct_pixel(out,gx,gy); -} -`; - - -// Production sparse correction variant. It shares the complete DS arithmetic -// with DEEP_CORRECT_WGSL, but consumes only indices emitted by the queued deep -// perturbation pass and launches via dispatchWorkgroupsIndirect. -const DEEP_CORRECT_QUEUE_WGSL=DEEP_CORRECT_WGSL - .replace( -`struct CorrectionStats{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -};`, -`struct CorrectionStats{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -struct SparseQueueStats{ - selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, -};`) - .replace( -`@group(0) @binding(4) var stats:CorrectionStats;`, -`@group(0) @binding(4) var stats:CorrectionStats; -@group(0) @binding(5) var sparseQueueStats:SparseQueueStats; -@group(0) @binding(6) var unknownQueue:array;`) - .replace( -`@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - correct_pixel(out,gx,gy); -}`, -`@compute @workgroup_size(64) -fn main(@builtin(global_invocation_id) gid:vec3){ - let qi=gid.x; - let queued=atomicLoad(&sparseQueueStats.dispatchCount); - if(qi>=queued){return;} - let out=unknownQueue[qi]; - atomicAdd(&sparseQueueStats.processed,1u); - if(out=p.tileW||ly>=p.tileH){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;} - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;} - let gx=f32(p.tileX+lx)+p.sampleX; - let gy=f32(p.tileY+ly)+p.sampleY; - correct_pixel(out,gx,gy); -}`); - -if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)')){ - throw new Error('failed to derive queued deep correction shader'); -} - -const LIKELY_QUEUE_WGSL=COMMON+String.raw` -struct QueueParams{width:u32,height:u32,mode:u32,sparseStride:u32}; -struct QueueStats{count:atomic,overflow:atomic,_p0:atomic,_p1:atomic}; -@group(0) @binding(0) var p:QueueParams; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var queue:array; -@group(0) @binding(3) var stats:QueueStats; -fn is_frontier(x:u32,y:u32,r:i32)->bool{let xi=i32(x);let yi=i32(y);var oy=-1;loop{if(oy>1){break;}var ox=-1;loop{if(ox>1){break;}if(ox!=0||oy!=0){let xx=xi+ox*r;let yy=yi+oy*r;if(xx>=0&&yy>=0&&xx>28u)&3u;if(cls==FIELD_ESCAPED||cls==FIELD_UNKNOWN){return true;}}}ox+=1;}oy+=1;}return false;} -@compute @workgroup_size(256) -fn main(@builtin(global_invocation_id) gid:vec3){let i=gid.x;let total=p.width*p.height;if(i>=total){return;}if(((fieldMeta[i]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}let x=i%p.width;let y=i/p.width;var selected=false;if(p.mode==0u){selected=true;}else{let radius:i32=select(2,select(3,4,p.mode>=3u),p.mode>=2u);selected=is_frontier(x,y,radius);if(!selected&&p.sparseStride>0u){selected=(x%p.sparseStride==0u)&&(y%p.sparseStride==0u);}}if(!selected){return;}let slot=atomicAdd(&stats.count,1u);if(slot p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var fieldSmooth:array; -@group(0) @binding(3) var outTex:texture_storage_2d; -fn hsv(h:f32,s:f32,v:f32)->vec3{ - let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f)); - if(i==0){return vec3(v,t,pp);} if(i==1){return vec3(q,v,pp);} if(i==2){return vec3(pp,v,t);} if(i==3){return vec3(pp,q,v);} if(i==4){return vec3(t,pp,v);} return vec3(v,pp,q); -} -fn current_palette(t0:f32)->vec3{ - let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5); - if(t<0.11){return mix(vec3(4,10,27),vec3(12,53,79),smoothstep(0.0,0.11,t))/255.0;} - if(t<0.25){return mix(vec3(12,53,79),vec3(31,156,184),smoothstep(0.11,0.25,t))/255.0;} - if(t<0.38){return mix(vec3(31,156,184),vec3(91,226,234),smoothstep(0.25,0.38,t))/255.0;} - if(t<0.50){return mix(vec3(91,226,234),vec3(66,53,151),smoothstep(0.38,0.50,t))/255.0;} - if(t<0.62){return mix(vec3(66,53,151),vec3(139,49,170),smoothstep(0.50,0.62,t))/255.0;} - if(t<0.73){return mix(vec3(139,49,170),vec3(232,72,145),smoothstep(0.62,0.73,t))/255.0;} - if(t<0.84){return mix(vec3(232,72,145),vec3(255,137,64),smoothstep(0.73,0.84,t))/255.0;} - if(t<0.93){return mix(vec3(255,137,64),vec3(255,211,99),smoothstep(0.84,0.93,t))/255.0;} - return mix(vec3(255,211,99),vec3(255,250,223),smoothstep(0.93,1.0,t))/255.0; -} -fn palette_color(phase:f32)->vec3{ - if(p.palette==1u){return hsv(phase,0.92,1.0);}if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3(g);}return current_palette(phase); -} -fn escaped_color(m:u32,sm:f32)->vec3{ - let phase=fract(p.shift+sm*p.cycle);let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.34+0.66*pow(edge,0.38);let floorc=select(vec3(2,5,15)/255.0,vec3(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv); -} -fn provisional_unknown(m:u32)->vec3{ - let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3(6,10,22)/255.0,vec3(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52); -} -fn base_color(i:u32)->vec3{ - let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3(0.0);}return escaped_color(m,fieldSmooth[i]); -} -fn linearize(c:vec3)->vec3{return pow(c,vec3(2.2));} -fn delinearize(c:vec3)->vec3{return pow(max(c,vec3(0.0)),vec3(1.0/2.2));} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.width||gid.y>=p.height){return;}let i=gid.y*p.width+gid.x;let m=fieldMeta[i];let cls=(m>>28u)&3u;var c=base_color(i);let x=i32(gid.x);let y=i32(gid.y); - if(cls==FIELD_UNKNOWN){ - var fill=vec3(0.0);var fillCount=0.0; - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];if(((mj>>28u)&3u)!=FIELD_UNKNOWN){fill+=linearize(base_color(j));fillCount+=1.0;}}} - if(fillCount>0.0){c=delinearize(fill/fillCount);} - } - if(p.edgeAA!=0u){ - var boundary=false;var sum=linearize(c);var cnt=1.0; - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}} - if(boundary){c=delinearize(sum/cnt);} - } - textureStore(outTex,vec2(gid.xy),vec4(c,1.0)); -} -` - - -const AA_RESOLVE_WGSL=String.raw` -@group(0) @binding(0) var a:texture_2d; -@group(0) @binding(1) var b:texture_2d; -@group(0) @binding(2) var c:texture_2d; -@group(0) @binding(3) var d:texture_2d; -@group(0) @binding(4) var outTex:texture_storage_2d; -fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);} -fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);} -fn lin3(v:vec3)->vec3{return vec3(to_linear(v.x),to_linear(v.y),to_linear(v.z));} -fn srgb3(v:vec3)->vec3{return vec3(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;} - let q=vec2(gid.xy); - let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb); - textureStore(outTex,q,vec4(srgb3(sum*0.25),1.0)); -} -`; - -const PRESENT_WGSL=String.raw` -struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32}; -@group(0) @binding(0) var samp:sampler; -@group(0) @binding(1) var tex:texture_2d; -@group(0) @binding(2) var p:Params; -struct VSOut{@builtin(position) pos:vec4,@location(0) uv:vec2}; -@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{ - var pos=array,3>(vec2(-1.0,-1.0),vec2(3.0,-1.0),vec2(-1.0,3.0)); - var uv=array,3>(vec2(0.0,1.0),vec2(2.0,1.0),vec2(0.0,-1.0)); - var o:VSOut;o.pos=vec4(pos[i],0.0,1.0);o.uv=uv[i];return o; -} -@fragment fn fs(in:VSOut)->@location(0) vec4{ - let uv=vec2(0.5)+(in.uv-vec2(0.5))*vec2(p.scaleX,p.scaleY)+vec2(p.offsetX,p.offsetY); - if(any(uv(0.0))||any(uv>vec2(1.0))){return vec4(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0); -} -`; - - -// v24.2.14 production screen path: defer UNKNOWN statistics out of the long -// perturbation kernel. The existing bucket histogram pass already scans the -// same field before sparse correction, so it can aggregate remaining/reason -// counters per workgroup without per-UNKNOWN global atomics in the primary. -// The counted primary is retained for export and verification paths. -const DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u);\n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}'); - -const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL - .replace('@group(0) @binding(2) var bucketState:array>;', '@group(0) @binding(2) var bucketState:array>;\n@group(0) @binding(3) var unresolvedStats:array>;') - .replace('var localCounts:array,8>;', 'var localCounts:array,8>;\nvar localReasons:array,8>;') - .replace('if(lane<8u){atomicStore(&localCounts[lane],0u);}', 'if(lane<8u){atomicStore(&localCounts[lane],0u);atomicStore(&localReasons[lane],0u);}') - .replace('atomicAdd(&localCounts[deep_bucket(packed)],1u);', `atomicAdd(&localCounts[deep_bucket(packed)],1u); - atomicAdd(&localReasons[0],1u); - let reason=(packed&REASON_MASK)>>REASON_SHIFT; - if(reason>=1u && reason<=6u){atomicAdd(&localReasons[reason],1u);}`) - .replace('if(c>0u){atomicAdd(&bucketState[lane],c);}', `if(c>0u){atomicAdd(&bucketState[lane],c);} - let r=atomicLoad(&localReasons[lane]); - if(r>0u){atomicAdd(&unresolvedStats[lane],r);}`); -if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('v24.2.14 post-stats shader derivation failed'); - -globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({ - version:'24.2.26-baseline-idle-refinement',DIRECT_F32_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_LIKELY_REFINE_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,LIKELY_QUEUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL -}); -})(); diff --git a/index.external.html b/index.external.html deleted file mode 100644 index 1b9bf1e..0000000 --- a/index.external.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - -Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement - - - - -
MANDELBROT DEEP ZOOM
中心
倍率  表示幅
計算 起動中… 描画
-
起動中
-
-
-
-
-
-
-
0.0080
-
.18
-
-
-
起動中
-
ドラッグ中は直前フレームを再投影して再利用。ホイール / ピンチでズーム、HキーでUI表示を切り替え。
-
- -
- -

高解像度 PNG 出力

-
- - - - -
- -
-
-
- - - - diff --git a/index.html b/index.html index ff9a2e2..93170eb 100644 --- a/index.html +++ b/index.html @@ -1,47 +1,45 @@ - + - -Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement + +Mandelbrot Deep Zoom v24.2.46 Stable Frame + Color UI -
MANDELBROT DEEP ZOOM
中心
倍率  表示幅
計算 起動中… 描画
+
MANDELBROT DEEP ZOOM333の部屋に戻る
中心
倍率  表示幅
計算 起動中… 描画
起動中
-
+
0.0080
.18
-
+
起動中
-
ドラッグ中は直前フレームを再投影して再利用。ホイール / ピンチでズーム、HキーでUI表示を切り替え。
@@ -82,21 +80,20 @@ fn cmul(a:vec2, b:vec2)->vec2{ } fn maxabs(v:vec2)->f32 { return max(abs(v.x),abs(v.y)); } const F32_U:f32=5.960464477539063e-8; -fn pow2_safe(e:i32)->f32 { - if(e < -126){ return 0.0; } - if(e > 126){ return 8.507059e37; } - return ldexp(1.0,e); -} -fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2, delta:vec2)->f32{ - let propagated=abs(errScaled*pow2_safe(scaleExp)); - let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30); - return propagated+reconstruction; +fn scale_scalar(v:f32,e:i32)->f32{ + // ldexp rounds the combined v*2^e result. Do not reject solely because e + // is below the normal f32 exponent: a large mantissa can still produce a + // perfectly normal representable result (critical around 10^38.461 zoom). + return ldexp(v,e); } fn scaled_to_f32(v:vec2, e:i32)->vec2{ - if(e < -126){ return vec2(0.0); } - if(e > 126){ return vec2(8.507059e37); } return ldexp(v,vec2(e)); } +fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2, delta:vec2)->f32{ + let propagated=abs(scale_scalar(errScaled,scaleExp)); + let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30); + return propagated+reconstruction; +} fn smooth_escape(n:u32, mag2:f32)->f32{ let u=log2(max(4.0000005,mag2)); return f32(n)+1.0-log2(max(1.0e-20,0.5*u)); @@ -142,7 +139,7 @@ fn main(@builtin(global_invocation_id) gid:vec3){ let mag=zr*zr+zi*zi; if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;} } - fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0; + fieldMeta[out]=pack_unknown(p.maxIter,REASON_OPERATION_LIMIT); fieldSmooth[out]=0.0; } `; @@ -151,6 +148,108 @@ fn main(@builtin(global_invocation_id) gid:vec3){ // of the verified Deep backend: a reduced-precision reference orbit is built // quickly on the CPU worker and this lightweight perturbation kernel preserves // sub-f32 pixel offsets once absolute Direct coordinates begin to collapse. + + +// Low/mid-depth accurate screen path. A cheap f32 seed accepts only pixels +// that escape very early; everything delicate is recomputed from iteration 0 +// with direct double-single arithmetic. This avoids paying full-screen Deep +// perturbation where absolute pixel coordinates are still widely separated. +const DIRECT_ACCURATE_SEED_WGSL=COMMON+String.raw` +struct Params{ + tileW:u32,tileH:u32,fullW:u32,fullH:u32, + tileX:u32,tileY:u32,maxIter:u32,lateThreshold:u32, + outputStride:u32,outputBase:u32,_u0:u32,_u1:u32, + centerReHi:f32,centerReLo:f32,centerImHi:f32,centerImLo:f32, + spanHi:f32,spanLo:f32,invWHi:f32,invWLo:f32, + sampleX:f32,sampleY:f32,_f0:f32,_f1:f32, +}; +@group(0) @binding(0) var p:Params; +@group(0) @binding(1) var fieldMeta:array; +@group(0) @binding(2) var fieldSmooth:array; +fn analytic(cr:f32,ci:f32)->bool{ + let y2=ci*ci;let x=cr-0.25;let q=x*x+y2; + let lhs=q*(q+x);let rhs=0.25*y2; + let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0); + if(lhs){ + if(gid.x>=p.tileW||gid.y>=p.tileH){return;} + let out=p.outputBase+gid.y*p.outputStride+gid.x; + let gx=f32(p.tileX+gid.x)+p.sampleX; + let gy=f32(p.tileY+gid.y)+p.sampleY; + let centerRe=p.centerReHi+p.centerReLo; + let centerIm=p.centerImHi+p.centerImLo; + let span=p.spanHi+p.spanLo; + let invW=p.invWHi+p.invWLo; + let cr=centerRe+(gx-0.5*f32(p.fullW))*span*invW; + let ci=centerIm+(0.5*f32(p.fullH)-gy)*span*invW; + if(analytic(cr,ci)){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN);fieldSmooth[out]=0.0;return;} + var zr=0.0;var zi=0.0;var n=0u; + loop{ + if(n>=p.maxIter){fieldMeta[out]=pack_unknown(p.maxIter,REASON_OPERATION_LIMIT);fieldSmooth[out]=0.0;return;} + let zr2=zr*zr;let zi2=zi*zi;zi=2.0*zr*zi+ci;zr=zr2-zi2+cr;n+=1u; + let mag=zr*zr+zi*zi; + if(mag>4.0){ + if(n p:Params; +@group(0) @binding(1) var fieldMeta:array; +@group(0) @binding(2) var fieldSmooth:array; +fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);} +fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);} +fn ds_prod(a:f32,b:f32)->DS{ + let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh; + var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e); +} +fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+a.l+b.l);} +fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);} +fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));} +fn ds_mul(a:DS,b:DS)->DS{let q=ds_prod(a.h,b.h);let e=q.l+a.h*b.l+a.l*b.h+a.l*b.l;return ds_quick(q.h,e);} +fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);} +fn ds_cmp(a:DS,b:DS)->i32{if(a.hb.h){return 1;}if(a.lb.l){return 1;}return 0;} +fn ds_value(a:DS)->f32{return a.h+a.l;} +@compute @workgroup_size(8,8) +fn main(@builtin(global_invocation_id) gid:vec3){ + if(gid.x>=p.tileW||gid.y>=p.tileH){return;} + let out=p.outputBase+gid.y*p.outputStride+gid.x; + if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} + let gx=f32(p.tileX+gid.x)+p.sampleX;let gy=f32(p.tileY+gid.y)+p.sampleY; + let invW=DS(p.invWHi,p.invWLo);let span=DS(p.spanHi,p.spanLo);let scale=ds_mul(span,invW); + let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_scale(scale,gx-0.5*f32(p.fullW))); + let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_scale(scale,0.5*f32(p.fullH)-gy)); + var zr=DS(0.0,0.0);var zi=DS(0.0,0.0);var n=0u; + loop{ + if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;} + let zr2=ds_mul(zr,zr);let zi2=ds_mul(zi,zi);let zri=ds_mul(zr,zi); + zr=ds_add(ds_sub(zr2,zi2),cr);zi=ds_add(ds_scale(zri,2.0),ci);n+=1u; + let mag=ds_add(ds_mul(zr,zr),ds_mul(zi,zi)); + if(ds_cmp(mag,DS(4.0,0.0))>0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;} + let av=max(abs(ds_value(zr)),abs(ds_value(zi))); + if(av!=av||av>1.0e20){fieldMeta[out]=pack_unknown(n,REASON_RANGE);fieldSmooth[out]=0.0;return;} + } +} +`; + const FAST_PERTURB_WGSL=COMMON+String.raw` struct Params{ tileW:u32, tileH:u32, fullW:u32, fullH:u32, @@ -179,26 +278,18 @@ fn render_fast(out:u32,gx:f32,gy:f32){ var scaleExp=p.spanExp; var n=0u; var m=0u; loop{ - if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;} + if(n>=p.maxIter){mark_fast_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);return;} let rp=refs[min(m,p.refLen)]; let delta=scaled_to_f32(w,scaleExp); let z=rp.hi+(rp.lo+delta); let mag=dot(z,z); if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} - - // Rebase the perturbation when it grows beyond the reference value. This - // keeps the fast path usable across pans without any verified correction. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); - scaleExp=0; m=0u; - continue; - } + // Reference-near-zero episodes are not an automatic failure. Continue the + // valid perturbation recurrence; explicit range/reference guards remain. if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;} let r=rp; let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); - let sq=cmul(w,w)*pow2_safe(scaleExp); + let sq=scaled_to_f32(cmul(w,w),scaleExp); w=linear+sq+d; m+=1u; n+=1u; let mm=max(maxabs(w),maxabs(d)); if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;} @@ -211,6 +302,13 @@ fn render_fast(out:u32,gx:f32,gy:f32){ fn main(@builtin(global_invocation_id) gid:vec3){ if(gid.x>=p.tileW||gid.y>=p.tileH){return;} let out=p.outputBase+gid.y*p.outputStride+gid.x; + if(p.unknownOnly!=0u){ + let prior=fieldMeta[out];let cls=(prior>>28u)&3u; + if(cls!=FIELD_UNKNOWN){return;} + let reason=(prior>>20u)&0xffu; + // mode 2 repairs only untouched coverage holes (zero-filled metadata). + if(p.unknownOnly==2u && reason!=0u){return;} + } let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; render_fast(out,gx,gy); @@ -264,7 +362,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd); let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd); let limit=select(1.0e-3,1.0e-4,strictMode); - if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);} + if(errAbs<=limit){mark_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);}else{mark_unresolved(out,n,REASON_ERROR_BOUND);} return; } let rp=refs[m]; @@ -276,26 +374,23 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return; } - - // Rebase only when dc remains numerically representable in the new scale. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u; - errScaled+=1.0*F32_U*maxabs(d); - continue; - } + // Keep the perturbation recurrence through reference-near-zero episodes. + // Escape acceptance is still guarded by the propagated absolute error. if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;} let r=rp; let refAbs=maxabs(r.hi)+maxabs(r.lo); - let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp)); - let gain=2.0*refAbs+2.0*wAbs*p2; - let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30); + let wAbs=maxabs(w); let dAbs=maxabs(d); + let actualDelta=scaled_to_f32(w,scaleExp); + let deltaAbs=maxabs(actualDelta); + let sq=scaled_to_f32(cmul(w,w),scaleExp); + let sqAbs=maxabs(sq); + let gain=2.0*refAbs+2.0*deltaAbs; + let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30); errScaled=gain*errScaled+roundErr; let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); - // delta^2 / 2^scaleExp = w^2 * 2^scaleExp - let sq=cmul(w,w)*pow2_safe(scaleExp); + // delta^2 / 2^scaleExp = w^2 * 2^scaleExp. Scale the product + // directly so e<-126 does not discard a representable combined value. w=linear+sq+d; m+=1u; n+=1u; if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;} let mm=max(maxabs(w),maxabs(d)); @@ -318,23 +413,6 @@ fn main(@builtin(global_invocation_id) gid:vec3){ `; -// Idle refinement is compiled as a separate guarded-Deep module so the -// production Fast/Deep main shaders remain byte-for-byte identical to v24.2.18. -const DEEP_LIKELY_REFINE_WGSL=DEEP_PERTURB_WGSL.replace( - '@group(0) @binding(4) var unresolved:UnresolvedHead;', - '@group(0) @binding(4) var unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var refineQueue:array;\n@group(1) @binding(1) var refineBatch:RefineBatch;' -)+String.raw` -@compute @workgroup_size(64) -fn refine_likely(@builtin(global_invocation_id) gid:vec3){ - let slot=refineBatch.base+gid.x; - if(slot>=refineBatch.end){return;} - let out=refineQueue[slot]; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;} - let x=out%p.fullW; let y=out/p.fullW; - render_pixel(out,f32(x)+p.sampleX,f32(y)+p.sampleY,p.strict!=0u); -} -`; - // Production Deep sparse path: keep the long-running perturbation kernel // untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets. // Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS @@ -361,6 +439,11 @@ fn deep_bucket(packed:u32)->u32{ let n=packed&ITER_MASK; return min(7u,(n*8u)/max(1u,p.maxIter)); } +fn selected_unknown(packed:u32)->bool{ + if(((packed>>28u)&3u)!=FIELD_UNKNOWN){return false;} + if(p.unknownOnly==2u){let reason=(packed&REASON_MASK)>>REASON_SHIFT;return reason>=1u && reason<=5u;} + return true; +} @compute @workgroup_size(64) fn main( @builtin(local_invocation_id) lid3:vec3, @@ -374,7 +457,7 @@ fn main( if(lx>28u)&3u)==FIELD_UNKNOWN){ + if(selected_unknown(packed)){ atomicAdd(&localCounts[deep_bucket(packed)],1u); } } @@ -449,6 +532,11 @@ fn deep_bucket(packed:u32)->u32{ let n=packed&ITER_MASK; return min(7u,(n*8u)/max(1u,p.maxIter)); } +fn selected_unknown(packed:u32)->bool{ + if(((packed>>28u)&3u)!=FIELD_UNKNOWN){return false;} + if(p.unknownOnly==2u){let reason=(packed&REASON_MASK)>>REASON_SHIFT;return reason>=1u && reason<=5u;} + return true; +} @compute @workgroup_size(64) fn main( @builtin(local_invocation_id) lid3:vec3, @@ -470,7 +558,7 @@ fn main( if(lx>28u)&3u)==FIELD_UNKNOWN){ + if(selected_unknown(packed)){ hit=1u; bucket=deep_bucket(packed); atomicAdd(&localCounts[bucket],1u); @@ -519,8 +607,8 @@ struct CDS{ r:DS, i:DS }; @group(0) @binding(3) var fieldSmooth:array; @group(0) @binding(4) var stats:CorrectionStats; -fn count_remaining(out:u32){ - let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu; +fn count_remaining(out:u32,n:u32,reason:u32){ + fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0; atomicAdd(&stats.remaining,1u); if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);} else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);} @@ -576,8 +664,6 @@ fn ds_scale(a:DS,b:f32)->DS{ return ds_quick(q.h,q.l+a.l*b); } fn ds_pow2(a:DS,e:i32)->DS{ - if(e < -126){return DS(0.0,0.0);} - if(e > 126){return DS(8.507059e37,0.0);} return DS(ldexp(a.h,e),ldexp(a.l,e)); } fn ds_cmp(a:DS,b:DS)->i32{ @@ -610,7 +696,7 @@ fn correct_pixel(out:u32,gx:f32,gy:f32){ var scaleExp=p.spanExp; var n=0u; var m=0u; loop{ - if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;} + if(n>=p.maxIter){count_remaining(out,p.maxIter,REASON_OPERATION_LIMIT);return;} let rp=refs[m]; let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y)); let delta=cds_pow2(w,scaleExp); @@ -620,29 +706,29 @@ fn correct_pixel(out:u32,gx:f32,gy:f32){ accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag)))); return; } - let dmag=cds_mag2(delta); - if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){ - if(p.spanExp-scaleExp < -96){count_remaining(out);return;} - w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u; - continue; - } - if(m>=p.refLen){count_remaining(out);return;} + // DS correction follows the same continuous perturbation recurrence. + if(m>=p.refLen){count_remaining(out,n,REASON_REFERENCE_END);return;} let linear=cds_scale(cds_mul(r,w),2.0); let sq=cds_pow2(cds_mul(w,w),scaleExp); w=cds_add(cds_add(linear,sq),d); m+=1u; n+=1u; let mm=max(cds_maxabs(w),cds_maxabs(d)); - if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;} + if(mm>=1.0e30 || mm!=mm){count_remaining(out,n,REASON_RANGE);return;} if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;} else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;} - if(scaleExp>126){count_remaining(out);return;} + if(scaleExp>126){count_remaining(out,n,REASON_RANGE);return;} } } @compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) gid:vec3){ if(gid.x>=p.tileW||gid.y>=p.tileH){return;} let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} + let prior=fieldMeta[out]; + if(((prior>>28u)&3u)!=FIELD_UNKNOWN){return;} + if(p.unknownOnly==2u){ + let reason=(prior&REASON_MASK)>>REASON_SHIFT; + if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;} + } let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; correct_pixel(out,gx,gy); @@ -677,7 +763,12 @@ struct SparseQueueStats{ fn main(@builtin(global_invocation_id) gid:vec3){ if(gid.x>=p.tileW||gid.y>=p.tileH){return;} let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} + let prior=fieldMeta[out]; + if(((prior>>28u)&3u)!=FIELD_UNKNOWN){return;} + if(p.unknownOnly==2u){ + let reason=(prior&REASON_MASK)>>REASON_SHIFT; + if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;} + } let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; correct_pixel(out,gx,gy); @@ -694,7 +785,12 @@ fn main(@builtin(global_invocation_id) gid:vec3){ let ly=local/p.outputStride; let lx=local-ly*p.outputStride; if(lx>=p.tileW||ly>=p.tileH){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;} - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;} + let prior=fieldMeta[out]; + if(((prior>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;} + if(p.unknownOnly==2u){ + let reason=(prior&REASON_MASK)>>REASON_SHIFT; + if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;} + } let gx=f32(p.tileX+lx)+p.sampleX; let gy=f32(p.tileY+ly)+p.sampleY; correct_pixel(out,gx,gy); @@ -704,18 +800,6 @@ if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.inclu throw new Error('failed to derive queued deep correction shader'); } -const LIKELY_QUEUE_WGSL=COMMON+String.raw` -struct QueueParams{width:u32,height:u32,mode:u32,sparseStride:u32}; -struct QueueStats{count:atomic,overflow:atomic,_p0:atomic,_p1:atomic}; -@group(0) @binding(0) var p:QueueParams; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var queue:array; -@group(0) @binding(3) var stats:QueueStats; -fn is_frontier(x:u32,y:u32,r:i32)->bool{let xi=i32(x);let yi=i32(y);var oy=-1;loop{if(oy>1){break;}var ox=-1;loop{if(ox>1){break;}if(ox!=0||oy!=0){let xx=xi+ox*r;let yy=yi+oy*r;if(xx>=0&&yy>=0&&xx>28u)&3u;if(cls==FIELD_ESCAPED||cls==FIELD_UNKNOWN){return true;}}}ox+=1;}oy+=1;}return false;} -@compute @workgroup_size(256) -fn main(@builtin(global_invocation_id) gid:vec3){let i=gid.x;let total=p.width*p.height;if(i>=total){return;}if(((fieldMeta[i]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}let x=i%p.width;let y=i/p.width;var selected=false;if(p.mode==0u){selected=true;}else{let radius:i32=select(2,select(3,4,p.mode>=3u),p.mode>=2u);selected=is_frontier(x,y,radius);if(!selected&&p.sparseStride>0u){selected=(x%p.sparseStride==0u)&&(y%p.sparseStride==0u);}}if(!selected){return;}let slot=atomicAdd(&stats.count,1u);if(slotvec3{ let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f)); if(i==0){return vec3(v,t,pp);} if(i==1){return vec3(q,v,pp);} if(i==2){return vec3(pp,v,t);} if(i==3){return vec3(pp,q,v);} if(i==4){return vec3(t,pp,v);} return vec3(v,pp,q); } +fn rgb8(r:f32,g:f32,b:f32)->vec3{return vec3(r,g,b)/255.0;} +fn tri_phase(t0:f32)->f32{return 1.0-abs(2.0*fract(t0)-1.0);} +fn ramp5(t:f32,c0:vec3,c1:vec3,c2:vec3,c3:vec3,c4:vec3)->vec3{ + if(t<0.25){return mix(c0,c1,smoothstep(0.0,0.25,t));} + if(t<0.50){return mix(c1,c2,smoothstep(0.25,0.50,t));} + if(t<0.75){return mix(c2,c3,smoothstep(0.50,0.75,t));} + return mix(c3,c4,smoothstep(0.75,1.0,t)); +} fn current_palette(t0:f32)->vec3{ let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5); - if(t<0.11){return mix(vec3(4,10,27),vec3(12,53,79),smoothstep(0.0,0.11,t))/255.0;} - if(t<0.25){return mix(vec3(12,53,79),vec3(31,156,184),smoothstep(0.11,0.25,t))/255.0;} - if(t<0.38){return mix(vec3(31,156,184),vec3(91,226,234),smoothstep(0.25,0.38,t))/255.0;} - if(t<0.50){return mix(vec3(91,226,234),vec3(66,53,151),smoothstep(0.38,0.50,t))/255.0;} - if(t<0.62){return mix(vec3(66,53,151),vec3(139,49,170),smoothstep(0.50,0.62,t))/255.0;} - if(t<0.73){return mix(vec3(139,49,170),vec3(232,72,145),smoothstep(0.62,0.73,t))/255.0;} - if(t<0.84){return mix(vec3(232,72,145),vec3(255,137,64),smoothstep(0.73,0.84,t))/255.0;} - if(t<0.93){return mix(vec3(255,137,64),vec3(255,211,99),smoothstep(0.84,0.93,t))/255.0;} - return mix(vec3(255,211,99),vec3(255,250,223),smoothstep(0.93,1.0,t))/255.0; + if(t<0.11){return mix(rgb8(4.0,10.0,27.0),rgb8(12.0,53.0,79.0),smoothstep(0.0,0.11,t));} + if(t<0.25){return mix(rgb8(12.0,53.0,79.0),rgb8(31.0,156.0,184.0),smoothstep(0.11,0.25,t));} + if(t<0.38){return mix(rgb8(31.0,156.0,184.0),rgb8(91.0,226.0,234.0),smoothstep(0.25,0.38,t));} + if(t<0.50){return mix(rgb8(91.0,226.0,234.0),rgb8(66.0,53.0,151.0),smoothstep(0.38,0.50,t));} + if(t<0.62){return mix(rgb8(66.0,53.0,151.0),rgb8(139.0,49.0,170.0),smoothstep(0.50,0.62,t));} + if(t<0.73){return mix(rgb8(139.0,49.0,170.0),rgb8(232.0,72.0,145.0),smoothstep(0.62,0.73,t));} + if(t<0.84){return mix(rgb8(232.0,72.0,145.0),rgb8(255.0,137.0,64.0),smoothstep(0.73,0.84,t));} + if(t<0.93){return mix(rgb8(255.0,137.0,64.0),rgb8(255.0,211.0,99.0),smoothstep(0.84,0.93,t));} + return mix(rgb8(255.0,211.0,99.0),rgb8(255.0,250.0,223.0),smoothstep(0.93,1.0,t)); } fn palette_color(phase:f32)->vec3{ - if(p.palette==1u){return hsv(phase,0.92,1.0);}if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3(g);}return current_palette(phase); + if(p.palette==0u){return current_palette(phase);} + if(p.palette==1u){return hsv(phase,0.92,1.0);} + if(p.palette==2u){let g=(18.0+237.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3(g);} + let t=tri_phase(phase); + if(p.palette==3u){return ramp5(t,rgb8(0.0,0.0,2.0),rgb8(40.0,8.0,84.0),rgb8(141.0,36.0,101.0),rgb8(228.0,88.0,48.0),rgb8(252.0,255.0,166.0));} + if(p.palette==5u){return ramp5(t,rgb8(1.0,4.0,14.0),rgb8(7.0,36.0,92.0),rgb8(0.0,132.0,178.0),rgb8(116.0,232.0,240.0),rgb8(242.0,254.0,255.0));} + if(p.palette==7u){return ramp5(t,rgb8(3.0,6.0,20.0),rgb8(63.0,26.0,132.0),rgb8(10.0,146.0,174.0),rgb8(48.0,224.0,140.0),rgb8(229.0,255.0,184.0));} + if(p.palette==9u){return ramp5(t,rgb8(0.0,5.0,20.0),rgb8(4.0,42.0,82.0),rgb8(0.0,113.0,136.0),rgb8(34.0,176.0,162.0),rgb8(221.0,246.0,202.0));} + if(p.palette==10u){return ramp5(t,rgb8(5.0,4.0,2.0),rgb8(44.0,24.0,4.0),rgb8(132.0,83.0,10.0),rgb8(228.0,171.0,46.0),rgb8(255.0,245.0,198.0));} + if(p.palette==11u){return ramp5(t,rgb8(3.0,0.0,16.0),rgb8(250.0,0.0,132.0),rgb8(0.0,228.0,255.0),rgb8(126.0,255.0,0.0),rgb8(255.0,247.0,255.0));} + if(p.palette==13u){return ramp5(t,rgb8(0.0,0.0,14.0),rgb8(21.0,16.0,152.0),rgb8(0.0,188.0,255.0),rgb8(220.0,0.0,255.0),rgb8(255.0,255.0,255.0));} + if(p.palette==15u){return ramp5(t,rgb8(2.0,8.0,26.0),rgb8(18.0,85.0,210.0),rgb8(192.0,244.0,255.0),rgb8(255.0,134.0,36.0),rgb8(255.0,242.0,196.0));} + if(p.palette==17u){return ramp5(t,rgb8(20.0,1.0,14.0),rgb8(204.0,26.0,72.0),rgb8(255.0,226.0,182.0),rgb8(0.0,202.0,226.0),rgb8(226.0,249.0,255.0));} + if(p.palette==18u){return ramp5(t,rgb8(22.0,8.0,24.0),rgb8(171.0,36.0,122.0),rgb8(255.0,211.0,149.0),rgb8(45.0,204.0,176.0),rgb8(232.0,255.0,243.0));} + return current_palette(phase); } fn escaped_color(m:u32,sm:f32)->vec3{ - let phase=fract(p.shift+sm*p.cycle);let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.34+0.66*pow(edge,0.38);let floorc=select(vec3(2,5,15)/255.0,vec3(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv); -} -fn provisional_unknown(m:u32)->vec3{ - let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3(6,10,22)/255.0,vec3(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52); + // Direct phase mapping: no iteration-band/anchor compensation. + let phase=fract(p.shift+sm*p.cycle); + let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.30+0.70*pow(edge,0.40);let floorc=select(vec3(0.0,0.0,0.0),vec3(2.0/255.0),p.palette==2u);return mix(floorc,c,mixv); } fn base_color(i:u32)->vec3{ - let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3(0.0);}return escaped_color(m,fieldSmooth[i]); + let m=fieldMeta[i];return escaped_color(m,fieldSmooth[i]); } fn linearize(c:vec3)->vec3{return pow(c,vec3(2.2));} fn delinearize(c:vec3)->vec3{return pow(max(c,vec3(0.0)),vec3(1.0/2.2));} @compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.width||gid.y>=p.height){return;}let i=gid.y*p.width+gid.x;let m=fieldMeta[i];let cls=(m>>28u)&3u;var c=base_color(i);let x=i32(gid.x);let y=i32(gid.y); + if(gid.x>=p.width||gid.y>=p.height){return;} + let i=gid.y*p.width+gid.x;let m=fieldMeta[i];let cls=(m>>28u)&3u; if(cls==FIELD_UNKNOWN){ - var fill=vec3(0.0);var fillCount=0.0; - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];if(((mj>>28u)&3u)!=FIELD_UNKNOWN){fill+=linearize(base_color(j));fillCount+=1.0;}}} - if(fillCount>0.0){c=delinearize(fill/fillCount);} + let reason=(m>>20u)&0xffu; + // Iteration-limit pixels are not a numeric hole: display them as opaque + // black so temporal history cannot paint stale exterior colour through. + if(reason==6u){textureStore(outTex,vec2(gid.xy),vec4(0.0,0.0,0.0,1.0));} + else{textureStore(outTex,vec2(gid.xy),vec4(0.0));} + return; } + if(cls!=FIELD_ESCAPED){ + // Certified / likely interior is real image content, not a temporal hole. + // Keep it opaque so a reprojected history frame can never leak exterior + // colour into an already-known Mandelbrot interior pixel. + textureStore(outTex,vec2(gid.xy),vec4(0.0,0.0,0.0,1.0)); + return; + } + var c=escaped_color(m,fieldSmooth[i]); if(p.edgeAA!=0u){ - var boundary=false;var sum=linearize(c);var cnt=1.0; - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}} - if(boundary){c=delinearize(sum/cnt);} + let x=i32(gid.x);let y=i32(gid.y);var sum=linearize(c);var cnt=1.0; + for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){ + if(ox==0&&oy==0){continue;} + let xx=x+ox;let yy=y+oy; + if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;} + let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j]; + if(((mj>>28u)&3u)==FIELD_ESCAPED){sum+=linearize(escaped_color(mj,fieldSmooth[j]));cnt+=1.0;} + }} + c=delinearize(sum/cnt); } textureStore(outTex,vec2(gid.xy),vec4(c,1.0)); } @@ -788,35 +911,443 @@ fn srgb3(v:vec3)->vec3{return vec3(to_srgb(v.x),to_srgb(v.y),to_s @compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) gid:vec3){ let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;} - let q=vec2(gid.xy); - let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb); - textureStore(outTex,q,vec4(srgb3(sum*0.25),1.0)); + let q=vec2(gid.xy);let va=textureLoad(a,q,0);let vb=textureLoad(b,q,0);let vc=textureLoad(c,q,0);let vd=textureLoad(d,q,0); + let alpha=(va.a+vb.a+vc.a+vd.a)*0.25; + let premulLin=(lin3(va.rgb)*va.a+lin3(vb.rgb)*vb.a+lin3(vc.rgb)*vc.a+lin3(vd.rgb)*vd.a)*0.25; + if(alpha<=0.00001){textureStore(outTex,q,vec4(0.0));return;} + let straightLin=premulLin/alpha; + textureStore(outTex,q,vec4(srgb3(straightLin)*alpha,alpha)); } `; const PRESENT_WGSL=String.raw` -struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32}; +struct Params{ + frontScaleX:f32,frontScaleY:f32,frontOffsetX:f32,frontOffsetY:f32, + histScaleX:f32,histScaleY:f32,histOffsetX:f32,histOffsetY:f32, + historyEnabled:f32,_p0:f32,_p1:f32,_p2:f32, +}; @group(0) @binding(0) var samp:sampler; -@group(0) @binding(1) var tex:texture_2d; -@group(0) @binding(2) var p:Params; +@group(0) @binding(1) var frontTex:texture_2d; +@group(0) @binding(2) var historyTex:texture_2d; +@group(0) @binding(3) var p:Params; struct VSOut{@builtin(position) pos:vec4,@location(0) uv:vec2}; @vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{ var pos=array,3>(vec2(-1.0,-1.0),vec2(3.0,-1.0),vec2(-1.0,3.0)); var uv=array,3>(vec2(0.0,1.0),vec2(2.0,1.0),vec2(0.0,-1.0)); var o:VSOut;o.pos=vec4(pos[i],0.0,1.0);o.uv=uv[i];return o; } +fn mapped_uv(uv:vec2,scale:vec2,offset:vec2)->vec2{ + return vec2(0.5)+(uv-vec2(0.5))*scale+offset; +} +fn inside(uv:vec2)->bool{return all(uv>=vec2(0.0))&&all(uv<=vec2(1.0));} @fragment fn fs(in:VSOut)->@location(0) vec4{ - let uv=vec2(0.5)+(in.uv-vec2(0.5))*vec2(p.scaleX,p.scaleY)+vec2(p.offsetX,p.offsetY); - if(any(uv(0.0))||any(uv>vec2(1.0))){return vec4(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0); + let fuv=mapped_uv(in.uv,vec2(p.frontScaleX,p.frontScaleY),vec2(p.frontOffsetX,p.frontOffsetY)); + var front=vec4(0.0); + if(inside(fuv)){front=textureSampleLevel(frontTex,samp,fuv,0.0);} + if(p.historyEnabled<0.5||front.a>=0.999){return front;} + let huv=mapped_uv(in.uv,vec2(p.histScaleX,p.histScaleY),vec2(p.histOffsetX,p.histOffsetY)); + var hist=vec4(0.0); + if(inside(huv)){hist=textureSampleLevel(historyTex,samp,huv,0.0);} + return front+hist*(1.0-front.a); } `; -// v24.2.14 production screen path: defer UNKNOWN statistics out of the long -// perturbation kernel. The existing bucket histogram pass already scans the -// same field before sparse correction, so it can aggregate remaining/reason -// counters per workgroup without per-UNKNOWN global atomics in the primary. -// The counted primary is retained for export and verification paths. +const BLA_GPU_SHADOW_WGSL=COMMON+String.raw` +// Comparison-only BLA shadow kernel. It never writes visible Mandelbrot +// field/color/history resources. Each invocation computes one deterministic +// screen sample twice: (1) the existing FAST scaled perturbation recurrence, +// (2) the guarded BLA accelerator with checkpoint/replay. Any unsafe BLA +// candidate is rejected and replayed through the baseline recurrence. +struct ShadowParams{ + maxIter:u32, refLen:u32, sampleCount:u32, levelCount:u32, + fullW:f32, refPixelX:f32, refPixelY:f32, spanMantHi:f32, + spanExp:i32, safety:f32, _p0:f32, _p1:f32, +}; +struct RefPoint{ hi:vec2, lo:vec2 }; +struct BlaEntry{ + aHi:vec2, aLo:vec2, bHi:vec2, bLo:vec2, + q:f32, aAbs:f32, bAbs:f32, aRes:f32, + bRes:f32, skip:u32, start:u32, ok:u32, +}; +struct LevelMeta{ offset:u32, count:u32 }; +struct ShadowResult{ + baseCode:u32, baseIter:u32, baseSteps:u32, blaCode:u32, + blaIter:u32, blaSteps:u32, blaApps:u32, blaSkipped:u32, + blaRejected:u32, blaReplays:u32, replaySteps:u32, maxSkip:u32, + internalFailure:u32, _r0:u32, _r1:u32, _r2:u32, +}; +struct PState{ d:vec2, w:vec2, scaleExp:i32, n:u32 }; +struct AdvanceResult{ s:PState, code:u32, iter:u32, steps:u32 }; +struct BlaRunResult{ + code:u32, iter:u32, steps:u32, apps:u32, + skipped:u32, rejected:u32, replays:u32, replaySteps:u32, + maxSkip:u32, internalFailure:u32, +}; +@group(0) @binding(0) var p:ShadowParams; +@group(0) @binding(1) var refs:array; +@group(0) @binding(2) var blas:array; +@group(0) @binding(3) var levels:array; +@group(0) @binding(4) var samples:array>; +@group(0) @binding(5) var results:array; + +const CODE_MAX_ITER:u32=0u; +const CODE_ESCAPED:u32=1u; +const CODE_REFERENCE_END:u32=2u; +const CODE_RANGE:u32=3u; +const CODE_CONTINUE:u32=4u; +const F32_MAX:f32=bitcast(0x7f7fffffu); +const F32_MIN_NORMAL:f32=1.1754943508222875e-38; +const F32_MIN_SUB_BITS:u32=1u; +const F32_INF_BITS:u32=0x7f800000u; +const BAIL_GUARD:f32=1.75; + +fn finite_f(x:f32)->bool{return x==x && abs(x)<=F32_MAX;} +fn finite_v(v:vec2)->bool{return finite_f(v.x)&&finite_f(v.y);} +fn next_up_pos(x0:f32)->f32{ + let x=max(0.0,x0); + if(x!=x){return F32_MAX;} + if(x==0.0){return bitcast(F32_MIN_SUB_BITS);} + let u=bitcast(x); + if(u>=0x7f7fffffu){return F32_MAX;} + return bitcast(u+1u); +} +fn next_down_pos(x0:f32)->f32{ + let x=max(0.0,x0); + if(x!=x||x==0.0){return 0.0;} + let u=bitcast(x); + if(u<=F32_MIN_SUB_BITS){return 0.0;} + return bitcast(u-1u); +} +fn up_add(a:f32,b:f32)->f32{ + let s=a+b; + if(!finite_f(s)){return F32_MAX;} + return next_up_pos(s); +} +fn up_mul_scale(a:f32,b:f32,e:i32)->f32{ + if(!(a>0.0)&&!(b>0.0)){return 0.0;} + if(a<=0.0||b<=0.0){return 0.0;} + if(!finite_f(a)||!finite_f(b)){return F32_MAX;} + // WGSL frexp has indeterminate results for subnormal f32 inputs. + // Normalize any positive subnormal exactly by 2^24 before calling frexp. + var aa=a;var bb=b;var ee=e; + if(aa(F32_MIN_SUB_BITS);} + if(!finite_f(y)){return F32_MAX;} + return next_up_pos(y); +} +fn up_mul(a:f32,b:f32)->f32{return up_mul_scale(a,b,0);} +fn up_scale(a:f32,e:i32)->f32{ + if(a<=0.0){return 0.0;} + let y=ldexp(a,e); + if(y==0.0){return bitcast(F32_MIN_SUB_BITS);} + if(!finite_f(y)){return F32_MAX;} + return next_up_pos(y); +} +fn coef_mul(h:vec2,l:vec2,x:vec2)->vec2{ + return cmul(h,x)+cmul(l,x); +} +fn state_z(s:PState)->vec2{ + let rp=refs[min(s.n,p.refLen)]; + let delta=scaled_to_f32(s.w,s.scaleExp); + return rp.hi+(rp.lo+delta); +} +fn normalize_state(s0:PState)->PState{ + var s=s0; + var mm=max(maxabs(s.w),maxabs(s.d)); + var k=0u; + loop{ + if(!(mm>65536.0)||k>=24u){break;} + s.w*=0.0000152587890625; s.d*=0.0000152587890625; s.scaleExp+=16; k+=1u; + mm=max(maxabs(s.w),maxabs(s.d)); + } + k=0u; + loop{ + if(!(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp)||k>=24u){break;} + s.w*=65536.0; s.d*=65536.0; s.scaleExp-=16; k+=1u; + mm=max(maxabs(s.w),maxabs(s.d)); + } + return s; +} +fn advance_baseline(s0:PState,targetN:u32)->AdvanceResult{ + var s=s0; var steps=0u; + loop{ + if(s.n>=targetN||s.n>=p.maxIter){return AdvanceResult(s,CODE_CONTINUE,s.n,steps);} + let z=state_z(s); let mag=dot(z,z); + if(mag>4.0){return AdvanceResult(s,CODE_ESCAPED,s.n,steps);} + if(s.n>=p.refLen){return AdvanceResult(s,CODE_REFERENCE_END,s.n,steps);} + let r=refs[s.n]; + let linear=2.0*(cmul(r.hi,s.w)+cmul(r.lo,s.w)); + let sq=scaled_to_f32(cmul(s.w,s.w),s.scaleExp); + s.w=linear+sq+s.d; s.n+=1u; steps+=1u; + let mm=max(maxabs(s.w),maxabs(s.d)); + if(mm>=1.0e30||mm!=mm){return AdvanceResult(s,CODE_RANGE,s.n,steps);} + if(mm>65536.0){s.w*=0.0000152587890625;s.d*=0.0000152587890625;s.scaleExp+=16;} + else if(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp){s.w*=65536.0;s.d*=65536.0;s.scaleExp-=16;} + if(s.scaleExp>126){return AdvanceResult(s,CODE_RANGE,s.n,steps);} + } +} +fn run_baseline(g:vec2)->AdvanceResult{ + let dx=(g.x-p.refPixelX)/p.fullW; + let dy=(p.refPixelY-g.y)/p.fullW; + var s=PState(vec2(p.spanMantHi*dx,p.spanMantHi*dy),vec2(0.0),p.spanExp,0u); + let r=advance_baseline(s,p.maxIter); + if(r.code!=CODE_CONTINUE){return r;} + return AdvanceResult(r.s,CODE_MAX_ITER,p.maxIter,r.steps); +} +fn get_bla(n:u32,lev:u32)->BlaEntry{ + let lm=levels[lev]; + let j=(n-1u)>>lev; + return blas[lm.offset+j]; +} +fn has_bla(n:u32,lev:u32)->bool{ + if(n<1u||lev>=p.levelCount){return false;} + let skip=1u<>lev; + return jf32{ + // This is a LOWER bound on the admissible error budget. The 128-ulp + // shrink covers the few f32 operations before the final nextDown. + let denom=max(1.0,p.fullW)*max(1.0,f32(p.maxIter)); + let base=(abs(p.spanMantHi)*p.safety/denom)*(1.0-128.0*F32_U); + if(!(base>0.0)){return 0.0;} + let y=ldexp(base,p.spanExp-scaleExp); + if(!(y>0.0)||!finite_f(y)){return 0.0;} + return next_down_pos(y); +} +fn physical_error(errScaled:f32,s:PState,z:vec2)->f32{ + let propagated=up_scale(errScaled,s.scaleExp); + let delta=scaled_to_f32(s.w,s.scaleExp); + let recon=up_mul(8.0*F32_U,up_add(up_add(maxabs(z),maxabs(delta)),1.0e-30)); + return up_add(propagated,recon); +} +fn run_bla(g:vec2)->BlaRunResult{ + let dx=(g.x-p.refPixelX)/p.fullW; + let dy=(p.refPixelY-g.y)/p.fullW; + var s=PState(vec2(p.spanMantHi*dx,p.spanMantHi*dy),vec2(0.0),p.spanExp,0u); + var anchor=s; + var errScaled=0.0; + var steps=0u; var apps=0u; var skipped=0u; var rejected=0u; + var replays=0u; var replaySteps=0u; var maxSkip=0u; var internalFailure=0u; + loop{ + if(s.n>=p.maxIter){ + if(errScaled>0.0){ + let z=state_z(s); let e=physical_error(errScaled,s,z); + if(up_add(next_up_pos(length(z)),e)>=2.0){ + let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps; + if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + s=rr.s;errScaled=0.0;anchor=s; + } + } + return BlaRunResult(CODE_MAX_ITER,p.maxIter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure); + } + let z=state_z(s); let zUpper=next_up_pos(length(z)); + if(errScaled>0.0){ + let e=physical_error(errScaled,s,z); + if(up_add(zUpper,e)>=BAIL_GUARD){ + let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps; + if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + s=rr.s;errScaled=0.0;anchor=s;continue; + } + } + if(dot(z,z)>4.0){ + if(errScaled>0.0){ + let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps; + if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + s=rr.s;errScaled=0.0;anchor=s;continue; + } + return BlaRunResult(CODE_ESCAPED,s.n,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure); + } + if(s.n>=p.refLen){return BlaRunResult(CODE_REFERENCE_END,s.n,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + + var found=false; var chosen:BlaEntry; var chosenW=vec2(0.0); var chosenErr=0.0; + if(s.n>=1u&&p.levelCount>0u){ + let za=length(s.w); let ca=length(s.d); let budget=target_scaled(s.scaleExp); + var li=i32(p.levelCount)-1; + loop{ + if(li<0){break;} + let lev=u32(li); + if(has_bla(s.n,lev)){ + let b=get_bla(s.n,lev); + if(b.ok!=0u&&b.start==s.n&&b.skip>0u&&s.n+b.skip<=p.maxIter&&s.n+b.skip<=p.refLen&&budget>0.0){ + let quad=up_add(up_add(up_mul(za,za),up_mul(za,ca)),up_mul(ca,ca)); + let local=up_mul_scale(b.q,quad,s.scaleExp); + let carry=up_mul(b.aAbs,errScaled); + let coeffA=up_mul(up_mul(up_mul(b.aRes,za),4.0),F32_U); + let coeffB=up_mul(up_mul(up_mul(b.bRes,ca),4.0),F32_U); + let opMag=up_add(up_mul(b.aAbs,za),up_mul(b.bAbs,ca)); + let roundErr=up_mul(up_mul(64.0,F32_U),opMag); + let eOut=up_add(up_add(up_add(carry,local),up_add(coeffA,coeffB)),roundErr); + if(finite_f(eOut)&&eOut<=budget){ + let nw=coef_mul(b.aHi,b.aLo,s.w)+coef_mul(b.bHi,b.bLo,s.d); + if(finite_v(nw)){ + var trial=PState(s.d,nw,s.scaleExp,s.n+b.skip); + var trialErr=eOut; + var mm=max(maxabs(trial.w),maxabs(trial.d)); var rk=0u; + loop{ + if(!(mm>65536.0)||rk>=24u){break;} + trial.w*=0.0000152587890625;trial.d*=0.0000152587890625;trialErr*=0.0000152587890625;trial.scaleExp+=16;rk+=1u; + mm=max(maxabs(trial.w),maxabs(trial.d)); + } + rk=0u; + loop{ + if(!(mm>0.0&&mm<0.0000152587890625&&trial.scaleExp>p.spanExp)||rk>=24u){break;} + trial.w*=65536.0;trial.d*=65536.0;trialErr*=65536.0;trial.scaleExp-=16;rk+=1u; + mm=max(maxabs(trial.w),maxabs(trial.d)); + } + if(trial.scaleExp<=126&&finite_v(trial.w)&&finite_v(trial.d)&&finite_f(trialErr)){ + let ez=state_z(trial); let ee=physical_error(trialErr,trial,ez); + if(up_add(next_up_pos(length(ez)),ee)0.0){ + let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps; + if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + s=rr.s;errScaled=0.0;anchor=s;continue; + } + let rr=advance_baseline(s,s.n+1u);steps+=rr.steps; + if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + s=rr.s;anchor=s; + } +} +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid:vec3){ + let i=gid.x;if(i>=p.sampleCount){return;} + let g=samples[i]; + let b=run_baseline(g); let s=run_bla(g); + results[i]=ShadowResult(b.code,b.iter,b.steps,s.code,s.iter,s.steps,s.apps,s.skipped,s.rejected,s.replays,s.replaySteps,s.maxSkip,s.internalFailure,0u,0u,0u); +} +`; + + +// Canary stage: derived from the exact GPU-shadow BLA core. It does NOT +// replace the production FAST kernel. It recomputes a deterministic 1/64 +// pixel lattice, compares candidate metadata with the already completed +// baseline fieldMeta, and writes only to a private candidate buffer. +// A separate apply pass is allowed only after CPU readback proves zero +// mismatches / zero numeric UNKNOWN / zero internal failures. +const BLA_GPU_CANARY_WGSL=BLA_GPU_SHADOW_WGSL + .replace('@group(0) @binding(5) var results:array;', `@group(0) @binding(5) var candidateMeta:array; +@group(0) @binding(6) var sampleIndices:array; +@group(0) @binding(7) var baselineMeta:array; +@group(0) @binding(8) var canaryStats:array>;`) + .replace(`@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid:vec3){ + let i=gid.x;if(i>=p.sampleCount){return;} + let g=samples[i]; + let b=run_baseline(g); let s=run_bla(g); + results[i]=ShadowResult(b.code,b.iter,b.steps,s.code,s.iter,s.steps,s.apps,s.skipped,s.rejected,s.replays,s.replaySteps,s.maxSkip,s.internalFailure,0u,0u,0u); +}`, `fn canary_meta(code:u32,iter:u32)->u32{ + if(code==CODE_ESCAPED){return pack_meta(iter,FIELD_ESCAPED);} + if(code==CODE_MAX_ITER){return pack_unknown(p.maxIter,REASON_OPERATION_LIMIT);} + if(code==CODE_REFERENCE_END){return pack_unknown(iter,REASON_REFERENCE_END);} + if(code==CODE_RANGE){return pack_unknown(iter,REASON_RANGE);} + return pack_unknown(iter,REASON_ERROR_BOUND); +} +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid:vec3){ + let i=gid.x;if(i>=p.sampleCount){return;} + let s=run_bla(samples[i]); + let cm=canary_meta(s.code,s.iter); + let bm=baselineMeta[sampleIndices[i]]; + candidateMeta[i]=cm; + atomicAdd(&canaryStats[0],1u); + if(cm!=bm){atomicAdd(&canaryStats[1],1u);} + if(s.code==CODE_REFERENCE_END||s.code==CODE_RANGE){atomicAdd(&canaryStats[2],1u);} + if(s.internalFailure!=0u){atomicAdd(&canaryStats[3],s.internalFailure);} + atomicAdd(&canaryStats[4],s.apps); + atomicAdd(&canaryStats[5],s.skipped); + atomicAdd(&canaryStats[6],s.rejected); + atomicAdd(&canaryStats[7],s.replays); + atomicAdd(&canaryStats[8],s.replaySteps); + atomicMax(&canaryStats[9],s.maxSkip); +}`); +if(BLA_GPU_CANARY_WGSL===BLA_GPU_SHADOW_WGSL||!BLA_GPU_CANARY_WGSL.includes('candidateMeta'))throw new Error('BLA canary shader derivation failed'); + +const BLA_GPU_CANARY_APPLY_WGSL=String.raw` +struct Params{ count:u32,_p0:u32,_p1:u32,_p2:u32 }; +@group(0) @binding(0) var p:Params; +@group(0) @binding(1) var sampleIndices:array; +@group(0) @binding(2) var candidateMeta:array; +@group(0) @binding(3) var fieldMeta:array; +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid:vec3){ + let i=gid.x;if(i>=p.count){return;} + fieldMeta[sampleIndices[i]]=candidateMeta[i]; +} +`; + + +// Simplified production-candidate BLA kernel. Not connected to visible rendering in v24.2.52. +// Unsafe approximate paths restart the existing FAST recurrence instead of emitting UNKNOWN. +const BLA_GPU_FAST_CANDIDATE_WGSL=COMMON+String.raw` +struct Params{maxIter:u32,refLen:u32,sampleCount:u32,levelCount:u32,fullW:f32,refPixelX:f32,refPixelY:f32,spanMant:f32,spanExp:i32,safety:f32,_p0:f32,_p1:f32}; +struct RefPoint{hi:vec2,lo:vec2}; +struct Bla{aHi:vec2,aLo:vec2,bHi:vec2,bLo:vec2,q:f32,aAbs:f32,bAbs:f32,aRes:f32,bRes:f32,skip:u32,start:u32,ok:u32}; +struct Lev{offset:u32,count:u32};struct Result{packed:u32,smoothBits:u32,steps:u32,aux:u32};struct S{d:vec2,w:vec2,e:i32,n:u32}; +@group(0) @binding(0) var p:Params;@group(0) @binding(1) var refs:array;@group(0) @binding(2) var bt:array;@group(0) @binding(3) var lm:array;@group(0) @binding(4) var pts:array>;@group(0) @binding(5) var out:array; +const GUARD:f32=1.75;fn fin(x:f32)->bool{return x==x&&abs(x)<=bitcast(0x7f7fffffu);}fn fin2(v:vec2)->bool{return fin(v.x)&&fin(v.y);}fn cof(h:vec2,l:vec2,x:vec2)->vec2{return cmul(h,x)+cmul(l,x);}fn zof(s:S)->vec2{let r=refs[min(s.n,p.refLen)];return r.hi+(r.lo+scaled_to_f32(s.w,s.e));} +fn baseline(g:vec2,pre:u32)->Result{let dx=(g.x-p.refPixelX)/p.fullW;let dy=(p.refPixelY-g.y)/p.fullW;var s=S(vec2(p.spanMant*dx,p.spanMant*dy),vec2(0),p.spanExp,0u);var st=pre;loop{if(s.n>=p.maxIter){return Result(pack_unknown(p.maxIter,REASON_OPERATION_LIMIT),0u,st,1u);}let z=zof(s);let mag=dot(z,z);if(mag>4.0){return Result(pack_meta(s.n,FIELD_ESCAPED),bitcast(smooth_escape(s.n,mag)),st,1u);}if(s.n>=p.refLen){return Result(pack_unknown(s.n,REASON_REFERENCE_END),0u,st,1u);}let r=refs[s.n];let lin=2.0*(cmul(r.hi,s.w)+cmul(r.lo,s.w));let sq=scaled_to_f32(cmul(s.w,s.w),s.e);s.w=lin+sq+s.d;s.n++;st++;let mm=max(maxabs(s.w),maxabs(s.d));if(mm>=1e30||mm!=mm){return Result(pack_unknown(s.n,REASON_RANGE),0u,st,1u);}if(mm>65536.0){s.w/=65536.0;s.d/=65536.0;s.e+=16;}else if(mm>0.0&&mm<1.0/65536.0&&s.e>p.spanExp){s.w*=65536.0;s.d*=65536.0;s.e-=16;}if(s.e>126){return Result(pack_unknown(s.n,REASON_RANGE),0u,st,1u);}}} +fn has(n:u32,l:u32)->bool{if(n<1u||l>=p.levelCount){return false;}let sk=1u<>l)Bla{return bt[lm[l].offset+((n-1u)>>l)];} +fn budget(e:i32)->f32{let b=abs(p.spanMant)*p.safety/(max(1.0,p.fullW)*max(1.0,f32(p.maxIter)));if(!(b>0)){return 0;}let y=ldexp(b,p.spanExp-e);return select(0.0,y,y>0&&fin(y));}fn perr(er:f32,s:S,z:vec2)->f32{return abs(ldexp(er,s.e))+8.0*F32_U*(maxabs(z)+maxabs(scaled_to_f32(s.w,s.e))+1e-30);} +fn run(g:vec2)->Result{let dx=(g.x-p.refPixelX)/p.fullW;let dy=(p.refPixelY-g.y)/p.fullW;var s=S(vec2(p.spanMant*dx,p.spanMant*dy),vec2(0),p.spanExp,0u);var er=0.0;var ap=false;var st=0u;loop{if(s.n>=p.maxIter){if(ap&&length(zof(s))+perr(er,s,zof(s))>=2.0){return baseline(g,st);}return Result(pack_unknown(p.maxIter,REASON_OPERATION_LIMIT),0u,st,0u);}let z=zof(s);if(ap&&length(z)+perr(er,s,z)>=GUARD){return baseline(g,st);}let mag=dot(z,z);if(mag>4.0){if(ap){return baseline(g,st);}return Result(pack_meta(s.n,FIELD_ESCAPED),bitcast(smooth_escape(s.n,mag)),st,0u);}if(s.n>=p.refLen){return baseline(g,st);}var found=false;var ne=0.0;if(s.n>=1u){let za=length(s.w);let ca=length(s.d);let bd=budget(s.e);var li=i32(p.levelCount)-1;loop{if(li<0){break;}let l=u32(li);if(has(s.n,l)){let b=get_bla(s.n,l);if(b.ok!=0u&&b.start==s.n&&b.skip>0u&&s.n+b.skip<=p.maxIter&&s.n+b.skip<=p.refLen&&bd>0){let qd=za*za+za*ca+ca*ca;let local=abs(ldexp(b.q*qd,s.e));let eo=b.aAbs*er+local+b.aRes*za*4.0*F32_U+b.bRes*ca*4.0*F32_U;if(fin(eo)&&eo<=bd){let nw=cof(b.aHi,b.aLo,s.w)+cof(b.bHi,b.bLo,s.d);if(fin2(nw)){var t=S(s.d,nw,s.e,s.n+b.skip);var te=eo;var mm=max(maxabs(t.w),maxabs(t.d));var k=0u;loop{if(!(mm>65536.0)||k>=24u){break;}t.w/=65536.0;t.d/=65536.0;te/=65536.0;t.e+=16;k++;mm=max(maxabs(t.w),maxabs(t.d));}loop{if(!(mm>0.0&&mm<1.0/65536.0&&t.e>p.spanExp)||k>=48u){break;}t.w*=65536.0;t.d*=65536.0;te*=65536.0;t.e-=16;k++;mm=max(maxabs(t.w),maxabs(t.d));}if(t.e<=126&&fin2(t.w)&&fin2(t.d)&&fin(te)&&length(zof(t))+perr(te,t,zof(t))=1e30||mm!=mm){return baseline(g,st);}if(mm>65536.0){s.w/=65536.0;s.d/=65536.0;s.e+=16;}else if(mm>0.0&&mm<1.0/65536.0&&s.e>p.spanExp){s.w*=65536.0;s.d*=65536.0;s.e-=16;}if(s.e>126){return baseline(g,st);}}} +@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id:vec3){if(id.x p:Params;@group(0) @binding(1) var refs:array;@group(0) @binding(2) var bt:array;@group(0) @binding(3) var lm:array;@group(0) @binding(4) var pts:array>;@group(0) @binding(5) var out:array;', + '@group(0) @binding(0) var p:Params;@group(0) @binding(1) var refs:array;@group(0) @binding(2) var bt:array;@group(0) @binding(3) var lm:array;@group(0) @binding(4) var fieldMeta:array;@group(0) @binding(5) var fieldSmooth:array;@group(0) @binding(6) var unresolved:array>;' + ) + .replace( + '@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id:vec3){if(id.x(r.smoothBits); + let cls=(r.packed>>28u)&3u; + if(cls==FIELD_UNKNOWN){let reason=(r.packed>>20u)&255u;atomicAdd(&unresolved[0],1u);if(reason>=1u&&reason<=6u){atomicAdd(&unresolved[reason],1u);}} + } + @compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) gid:vec3){ + if(gid.x>=p.width||gid.y>=p.height){return;} + let g=vec2(f32(p.tileX+gid.x)+0.5,f32(p.tileY+gid.y)+0.5); + let i=p.outputBase+gid.y*p.outputStride+gid.x; + store_result(i,run(g)); + }` + ); +if(BLA_GPU_FAST_FRAME_WGSL===BLA_GPU_FAST_CANDIDATE_WGSL||!BLA_GPU_FAST_FRAME_WGSL.includes('store_result'))throw new Error('adaptive FAST BLA shader derivation failed'); + +// Screen path: collect UNKNOWN statistics in the bucket histogram pass rather +// than using per-UNKNOWN global atomics in the primary perturbation kernel. const DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u);\n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}'); const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL @@ -830,10 +1361,10 @@ const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL .replace('if(c>0u){atomicAdd(&bucketState[lane],c);}', `if(c>0u){atomicAdd(&bucketState[lane],c);} let r=atomicLoad(&localReasons[lane]); if(r>0u){atomicAdd(&unresolvedStats[lane],r);}`); -if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('v24.2.14 post-stats shader derivation failed'); +if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('post-stats shader derivation failed'); globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({ - version:'24.2.26-baseline-idle-refinement',DIRECT_F32_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_LIKELY_REFINE_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,LIKELY_QUEUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL + version:'24.2.55-no-black-hole-async-recovery',DIRECT_F32_WGSL,DIRECT_ACCURATE_SEED_WGSL,DIRECT_DS_CORRECT_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL,BLA_GPU_SHADOW_WGSL,BLA_GPU_CANARY_WGSL,BLA_GPU_CANARY_APPLY_WGSL,BLA_GPU_FAST_CANDIDATE_WGSL,BLA_GPU_FAST_FRAME_WGSL }); })(); @@ -843,14 +1374,14 @@ globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({ const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません'); const $=s=>document.querySelector(s),canvas=$('#view'); const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n; -const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000; +const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000,PALETTE_MAX=18,VALID_PALETTE_IDS=new Set([0,1,2,3,5,7,9,10,11,13,15,17,18]); const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20; const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity']; -const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96,REFINE_QUEUE_PARAM_BYTES=16,REFINE_STATS_BYTES=16,REFINE_BATCH_BYTES=16; -const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorCycleDir:1,colorShiftDir:1,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,refinementRunning:false,refinementStage:0,refinementQueue:0,refinedIter:0}; -let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,idleRefineTimer=0,lastWrittenHash='',navigationHash=''; +const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96; +const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,multiRefPasses:0,multiRefRecovered:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,qualityStage:'idle',qualityIter:0,qualityFinalIter:0,previewMs:0,refineMs:0,refinePending:false,temporalFill:false,numericRecoveryPending:false,numericRecoveryRunning:false,gpuStage:'',gpuTileRows:0}; +let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,refineTimer=0,lastWrittenHash='',navigationHash=''; const viewHistory=[];let viewHistoryIndex=-1; -const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,idleRefineBatches:0,idleRefineQueued:0}; +const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,multiRefBuilds:0,numericSubmits:0,correctionSubmits:0,gpuSyncWaits:0,blaShadowBuilds:0,blaShadowFailures:0,blaShadowLatest:null,blaGpuShadowRuns:0,blaGpuShadowFailures:0,blaGpuShadowLatest:null,blaGpuCanaryRuns:0,blaGpuCanaryFailures:0,blaGpuCanaryApplied:0,blaGpuCanaryLatest:null,blaFastProbeRuns:0,blaFastEnabledFrames:0,blaFastBaselineFrames:0,blaFastProbeLatest:null,blaFastFailures:0}; // ── exact fixed-point view state ───────────────────────────────────────── function one(bits=state.bits){return 1n<=4)} -function chooseBackend(snap=snapshot(),w=canvas.width){const deep=state.renderMode==='accurate',fastExtended=!deep&&fastNeedsExtended(snap,w);return{backend:deep?'deep':fastExtended?'fast-extended':'direct',deep,fastExtended,probe:false,reason:deep?'manual-accurate':fastExtended?'manual-fast-extended':'manual-fast'}} -function deepNeeded(){return state.renderMode==='accurate'} +function accurateHybridSafe(snap=snapshot(),w=canvas.width){return directPixelRatio(snap,w)>=32} +function chooseBackend(snap=snapshot(),w=canvas.width){const accurate=state.renderMode==='accurate',accurateHybrid=accurate&&accurateHybridSafe(snap,w),deep=accurate&&!accurateHybrid,fastExtended=!accurate&&fastNeedsExtended(snap,w);return{backend:accurateHybrid?'accurate-direct-ds':deep?'deep':fastExtended?'fast-extended':'direct',deep,accurateHybrid,fastExtended,probe:false,reason:accurateHybrid?'accurate-direct-ds-safe-spacing':deep?'manual-accurate-deep':fastExtended?'manual-fast-extended':'manual-fast'}} +function deepNeeded(snap=snapshot(),w=canvas.width){return state.renderMode==='accurate'} function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,renderMode:state.renderMode}} function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')} function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()} function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,renderMode:v.renderMode||state.renderMode});ensurePrecision();syncControls();saveHash(false);markDirty()} // ── iteration / quality policy ─────────────────────────────────────────── -function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))} -function effectiveColorCycle(iter=maxIter()){const base=Math.max(1,state.baseIter),effective=Math.max(base,Number(iter)||base);return state.cycle*base/effective} +function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),w=Math.max(1,canvas.width||innerWidth||1),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),q=state.processMode==='power'?.72:state.processMode==='fine'?1.25:state.processMode==='validate'?1.5:1,bonus=Math.floor(48*q*screenBits);return Math.min(12000,Math.max(state.baseIter,state.baseIter+bonus))} +function previewIter(finalIter=maxIter()){ + const z=Math.max(0,zoomExp()),m=state.processMode==='power'?.72:state.processMode==='fine'?1.28:state.processMode==='validate'?1.55:1; + // The old fixed 1100-iteration ceiling starved deep previews: around 10^12, + // finalIter is ~2700 while many visible boundary pixels need >1100 steps. + // Make the first pass track final quality as zoom deepens, with a mode-aware + // ceiling so responsiveness is still bounded on extreme views. + const legacy=Math.round(m*(420+24*z)); + const fractionBase=Math.min(.84,.48+.03*z); + const fraction=state.processMode==='power'?Math.min(.68,fractionBase*.82):state.processMode==='fine'?Math.min(.90,fractionBase+.04):state.processMode==='validate'?Math.min(.94,fractionBase+.08):fractionBase; + const deepFloor=Math.round(finalIter*fraction); + const cap=state.processMode==='power'?1800:state.processMode==='fine'?4800:state.processMode==='validate'?6000:3600; + return Math.min(finalIter,Math.max(state.baseIter,Math.min(cap,Math.max(legacy,deepFloor)))); +} +function canUseStableReprojection(){ + if(!renderer?.historyReady||!state.frameView)return false; + // Covers normal wheel/pinch, one 2x toolbar zoom, and ordinary pans while + // avoiding grotesque reuse after a reset or a distant shared-URL jump. + return reprojectionSafe(renderer.presentTransform(),2.25,.45); +} +function qualityStages(finalIter=maxIter()){ + // Once a completed nearby frame exists, temporal reprojection is the + // preview. Do not replace it with a lower-iteration numerical frame: that + // was the source of the stressful black patches during small zoom/pan. + // Accurate mode also skips its expensive low-iteration Deep pass entirely; + // the final Deep result is computed once and swapped in atomically. + if(state.renderMode==='accurate'||canUseStableReprojection())return[finalIter]; + const p=previewIter(finalIter); + return finalIter>p?[p,finalIter]:[p]; +} +function refinementDelay(stageIndex){ + // Full-quality latency matters more than leaving a long idle gap after the + // first preview. Keep a short interaction settling window only. + if(stageIndex<=1)return state.processMode==='validate'?300:state.processMode==='fine'?240:state.processMode==='power'?420:160; + return state.processMode==='validate'?240:state.processMode==='fine'?200:360; +} +function sameViewSnapshot(a,b){return !!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span} +function reprojectionSafe(t,maxZoom=1.30,maxShift=.18){const s=Math.abs(Number(t?.scaleX));if(!Number.isFinite(s)||s<=0)return false;const z=Math.max(s,1/s),shift=Math.hypot(Number(t?.offsetX)||0,Number(t?.offsetY)||0);return z<=maxZoom&&shift<=maxShift} + function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))} function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)} +function fract01(x){return x-Math.floor(x)} +function colorStyleSnapshot(){return{palette:state.palette,cycle:state.cycle,shift:state.shift,hq:state.hq}} +function colorStyleKey(style=colorStyleSnapshot()){return[style.palette,style.cycle,style.shift,style.hq?1:0].join(':')} function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)} function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}} @@ -915,7 +1487,7 @@ self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=perfor class ReferenceService{ constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16} ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1,fresh=false){const key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,'guarded-perturb-v24.2.26'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} + request(snap,iter,width=1,height=1,fresh=false){const key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,'guarded-perturb-v24.2.35'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} } @@ -938,12 +1510,115 @@ self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=perfor class FastReferenceService{ constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=12} ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([fastReferenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'fast reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.2.26'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} + request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.2.35'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} destroy(){this.cancelPending('destroyed');this.cache.clear()} } const fastRefs=new FastReferenceService(); +// ── BLA shadow laboratory ──────────────────────────────────────────────── +// Safety rule for v24.2.49: BLA never participates in visible rendering. +// A separate worker builds a real hierarchical BLA table from the exact same +// packed reference orbit used by the existing perturbation kernel and compares +// its result against baseline perturbation on deterministic screen samples. +// Any mismatch therefore blocks promotion instead of creating a black/UNKNOWN +// pixel or changing the image. +function blaShadowWorkerSource(){return String.raw` +'use strict'; +function cabs(ar,ai){return Math.hypot(ar,ai)} +function cmul(ar,ai,br,bi){return[ar*br-ai*bi,ar*bi+ai*br]} +function cadd(ar,ai,br,bi){return[ar+br,ai+bi]} +function finite2(a,b){return Number.isFinite(a)&&Number.isFinite(b)} +function ldexp(v,e){if(!Number.isFinite(v)||v===0)return v;return v*Math.pow(2,e)} +function unpackRefs(buf,refLen){const dv=new DataView(buf),rr=new Float64Array(refLen+1),ri=new Float64Array(refLen+1);for(let i=0;i<=refLen;i++){const o=i*16;rr[i]=dv.getFloat32(o,true)+dv.getFloat32(o+8,true);ri[i]=dv.getFloat32(o+4,true)+dv.getFloat32(o+12,true)}return{rr,ri}} +function item(start,skip,ar,ai,br,bi,dr,di,er,ei,fr,fi){return{start,skip,ar,ai,br,bi,dr,di,er,ei,fr,fi,q:Math.max(cabs(dr,di),.5*cabs(er,ei),cabs(fr,fi))}} +function buildTable(rr,ri,refLen){const levels=[];let l0=[];for(let m=1;m0)u[0]++;else u[0]--;return a[0]} +function upF32(x){if(!(x>=0)||!Number.isFinite(x))return Math.fround(x);let y=Math.fround(x);if(y=b.q&&aAbs>=cabs(b.ar,b.ai)&&bAbs>=cabs(b.br,b.bi);const o=n*stride;dv.setFloat32(o,arh,true);dv.setFloat32(o+4,aih,true);dv.setFloat32(o+8,arl,true);dv.setFloat32(o+12,ail,true);dv.setFloat32(o+16,brh,true);dv.setFloat32(o+20,bih,true);dv.setFloat32(o+24,brl,true);dv.setFloat32(o+28,bil,true);dv.setFloat32(o+32,q,true);dv.setFloat32(o+36,aAbs,true);dv.setFloat32(o+40,bAbs,true);dv.setFloat32(o+44,aRes,true);dv.setFloat32(o+48,bRes,true);dv.setUint32(o+52,b.skip,true);dv.setUint32(o+56,b.start,true);dv.setUint32(o+60,ok?1:0,true);n++}}return{buffer:buf,offsets,counts,total,stride}} +function at(levels,n,d){const skip=1<>d,lev=levels[d];return lev&&j4)return{cls:1,iter:n,wr,wi,physical};if(n>=refLen)return{cls:0,iter:n,wr,wi,physical};const lr=2*(rr[n]*wr-ri[n]*wi),li=2*(rr[n]*wi+ri[n]*wr),sq=[wr*wr-wi*wi,2*wr*wi];wr=lr+sq[0]+dcr;wi=li+sq[1]+dci;n++;physical++}return{cls:0,iter:maxIter,wr,wi,physical}} +function shadow(rr,ri,levels,refLen,dcr,dci,maxIter,pixelSpacing,safety,allowBla=true){ + let wr=0,wi=0,n=0,physical=0,apps=0,skipped=0,rejected=0,maxSkip=0,replaySteps=0,replays=0,err=0; + let anchorWr=0,anchorWi=0,anchorN=0; + const ca=cabs(dcr,dci),L=Math.max(1,maxIter),BAIL_GUARD=1.75; + function replayTo(target){ + wr=anchorWr;wi=anchorWi;n=anchorN;err=0;replays++; + while(n4)return{escaped:true,iter:n}; + const lr=2*(rr[n]*wr-ri[n]*wi),li=2*(rr[n]*wi+ri[n]*wr),sqR=wr*wr-wi*wi,sqI=2*wr*wi; + wr=lr+sqR+dcr;wi=li+sqI+dci;n++;physical++;replaySteps++; + } + anchorWr=wr;anchorWi=wi;anchorN=n; + return{escaped:false}; + } + while(n0&&zmag+err>=BAIL_GUARD){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};continue;} + if(zr*zr+zi*zi>4){if(err>0){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};continue;}return{cls:1,iter:n,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};} + if(n>=refLen)return{cls:0,iter:n,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays}; + let chosen=null; + if(n>=1&&allowBla){ + const za=cabs(wr,wi); + for(let d=levels.length-1;d>=0;d--){ + const b=at(levels,n,d);if(!b||n+b.skip>Math.min(maxIter,refLen)||!Number.isFinite(b.q))continue; + const local=b.q*(za*za+za*ca+ca*ca),budget=cabs(b.br,b.bi)*pixelSpacing/L*safety,eOut=cabs(b.ar,b.ai)*err+local; + if(!(Number.isFinite(eOut)&&eOut<=budget)){rejected++;continue;} + const az=cmul(b.ar,b.ai,wr,wi),bc=cmul(b.br,b.bi,dcr,dci),nw=cadd(az[0],az[1],bc[0],bc[1]),end=n+b.skip,ezr=rr[end]+nw[0],ezi=ri[end]+nw[1]; + if(cabs(ezr,ezi)+eOut>=BAIL_GUARD){rejected++;continue;} + chosen={b,nw,eOut};break; + } + } + if(chosen){wr=chosen.nw[0];wi=chosen.nw[1];err=chosen.eOut;n+=chosen.b.skip;physical++;apps++;skipped+=chosen.b.skip-1;if(chosen.b.skip>maxSkip)maxSkip=chosen.b.skip;continue;} + // A normal perturbation step is an exact checkpoint only if no earlier BLA + // uncertainty remains. Otherwise replay first, then continue baseline. + if(err>0){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};continue;} + const lr=2*(rr[n]*wr-ri[n]*wi),li=2*(rr[n]*wi+ri[n]*wr),sqR=wr*wr-wi*wi,sqI=2*wr*wi; + wr=lr+sqR+dcr;wi=li+sqI+dci;n++;physical++;anchorWr=wr;anchorWi=wi;anchorN=n; + } + if(err>0&&cabs(rr[Math.min(n,refLen)]+wr,ri[Math.min(n,refLen)]+wi)+err>=2){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};} + return{cls:0,iter:maxIter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays}; +} +function samplePoints(w,h,count){const side=Math.max(3,Math.ceil(Math.sqrt(Math.max(1,count)))),out=[];for(let j=0;j=count)break;const x=(i+.5)*w/side,y=(j+.5)*h/side;out.push([x,y])}return out} +self.onmessage=e=>{const d=e.data;if(!d||(d.type!=='validate'&&d.type!=='build'))return;const t0=performance.now();try{const {rr,ri}=unpackRefs(d.refs,d.refLen),tb0=performance.now(),levels=buildTable(rr,ri,d.refLen),packed=flattenTable(levels),tableBuildMs=performance.now()-tb0,span=ldexp(d.spanMant,d.spanExp),pixelSpacing=Math.abs(span)/Math.max(1,d.width),buildOnly=d.type==='build',pts=buildOnly?[]:samplePoints(d.width,d.height,d.samples||49),referenceFull=d.refLen>=d.maxIter;let classMismatch=0,escapeIterationMismatch=0,logical=0,physical=0,apps=0,skipped=0,rejected=0,maxSkip=0,maxEndpointDelta=0,replaySteps=0,replays=0;for(const [x,y] of pts){const dcr=(x-d.refPixelX)*span/d.width,dci=(d.refPixelY-y)*span/d.width,b=baseline(rr,ri,d.refLen,dcr,dci,d.maxIter),sh=shadow(rr,ri,levels,d.refLen,dcr,dci,d.maxIter,pixelSpacing,d.safety||0.015625,referenceFull);logical+=b.physical;physical+=sh.physical;apps+=sh.apps;skipped+=sh.skipped;rejected+=sh.rejected;replaySteps+=sh.replaySteps||0;replays+=sh.replays||0;maxSkip=Math.max(maxSkip,sh.maxSkip);if(b.cls!==sh.cls)classMismatch++;if(b.cls===1&&sh.cls===1&&b.iter!==sh.iter)escapeIterationMismatch++;if(b.cls===sh.cls&&b.iter===sh.iter){const de=Math.hypot(b.wr-sh.wr,b.wi-sh.wi);if(Number.isFinite(de))maxEndpointDelta=Math.max(maxEndpointDelta,de)}}const eligibleForBla=referenceFull&&span!==0,gatePassed=eligibleForBla&&(buildOnly||(classMismatch===0&&escapeIterationMismatch===0&&!Number.isNaN(maxEndpointDelta)));const stats={samples:pts.length,classMismatch,escapeIterationMismatch,logicalIterations:logical,physicalSteps:physical,blaApplications:apps,blaSkippedIterations:skipped,blaRejected:rejected,blaReplaySteps:replaySteps,blaReplays:replays,maxSkip,skipRate:logical?Math.max(0,(logical-physical)/logical):0,candidateSkipRate:logical?skipped/logical:0,maxEndpointDelta,tableLevels:levels.length,tableEntries:packed.total,tableStride:packed.stride,gpuPacking:'A/B hi+lo; bounds upward-rounded',unsafeRawF32Rejected:true,gpuShadowEligible:gatePassed,tableBuildMs,totalMs:performance.now()-t0,spanUnderflow:span===0,referenceFull,eligibleForBla,buildOnly,productionEligible:false,gatePassed};postMessage({type:'validated',id:d.id,key:d.key,stats,table:packed.buffer,levelOffsets:packed.offsets,levelCounts:packed.counts},[packed.buffer])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} +`} +class BlaShadowService{ + constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.latest=null;this.failed=false} + ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([blaShadowWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error'){runtime.blaShadowFailures++;p.reject(new Error(d.error));return}const result={...d,source:p.source,createdAt:performance.now()};this.latest=result;runtime.blaShadowLatest=result.stats;runtime.blaShadowBuilds++;p.resolve(result);updateStats()};this.worker.onerror=e=>{this.failed=true;runtime.blaShadowFailures++;for(const p of this.pending.values())p.reject(new Error(e.message||'BLA shadow worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} + request({ctx,snap,iter,width,height,refPixel,samples=49,safety=1/64}){if(!ctx?.refs||!ctx.refLen)return Promise.reject(new Error('BLA shadow requires perturbation reference'));if(!this.ensure())return Promise.reject(new Error('BLA shadow Workerを作成できません'));if(this.pending.size)this.cancelPending('superseded BLA shadow request');const id=++this.serial,se=spanMantExp(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,ctx.refLen,samples,safety].join(':');const copy=ctx.refs.slice(0),source={bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y};return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source});this.worker.postMessage({type:'validate',id,key,refs:copy,refLen:ctx.refLen,maxIter:iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y,spanMant:se.mant,spanExp:se.exp,samples,safety},[copy])})} + requestBuild({ctx,snap,iter,width,height,refPixel,safety=1/64}){if(!ctx?.refs||!ctx.refLen)return Promise.reject(new Error('BLA build requires perturbation reference'));if(!this.ensure())return Promise.reject(new Error('BLA Workerを作成できません'));if(this.pending.size)this.cancelPending('superseded BLA build request');const id=++this.serial,se=spanMantExp(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,ctx.refLen,'build',safety].join(':'),copy=ctx.refs.slice(0),source={bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y};return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source});this.worker.postMessage({type:'build',id,key,refs:copy,refLen:ctx.refLen,maxIter:iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y,spanMant:se.mant,spanExp:se.exp,safety},[copy])})} + cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear()} + destroy(){this.cancelPending('destroyed');if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}this.latest=null} +} +const blaShadow=new BlaShadowService();let blaShadowTimer=0,numericRecoveryTimer=0; +function blaSamplePoints(w,h,count){const side=Math.max(3,Math.ceil(Math.sqrt(Math.max(1,count)))),out=[];for(let j=0;j=count)break;out.push([(i+.5)*w/side,(j+.5)*h/side])}return out} +function blaCanaryGrid(w,h){const points=[],indices=[];for(let y=5;y{blaShadowTimer=0;if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot()))return; + const gpuGateRun=kind==='fast'&&state.processMode==='validate',samples=gpuGateRun?289:49; + blaShadow.request({ctx,snap,iter,width:canvas.width,height:canvas.height,refPixel,samples,safety:1/64}).then(async result=>{ + if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot()))return; + if(gpuGateRun&&result.stats.gpuShadowEligible&&renderer){ + const gpu=await renderer.runBlaGpuShadow({shadow:result,ctx,snap,iter,refPixel,safety:1/64}); + if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot()))return; + if(gpu?.gatePassed&&renderer.blaGpuGate?.armed){await renderer.runBlaGpuCanary({shadow:result,ctx,snap,iter,refPixel,safety:1/64,token});} + } + }).catch(e=>{if(!/cancelled|superseded/i.test(String(e&&e.message||e))){runtime.blaShadowFailures++;console.warn('BLA shadow validation skipped:',e)}}) + },700) +} + // ── WebGPU renderer ────────────────────────────────────────────────────── function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})} function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}} @@ -955,18 +1630,191 @@ class WebGpuRenderer{ this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''}; this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)}; this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat(); - this.frame=null;this.deepCtx=null;this.fastCtx=null;this.refineDeepCtx=null;this.exportWs=null;this.refinePipelinePromise=null;this.likelyQueue=null;this.deepLikelyRefine=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'}); + this.frame=null;this.deepCtx=null;this.fastCtx=null;this.blaFastCtx=null;this.exportWs=null;this.blaFastCandidatePipeline=null;this.blaFastFramePipeline=null;this.blaFastCompilePromise=null;this.blaFastWarmScheduled=false;this.blaFastDisabled=false;this.blaGpuShadowPipeline=null;this.blaGpuShadowCompilePromise=null;this.blaGpuShadowDisabled=false;this.blaGpuCanaryPipeline=null;this.blaGpuCanaryApplyPipeline=null;this.blaGpuCanaryCompilePromise=null;this.blaGpuCanaryDisabled=false;this.blaGpuGate={consecutive:0,totalSamples:0,distinctViews:0,viewKeys:[],armed:false,lastReason:'not-tested'};this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.activeGpuStage='';this.lastCompletedGpuStage='';this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false;this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});this.reprojectSampler=device.createSampler({magFilter:'nearest',minFilter:'nearest'}); device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)}); this.ready=this.initPipelines(); - device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()}); + device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;const where=this.activeGpuStage||this.lastCompletedGpuStage||'unknown-stage';state.gpuStage=where;state.gpuError='device lost @ '+where+': '+this.lossReason;if(renderer===this){renderer=null;markDirty(false);initRenderer()}}); } - configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})} - async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m} + configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'premultiplied'})} + async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>'line '+x.lineNum+':'+x.linePos+' '+x.message).join('\n'))}return m} + resetBlaGpuGate(reason='reset'){ + this.blaGpuGate={consecutive:0,totalSamples:0,distinctViews:0,viewKeys:[],armed:false,lastReason:reason}; + runtime.blaGpuCanaryLatest={gateArmed:false,reason}; + } + updateBlaGpuGate(stats,viewKey=''){ + const pass=!!(stats&&stats.gatePassed&&stats.exactMismatch===0&&stats.blaDerivedUnknown===0&&stats.internalFailure===0&&stats.samples>=289); + if(!pass){this.resetBlaGpuGate(stats?.error||'gpu-shadow-mismatch');return false} + this.blaGpuGate.consecutive++;this.blaGpuGate.totalSamples+=stats.samples;this.blaGpuGate.lastReason='gpu-shadow-pass'; + if(viewKey&&!this.blaGpuGate.viewKeys.includes(viewKey)){if(this.blaGpuGate.viewKeys.length<8)this.blaGpuGate.viewKeys.push(viewKey);this.blaGpuGate.distinctViews=this.blaGpuGate.viewKeys.length} + this.blaGpuGate.armed=this.blaGpuGate.consecutive>=3&&this.blaGpuGate.totalSamples>=867&&this.blaGpuGate.distinctViews>=3; + return this.blaGpuGate.armed; + } + warmBlaFastPipelines(){ + if(this.blaFastCandidatePipeline&&this.blaFastFramePipeline)return Promise.resolve({probe:this.blaFastCandidatePipeline,frame:this.blaFastFramePipeline}); + if(this.blaFastDisabled)return Promise.resolve(null); + if(this.blaFastCompilePromise)return this.blaFastCompilePromise; + this.blaFastCompilePromise=(async()=>{ + let scope=false; + try{ + if(this.device.pushErrorScope){this.device.pushErrorScope('validation');scope=true} + const [pm,fm]=await Promise.all([this.module('bla-fast-probe',G.BLA_GPU_FAST_CANDIDATE_WGSL),this.module('bla-fast-frame',G.BLA_GPU_FAST_FRAME_WGSL)]); + const make=async module=>this.device.createComputePipelineAsync?this.device.createComputePipelineAsync({layout:'auto',compute:{module,entryPoint:'main'}}):this.device.createComputePipeline({layout:'auto',compute:{module,entryPoint:'main'}}); + const [probe,frame]=await Promise.all([make(pm),make(fm)]); + if(scope&&this.device.popErrorScope){const e=await this.device.popErrorScope();scope=false;if(e)throw e} + this.blaFastCandidatePipeline=probe;this.blaFastFramePipeline=frame;return{probe,frame}; + }catch(e){ + try{if(scope&&this.device.popErrorScope)await this.device.popErrorScope()}catch{} + this.blaFastDisabled=true;runtime.blaFastFailures++;runtime.blaFastProbeLatest={enabled:false,reason:'compile-failed',error:String(e&&e.message||e)};console.warn('Adaptive FAST BLA disabled:',e);updateStats();return null; + }finally{this.blaFastCompilePromise=null} + })(); + return this.blaFastCompilePromise; + } + scheduleBlaFastWarmup(){if(this.blaFastWarmScheduled||this.blaFastCandidatePipeline||this.blaFastCompilePromise||this.blaFastDisabled)return;this.blaFastWarmScheduled=true;const run=()=>{this.blaFastWarmScheduled=false;if(renderer===this&&!state.rendering&&!this.blaFastCandidatePipeline&&!this.blaFastDisabled)this.warmBlaFastPipelines()};if(typeof requestIdleCallback==='function')requestIdleCallback(run,{timeout:2500});else setTimeout(run,1200)} + destroyBlaFastContext(){if(!this.blaFastCtx)return;destroy(this.blaFastCtx.tableB);destroy(this.blaFastCtx.levelsB);this.blaFastCtx=null} + setBlaFastContext(shadow){ + const key=shadow?.key||'';if(this.blaFastCtx&&this.blaFastCtx.key===key)return this.blaFastCtx;this.destroyBlaFastContext(); + const levelCount=Math.min(20,shadow?.levelOffsets?.length||0);if(!levelCount||!shadow?.table?.byteLength)return null; + const levelData=new Uint32Array(levelCount*2);for(let i=0;i>>0;levelData[2*i+1]=shadow.levelCounts[i]>>>0} + const d=this.device,B=GPUBufferUsage,tableB=buf(d,shadow.table.byteLength,B.STORAGE|B.COPY_DST,'bla-fast-table'),levelsB=buf(d,levelData.byteLength,B.STORAGE|B.COPY_DST,'bla-fast-levels');d.queue.writeBuffer(tableB,0,shadow.table);d.queue.writeBuffer(levelsB,0,levelData); + this.blaFastCtx={key,tableB,levelsB,levelCount,tableBytes:shadow.table.byteLength};return this.blaFastCtx; + } + blaFastProbeParams(sampleCount,iter,snap,refPixel,safety=1/64){const se=spanMantExp(snap);return writeU32F32(48,d=>{d.setUint32(0,iter,true);d.setUint32(4,this.fastCtx.refLen,true);d.setUint32(8,sampleCount,true);d.setUint32(12,this.blaFastCtx.levelCount,true);d.setFloat32(16,canvas.width,true);d.setFloat32(20,Math.fround(refPixel.x),true);d.setFloat32(24,Math.fround(refPixel.y),true);d.setFloat32(28,Math.fround(se.mant),true);d.setInt32(32,se.exp,true);d.setFloat32(36,Math.fround(safety),true)})} + blaFastFrameParams(w,h,tileX,tileY,iter,snap,refPixel,outputStride,outputBase,safety=1/64){const se=spanMantExp(snap);return writeU32F32(80,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,tileX,true);d.setUint32(12,tileY,true);d.setUint32(16,iter,true);d.setUint32(20,this.fastCtx.refLen,true);d.setUint32(24,this.blaFastCtx.levelCount,true);d.setUint32(28,outputStride,true);d.setUint32(32,outputBase,true);d.setFloat32(48,canvas.width,true);d.setFloat32(52,Math.fround(refPixel.x),true);d.setFloat32(56,Math.fround(refPixel.y),true);d.setFloat32(60,Math.fround(se.mant),true);d.setInt32(64,se.exp,true);d.setFloat32(68,Math.fround(safety),true)})} + async runBlaFastProbe({shadow,snap,iter,refPixel,safety=1/64,samples=64}){ + if(this.blaFastDisabled||!this.blaFastCandidatePipeline)return{enabled:false,reason:'pipeline-not-ready'}; + if(!this.fastCtx||this.fastCtx.refLen>>28)&3,it=packed&ITER_MASK;if(aux)fallback++;if(cls===FIELD_UNKNOWN)unknown++;logical+=Math.max(1,it);physical+=steps}const fallbackRate=fallback/n,operationReduction=logical?1-physical/logical:0,enabled=fallbackRate<=0.125&&operationReduction>=0.5;const out={enabled,reason:enabled?'probe-pass':'probe-cost',samples:n,fallback,fallbackRate,logicalSteps:logical,physicalSteps:physical,operationReduction,gpuMs,unknown,tableBytes:bc.tableBytes};runtime.blaFastProbeRuns++;runtime.blaFastProbeLatest=out;updateStats();return out; + }finally{if(mapped)try{readB.unmap()}catch{};[pbuf,ptsB,outB,readB].forEach(destroy)} + } + encodeBlaFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.blaFastFramePipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:this.blaFastCtx.tableB}},{binding:3,resource:{buffer:this.blaFastCtx.levelsB}},{binding:4,resource:{buffer:meta}},{binding:5,resource:{buffer:smooth}},{binding:6,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass({label:'adaptive-bla-fast'});pass.setPipeline(this.blaFastFramePipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} + async ensureBlaGpuCanaryPipelines(){ + if(this.blaGpuCanaryPipeline&&this.blaGpuCanaryApplyPipeline)return{compare:this.blaGpuCanaryPipeline,apply:this.blaGpuCanaryApplyPipeline}; + if(this.blaGpuCanaryDisabled||!this.blaGpuGate.armed)return null; + if(this.blaGpuCanaryCompilePromise)return this.blaGpuCanaryCompilePromise; + this.blaGpuCanaryCompilePromise=(async()=>{ + let scope=false; + try{ + if(this.device.pushErrorScope){this.device.pushErrorScope('validation');scope=true} + const [cm,am]=await Promise.all([this.module('bla-gpu-canary-compare',G.BLA_GPU_CANARY_WGSL),this.module('bla-gpu-canary-apply',G.BLA_GPU_CANARY_APPLY_WGSL)]); + const compare=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}}),apply=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}}); + if(scope&&this.device.popErrorScope){const e=await this.device.popErrorScope();scope=false;if(e)throw e} + this.blaGpuCanaryPipeline=compare;this.blaGpuCanaryApplyPipeline=apply;return{compare,apply}; + }catch(e){ + try{if(scope&&this.device.popErrorScope)await this.device.popErrorScope()}catch{} + this.blaGpuCanaryDisabled=true;this.resetBlaGpuGate('canary-compile-failed');runtime.blaGpuCanaryFailures++;runtime.blaGpuCanaryLatest={gateArmed:false,compileFailed:true,error:String(e&&e.message||e)};console.warn('BLA GPU canary disabled:',e);updateStats();return null; + }finally{this.blaGpuCanaryCompilePromise=null} + })(); + return this.blaGpuCanaryCompilePromise; + } + async ensureBlaGpuShadowPipeline(){ + if(this.blaGpuShadowPipeline)return this.blaGpuShadowPipeline; + if(this.blaGpuShadowDisabled)return null; + if(this.blaGpuShadowCompilePromise)return this.blaGpuShadowCompilePromise; + this.blaGpuShadowCompilePromise=(async()=>{ + try{ + this.device.pushErrorScope?.('validation'); + const m=await this.module('bla-gpu-shadow',G.BLA_GPU_SHADOW_WGSL); + const pipe=this.device.createComputePipeline({layout:'auto',compute:{module:m,entryPoint:'main'}}); + if(this.device.popErrorScope){const e=await this.device.popErrorScope();if(e)throw e} + this.blaGpuShadowPipeline=pipe;return pipe; + }catch(e){ + try{if(this.device.popErrorScope)await this.device.popErrorScope()}catch{} + this.blaGpuShadowDisabled=true;runtime.blaGpuShadowFailures++; + runtime.blaGpuShadowLatest={gatePassed:false,compileFailed:true,error:String(e&&e.message||e),productionEligible:false}; + console.warn('BLA GPU shadow disabled:',e);updateStats();return null; + }finally{this.blaGpuShadowCompilePromise=null} + })(); + return this.blaGpuShadowCompilePromise; + } + async runBlaGpuShadow({shadow,ctx,snap,iter,refPixel,safety=1/64}){ + await this.ready; + if(!shadow?.stats?.gatePassed||!shadow?.stats?.gpuShadowEligible)return{skipped:true,reason:'cpu-gate'}; + if(!ctx?.refs||ctx.refLen>>0;levelData[2*i+1]=shadow.levelCounts[i]>>>0;} + const B=GPUBufferUsage,d=this.device,resultStride=64,resultBytes=resultStride*sampleCount; + const pbuf=buf(d,48,B.UNIFORM|B.COPY_DST,'bla-shadow-params'),refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-reference'),tableB=buf(d,shadow.table.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-table'),levelsB=buf(d,levelData.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-levels'),samplesB=buf(d,sampleData.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-samples'),resultsB=buf(d,resultBytes,B.STORAGE|B.COPY_SRC,'bla-shadow-results'),readB=buf(d,resultBytes,B.COPY_DST|B.MAP_READ,'bla-shadow-readback'); + const se=spanMantExp(snap),mantHi=Math.fround(se.mant),params=writeU32F32(48,dv=>{dv.setUint32(0,iter,true);dv.setUint32(4,ctx.refLen,true);dv.setUint32(8,sampleCount,true);dv.setUint32(12,levelCount,true);dv.setFloat32(16,canvas.width,true);dv.setFloat32(20,Math.fround(refPixel.x),true);dv.setFloat32(24,Math.fround(refPixel.y),true);dv.setFloat32(28,mantHi,true);dv.setInt32(32,se.exp,true);dv.setFloat32(36,Math.fround(safety),true)}); + let mapped=false,scopePushed=false; + try{ + d.queue.writeBuffer(pbuf,0,params);d.queue.writeBuffer(refsB,0,ctx.refs);d.queue.writeBuffer(tableB,0,shadow.table);d.queue.writeBuffer(levelsB,0,levelData);d.queue.writeBuffer(samplesB,0,sampleData); + if(d.pushErrorScope){d.pushErrorScope('validation');scopePushed=true;}this.activeGpuStage='bla-shadow-comparison'; + const bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:refsB}},{binding:2,resource:{buffer:tableB}},{binding:3,resource:{buffer:levelsB}},{binding:4,resource:{buffer:samplesB}},{binding:5,resource:{buffer:resultsB}}]}),enc=d.createCommandEncoder({label:'bla-shadow-comparison'}),pass=enc.beginComputePass({label:'bla-shadow-comparison'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(sampleCount/64));pass.end();enc.copyBufferToBuffer(resultsB,0,readB,0,resultBytes);d.queue.submit([enc.finish()]);await d.queue.onSubmittedWorkDone();this.lastCompletedGpuStage='bla-shadow-comparison';this.activeGpuStage=''; + if(scopePushed&&d.popErrorScope){const e=await d.popErrorScope();scopePushed=false;if(e)throw e} + await readB.mapAsync(GPUMapMode.READ);mapped=true;const raw=new Uint32Array(readB.getMappedRange().slice(0)); + let classMismatch=0,escapeIterationMismatch=0,exactMismatch=0,blaDerivedUnknown=0,internalFailure=0,baseSteps=0,blaSteps=0,apps=0,skipped=0,rejected=0,replays=0,replaySteps=0,maxSkip=0; + const rows=[]; + for(let i=0;i>>0} + const levelCount=Math.min(20,shadow.levelOffsets?.length||0),levelData=new Uint32Array(Math.max(1,levelCount)*2); + for(let i=0;i>>0;levelData[2*i+1]=shadow.levelCounts[i]>>>0} + const d=this.device,B=GPUBufferUsage,statsBytes=64; + const pbuf=buf(d,48,B.UNIFORM|B.COPY_DST,'bla-canary-params'),refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-reference'),tableB=buf(d,shadow.table.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-table'),levelsB=buf(d,levelData.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-levels'),samplesB=buf(d,sampleData.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-samples'),indicesB=buf(d,indexData.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-indices'),candidateB=buf(d,sampleCount*4,B.STORAGE|B.COPY_SRC,'bla-canary-candidate-meta'),statsB=buf(d,statsBytes,B.STORAGE|B.COPY_SRC|B.COPY_DST,'bla-canary-stats'),readB=buf(d,statsBytes,B.COPY_DST|B.MAP_READ,'bla-canary-readback'),applyP=buf(d,16,B.UNIFORM|B.COPY_DST,'bla-canary-apply-params'); + const se=spanMantExp(snap),mantHi=Math.fround(se.mant),params=writeU32F32(48,dv=>{dv.setUint32(0,iter,true);dv.setUint32(4,ctx.refLen,true);dv.setUint32(8,sampleCount,true);dv.setUint32(12,levelCount,true);dv.setFloat32(16,f.w,true);dv.setFloat32(20,Math.fround(refPixel.x),true);dv.setFloat32(24,Math.fround(refPixel.y),true);dv.setFloat32(28,mantHi,true);dv.setInt32(32,se.exp,true);dv.setFloat32(36,Math.fround(safety),true)}); + let mapped=false,scope=false; + try{ + d.queue.writeBuffer(pbuf,0,params);d.queue.writeBuffer(refsB,0,ctx.refs);d.queue.writeBuffer(tableB,0,shadow.table);d.queue.writeBuffer(levelsB,0,levelData);d.queue.writeBuffer(samplesB,0,sampleData);d.queue.writeBuffer(indicesB,0,indexData);d.queue.writeBuffer(statsB,0,new Uint32Array(statsBytes/4)); + if(d.pushErrorScope){d.pushErrorScope('validation');scope=true}this.activeGpuStage='bla-canary-compare'; + const bg=d.createBindGroup({layout:pipes.compare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:refsB}},{binding:2,resource:{buffer:tableB}},{binding:3,resource:{buffer:levelsB}},{binding:4,resource:{buffer:samplesB}},{binding:5,resource:{buffer:candidateB}},{binding:6,resource:{buffer:indicesB}},{binding:7,resource:{buffer:f.meta}},{binding:8,resource:{buffer:statsB}}]}),enc=d.createCommandEncoder({label:'bla-canary-compare'}),pass=enc.beginComputePass({label:'bla-canary-compare'});pass.setPipeline(pipes.compare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(sampleCount/64));pass.end();enc.copyBufferToBuffer(statsB,0,readB,0,statsBytes);d.queue.submit([enc.finish()]);await d.queue.onSubmittedWorkDone();this.lastCompletedGpuStage='bla-canary-compare';this.activeGpuStage=''; + if(scope&&d.popErrorScope){const e=await d.popErrorScope();scope=false;if(e)throw e} + await readB.mapAsync(GPUMapMode.READ);mapped=true;const raw=new Uint32Array(readB.getMappedRange().slice(0));readB.unmap();mapped=false; + const stats={samples:sampleCount,checked:raw[0]||0,exactMismatch:raw[1]||0,blaDerivedUnknown:raw[2]||0,internalFailure:raw[3]||0,blaApplications:raw[4]||0,blaSkippedIterations:raw[5]||0,blaRejected:raw[6]||0,blaReplays:raw[7]||0,replaySteps:raw[8]||0,maxSkip:raw[9]||0,gateArmed:true,applied:false,metaOnly:true,productionEligible:false}; + stats.gatePassed=stats.checked===sampleCount&&stats.exactMismatch===0&&stats.blaDerivedUnknown===0&&stats.internalFailure===0; + runtime.blaGpuCanaryRuns++;runtime.blaGpuCanaryLatest=stats; + if(!stats.gatePassed){this.resetBlaGpuGate('canary-mismatch');stats.gateArmed=false;runtime.blaGpuCanaryLatest=stats;runtime.blaGpuCanaryFailures++;updateStats();return stats} + if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot())||this.frame!==f){stats.skippedApply='stale-view';updateStats();return stats} + // Only now may the candidate metadata touch the production field. Every + // value was compared bit-for-bit against that same fieldMeta first, so + // this write is semantically idempotent. smooth/color remain baseline. + d.queue.writeBuffer(applyP,0,new Uint32Array([sampleCount,0,0,0])); + if(d.pushErrorScope){d.pushErrorScope('validation');scope=true}this.activeGpuStage='bla-canary-apply'; + const abg=d.createBindGroup({layout:pipes.apply.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:applyP}},{binding:1,resource:{buffer:indicesB}},{binding:2,resource:{buffer:candidateB}},{binding:3,resource:{buffer:f.meta}}]}),ae=d.createCommandEncoder({label:'bla-canary-apply'}),ap=ae.beginComputePass({label:'bla-canary-apply'});ap.setPipeline(pipes.apply);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(sampleCount/64));ap.end();d.queue.submit([ae.finish()]);await d.queue.onSubmittedWorkDone();this.lastCompletedGpuStage='bla-canary-apply';this.activeGpuStage=''; + if(scope&&d.popErrorScope){const e=await d.popErrorScope();scope=false;if(e)throw e} + stats.applied=true;runtime.blaGpuCanaryApplied+=sampleCount;runtime.blaGpuCanaryLatest=stats;updateStats();return stats; + }catch(e){ + this.activeGpuStage='';try{if(scope&&d.popErrorScope)await d.popErrorScope()}catch{};this.resetBlaGpuGate('canary-runtime-error');runtime.blaGpuCanaryFailures++;const stats={gatePassed:false,gateArmed:false,error:String(e&&e.message||e),productionEligible:false};runtime.blaGpuCanaryLatest=stats;console.warn('BLA GPU canary failed:',e);updateStats();return stats; + }finally{if(mapped)try{readB.unmap()}catch{};for(const x of [pbuf,refsB,tableB,levelsB,samplesB,indicesB,candidateB,statsB,readB,applyP])destroy(x)} + } + async initPipelines(){ this.device.pushErrorScope?.('validation'); try{ - const [dm,fm,xm,xhm,xpm,xsm,zm,zqm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]); + const [dm,dasm,dadm,fm,xm,xhm,xpm,xsm,zm,zqm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('accurate-direct-seed',G.DIRECT_ACCURATE_SEED_WGSL),this.module('accurate-direct-ds',G.DIRECT_DS_CORRECT_WGSL),this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]); this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}}); + this.accurateDirectSeed=this.device.createComputePipeline({layout:'auto',compute:{module:dasm,entryPoint:'main'}}); + this.accurateDirectDs=this.device.createComputePipeline({layout:'auto',compute:{module:dadm,entryPoint:'main'}}); this.fast=this.device.createComputePipeline({layout:'auto',compute:{module:fm,entryPoint:'main'}}); this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}}); const [xpsm,xhsm]=await Promise.all([this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL),this.module('deep-bucket-histogram-stats-production',G.DEEP_BUCKET_HIST_STATS_WGSL)]); @@ -982,18 +1830,16 @@ class WebGpuRenderer{ this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}}); this.context=canvas.getContext('webgpu'); if(!this.context)throw new Error('WebGPU canvas contextを取得できません'); - webgpuCanvasClaimed=true;this.configure(); + this.configure(); }finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}} } - async ensureRefinePipelines(){if(this.likelyQueue&&this.deepLikelyRefine)return;if(this.refinePipelinePromise)return this.refinePipelinePromise;this.refinePipelinePromise=(async()=>{const [lqm,drm]=await Promise.all([this.module('likely-refine-queue',G.LIKELY_QUEUE_WGSL),this.module('deep-likely-refine',G.DEEP_LIKELY_REFINE_WGSL)]);this.likelyQueue=this.device.createComputePipeline({layout:'auto',compute:{module:lqm,entryPoint:'main'}});this.deepLikelyRefine=this.device.createComputePipeline({layout:'auto',compute:{module:drm,entryPoint:'refine_likely'}})})().finally(()=>{this.refinePipelinePromise=null});return this.refinePipelinePromise} - frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','refineQueue','refineStats','refineQueueParams','refineBatchParams','refineRead','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null} + frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','numericParams','colorParams','presentParams','front','back','history'])destroy(this.frame[k]);this.frame=null;this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false} ensureFrame(w,h){ const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage; - this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,refineQueue:null,refineStats:null,refineQueueParams:null,refineBatchParams:null,refineRead:null,numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame; + this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,48,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'}),history:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.TEXTURE_BINDING|T.COPY_DST,label:'stable-history'})};return this.frame; } ensureDeepBucketState(f=this.frame){if(!f)return null;if(!f.deepBucketState){const d=this.device,B=GPUBufferUsage;f.deepBucketState=buf(d,DEEP_BUCKET_STATE_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-bucket-state')}return f} ensureDeepQueueWorkspace(f=this.frame){if(!f)return null;this.ensureDeepBucketState(f);if(f.deepQueue&&f.deepQueueStats&&f.deepIndirect)return f;const d=this.device,B=GPUBufferUsage;f.deepQueueStats=buf(d,SPARSE_QUEUE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue-stats');f.deepQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue');f.deepIndirect=buf(d,SPARSE_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'deep-correction-indirect');return f} - ensureRefineWorkspace(f=this.frame){if(!f)return null;if(f.refineQueue&&f.refineStats&&f.refineQueueParams&&f.refineBatchParams&&f.refineRead)return f;const d=this.device,B=GPUBufferUsage;f.refineQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-queue');f.refineStats=buf(d,REFINE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-stats');f.refineQueueParams=buf(d,REFINE_QUEUE_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-queue-params');f.refineBatchParams=buf(d,REFINE_BATCH_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-batch-params');f.refineRead=buf(d,REFINE_STATS_BYTES,B.COPY_DST|B.MAP_READ,'likely-refine-readback');return f} ensureExportWorkspace(){ if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S; this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs; @@ -1001,17 +1847,18 @@ class WebGpuRenderer{ exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null} setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}} destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null} - setRefineDeepContext(ctx){if(this.refineDeepCtx&&this.refineDeepCtx.key===ctx.key)return;this.destroyRefineDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'idle-refinement-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.refineDeepCtx={...ctx,refsB}} - destroyRefineDeepContext(){if(this.refineDeepCtx)destroy(this.refineDeepCtx.refsB);this.refineDeepCtx=null} setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}} - destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null} + destroyFastContext(){this.destroyBlaFastContext();if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null} directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})} + accurateDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,outputStride=fullW,outputBase=0,lateThreshold=24,sx=.5,sy=.5){const re=fixedNum(snap.re,snap.bits),im=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),inv=1/Math.max(1,fullW),reHi=Math.fround(re),imHi=Math.fround(im),spHi=Math.fround(sp),invHi=Math.fround(inv);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,lateThreshold,outputStride,outputBase,0,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,reHi,true);d.setFloat32(52,Math.fround(re-reHi),true);d.setFloat32(56,imHi,true);d.setFloat32(60,Math.fround(im-imHi),true);d.setFloat32(64,spHi,true);d.setFloat32(68,Math.fround(sp-spHi),true);d.setFloat32(72,invHi,true);d.setFloat32(76,Math.fround(inv-invHi),true);d.setFloat32(80,sx,true);d.setFloat32(84,sy,true)})} deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,0,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - colorParamsData(w,h,iter=state.fieldView?.iter??maxIter()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,effectiveColorCycle(iter),true);d.setFloat32(20,state.shift,true)})} + fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0,unknownOnly=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} + colorParamsData(w,h,_iter=state.fieldView?.iter??maxIter(),style=colorStyleSnapshot()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,style.palette,true);d.setUint32(12,style.hq?1:0,true);d.setFloat32(16,style.cycle,true);d.setFloat32(20,style.shift,true)})} encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,pipeline=this.deepPostStats,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} + encodeAccurateDirectSeed(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.accurateDirectSeed.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass({label:'accurate-direct-seed'});pass.setPipeline(this.accurateDirectSeed);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} + encodeAccurateDirectDs(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.accurateDirectDs.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass({label:'accurate-direct-ds'});pass.setPipeline(this.accurateDirectDs);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.fast.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fast);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHist.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHist);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHistStats.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} @@ -1019,25 +1866,139 @@ class WebGpuRenderer{ encodeDeepBucketScatter(encoder,{pbuf,meta,bucketState,queueStats,queue,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} encodeQueuedDeepCorrection(encoder,{pbuf,meta,smooth,unresolved,queueStats,queue,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.correctQueued.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{binding:5,resource:{buffer:queueStats}},{binding:6,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.correctQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()} encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})} - async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null){ - await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep){this.setDeepContext(deepContext);this.ensureDeepBucketState(f)}else if(fastExtended){this.setFastContext(fastContext)}d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep)d.queue.writeBuffer(f.deepBucketState,0,new Uint32Array(DEEP_BUCKET_STATE_BYTES/4)); - const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;const params=deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,refX,refY):fastExtended?this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap);d.queue.writeBuffer(f.numericParams,0,params); - d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'}); - if(deep){this.encodeDeepPostStatsNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeDeepBucketHistogramStats(encoder,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,unresolved:f.unresolved,w:f.w,h:f.h});} - else if(fastExtended){this.encodeFastNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});} - else{this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});} - if(!deep){const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();} - d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;if(!deep)[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true; + initialNumericRows(w,iter,deep){ + // Structural workload cap, not a zoom threshold. The first submission is + // intentionally conservative; subsequent strips self-tune from measured + // queue completion time. This keeps one GPU submission bounded even when + // maxIter reaches 12000. + const workBudget=deep?24000000:48000000; + return Math.max(1,Math.min(128,Math.floor(workBudget/Math.max(1,w*iter)))); } - async buildLikelyRefineQueue(token,mode=1,sparseStride=4){await this.ready;await this.ensureRefinePipelines();if(token!==state.token)return{count:0,overflow:0,cancelled:true};const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;d.queue.writeBuffer(f.refineStats,0,new Uint32Array(REFINE_STATS_BYTES/4));d.queue.writeBuffer(f.refineQueueParams,0,new Uint32Array([f.w,f.h,mode>>>0,sparseStride>>>0]));const e=d.createCommandEncoder({label:'likely-refine-queue-build'}),bg=d.createBindGroup({layout:this.likelyQueue.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.refineQueueParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.refineQueue}},{binding:3,resource:{buffer:f.refineStats}}]}),pass=e.beginComputePass();pass.setPipeline(this.likelyQueue);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.n/256));pass.end();e.copyBufferToBuffer(f.refineStats,0,f.refineRead,0,REFINE_STATS_BYTES);d.queue.submit([e.finish()]);await f.refineRead.mapAsync(GPUMapMode.READ);const a=new Uint32Array(f.refineRead.getMappedRange().slice(0));f.refineRead.unmap();if(token!==state.token)return{count:0,overflow:0,cancelled:true};return{count:Math.min(f.n,a[0]||0),overflow:a[1]||0,cancelled:false}} - async refineLikelyBatch({snap,iter,deepContext,token,base,end,forceStrict=false}){await this.ready;await this.ensureRefinePipelines();if(token!==state.token||base>=end)return false;const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setRefineDeepContext(deepContext);const ref=referencePixelForSource(deepContext?.source,snap,f.w,f.h);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,ref.x,ref.y,0,f.w,0));d.queue.writeBuffer(f.refineBatchParams,0,new Uint32Array([base>>>0,end>>>0,0,0]));const pipeline=this.deepLikelyRefine,bg0=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.refineDeepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),bg1=d.createBindGroup({layout:pipeline.getBindGroupLayout(1),entries:[{binding:0,resource:{buffer:f.refineQueue}},{binding:1,resource:{buffer:f.refineBatchParams}}]}),e=d.createCommandEncoder({label:'likely-idle-refine'}),pass=e.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg0);pass.setBindGroup(1,bg1);pass.dispatchWorkgroups(Math.ceil((end-base)/64));pass.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.idleRefineBatches++;return true} - async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true} + adaptNumericRows(rows,elapsedMs,remaining,deep){ + if(remaining<=0)return rows; + // Never grow beyond the initial work-bound. Timing feedback may only + // reduce a strip if this adapter is slower than expected, so the + // worst-case pixel-iteration cap remains invariant across the frame. + const targetMs=deep?18:22,ms=Math.max(.25,Number(elapsedMs)||targetMs); + if(ms<=targetMs*1.25)return Math.max(1,Math.min(rows,remaining)); + const factor=Math.max(.5,Math.min(1,Math.sqrt(targetMs/ms))); + return Math.max(1,Math.min(rows,remaining,Math.round(rows*factor))); + } + async submitStage(encoder,stage,token,counter='numeric'){ + this.activeGpuStage=stage;state.gpuStage=stage; + const t0=performance.now();this.device.queue.submit([encoder.finish()]); + await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++; + const ms=performance.now()-t0;this.lastCompletedGpuStage=stage;this.activeGpuStage=''; + if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++; + return token===undefined||token===state.token?ms:null; + } + submitStageDeferred(encoder,stage,counter='numeric'){this.activeGpuStage=stage;state.gpuStage=stage;this.device.queue.submit([encoder.finish()]);if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++;return stage} + async waitStageBurst(stage,token,t0,count=1){await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++;const ms=(performance.now()-t0)/Math.max(1,count);this.lastCompletedGpuStage=stage;this.activeGpuStage='';return token===undefined||token===state.token?ms:null} + async computePerturbFrameTiled(snap,iter,deep,token,forceStrict,referencePixel,fastExtended,colorStyle,colorKey,blaPlan=null){ + const f=this.ensureFrame(canvas.width,canvas.height),d=this.device; + d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4)); + const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,useBla=!deep&&!!blaPlan?.enabled&&!!this.blaFastFramePipeline&&!!this.blaFastCtx; + let rows=this.initialNumericRows(f.w,iter,deep),lastUi=0,burstCount=0,burstStart=performance.now(),lastStage='',burstLimit=deep?2:(useBla?8:4); + // When the distributed probe reports a very clean BLA view, allow only a + // modest 2x strip growth. Even a pathological local fallback therefore + // stays far below the old unbounded full-frame dispatch. + if(useBla&&blaPlan.operationReduction>=.75&&blaPlan.fallbackRate<=.0625)rows=Math.min(256,rows*2); + for(let y=0;y=burstLimit||y>=f.h){ + const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false; + rows=this.adaptNumericRows(rows,ms,f.h-y,deep);burstCount=0;burstStart=performance.now(); + const now=performance.now();if(now-lastUi>100){lastUi=now;state.lastEngine='WebGPU · '+(deep?'正確':useBla?'高速BLA':'高速拡張')+' 計算 '+Math.round(100*y/f.h)+'%';updateStats()} + } + } + if(!deep){ + // Coverage contract: zero metadata means a pixel was never written by + // the tiled primary path. Re-run only those pixels through the original + // FAST recurrence before colouring. This pass is O(pixels) for the + // trivial guard but performs iterations only for actual coverage holes. + d.queue.writeBuffer(f.numericParams,0,this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY,f.w,0,2)); + d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle)); + const e=d.createCommandEncoder({label:'fast-coverage-repair-and-color'}); + this.encodeFastNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h}); + const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=e.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end(); + const ms=await this.submitStage(e,'fast coverage + color',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey; + } + if(!deep){if(useBla)runtime.blaFastEnabledFrames++;else runtime.blaFastBaselineFrames++;} + runtime.gpuFrames++;return true; + } + async computeAccurateDirectHybrid(snap,iter,token,colorStyle,colorKey){ + const f=this.ensureFrame(canvas.width,canvas.height),d=this.device; + // Pass 1: cheap f32 seed. Analytically proven main-cardioid/period-2 pixels + // are final, and only very early escapes (<24) are trusted. Everything + // else remains UNKNOWN for the DS pass below. + d.queue.writeBuffer(f.numericParams,0,this.accurateDirectParams(f.w,f.h,f.w,f.h,0,0,iter,snap,f.w,0,24)); + let e=d.createCommandEncoder({label:'accurate-direct-seed-frame'});this.encodeAccurateDirectSeed(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h}); + this.submitStageDeferred(e,'accurate direct seed','numeric'); + // Pass 2: recompute only UNKNOWN pixels from iteration zero in double-single. + // Each command buffer remains work-bounded, but several are queued before + // one CPU/GPU synchronization to avoid the latency regression of v24.2.53. + let rows=this.initialNumericRows(f.w,iter,true),y=0,burstCount=0,burstStart=performance.now(),lastStage='accurate direct seed',lastUi=0; + while(y=2||y>=f.h){const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;rows=this.adaptNumericRows(rows,ms,f.h-y,true);burstCount=0;burstStart=performance.now();const now=performance.now();if(now-lastUi>100){lastUi=now;state.lastEngine='WebGPU · 正確DS '+Math.round(100*y/f.h)+'%';updateStats()}} + } + d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));e=d.createCommandEncoder({label:'accurate-direct-color'});const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=e.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();const cms=await this.submitStage(e,'accurate direct color',token,'numeric');if(cms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuFrames++;return true; + } + async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null,blaPlan=null,accurateHybrid=false){ + await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle); + if(accurateHybrid)return this.computeAccurateDirectHybrid(snap,iter,token,colorStyle,colorKey); + if(deep){this.setDeepContext(deepContext);return this.computePerturbFrameTiled(snap,iter,true,token,forceStrict,referencePixel,false,colorStyle,colorKey)} + if(fastExtended){this.setFastContext(fastContext);return this.computePerturbFrameTiled(snap,iter,false,token,false,referencePixel,true,colorStyle,colorKey,blaPlan)} + // Direct f32 remains the exact v24.2.42 single-pass path. It is only used + // while absolute f32 pixel coordinates retain sufficient separation. + d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4)); + d.queue.writeBuffer(f.numericParams,0,this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap)); + d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const encoder=d.createCommandEncoder({label:'mandelbrot-direct-frame'}); + this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h}); + const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end(); + const ms=await this.submitStage(encoder,'direct primary',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuFrames++;return true; + } + async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuRecolors++;return true} presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}} - presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])} + commitHistory(view=state.frameView){if(!this.frame||!view)return false;const f=this.frame,d=this.device,e=d.createCommandEncoder({label:'commit-stable-history'});e.copyTextureToTexture({texture:f.front},{texture:f.history},[f.w,f.h]);d.queue.submit([e.finish()]);this.historyView=view;this.historyColorKey=this.frontColorKey;this.historyReady=true;return true} + invalidateHistory(){this.historyView=null;this.historyColorKey='';this.historyReady=false} + presentFrame(transform=this.presentTransform()){if(!this.frame)return;const f=this.frame,d=this.device,pb=f.presentParams,interactive=state.pointerActive||state.wheelActive,finite=Number.isFinite(transform?.scaleX)&&Number.isFinite(transform?.scaleY)&&Number.isFinite(transform?.offsetX)&&Number.isFinite(transform?.offsetY)&&transform.scaleX>0&&transform.scaleY>0,ft=finite?transform:{scaleX:1,scaleY:1,offsetX:0,offsetY:0},historyCandidate=!!(state.temporalFill&&this.historyReady&&this.historyView&&this.historyColorKey===colorStyleKey()),rawHistory=historyCandidate?this.presentTransform(this.historyView):{scaleX:1,scaleY:1,offsetX:0,offsetY:0},useHistory=!!(historyCandidate&&reprojectionSafe(rawHistory,2.25,.45)),ht=useHistory?rawHistory:{scaleX:1,scaleY:1,offsetX:0,offsetY:0};d.queue.writeBuffer(pb,0,new Float32Array([ft.scaleX,ft.scaleY,ft.offsetX,ft.offsetY,ht.scaleX,ht.scaleY,ht.offsetX,ht.offsetY,useHistory?1:0,0,0,0]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:interactive?this.reprojectSampler:this.sampler},{binding:1,resource:f.front.createView()},{binding:2,resource:f.history.createView()},{binding:3,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:0,g:0,b:0,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])} async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i=Math.max(32,Math.floor(n*.0005));return true} +function numericalFailureCount(stats){const r=stats&&stats.reasons||{};return (r.errorBound||0)+(r.escapeUncertain||0)+(r.referenceEnd||0)+(r.rebaseGap||0)+(r.range||0)} +function mergeCorrectionStats(before,after){const br=before?.reasons||{},ar=after?.reasons||{},numericBefore=numericalFailureCount(before),nonNumeric=Math.max(0,(before?.total||0)-numericBefore),reasons={...ar,operationLimit:(br.operationLimit||0)+(ar.operationLimit||0)};return{total:nonNumeric+(after?.total||0),corrected:after?.corrected||0,reasons}} +// A broad failure is cheaper to normalize once with the full-frame Deep pass; +// sparse/moderate failures go directly to local references. +function shouldPromoteFastToDeep(stats,n){const failed=numericalFailureCount(stats);return failed>=Math.max(60000,Math.floor(n*.08))} +function tileRecoveryConfig(){const z=zoomExp(),deep=z>=32;if(state.processMode==='power')return{size:deep?224:320,maxRefs:4,minFailed:deep?4:48};if(state.processMode==='fine')return{size:deep?128:192,maxRefs:20,minFailed:deep?1:8};if(state.processMode==='validate')return{size:deep?112:160,maxRefs:32,minFailed:1};return{size:deep?160:224,maxRefs:12,minFailed:deep?1:12}} +const REASON_KEYS=['','errorBound','escapeUncertain','referenceEnd','rebaseGap','range','operationLimit']; +function summarizeUnknownMeta(meta){const reasons={errorBound:0,escapeUncertain:0,referenceEnd:0,rebaseGap:0,range:0,operationLimit:0};let total=0;for(const m of meta){if(((m>>>28)&3)!==FIELD_UNKNOWN)continue;total++;const reason=(m>>>REASON_SHIFT)&0xff,k=REASON_KEYS[reason];if(k)reasons[k]++}return{total,corrected:0,reasons}} +function buildNumericalFailureTiles(meta,w,h,size=tileRecoveryConfig().size){const cols=Math.ceil(w/size),rows=Math.ceil(h/size),tiles=Array.from({length:cols*rows},(_,i)=>{const tx=i%cols,ty=(i/cols)|0,x=tx*size,y=ty*size;return{x,y,w:Math.min(size,w-x),h:Math.min(size,h-y),numeric:0,sumX:0,sumY:0,minX:w,minY:h,maxX:-1,maxY:-1,reasons:{errorBound:0,escapeUncertain:0,referenceEnd:0,rebaseGap:0,range:0}}});for(let y=0;y>>28)&3;if(cls!==FIELD_UNKNOWN)continue;const reason=(m>>>REASON_SHIFT)&0xff;if(reason<1||reason>5)continue;const t=tiles[ty*cols+((x/size)|0)],k=REASON_KEYS[reason];t.numeric++;t.sumX+=x+.5;t.sumY+=y+.5;t.minX=Math.min(t.minX,x);t.maxX=Math.max(t.maxX,x);t.minY=Math.min(t.minY,y);t.maxY=Math.max(t.maxY,y);t.reasons[k]++}}return tiles.filter(t=>t.numeric>0).sort((a,b)=>b.numeric-a.numeric)} +function tileReferenceSnapshot(snap,tile,fullW,fullH){const cx=tile.numeric?tile.sumX/tile.numeric:tile.x+tile.w*.5,cy=tile.numeric?tile.sumY/tile.numeric:tile.y+tile.h*.5,bw=Math.max(1,tile.maxX-tile.minX+1),bh=Math.max(1,tile.maxY-tile.minY+1),refW=Math.max(8,Math.min(tile.w,Math.ceil(bw*1.75))),refH=Math.max(8,Math.min(tile.h,Math.ceil(bh*1.75))),den=2n*BigInt(fullW),nx=BigInt(Math.round(2*cx-fullW)),ny=BigInt(Math.round(fullH-2*cy)),re=snap.re+roundDivSigned(snap.span*nx,den),im=snap.im+roundDivSigned(snap.span*ny,den),localSpan=(()=>{const q=roundDivSigned(snap.span*BigInt(refW),BigInt(fullW));return q>0n?q:1n})();return{snap:{bits:snap.bits,re,im,span:localSpan},width:refW,height:refH}} +async function recoverNumericalUnknownTiles(r,snap,iter,token,initialStats){const initialFailed=numericalFailureCount(initialStats),cfg=tileRecoveryConfig();if(initialFailed0;round++){const meta=await r.readMetaAll();if(token!==state.token)return null;stats=summarizeUnknownMeta(meta);const failed=numericalFailureCount(stats);if(failed=roundCap||budget<=0)break;const local=tileReferenceSnapshot(snap,tile,canvas.width,canvas.height);state.lastEngine='WebGPU · 局所reference '+(passes+1)+'/'+cfg.maxRefs+' · '+tile.numeric+' px';updateStats();const ctx=await refs.request(local.snap,iter,local.width,local.height);if(token!==state.token)return null;if(ctx.checkpointMismatch)throw new Error('tile reference guard checkpoint mismatch');const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height),next=await r.recoverUnknownTile(snap,iter,token,ctx,tile,refPixel);if(!next||token!==state.token)return null;attempted+=tile.numeric;passes++;used++;budget--;state.multiRefPasses=passes;state.correctedPixels+=next.corrected||0}if(!used)break}if(passes){const meta=await r.readMetaAll();if(token!==state.token)return null;stats=summarizeUnknownMeta(meta)}const recovered=Math.max(0,initialFailed-numericalFailureCount(stats));state.multiRefRecovered=recovered;state.unresolved=stats.total;state.unknownReasons=stats.reasons;return{stats,passes,recovered,attempted}} + +function scheduleFastNumericRecovery(r,snap,iter,token,initialStats,initialRefPixel){ + if(numericalFailureCount(initialStats)<=0)return false; + if(numericRecoveryTimer){clearTimeout(numericRecoveryTimer);numericRecoveryTimer=0;} + state.numericRecoveryPending=true; + // The primary FAST image is already visible. Give input/recolour handlers a + // short turn, then repair only if the view is still current. + numericRecoveryTimer=setTimeout(()=>{numericRecoveryTimer=0;runFastNumericRecovery(r,snap,iter,token,initialStats,initialRefPixel);},60); + return true; +} +async function runFastNumericRecovery(r,snap,iter,token,initialStats,initialRefPixel){ + if(token!==state.token||renderer!==r||state.dirty||!sameViewSnapshot(snap,snapshot())){state.numericRecoveryPending=false;return;} + state.numericRecoveryPending=false;state.numericRecoveryRunning=true;state.drawState='REFINING'; + let stats=initialStats,referencePixel=initialRefPixel,ctx=null; + try{ + // First try the inexpensive local-reference recovery on the existing FAST + // field. It often removes isolated perturbation failures without touching + // the rest of the frame. + if(numericalFailureCount(stats)>0){ + state.lastEngine='WebGPU · 背景で数値穴を局所補修';updateStats(); + const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return;stats=mr.stats; + } + // If local references were insufficient, perform the old full Deep path + // in the background. The visible FAST/front texture is not replaced until + // Deep + correction + recolour has completed, so this can no longer create + // a black rectangle or extend the interaction-to-first-image latency. + if(numericalFailureCount(stats)>0){ + state.lastEngine='WebGPU · 背景で正確系補修';updateStats(); + const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height); + if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;} + else{ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('background reference guard checkpoint mismatch');} + const ok=await r.computeFrame(snap,iter,true,ctx,token,state.processMode==='validate',referencePixel,false,null);if(!ok||token!==state.token)return; + stats=await r.readUnresolvedStats();if(token!==state.token)return; + const n=canvas.width*canvas.height; + if(shouldApplySparseCorrection(numericalFailureCount(stats),n)){ + const before=stats,corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return;state.correctionPasses++; + stats=mergeCorrectionStats(before,await r.readUnresolvedStats());if(token!==state.token)return; + } + if(numericalFailureCount(stats)>0){const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return;stats=mr.stats;} + } + state.unresolved=stats.total;state.unknownReasons=stats.reasons; + const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return; + state.frameView=snap;state.fieldView={...state.fieldView,...snap,iter,w:canvas.width,h:canvas.height,backend:ctx?'deep-background-recovery':state.fieldView?.backend||'fast-extended',progressive:true}; + // Keep temporal fill active until every numeric UNKNOWN is gone. Max-iter + // non-escape pixels are not numeric failures and may still be black. + if(numericalFailureCount(stats)===0){r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});r.commitHistory(snap);state.temporalFill=false;state.drawState='COVERED';} + else{state.temporalFill=!!r.historyReady;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});state.drawState='REFINED';} + }catch(e){if(token===state.token){state.gpuError='背景数値補修: '+String(e&&e.message||e);console.warn('background numeric recovery failed:',e);}} + finally{ + if(token===state.token){state.numericRecoveryRunning=false;state.numericRecoveryPending=false;updateStats();if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor);} + } } -function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true} function referencePixelForSource(src,snap,w,h){if(!src)return{x:w*.5,y:h*.5};const b=Math.max(snap.bits,src.bits),span=align(snap.span,snap.bits,b),dr=align(src.re,src.bits,b)-align(snap.re,snap.bits,b),di=align(src.im,src.bits,b)-align(snap.im,snap.bits,b);return{x:w*.5+fixedRatio(dr,span)*w,y:h*.5-fixedRatio(di,span)*w}} function reusableDeepReference(snap,iter,w,h){const ctx=renderer&&renderer.deepCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} function reusableFastReference(snap,iter,w,h){const ctx=renderer&&renderer.fastCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} // ── GPU startup / rendering orchestration ──────────────────────────────── -async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise} -function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx} -function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span} -function cancelIdleRefinement(){if(idleRefineTimer){clearTimeout(idleRefineTimer);idleRefineTimer=0}state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0} -function idleRefinementTargets(baseIter){if(baseIter>=8000)return[];const z=zoomExp(),mode=state.processMode,mul=mode==='power'?.75:mode==='fine'?1.25:mode==='validate'?1.4:1,cap=Math.min(8000,Math.max(baseIter*2,Math.round((1200+300*z)*mul))),raw=mode==='power'?[Math.min(cap,baseIter*2)]:[Math.min(cap,baseIter*2),Math.min(cap,baseIter*4),cap],out=[];for(const v of raw){const n=Math.max(baseIter+1,Math.floor(v));if(n>baseIter&&(!out.length||n!==out[out.length-1]))out.push(n)}return out} -function idleRefinementBatchBudget(){const m=state.processMode;if(m==='power')return 900000;if(m==='fine')return 2800000;if(m==='validate')return 1800000;return 1800000} -function idleRefinementQueuePolicy(stage){if(stage<=0)return{mode:1,stride:4};if(stage===1)return{mode:2,stride:8};return{mode:3,stride:16}} -function idleRefinementSliceMs(){return state.processMode==='power'?100:state.processMode==='fine'?220:160} -function idleRefinementPauseMs(){return state.processMode==='power'?160:80} -function canIdleRefine(token,snap,baseIter){return token===state.token&&renderer&&!state.rendering&&!state.pointerActive&&!state.wheelActive&&sameSnapshot(snap,snapshot())&&state.fieldView&&state.fieldView.iter===baseIter&&state.fieldView.w===canvas.width&&state.fieldView.h===canvas.height} -const sleep=ms=>new Promise(r=>setTimeout(r,ms)); -async function paintIdleRefinement(token,snap,baseIter){while(state.recoloring&&canIdleRefine(token,snap,baseIter))await sleep(12);if(!canIdleRefine(token,snap,baseIter))return false;await recolor();while((state.recoloring||state.recolorPending)&&canIdleRefine(token,snap,baseIter))await sleep(8);return canIdleRefine(token,snap,baseIter)} -function scheduleIdleRefinement(snap,baseIter,decision,token){cancelIdleRefinement();if(!renderer||(!decision.deep&&!decision.fastExtended))return;const targets=idleRefinementTargets(baseIter);if(!targets.length)return;idleRefineTimer=setTimeout(()=>{idleRefineTimer=0;runIdleRefinement(snap,baseIter,decision,token,targets)},320)} -async function runIdleRefinement(snap,baseIter,decision,token,targets){if(!canIdleRefine(token,snap,baseIter))return;state.refinementRunning=true;state.refinedIter=baseIter;updateStats();try{for(let stage=0;stage=idleRefinementSliceMs()){const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;updateStats();lastPaint=performance.now();await sleep(idleRefinementPauseMs())}}const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;state.fieldView.refinedIter=targetIter;updateStats();await sleep(idleRefinementPauseMs())}}catch(e){if(token===state.token){console.warn('idle refinement stopped:',e);state.gpuError=String(e&&e.message||e)}}finally{if(token===state.token){state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0;updateStats()}}} - -function cancelRender(){cancelIdleRefinement();state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled')} +async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=true;state.gpuUnavailable=true;state.gpuError='WebGPU非対応 · 重いfallbackは無効';updateStats();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=true;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません · fallback無効';updateStats();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuUnavailable=false;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e)+' · fallback無効';updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise} +function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;state.refinePending=false;state.numericRecoveryPending=false;state.numericRecoveryRunning=false;runtime.blaShadowLatest=null;runtime.blaGpuShadowLatest=null;if(refineTimer){clearTimeout(refineTimer);refineTimer=0}if(blaShadowTimer){clearTimeout(blaShadowTimer);blaShadowTimer=0}if(numericRecoveryTimer){clearTimeout(numericRecoveryTimer);numericRecoveryTimer=0}blaShadow.cancelPending('render cancelled');refs.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled')} function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()} function schedule(){if(!raf)raf=requestAnimationFrame(loop)} +async function renderQualityStage(r,snap,iter,token,isFinal=false){ + let decision=chooseBackend(snap,canvas.width,isFinal),deep=decision.deep,accurateHybrid=!!decision.accurateHybrid,fastExtended=decision.fastExtended; + state.backendDecision=decision;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false,blaPlan=null; + if(deep){ + const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height); + if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true} + else{state.lastEngine='WebGPU · '+(isFinal?'最終':'preview')+' 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch')} + }else if(fastExtended){ + const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height); + if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true} + else{state.lastEngine='WebGPU · '+(isFinal?'最終':'preview')+' 高速拡張 reference準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height)} + } + if(fastExtended&&isFinal&&!r.blaFastDisabled){ + if(r.blaFastCandidatePipeline&&r.blaFastFramePipeline&&fastCtx?.refLen>=iter){ + try{ + const shadow=await blaShadow.requestBuild({ctx:fastCtx,snap,iter,width:canvas.width,height:canvas.height,refPixel:referencePixel,safety:1/64});if(token!==state.token)return null; + if(shadow.stats.gatePassed&&shadow.stats.gpuShadowEligible){blaPlan=await r.runBlaFastProbe({shadow,snap,iter,refPixel:referencePixel,safety:1/64,samples:64});if(blaPlan?.enabled){blaPlan.safety=1/64;decision={...decision,backend:'fast-bla',reason:'adaptive-bla-probe-pass',probe:true};state.backendDecision=decision;}}else{runtime.blaFastProbeLatest={enabled:false,reason:'cpu-gate'};} + }catch(e){runtime.blaFastFailures++;runtime.blaFastProbeLatest={enabled:false,reason:'probe-error',error:String(e&&e.message||e)};blaPlan=null;console.warn('Adaptive FAST BLA probe skipped:',e)} + }else if(fastCtx?.refLen{if(renderer===r&&!r.blaFastDisabled)r.warmBlaFastPipelines()},250)} + } + state.lastEngine='WebGPU · '+(isFinal?'最終品質':'即時preview')+' '+(accurateHybrid?'正確DS':deep?'正確':blaPlan?.enabled?'高速BLA':fastExtended?'高速拡張':'高速');updateStats(); + let ok=await r.computeFrame(snap,iter,deep,ctx,token,isFinal&&state.processMode==='validate',referencePixel,fastExtended,fastCtx,blaPlan,accurateHybrid);if(!ok||token!==state.token)return null; + if(deep){ + if(isFinal){ + let stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0; + const n=canvas.width*canvas.height; + if(shouldApplySparseCorrection(numericalFailureCount(stats),n)){ + const beforeCorrection=stats,corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return null;state.correctionPasses=1; + stats=mergeCorrectionStats(beforeCorrection,await r.readUnresolvedStats());if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected; + } + if(numericalFailureCount(stats)>0){const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;state.unresolved=stats.total;state.unknownReasons=stats.reasons;} + } + const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null; + }else if(accurateHybrid){ + // DS-direct hybrid resolves its own delicate pixels and colours the frame. + state.unresolved=0;state.unknownReasons=null; + }else if(isFinal&&fastExtended){ + // Do not block the visible frame on expensive Deep/multi-reference repair. + // Numeric UNKNOWN stays transparent and is filled from the previous stable + // frame while a same-view background repair runs. Incomplete numeric + // frames are never committed as stable history. + const stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons; + if(numericalFailureCount(stats)>0){scheduleFastNumericRecovery(r,snap,iter,token,stats,referencePixel);} + } + state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,accurateHybrid,fastExtended,backend:decision.backend,referenceReused,progressive:true}; + r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0}); + return{decision,referenceReused,shadowContext:deep?ctx:(fastExtended?fastCtx:null),referencePixel}; +} +function numericFrameComplete(){return numericalFailureCount({reasons:state.unknownReasons||{}})===0&&!state.numericRecoveryPending&&!state.numericRecoveryRunning} +function scheduleQualityRefinement(snap,token,stages,index){ + if(index>=stages.length||token!==state.token)return; + state.refinePending=true;if(refineTimer)clearTimeout(refineTimer); + refineTimer=setTimeout(()=>{refineTimer=0;runQualityRefinement(snap,token,stages,index)},refinementDelay(index)); +} +async function runQualityRefinement(snap,token,stages,index){ + if(token!==state.token||state.pointerActive||state.wheelActive||state.dirty||!sameViewSnapshot(snap,snapshot())){state.refinePending=false;return} + const r=renderer;if(!r){state.refinePending=false;return} + const iter=stages[index],isFinal=index===stages.length-1,t0=performance.now();state.rendering=true;state.refinePending=false;state.drawState='REFINING';state.qualityStage=isFinal?'final':'refining';state.qualityIter=iter;updateStats(); + try{ + const result=await renderQualityStage(r,snap,iter,token,isFinal);if(!result||token!==state.token)return; + state.refineMs=performance.now()-t0;state.rendering=false;state.drawState=isFinal?'COVERED':'REFINED';state.lastRender=state.refineMs;state.lastEngine='WebGPU · '+(isFinal?'最終品質':'中間品質')+' '+(result.decision.accurateHybrid?'正確DS':result.decision.deep?'正確':result.decision.backend==='fast-bla'?'高速BLA':result.decision.fastExtended?'高速拡張':'高速'); + if(isFinal){if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false;}else{state.temporalFill=!!r.historyReady;}r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});r.scheduleBlaFastWarmup();scheduleBlaShadowValidation(snap,iter,result.shadowContext,result.referencePixel,token,result.decision.fastExtended?'fast':result.decision.deep?'deep':'cpu');} + updateStats(); + if(!isFinal)scheduleQualityRefinement(snap,token,stages,index+1); + }catch(e){if(token!==state.token)return;state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU refinement error';updateStats();console.error(e)} +} async function renderFrame(){ - const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height); - state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats(); + const token=++state.token,snap=snapshot(),finalIter=maxIter(),holdStable=canUseStableReprojection(),stages=qualityStages(finalIter),iter=stages[0],isSingleFinal=stages.length===1,t0=performance.now(); + state.rendering=true;state.dirty=false;state.drawState=isSingleFinal&&holdStable?'REPROJECTED':'COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.multiRefPasses=0;state.multiRefRecovered=0;state.qualityStage=isSingleFinal?'final':'preview';state.qualityIter=iter;state.qualityFinalIter=finalIter;state.previewMs=0;state.refineMs=0;state.refinePending=false;runtime.renderStarts++;runtime.blaFastProbeLatest=null;updateStats(); try{ const r=renderer||await initRenderer();if(token!==state.token)return; - if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return} - const deep=decision.deep,fastExtended=decision.fastExtended;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false; - if(deep){const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height);if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 正確 reference再利用'}else{state.lastEngine='WebGPU · 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · 正確 perturbation'}} - else if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 高速拡張 reference再利用'}else{state.lastEngine='WebGPU · 高速拡張準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height);state.lastEngine='WebGPU · 高速拡張'}} - else state.lastEngine='WebGPU · 高速'; - const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate',referencePixel,fastExtended,fastCtx);if(!ok)return; - if(!deep){ - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false; - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0}); - if(fastExtended){const stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons} - const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token);return; - } - // Accurate mode keeps the previous committed frame visible while the new - // primary field is checked/corrected. The incomplete primary texture lives - // only in frame.back and is never presented to the viewer. - let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats(); - const n=canvas.width*canvas.height; - if(shouldApplySparseCorrection(state.unresolved,n)){ - state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats(); - const corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return;state.correctionPasses=1; - stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected; - } - const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return; - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · '+(referenceReused?'正確 reference再利用':'正確 reference自動選択')+(state.correctionPasses?' + DS correction':''); - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token); - }catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)} + if(!r){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU必須 · fallback無効';updateStats();return} + state.temporalFill=!!r.historyReady; + // Paint the old completed frame at the new view immediately. The numeric + // buffers may now be rewritten strip by strip, but the user keeps seeing + // a coherent image until the complete new colour frame is ready. + if(holdStable){r.presentFrame(r.presentTransform());state.drawState='REPROJECTED';updateStats()} + const result=await renderQualityStage(r,snap,iter,token,isSingleFinal);if(!result||token!==state.token)return; + const elapsed=performance.now()-t0;state.lastRender=elapsed;state.rendering=false;state.drawState=isSingleFinal?'COVERED':'REFINED'; + if(isSingleFinal){state.refineMs=elapsed;state.previewMs=0;state.lastEngine='WebGPU · 最終品質 '+(result.decision.accurateHybrid?'正確DS':result.decision.deep?'正確':result.decision.backend==='fast-bla'?'高速BLA':result.decision.fastExtended?'高速拡張':'高速');if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false;}else{state.temporalFill=!!r.historyReady;}r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});state.qualityStage='final';r.scheduleBlaFastWarmup();scheduleBlaShadowValidation(snap,iter,result.shadowContext,result.referencePixel,token,result.decision.fastExtended?'fast':result.decision.deep?'deep':'cpu');} + else{state.previewMs=elapsed;state.lastEngine='WebGPU · 即時preview '+(result.decision.accurateHybrid?'正確DS':result.decision.deep?'正確':result.decision.backend==='fast-bla'?'高速BLA':result.decision.fastExtended?'高速拡張':'高速');} + updateStats(); + if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor); + if(!isSingleFinal)scheduleQualityRefinement(snap,token,stages,1); + }catch(e){if(token!==state.token)return;state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)} +} +function requestRecolor(){ + state.colorRevision++;state.recolorPending=true; + // Keep the old completed frame valid until the replacement colour pass has + // actually finished. Invalidating history at slider-down caused apparent + // non-response/flicker when GPU work was already in flight. + if(!state.rendering&&!state.recoloring&&!state.numericRecoveryRunning)queueMicrotask(recolor); +} +async function recolor(){ + if(!renderer||!state.fieldView){state.recolorPending=true;return false} + if(state.rendering||state.recoloring||state.numericRecoveryRunning){state.recolorPending=true;return false} + state.recoloring=true;let painted=false; + try{ + while(state.recolorPending&&!state.rendering&&!state.numericRecoveryRunning&&renderer&&state.fieldView){ + state.recolorPending=false;const revision=state.colorRevision,token=state.token,ok=await renderer.recolor(token); + if(!ok||token!==state.token){state.recolorPending=true;continue} + renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true; + // Only promote a colour frame to stable history if it still represents + // the newest slider value. Rapid dragging therefore collapses to the + // latest value instead of replaying stale colour states. + if(revision===state.colorRevision&&state.qualityStage==='final'&&!state.refinePending&&numericFrameComplete()){renderer.commitHistory(state.frameView);state.temporalFill=false} + updateStats(); + } + return painted + }catch(e){console.error(e);return false} + finally{state.recoloring=false;if(state.recolorPending&&!state.rendering&&!state.numericRecoveryRunning)queueMicrotask(recolor)} } -function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · 正確モード非対応';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*effectiveColorCycle(iter)+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(ye.classList.remove('show'),1500)} function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')} $('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()}; $('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()}; -$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}}; +async function copyShareUrl(){ + saveHash(false);const url=location.href; + try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(url);toast('共有URLをコピーしました');return}} + catch{} + try{const ta=document.createElement('textarea');ta.value=url;ta.setAttribute('readonly','');ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();const ok=document.execCommand&&document.execCommand('copy');ta.remove();if(ok){toast('共有URLをコピーしました');return}}catch{} + try{if(navigator.share){await navigator.share({title:'MANDELBROT DEEP ZOOM',url});toast('共有しました');return}}catch(e){if(e?.name==='AbortError')return} + try{prompt('共有URLをコピーしてください',url)}catch{}toast('共有URLを表示しました') +} +$('#share').onclick=copyShareUrl; let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0; -function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色アニメ停止':'色を自動変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')} +function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色相変化停止':'色相変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')} function stopColorAuto(){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton()} -function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;const cmin=.001,cmax=.05,smin=0,smax=1;state.cycle+=state.colorCycleDir*.0006*dt;state.shift+=state.colorShiftDir*.012*dt;if(state.cycle>=cmax){state.cycle=cmax;state.colorCycleDir=-1}else if(state.cycle<=cmin){state.cycle=cmin;state.colorCycleDir=1}if(state.shift>=smax){state.shift=smax;state.colorShiftDir=-1}else if(state.shift<=smin){state.shift=smin;state.colorShiftDir=1}$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;recolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)} +function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;/* Hue animation advances shift directly. No cycle or phase compensation is applied. */state.shift=fract01(state.shift+.09*dt);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;requestRecolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)} $('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf)colorAutoRaf=requestAnimationFrame(colorAutoStep);else if(!state.colorAuto)stopColorAuto()}; -$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()}; +$('#palette').addEventListener('change',e=>{const pal=Math.max(0,Math.min(PALETTE_MAX,Number(e.target.value)|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;requestRecolor();saveHash(false)}); +function applyCycleControl(e){state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);requestRecolor()} +function applyShiftControl(e){if(state.colorAuto)stopColorAuto();state.shift=Math.max(0,Math.min(1,Number(e.target.value)||0));$('#shiftO').textContent=state.shift.toFixed(2);requestRecolor()} +$('#cycle').addEventListener('input',applyCycleControl);$('#cycle').addEventListener('change',()=>saveHash(false)); +$('#shift').addEventListener('input',applyShiftControl);$('#shift').addEventListener('change',()=>saveHash(false)); $('#renderMode').onchange=e=>{state.renderMode=e.target.value==='accurate'?'accurate':'fast';try{localStorage.setItem('mandelbrot.renderMode',state.renderMode)}catch{}markDirty()}; $('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}}; addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}}); @@ -1195,19 +2297,25 @@ async function runExport(){ } $('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png'); -function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.deep?'WebGPU 正確':decision.fastExtended?'WebGPU 高速':'WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'正確描画の補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.refinementRunning)status+=' · 補足精密化 '+state.refinementStage+' ('+state.refinedIter+' iter, 残 '+state.refinementQueue+')';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status} +function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.accurateHybrid?'WebGPU 正確DS':decision.deep?'WebGPU 正確':decision.fastExtended?'WebGPU 高速':'WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'WebGPU 復旧中':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'正確描画の補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved){const nf=numericalFailureCount({reasons:state.unknownReasons||{}}),op=state.unknownReasons?.operationLimit||0;status+=' · 未確定 '+state.unresolved+(nf||op?' (数値 '+nf+' / 上限 '+op+')':'');}if(state.correctionPasses)status+=' · DS補正';if(state.multiRefPasses)status+=' · 局所ref '+state.multiRefPasses+' / 回収 '+state.multiRefRecovered;if(state.qualityStage==='preview')status+=' · preview '+state.qualityIter+'/'+state.qualityFinalIter;if(state.refinePending)status+=' · 最終品質待機';if(state.numericRecoveryPending)status+=' · 数値穴補修待機';if(state.numericRecoveryRunning)status+=' · 数値穴補修中';if(state.qualityStage==='refining')status+=' · 精細化 '+state.qualityIter+'/'+state.qualityFinalIter;if(state.qualityStage==='final')status+=' · 最終 '+state.qualityIter;if(state.temporalFill)status+=' · 前フレーム保持';if(runtime.blaShadowLatest&&!runtime.blaShadowLatest.buildOnly){const bs=runtime.blaShadowLatest;status+=' · BLA影 '+bs.samples+'点 '+(!bs.eligibleForBla?(bs.referenceFull?'非対象(span)':'非対象(ref短)'):bs.gatePassed?'一致':'差異 '+(bs.classMismatch+bs.escapeIterationMismatch))+' / skip '+(100*bs.skipRate).toFixed(1)+'%';}if(runtime.blaGpuShadowLatest){const gs=runtime.blaGpuShadowLatest;status+=' · GPU影 '+(gs.gatePassed?'一致':gs.compileFailed?'コンパイル不可':gs.error?'実行不可':'差異 '+(gs.exactMismatch??'?'));if(renderer?.blaGpuGate)status+=' gate '+renderer.blaGpuGate.distinctViews+'/3';}if(runtime.blaGpuCanaryLatest){const cs=runtime.blaGpuCanaryLatest;status+=' · BLA canary '+(cs.applied?(cs.checked+' px一致'):cs.gateArmed?'待機':cs.error?'停止':'未解禁');}if(runtime.blaFastProbeLatest){const bp=runtime.blaFastProbeLatest;status+=' · FAST BLA '+(bp.enabled?'ON':'OFF')+(Number.isFinite(bp.operationReduction)?' / 削減 '+(100*bp.operationReduction).toFixed(0)+'%':'')+(Number.isFinite(bp.fallbackRate)?' / fallback '+(100*bp.fallbackRate).toFixed(0)+'%':'');}if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status} globalThis.__MANDEL_TEST__={ async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard',renderMode='fast'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.renderMode=renderMode==='accurate'?'accurate':'fast';state.hq=false;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{width:canvas.width,height:canvas.height,state:this.state()}}, async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)}, - state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter(),renderMode:state.renderMode,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused}), + async runBlaShadow({samples=289,safety=1/64}={}){const snap=snapshot(),iter=maxIter(),w=canvas.width,h=canvas.height;let ctx=null,refPixel=null;if(renderer?.deepCtx&&renderer.deepCtx.source?.iter===iter){ctx=renderer.deepCtx;refPixel=referencePixelForSource(ctx.source,snap,w,h)}else if(renderer?.fastCtx&&renderer.fastCtx.source?.iter===iter){ctx=renderer.fastCtx;refPixel=referencePixelForSource(ctx.source,snap,w,h)}else{ctx=await refs.request(snap,iter,w,h);refPixel=referencePixelForSource(ctx.source,snap,w,h)}const result=await blaShadow.request({ctx,snap,iter,width:w,height:h,refPixel,samples,safety});return{...result.stats,tableBytes:result.table.byteLength,levelOffsets:result.levelOffsets,levelCounts:result.levelCounts}}, + async runBlaGpuShadow({samples=49,safety=1/64}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),w=canvas.width,h=canvas.height,ctx=await fastRefs.request(snap,iter,w,h),refPixel=referencePixelForSource(ctx.source,snap,w,h),cpu=await blaShadow.request({ctx,snap,iter,width:w,height:h,refPixel,samples,safety});if(!cpu.stats.gpuShadowEligible)return{cpu:cpu.stats,gpu:{skipped:true,reason:'cpu-gate'}};const gpu=await renderer.runBlaGpuShadow({shadow:cpu,ctx,snap,iter,refPixel,safety});return{cpu:cpu.stats,gpu}}, + blaGpuGate:()=>renderer?{...renderer.blaGpuGate}:null, + async runBlaGpuCanary({safety=1/64}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');if(!renderer.blaGpuGate?.armed)return{skipped:true,reason:'gate-not-armed',gate:renderer.blaGpuGate?{...renderer.blaGpuGate}:null};const snap=snapshot(),iter=maxIter(),w=canvas.width,h=canvas.height,ctx=await fastRefs.request(snap,iter,w,h),refPixel=referencePixelForSource(ctx.source,snap,w,h),cpu=await blaShadow.request({ctx,snap,iter,width:w,height:h,refPixel,samples:289,safety});if(!cpu.stats.gpuShadowEligible)return{cpu:cpu.stats,canary:{skipped:true,reason:'cpu-gate'}};const canary=await renderer.runBlaGpuCanary({shadow:cpu,ctx,snap,iter,refPixel,safety,token:state.token});return{cpu:cpu.stats,canary}}, + state:()=>{const snap=snapshot(),se=spanMantExp(snap),reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),spanExp:se.exp,directPixelRatio:directPixelRatio(snap,canvas.width),width:canvas.width,height:canvas.height,iter:maxIter(),renderMode:state.renderMode,palette:state.palette,cycle:state.cycle,shift:state.shift,colorRevision:state.colorRevision,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,qualityStage:state.qualityStage,qualityIter:state.qualityIter,qualityFinalIter:state.qualityFinalIter,previewMs:state.previewMs,refineMs:state.refineMs,refinePending:state.refinePending,temporalFill:state.temporalFill,numericRecoveryPending:state.numericRecoveryPending,numericRecoveryRunning:state.numericRecoveryRunning,historyReady:!!renderer?.historyReady,multiRefPasses:state.multiRefPasses,multiRefRecovered:state.multiRefRecovered,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version,gpuStage:state.gpuStage,gpuTileRows:state.gpuTileRows,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,blaShadowBuilds:runtime.blaShadowBuilds,blaShadowFailures:runtime.blaShadowFailures,blaShadow:runtime.blaShadowLatest,blaGpuShadowRuns:runtime.blaGpuShadowRuns,blaGpuShadowFailures:runtime.blaGpuShadowFailures,blaGpuShadow:runtime.blaGpuShadowLatest,blaFastProbeRuns:runtime.blaFastProbeRuns,blaFastEnabledFrames:runtime.blaFastEnabledFrames,blaFastBaselineFrames:runtime.blaFastBaselineFrames,blaFastProbe:runtime.blaFastProbeLatest,blaFastDisabled:!!renderer?.blaFastDisabled}}, 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 probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded():!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))}, - async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),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,deep,strict,ss,unresolved:result.unresolved||0}} + setBlaFastEnabled(enabled=true){if(!renderer)return false;renderer.blaFastDisabled=!enabled;if(enabled&&renderer.blaFastCandidatePipeline&&renderer.blaFastFramePipeline)renderer.blaFastDisabled=false;runtime.blaFastProbeLatest=null;return !renderer.blaFastDisabled}, + async compareCurrentMetaToBaseline(){if(!renderer||!renderer.frame)throw new Error('WebGPU frame unavailable');const snap=snapshot(),iter=state.fieldView?.iter??maxIter(),w=canvas.width,h=canvas.height,fastExtended=fastNeedsExtended(snap,w);let fastCtx=null;if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const baseline=await renderer.renderTileMeta({snap,iter,deep:false,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:false}),current=await renderer.readMetaAll();let mismatch=0,first=-1,ha=2166136261>>>0,hb=2166136261>>>0;for(let i=0;i>>0,b=baseline[i]>>>0;if(a!==b){mismatch++;if(first<0)first=i}ha=Math.imul((ha^a)>>>0,16777619)>>>0;hb=Math.imul((hb^b)>>>0,16777619)>>>0}return{pixels:current.length,mismatch,first,currentChecksum:ha>>>0,baselineChecksum:hb>>>0,backend:state.fieldView?.backend||null,unresolved:baseline.unresolved||0}}, + async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w):!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))}, + async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),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,deep,strict,ss,unresolved:result.unresolved||0}} }; // ── boot / teardown ────────────────────────────────────────────────────── -addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();refs.destroy();fastRefs.destroy();if(renderer)renderer.destroy()},{once:true}); +addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();blaShadow.destroy();refs.destroy();fastRefs.destroy();if(renderer)renderer.destroy()},{once:true}); try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}state.renderMode=localStorage.getItem('mandelbrot.renderMode')==='accurate'?'accurate':'fast'}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule(); })(); diff --git a/package.json b/package.json deleted file mode 100644 index 26cffe4..0000000 --- a/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "mandelbrot-webgpu-v24", - "version": "24.2.26", - "private": true, - "type": "module", - "scripts": { - "test": "node scripts/test-all.mjs", - "build": "node scripts/build.mjs" - } -} diff --git a/script.js b/script.js deleted file mode 100644 index 865cb8f..0000000 --- a/script.js +++ /dev/null @@ -1,371 +0,0 @@ -(()=>{'use strict'; -const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません'); -const $=s=>document.querySelector(s),canvas=$('#view'); -const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n; -const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000; -const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20; -const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity']; -const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96,REFINE_QUEUE_PARAM_BYTES=16,REFINE_STATS_BYTES=16,REFINE_BATCH_BYTES=16; -const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorCycleDir:1,colorShiftDir:1,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,refinementRunning:false,refinementStage:0,refinementQueue:0,refinedIter:0}; -let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,idleRefineTimer=0,lastWrittenHash='',navigationHash=''; -const viewHistory=[];let viewHistoryIndex=-1; -const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,idleRefineBatches:0,idleRefineQueued:0}; - -// ── exact fixed-point view state ───────────────────────────────────────── -function one(bits=state.bits){return 1n<0?v<>BigInt(-d)} -function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x} -function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits} -function log2Fixed(v){return log2FixedAt(v,state.bits)} -function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v} -function f32ToFixed(x,bits){ - x=Math.fround(x);if(!Number.isFinite(x))throw new Error('non-finite f32 split component');if(x===0)return 0n; - const ab=new ArrayBuffer(4),dv=new DataView(ab);dv.setFloat32(0,x,false);const u=dv.getUint32(0,false),neg=(u>>>31)!==0,eb=(u>>>23)&255,frac=u&0x7fffff,m=BigInt(eb===0?frac:(0x800000|frac)),e=(eb===0?-149:eb-127-23),sh=bits+e; - let q=sh>=0?m<>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} -function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} -function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}} -function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))} -function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e} -function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}} -function f32Ulp(x){x=Math.abs(Math.fround(x));if(!Number.isFinite(x))return Infinity;if(x===0)return Math.pow(2,-149);if(x=4)} -function chooseBackend(snap=snapshot(),w=canvas.width){const deep=state.renderMode==='accurate',fastExtended=!deep&&fastNeedsExtended(snap,w);return{backend:deep?'deep':fastExtended?'fast-extended':'direct',deep,fastExtended,probe:false,reason:deep?'manual-accurate':fastExtended?'manual-fast-extended':'manual-fast'}} -function deepNeeded(){return state.renderMode==='accurate'} -function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,renderMode:state.renderMode}} -function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')} -function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()} -function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,renderMode:v.renderMode||state.renderMode});ensurePrecision();syncControls();saveHash(false);markDirty()} - -// ── iteration / quality policy ─────────────────────────────────────────── -function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))} -function effectiveColorCycle(iter=maxIter()){const base=Math.max(1,state.baseIter),effective=Math.max(base,Number(iter)||base);return state.cycle*base/effective} -function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))} -function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)} -function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)} -function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}} - -// ── high precision reference worker ───────────────────────────────────── -function referenceWorkerSource(){return String.raw` -'use strict'; -const MAX_REF=150001,MAX_LEVELS=20; -function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length} -function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q} -function fixedNum(v,b){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)),n=top*Math.pow(2,sh-b);return neg?-n:n} -function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}} -function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)return n+1}return 0} -function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]} -function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;kbest.score){best={...c,escape,score}}if(escape===0)break}return best} -function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<BAIL)escape=n+1}return{mismatch,checked}} -function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf} -self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceBits=d.bits,sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),limit=Math.min(MAX_REF-1,d.iter),bits=sourceBits+64,centerRef=orbit(bits,sourceRe<<64n,sourceIm<<64n,limit),chosen=chooseReference(sourceBits,sourceRe,sourceIm,sourceSpan,w,h,limit,centerRef.escape),re=chosen.re<<64n,im=chosen.im<<64n,ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,limit),v=verify(sourceBits,chosen.re,chosen.im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} -`} -class ReferenceService{ - constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16} - ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1,fresh=false){const key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,'guarded-perturb-v24.2.26'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} - cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} - destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} -} -const refs=new ReferenceService(); - -function fastReferenceBits(snap){const need=Math.ceil(Math.max(0,-log2FixedAt(snap.span,snap.bits)))+48;return Math.max(96,Math.min(snap.bits,need))} -function fastReferenceWorkerSource(){return String.raw` -'use strict'; -function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length} -function roundShift(v,b){if(b<=0)return v;const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q} -function fixedNum(v,b){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)),n=top*Math.pow(2,sh-b);return neg?-n:n} -function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}} -function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)return n+1}return 0} -function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]} -function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;kbest.score){best={...c,escape,score}}if(escape===0)break}return best} -function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf} -self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),bits=d.targetBits,centerRe=requant(sourceRe,d.sourceBits,bits),centerIm=requant(sourceIm,d.sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,d.iter),chosen=chooseReference(d.sourceBits,sourceRe,sourceIm,sourceSpan,w,h,d.iter,centerRef.escape),re=requant(chosen.re,d.sourceBits,bits),im=requant(chosen.im,d.sourceBits,bits),ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,d.iter),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} -`} -class FastReferenceService{ - constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=12} - ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([fastReferenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'fast reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.2.26'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} - cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} - destroy(){this.cancelPending('destroyed');this.cache.clear()} -} -const fastRefs=new FastReferenceService(); - -// ── WebGPU renderer ────────────────────────────────────────────────────── -function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})} -function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}} -function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a} -class WebGpuRenderer{ - constructor(adapter,device){ - this.adapter=adapter;this.device=device; - const ai=adapter.info||{}; - this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''}; - this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)}; - this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat(); - this.frame=null;this.deepCtx=null;this.fastCtx=null;this.refineDeepCtx=null;this.exportWs=null;this.refinePipelinePromise=null;this.likelyQueue=null;this.deepLikelyRefine=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'}); - device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)}); - this.ready=this.initPipelines(); - device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()}); - } - configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})} - async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m} - async initPipelines(){ - this.device.pushErrorScope?.('validation'); - try{ - const [dm,fm,xm,xhm,xpm,xsm,zm,zqm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]); - this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}}); - this.fast=this.device.createComputePipeline({layout:'auto',compute:{module:fm,entryPoint:'main'}}); - this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}}); - const [xpsm,xhsm]=await Promise.all([this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL),this.module('deep-bucket-histogram-stats-production',G.DEEP_BUCKET_HIST_STATS_WGSL)]); - this.deepPostStats=this.device.createComputePipeline({layout:'auto',compute:{module:xpsm,entryPoint:'main'}}); - this.deepBucketHist=this.device.createComputePipeline({layout:'auto',compute:{module:xhm,entryPoint:'main'}}); - this.deepBucketHistStats=this.device.createComputePipeline({layout:'auto',compute:{module:xhsm,entryPoint:'main'}}); - this.deepBucketPrefix=this.device.createComputePipeline({layout:'auto',compute:{module:xpm,entryPoint:'main'}}); - this.deepBucketScatter=this.device.createComputePipeline({layout:'auto',compute:{module:xsm,entryPoint:'main'}}); - this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}}); - this.correctQueued=this.device.createComputePipeline({layout:'auto',compute:{module:zqm,entryPoint:'main'}}); - this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}}); - this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}}); - this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}}); - this.context=canvas.getContext('webgpu'); - if(!this.context)throw new Error('WebGPU canvas contextを取得できません'); - webgpuCanvasClaimed=true;this.configure(); - }finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}} - } - async ensureRefinePipelines(){if(this.likelyQueue&&this.deepLikelyRefine)return;if(this.refinePipelinePromise)return this.refinePipelinePromise;this.refinePipelinePromise=(async()=>{const [lqm,drm]=await Promise.all([this.module('likely-refine-queue',G.LIKELY_QUEUE_WGSL),this.module('deep-likely-refine',G.DEEP_LIKELY_REFINE_WGSL)]);this.likelyQueue=this.device.createComputePipeline({layout:'auto',compute:{module:lqm,entryPoint:'main'}});this.deepLikelyRefine=this.device.createComputePipeline({layout:'auto',compute:{module:drm,entryPoint:'refine_likely'}})})().finally(()=>{this.refinePipelinePromise=null});return this.refinePipelinePromise} - frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','refineQueue','refineStats','refineQueueParams','refineBatchParams','refineRead','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null} - ensureFrame(w,h){ - const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage; - this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,refineQueue:null,refineStats:null,refineQueueParams:null,refineBatchParams:null,refineRead:null,numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame; - } - ensureDeepBucketState(f=this.frame){if(!f)return null;if(!f.deepBucketState){const d=this.device,B=GPUBufferUsage;f.deepBucketState=buf(d,DEEP_BUCKET_STATE_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-bucket-state')}return f} - ensureDeepQueueWorkspace(f=this.frame){if(!f)return null;this.ensureDeepBucketState(f);if(f.deepQueue&&f.deepQueueStats&&f.deepIndirect)return f;const d=this.device,B=GPUBufferUsage;f.deepQueueStats=buf(d,SPARSE_QUEUE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue-stats');f.deepQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue');f.deepIndirect=buf(d,SPARSE_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'deep-correction-indirect');return f} - ensureRefineWorkspace(f=this.frame){if(!f)return null;if(f.refineQueue&&f.refineStats&&f.refineQueueParams&&f.refineBatchParams&&f.refineRead)return f;const d=this.device,B=GPUBufferUsage;f.refineQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-queue');f.refineStats=buf(d,REFINE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-stats');f.refineQueueParams=buf(d,REFINE_QUEUE_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-queue-params');f.refineBatchParams=buf(d,REFINE_BATCH_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-batch-params');f.refineRead=buf(d,REFINE_STATS_BYTES,B.COPY_DST|B.MAP_READ,'likely-refine-readback');return f} - ensureExportWorkspace(){ - if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S; - this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs; - } - exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null} - setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}} - destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null} - setRefineDeepContext(ctx){if(this.refineDeepCtx&&this.refineDeepCtx.key===ctx.key)return;this.destroyRefineDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'idle-refinement-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.refineDeepCtx={...ctx,refsB}} - destroyRefineDeepContext(){if(this.refineDeepCtx)destroy(this.refineDeepCtx.refsB);this.refineDeepCtx=null} - setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}} - destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null} - directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})} - deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,0,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - colorParamsData(w,h,iter=state.fieldView?.iter??maxIter()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,effectiveColorCycle(iter),true);d.setFloat32(20,state.shift,true)})} - encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,pipeline=this.deepPostStats,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.fast.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fast);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHist.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHist);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHistStats.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeDeepBucketPrefix(encoder,{bucketState,queueStats,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketPrefix.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:bucketState}},{binding:1,resource:{buffer:queueStats}},{binding:2,resource:{buffer:indirect}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketPrefix);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end()} - encodeDeepBucketScatter(encoder,{pbuf,meta,bucketState,queueStats,queue,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeQueuedDeepCorrection(encoder,{pbuf,meta,smooth,unresolved,queueStats,queue,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.correctQueued.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{binding:5,resource:{buffer:queueStats}},{binding:6,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.correctQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()} - encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})} - async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null){ - await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep){this.setDeepContext(deepContext);this.ensureDeepBucketState(f)}else if(fastExtended){this.setFastContext(fastContext)}d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep)d.queue.writeBuffer(f.deepBucketState,0,new Uint32Array(DEEP_BUCKET_STATE_BYTES/4)); - const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;const params=deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,refX,refY):fastExtended?this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap);d.queue.writeBuffer(f.numericParams,0,params); - d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'}); - if(deep){this.encodeDeepPostStatsNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeDeepBucketHistogramStats(encoder,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,unresolved:f.unresolved,w:f.w,h:f.h});} - else if(fastExtended){this.encodeFastNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});} - else{this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});} - if(!deep){const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();} - d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;if(!deep)[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true; - } - async buildLikelyRefineQueue(token,mode=1,sparseStride=4){await this.ready;await this.ensureRefinePipelines();if(token!==state.token)return{count:0,overflow:0,cancelled:true};const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;d.queue.writeBuffer(f.refineStats,0,new Uint32Array(REFINE_STATS_BYTES/4));d.queue.writeBuffer(f.refineQueueParams,0,new Uint32Array([f.w,f.h,mode>>>0,sparseStride>>>0]));const e=d.createCommandEncoder({label:'likely-refine-queue-build'}),bg=d.createBindGroup({layout:this.likelyQueue.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.refineQueueParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.refineQueue}},{binding:3,resource:{buffer:f.refineStats}}]}),pass=e.beginComputePass();pass.setPipeline(this.likelyQueue);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.n/256));pass.end();e.copyBufferToBuffer(f.refineStats,0,f.refineRead,0,REFINE_STATS_BYTES);d.queue.submit([e.finish()]);await f.refineRead.mapAsync(GPUMapMode.READ);const a=new Uint32Array(f.refineRead.getMappedRange().slice(0));f.refineRead.unmap();if(token!==state.token)return{count:0,overflow:0,cancelled:true};return{count:Math.min(f.n,a[0]||0),overflow:a[1]||0,cancelled:false}} - async refineLikelyBatch({snap,iter,deepContext,token,base,end,forceStrict=false}){await this.ready;await this.ensureRefinePipelines();if(token!==state.token||base>=end)return false;const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setRefineDeepContext(deepContext);const ref=referencePixelForSource(deepContext?.source,snap,f.w,f.h);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,ref.x,ref.y,0,f.w,0));d.queue.writeBuffer(f.refineBatchParams,0,new Uint32Array([base>>>0,end>>>0,0,0]));const pipeline=this.deepLikelyRefine,bg0=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.refineDeepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),bg1=d.createBindGroup({layout:pipeline.getBindGroupLayout(1),entries:[{binding:0,resource:{buffer:f.refineQueue}},{binding:1,resource:{buffer:f.refineBatchParams}}]}),e=d.createCommandEncoder({label:'likely-idle-refine'}),pass=e.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg0);pass.setBindGroup(1,bg1);pass.dispatchWorkgroups(Math.ceil((end-base)/64));pass.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.idleRefineBatches++;return true} - async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true} - presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}} - presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])} - async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4)); - if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})} - const ca=this.colorParamsData(w,h,iter),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end(); - const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h,iter);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca); - for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()} - const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end(); - const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y=Math.max(32,Math.floor(n*.0005));return true} -function referencePixelForSource(src,snap,w,h){if(!src)return{x:w*.5,y:h*.5};const b=Math.max(snap.bits,src.bits),span=align(snap.span,snap.bits,b),dr=align(src.re,src.bits,b)-align(snap.re,snap.bits,b),di=align(src.im,src.bits,b)-align(snap.im,snap.bits,b);return{x:w*.5+fixedRatio(dr,span)*w,y:h*.5-fixedRatio(di,span)*w}} -function reusableDeepReference(snap,iter,w,h){const ctx=renderer&&renderer.deepCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} -function reusableFastReference(snap,iter,w,h){const ctx=renderer&&renderer.fastCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} - -// ── GPU startup / rendering orchestration ──────────────────────────────── -async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise} -function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx} -function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span} -function cancelIdleRefinement(){if(idleRefineTimer){clearTimeout(idleRefineTimer);idleRefineTimer=0}state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0} -function idleRefinementTargets(baseIter){if(baseIter>=8000)return[];const z=zoomExp(),mode=state.processMode,mul=mode==='power'?.75:mode==='fine'?1.25:mode==='validate'?1.4:1,cap=Math.min(8000,Math.max(baseIter*2,Math.round((1200+300*z)*mul))),raw=mode==='power'?[Math.min(cap,baseIter*2)]:[Math.min(cap,baseIter*2),Math.min(cap,baseIter*4),cap],out=[];for(const v of raw){const n=Math.max(baseIter+1,Math.floor(v));if(n>baseIter&&(!out.length||n!==out[out.length-1]))out.push(n)}return out} -function idleRefinementBatchBudget(){const m=state.processMode;if(m==='power')return 900000;if(m==='fine')return 2800000;if(m==='validate')return 1800000;return 1800000} -function idleRefinementQueuePolicy(stage){if(stage<=0)return{mode:1,stride:4};if(stage===1)return{mode:2,stride:8};return{mode:3,stride:16}} -function idleRefinementSliceMs(){return state.processMode==='power'?100:state.processMode==='fine'?220:160} -function idleRefinementPauseMs(){return state.processMode==='power'?160:80} -function canIdleRefine(token,snap,baseIter){return token===state.token&&renderer&&!state.rendering&&!state.pointerActive&&!state.wheelActive&&sameSnapshot(snap,snapshot())&&state.fieldView&&state.fieldView.iter===baseIter&&state.fieldView.w===canvas.width&&state.fieldView.h===canvas.height} -const sleep=ms=>new Promise(r=>setTimeout(r,ms)); -async function paintIdleRefinement(token,snap,baseIter){while(state.recoloring&&canIdleRefine(token,snap,baseIter))await sleep(12);if(!canIdleRefine(token,snap,baseIter))return false;await recolor();while((state.recoloring||state.recolorPending)&&canIdleRefine(token,snap,baseIter))await sleep(8);return canIdleRefine(token,snap,baseIter)} -function scheduleIdleRefinement(snap,baseIter,decision,token){cancelIdleRefinement();if(!renderer||(!decision.deep&&!decision.fastExtended))return;const targets=idleRefinementTargets(baseIter);if(!targets.length)return;idleRefineTimer=setTimeout(()=>{idleRefineTimer=0;runIdleRefinement(snap,baseIter,decision,token,targets)},320)} -async function runIdleRefinement(snap,baseIter,decision,token,targets){if(!canIdleRefine(token,snap,baseIter))return;state.refinementRunning=true;state.refinedIter=baseIter;updateStats();try{for(let stage=0;stage=idleRefinementSliceMs()){const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;updateStats();lastPaint=performance.now();await sleep(idleRefinementPauseMs())}}const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;state.fieldView.refinedIter=targetIter;updateStats();await sleep(idleRefinementPauseMs())}}catch(e){if(token===state.token){console.warn('idle refinement stopped:',e);state.gpuError=String(e&&e.message||e)}}finally{if(token===state.token){state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0;updateStats()}}} - -function cancelRender(){cancelIdleRefinement();state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled')} -function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()} -function schedule(){if(!raf)raf=requestAnimationFrame(loop)} -async function renderFrame(){ - const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height); - state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats(); - try{ - const r=renderer||await initRenderer();if(token!==state.token)return; - if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return} - const deep=decision.deep,fastExtended=decision.fastExtended;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false; - if(deep){const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height);if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 正確 reference再利用'}else{state.lastEngine='WebGPU · 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · 正確 perturbation'}} - else if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 高速拡張 reference再利用'}else{state.lastEngine='WebGPU · 高速拡張準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height);state.lastEngine='WebGPU · 高速拡張'}} - else state.lastEngine='WebGPU · 高速'; - const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate',referencePixel,fastExtended,fastCtx);if(!ok)return; - if(!deep){ - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false; - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0}); - if(fastExtended){const stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons} - const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token);return; - } - // Accurate mode keeps the previous committed frame visible while the new - // primary field is checked/corrected. The incomplete primary texture lives - // only in frame.back and is never presented to the viewer. - let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats(); - const n=canvas.width*canvas.height; - if(shouldApplySparseCorrection(state.unresolved,n)){ - state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats(); - const corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return;state.correctionPasses=1; - stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected; - } - const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return; - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · '+(referenceReused?'正確 reference再利用':'正確 reference自動選択')+(state.correctionPasses?' + DS correction':''); - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token); - }catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)} -} -function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · 正確モード非対応';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*effectiveColorCycle(iter)+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false}); -canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}}); -canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}}); -function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer); - -// ── URL / controls ─────────────────────────────────────────────────────── -function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');p.set('rm',state.renderMode);const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}} -function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';if(p.has('rm'))state.renderMode=p.get('rm')==='accurate'?'accurate':'fast';ensurePrecision();return true}catch{return false}} -function syncHistoryButtons(){} -function syncControls(){$('#renderMode').value=state.renderMode;$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()} -function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)} -function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')} -$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()}; -$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()}; -$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}}; -let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0; -function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色アニメ停止':'色を自動変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')} -function stopColorAuto(){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton()} -function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;const cmin=.001,cmax=.05,smin=0,smax=1;state.cycle+=state.colorCycleDir*.0006*dt;state.shift+=state.colorShiftDir*.012*dt;if(state.cycle>=cmax){state.cycle=cmax;state.colorCycleDir=-1}else if(state.cycle<=cmin){state.cycle=cmin;state.colorCycleDir=1}if(state.shift>=smax){state.shift=smax;state.colorShiftDir=-1}else if(state.shift<=smin){state.shift=smin;state.colorShiftDir=1}$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;recolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)} -$('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf)colorAutoRaf=requestAnimationFrame(colorAutoStep);else if(!state.colorAuto)stopColorAuto()}; -$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()}; -$('#renderMode').onchange=e=>{state.renderMode=e.target.value==='accurate'?'accurate':'fast';try{localStorage.setItem('mandelbrot.renderMode',state.renderMode)}catch{}markDirty()}; -$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}}; -addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}}); -addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}}); - -// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ────── -const exportJob={active:false,cancelled:false}; -function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)} -const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})(); -function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0} -function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out} -class StreamingPng{ - constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()} - async rows(filteredRows){await this.writer.write(filteredRows)} - async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})} - async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}} -} -function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}} -async function runExport(){ - if(exportJob.active)return; - const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return} - const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),fastExtended=!deep&&fastNeedsExtended(snap,w),strict=$('#exportPrecision').value==='strict'; - let ctx=null,fastCtx=null; - if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}else if(fastExtended){$('#exportStatus').textContent='高速拡張参照を準備中…';fastCtx=await fastRefs.request(snap,iter,w,h)} - const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1; - exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true; - let done=0,unresolvedSamples=0; - try{ - for(let y=0;y{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png'); - -function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.deep?'WebGPU 正確':decision.fastExtended?'WebGPU 高速':'WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'正確描画の補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.refinementRunning)status+=' · 補足精密化 '+state.refinementStage+' ('+state.refinedIter+' iter, 残 '+state.refinementQueue+')';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status} - -globalThis.__MANDEL_TEST__={ - async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard',renderMode='fast'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.renderMode=renderMode==='accurate'?'accurate':'fast';state.hq=false;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{width:canvas.width,height:canvas.height,state:this.state()}}, - async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)}, - state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter(),renderMode:state.renderMode,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused}), - 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 probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded():!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))}, - async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),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,deep,strict,ss,unresolved:result.unresolved||0}} -}; - -// ── boot / teardown ────────────────────────────────────────────────────── -addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();refs.destroy();fastRefs.destroy();if(renderer)renderer.destroy()},{once:true}); -try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}state.renderMode=localStorage.getItem('mandelbrot.renderMode')==='accurate'?'accurate':'fast'}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule(); -})(); diff --git a/scripts/build.mjs b/scripts/build.mjs deleted file mode 100644 index ac587b8..0000000 --- a/scripts/build.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import fs from 'node:fs/promises'; -const root=new URL('../',import.meta.url); -const template=await fs.readFile(new URL('../index.external.html',import.meta.url),'utf8'); -const kernels=await fs.readFile(new URL('../gpu-kernels.js',import.meta.url),'utf8'); -const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); -const safe=s=>s.replace(/<\/script/gi,'<\\/script'); -const standalone=template - .replace('',``) - .replace('',``) - .replace('','\n'); -await fs.writeFile(new URL('../index.html',import.meta.url),standalone); -for(const variant of ['standalone','hosted']){ - const dir=new URL(`../dist/${variant}/`,import.meta.url);await fs.rm(dir,{recursive:true,force:true});await fs.mkdir(dir,{recursive:true}); - if(variant==='standalone'){ - await fs.writeFile(new URL('index.html',dir),standalone); - }else{ - await fs.writeFile(new URL('index.html',dir),template); - await fs.copyFile(new URL('../gpu-kernels.js',import.meta.url),new URL('gpu-kernels.js',dir)); - await fs.copyFile(new URL('../script.js',import.meta.url),new URL('script.js',dir)); - await fs.writeFile(new URL('_headers',dir),`/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: no-referrer\n`); - } -} -console.log(JSON.stringify({status:'pass',outputs:['index.html','dist/standalone/index.html','dist/hosted'],standalone:'single-file'},null,2)); diff --git a/scripts/test-all.mjs b/scripts/test-all.mjs deleted file mode 100644 index dbcf029..0000000 --- a/scripts/test-all.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import {spawnSync} from 'node:child_process'; -const commands=[ - ['node',['--check','script.js']],['node',['--check','gpu-kernels.js']],['node',['--check','tests/webgpu-acceptance.js']], - ...['v24-wgsl-reserved.mjs','v24-source-contract.mjs','v24-index-contract.mjs','v24-standalone-ui-contract.mjs','v24-tree-contract.mjs','v24-deep-sparse-queue-model.mjs','v24-primary-post-stats-model.mjs','v24-reference-worker.mjs','v24-reference-recovery-model.mjs','v24-pan-reuse-model.mjs','v24-direct-model.mjs','v24-fast-precision-extension-model.mjs','v24-color-relative-model.mjs','v24-baseline-idle-refinement-model.mjs','v24-cpu-numeric-model.mjs','v24-sparse-correction-model.mjs','v24-bla-model.mjs','v24-geometry-contract.mjs','v24-coordinate-format.mjs','v24-png-stream-model.mjs','v24-acceptance-contract.mjs'].map(f=>['node',['tests/'+f]]) -]; -for(const [cmd,args] of commands){const r=spawnSync(cmd,args,{stdio:'inherit'});if(r.status!==0)process.exit(r.status??1)} -console.log(JSON.stringify({status:'pass',suite:'v24-static-and-cpu-model',realWebGPU:'run tests/webgpu-acceptance.html in a WebGPU-capable browser'},null,2)); diff --git a/tests/ACCURATE_HYBRID_WEBGPU_RESULTS.json b/tests/ACCURATE_HYBRID_WEBGPU_RESULTS.json new file mode 100644 index 0000000..f84d5e9 --- /dev/null +++ b/tests/ACCURATE_HYBRID_WEBGPU_RESULTS.json @@ -0,0 +1,13 @@ +{ + "environment": "Chromium 144 + SwiftShader/Vulkan, compute-only WebGPU", + "size": [320, 220], + "iterations": 750, + "lateEscapeThreshold": 24, + "cases": [ + {"name": "reset", "center": [-0.5, 0.0], "span": 3.4, "gpuMs": 203.3, "sampledPixels": 1200, "classificationMismatchVsCpuDouble": 0, "escapeIterationMismatchVsCpuDouble": 2}, + {"name": "seahorse-low", "center": [-0.743643887037151, 0.13182590420533], "span": 0.1, "gpuMs": 160.7, "sampledPixels": 1200, "classificationMismatchVsCpuDouble": 0, "escapeIterationMismatchVsCpuDouble": 0}, + {"name": "cusp-low", "center": [0.25, 0.0], "span": 0.85, "gpuMs": 87.8, "sampledPixels": 1200, "classificationMismatchVsCpuDouble": 0, "escapeIterationMismatchVsCpuDouble": 0} + ], + "stripOrderTest": {"pixels": 6144, "fullPassVs4RowQueuedStripsMismatch": 0}, + "note": "GPU time is software-GPU time and is only a relative engineering check." +} diff --git a/tests/ACCURATE_SEED_FUZZ.json b/tests/ACCURATE_SEED_FUZZ.json new file mode 100644 index 0000000..de52715 --- /dev/null +++ b/tests/ACCURATE_SEED_FUZZ.json @@ -0,0 +1,8 @@ +{ + "samples": 60000, + "mixture": "global Mandelbrot bounds + targeted known boundary neighborhoods", + "maxIter": 900, + "acceptedRule": "f32 escape iteration < 24", + "acceptedSamples": 33637, + "acceptedEscapeIterationMismatchVsCpuDouble": 0 +} diff --git a/tests/ACTUAL_WEBGPU_COMPILE.json b/tests/ACTUAL_WEBGPU_COMPILE.json new file mode 100644 index 0000000..967d415 --- /dev/null +++ b/tests/ACTUAL_WEBGPU_COMPILE.json @@ -0,0 +1,38 @@ +{ + "DIRECT_F32_WGSL": { + "errors": [], + "warnings": [] + }, + "DIRECT_ACCURATE_SEED_WGSL": { + "errors": [], + "warnings": [] + }, + "DIRECT_DS_CORRECT_WGSL": { + "errors": [], + "warnings": [] + }, + "FAST_PERTURB_WGSL": { + "errors": [], + "warnings": [] + }, + "DEEP_PERTURB_WGSL": { + "errors": [], + "warnings": [] + }, + "DEEP_CORRECT_WGSL": { + "errors": [], + "warnings": [] + }, + "COLOR_WGSL": { + "errors": [], + "warnings": [] + }, + "PRESENT_WGSL": { + "errors": [], + "warnings": [] + }, + "BLA_GPU_FAST_FRAME_WGSL": { + "errors": [], + "warnings": [] + } +} diff --git a/tests/BASELINE_KERNEL_HASHES.json b/tests/BASELINE_KERNEL_HASHES.json new file mode 100644 index 0000000..d2e91b7 --- /dev/null +++ b/tests/BASELINE_KERNEL_HASHES.json @@ -0,0 +1,10 @@ +{ + "DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922", + "FAST_PERTURB_WGSL": "b9db140f03fbd0aa36a5ca7dbbcbfec471e1587ebc632953f9eb73a35fb9a978", + "DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65", + "DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88", + "COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3", + "PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc", + "BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019", + "BLA_GPU_FAST_FRAME_WGSL": "2e4cdbd31d60c22d87fb04847af4d2c1d6de7d213da5cdd27234e5d8f9672553" +} diff --git a/tests/BLA_PREP_BENCHMARK.json b/tests/BLA_PREP_BENCHMARK.json new file mode 100644 index 0000000..cecc833 --- /dev/null +++ b/tests/BLA_PREP_BENCHMARK.json @@ -0,0 +1,9 @@ +{ + "scenario": "classic Seahorse Valley, 654x690, span 3.4e-13, 2900 iterations, 64-point production gate equivalent", + "oldValidate64Ms": 446.9, + "oldTableBuildMs": 58.4, + "newBuildOnlyMs": 60.0, + "newTableBuildMs": 59.4, + "estimatedSynchronousPrepSavedMs": 386.9, + "note": "Node/V8 worker-model timing, not GPU frame time. The production path now builds the table only; the 64-point decision remains on the GPU probe." +} diff --git a/tests/KERNEL_HASHES_V24_2_54.json b/tests/KERNEL_HASHES_V24_2_54.json new file mode 100644 index 0000000..7531d51 --- /dev/null +++ b/tests/KERNEL_HASHES_V24_2_54.json @@ -0,0 +1,23 @@ +{ + "version": "9af1429215fe356104bcbe991c98d101bbf241a68a38232533918454505014d3", + "DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922", + "DIRECT_ACCURATE_SEED_WGSL": "478146d2f8a0bb5608850dcbbdfab183bc6e9105eab2afb4122892f7353349c3", + "DIRECT_DS_CORRECT_WGSL": "6c61934c796c67f08ebdee0b7156c8c8d891104f3fd1d2408bad16d9a76f7c00", + "FAST_PERTURB_WGSL": "b9db140f03fbd0aa36a5ca7dbbcbfec471e1587ebc632953f9eb73a35fb9a978", + "DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65", + "DEEP_PERTURB_POSTSTATS_WGSL": "364d916da3d90b049ac24423a8112dd587ebd756fa97c99a6717f869c6b6cfa1", + "DEEP_BUCKET_HIST_WGSL": "3458f908fa239b16667e5c82dae25274d5493d647595230574a3d65622a54aaf", + "DEEP_BUCKET_HIST_STATS_WGSL": "b464d503703646e3c990e10a0cb40dacdc3677447b7a35e03c80a50bfca23824", + "DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da", + "DEEP_BUCKET_SCATTER_WGSL": "7575c4fc5f4d5674dfc3997189212ffc6fbe240a9d8b8b3f8ef883cd4eae47a1", + "DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88", + "DEEP_CORRECT_QUEUE_WGSL": "1337469b0673a4da20670daf220bec5883366e2454acc8c7923e472a5cec7a7f", + "COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3", + "AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342", + "PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc", + "BLA_GPU_SHADOW_WGSL": "e830339a150dd5722ca7c58674193174606adaf488bfd817408f2eeb4583514f", + "BLA_GPU_CANARY_WGSL": "3c219f657b2890b40f7cfa0297e13a670819d4fed333fcb30cb37196c5755f55", + "BLA_GPU_CANARY_APPLY_WGSL": "d02bb3ef007680a687960125ee9dded1fc4b8a4b091a6d924408a2e1f94a05e1", + "BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019", + "BLA_GPU_FAST_FRAME_WGSL": "2e4cdbd31d60c22d87fb04847af4d2c1d6de7d213da5cdd27234e5d8f9672553" +} \ No newline at end of file diff --git a/tests/KERNEL_HASHES_V24_2_55.json b/tests/KERNEL_HASHES_V24_2_55.json new file mode 100644 index 0000000..df75a7a --- /dev/null +++ b/tests/KERNEL_HASHES_V24_2_55.json @@ -0,0 +1,23 @@ +{ + "version": "62ebee65d5378e9379394b24229266de255fa2f241a2eecd31fbab0721d2ae00", + "DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922", + "DIRECT_ACCURATE_SEED_WGSL": "478146d2f8a0bb5608850dcbbdfab183bc6e9105eab2afb4122892f7353349c3", + "DIRECT_DS_CORRECT_WGSL": "6c61934c796c67f08ebdee0b7156c8c8d891104f3fd1d2408bad16d9a76f7c00", + "FAST_PERTURB_WGSL": "2cc5841740456f10ed3f64005e72ac6ff87d2c38926260a1fcb1521a389013a0", + "DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65", + "DEEP_PERTURB_POSTSTATS_WGSL": "364d916da3d90b049ac24423a8112dd587ebd756fa97c99a6717f869c6b6cfa1", + "DEEP_BUCKET_HIST_WGSL": "3458f908fa239b16667e5c82dae25274d5493d647595230574a3d65622a54aaf", + "DEEP_BUCKET_HIST_STATS_WGSL": "b464d503703646e3c990e10a0cb40dacdc3677447b7a35e03c80a50bfca23824", + "DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da", + "DEEP_BUCKET_SCATTER_WGSL": "7575c4fc5f4d5674dfc3997189212ffc6fbe240a9d8b8b3f8ef883cd4eae47a1", + "DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88", + "DEEP_CORRECT_QUEUE_WGSL": "1337469b0673a4da20670daf220bec5883366e2454acc8c7923e472a5cec7a7f", + "COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3", + "AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342", + "PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc", + "BLA_GPU_SHADOW_WGSL": "e830339a150dd5722ca7c58674193174606adaf488bfd817408f2eeb4583514f", + "BLA_GPU_CANARY_WGSL": "3c219f657b2890b40f7cfa0297e13a670819d4fed333fcb30cb37196c5755f55", + "BLA_GPU_CANARY_APPLY_WGSL": "d02bb3ef007680a687960125ee9dded1fc4b8a4b091a6d924408a2e1f94a05e1", + "BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019", + "BLA_GPU_FAST_FRAME_WGSL": "2e4cdbd31d60c22d87fb04847af4d2c1d6de7d213da5cdd27234e5d8f9672553" +} \ No newline at end of file diff --git a/tests/SCHEDULER_SYNC_MODEL.json b/tests/SCHEDULER_SYNC_MODEL.json new file mode 100644 index 0000000..8c02ccd --- /dev/null +++ b/tests/SCHEDULER_SYNC_MODEL.json @@ -0,0 +1,37 @@ +[ + { + "iter": 900, + "baselineRows": 31, + "v53Waits": 31, + "v54FastWaits": 8, + "v54BlaWaits": 2 + }, + { + "iter": 1500, + "baselineRows": 19, + "v53Waits": 50, + "v54FastWaits": 13, + "v54BlaWaits": 4 + }, + { + "iter": 3000, + "baselineRows": 9, + "v53Waits": 105, + "v54FastWaits": 27, + "v54BlaWaits": 7 + }, + { + "iter": 6000, + "baselineRows": 4, + "v53Waits": 235, + "v54FastWaits": 59, + "v54BlaWaits": 15 + }, + { + "iter": 12000, + "baselineRows": 2, + "v53Waits": 470, + "v54FastWaits": 118, + "v54BlaWaits": 30 + } +] diff --git a/tests/legacy-v23/numeric-policy-v23.json b/tests/legacy-v23/numeric-policy-v23.json deleted file mode 100644 index c854853..0000000 --- a/tests/legacy-v23/numeric-policy-v23.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "format": "mandelbrot-numeric-policy-v1", - "id": "numeric-policy-v23", - "rendererVersion": 23, - "pixelContract": { - "mapping": "centered-rational", - "fixedPointRounding": "nearest-ties-away-from-zero", - "fixedPointIntegerMismatchMax": 0, - "float64CoordinateUlpMax": 4, - "tileSeamPixelMismatchMax": 0 - }, - "validated": { - "falseEscapedMax": 0, - "falseInteriorProvenMax": 0, - "unresolvedMax": 0, - "escapeIterationAbsMax": 0, - "magnitudeRelativeErrorMax": 1e-12, - "referenceGuardBits": 64 - }, - "balanced": { - "falseEscapedMax": 0, - "falseInteriorProvenMax": 0, - "interiorLikelyMismatch": "record", - "unresolved": "record" - }, - "color": { - "smoothCorrectionRelativeErrorMax": 0.00003, - "simdScalarRelativeErrorMax": 1e-7 - } -} diff --git a/tests/scenes.json b/tests/scenes.json deleted file mode 100644 index 2d3001b..0000000 --- a/tests/scenes.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "format": "mandelbrot-scene-corpus-v2", - "rendererVersion": 23, - "pixelContract": "centered-rational-ties-away-from-zero", - "numericPolicy": "numeric-policy-v23", - "scenes": [ - { - "id": "z0", - "re": "-0.5", - "im": "0", - "span": "3.4", - "coordinate": {"kind":"exact-rational","reNumerator":"-1","reDenominator":"2","imNumerator":"0","imDenominator":"1"}, - "oracle": {"kind":"overview","membership":"mixed"}, - "tags": ["shallow","overview"] - }, - { - "id": "period2-cusp-z14", - "re": "-0.75", - "im": "0", - "span": "3.4e-14", - "coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"}, - "oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"}, - "tags": ["deep","boundary","parabolic"] - }, - { - "id": "swirly-seahorses-z12", - "re": "-0.7453983606667815", - "im": "0.1125046349959942", - "span": "3.52e-12", - "coordinate": {"kind":"exact-rational","reNumerator":"-7453983606667815","reDenominator":"10000000000000000","imNumerator":"1125046349959942","imDenominator":"10000000000000000"}, - "oracle": {"kind":"published-performance-scene","membership":"mixed","iterations":2000,"source":"https://github.com/ckormanyos/mandelbrot#swirly-seahorses-and-mini-mandelbrot"}, - "tags": ["deep","boundary","performance"] - }, - { - "id": "period2-cusp-z20", - "re": "-0.75", - "im": "0", - "span": "3.4e-20", - "coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"}, - "oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"}, - "tags": ["deep","boundary","warm-reference","parabolic"] - }, - { - "id": "period2-cusp-z100", - "re": "-0.75", - "im": "0", - "span": "3.4e-100", - "coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"}, - "oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"}, - "tags": ["deep","precision","parabolic"] - }, - { - "id": "period3-interior", - "re": "-0.122561166876653619975245551820735654052", - "im": "0.744861766619744236593170428604392367240", - "span": "1e-8", - "coordinate": {"kind":"decimal","significantDigits":39,"guardDigitsAtViewport":27}, - "oracle": {"kind":"polynomial-root","polynomial":"c^3+2c^2+c+1","membership":"interior","period":3}, - "tags": ["interior","periodic"] - }, - { - "id": "period2-cusp-e280", - "re": "-0.75", - "im": "0", - "span": "1e-280", - "coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"}, - "oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"}, - "tags": ["deep","deep-scaled-boundary","parabolic"] - } - ], - "viewports": [ - {"id":"mobile","cssWidth":390,"cssHeight":844,"dpr":3}, - {"id":"desktop","cssWidth":1440,"cssHeight":900,"dpr":2}, - {"id":"4k","cssWidth":3840,"cssHeight":2160,"dpr":1} - ] -} diff --git a/tests/static_regression.py b/tests/static_regression.py new file mode 100644 index 0000000..e48f030 --- /dev/null +++ b/tests/static_regression.py @@ -0,0 +1,38 @@ +from pathlib import Path +import re, subprocess, tempfile, json +ROOT=Path(__file__).resolve().parents[1] +CUR=ROOT/'index.html' +OLD=ROOT/'tests'/'KERNEL_HASHES_V24_2_54.json' +NEW=ROOT/'tests'/'KERNEL_HASHES_V24_2_55.json' + +def scripts(path): return re.findall(r']*>(.*?)',path.read_text(),re.S) + +def syntax(path): + for i,code in enumerate(scripts(path)): + q=Path(tempfile.gettempdir())/f'mandel_v55_check_{i}.js'; q.write_text(code) + r=subprocess.run(['node','--check',str(q)],capture_output=True,text=True) + assert r.returncode==0, r.stderr + +def kernels(path): + code=scripts(path)[0] + q=Path(tempfile.gettempdir())/'mandel_v55_kernel_dump.js' + q.write_text(code+"\nconst crypto=require('crypto');const G=globalThis.MANDEL_WEBGPU_KERNELS;const out={};for(const k of Object.keys(G)){if(typeof G[k]==='string')out[k]=crypto.createHash('sha256').update(G[k]).digest('hex')}console.log(JSON.stringify(out));") + r=subprocess.run(['node',str(q)],capture_output=True,text=True,check=True) + return json.loads(r.stdout.strip().splitlines()[-1]) + +syntax(CUR) +old=json.loads(OLD.read_text()); cur=kernels(CUR); expected=json.loads(NEW.read_text()) +assert cur==expected +changed=[k for k,v in old.items() if cur.get(k)!=v] +assert changed==['version','FAST_PERTURB_WGSL'], changed +html=CUR.read_text() +assert "version:'24.2.55-no-black-hole-async-recovery'" in html +assert 'p.unknownOnly==2u && reason!=0u' in html +assert "fast-coverage-repair-and-color" in html +assert 'reprojectionSafe(rawHistory,2.25,.45)' in html +assert 'scheduleFastNumericRecovery' in html and 'runFastNumericRecovery' in html +assert 'numericFrameComplete()' in html +assert "requestBuild({ctx,snap,iter,width,height,refPixel,safety=1/64})" in html +assert "type:'build'" in html and 'buildOnly=d.type===\'build\'' in html +assert not list(ROOT.glob('index.baseline*.html')) +print('PASS: syntax; only FAST repair gate changed in production kernels; coverage repair, broad temporal fill, async recovery, build-only BLA prep, single HTML') diff --git a/tests/v24-acceptance-contract.mjs b/tests/v24-acceptance-contract.mjs deleted file mode 100644 index 9582170..0000000 --- a/tests/v24-acceptance-contract.mjs +++ /dev/null @@ -1,10 +0,0 @@ -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(/renderMode:'fast'/.test(s)&&/fast mode did not select Direct/.test(s),'manual fast acceptance gate missing'); -must(/renderMode:'accurate'/.test(s)&&/accurate mode did not select Deep/.test(s),'manual accurate acceptance gate missing'); -must(/referenceReused/.test(s)&&/pan did not reuse Deep reference/.test(s),'Deep pan reference reuse acceptance gate missing'); -must(/corrected production frame produced UNKNOWN/.test(s),'corrected Deep UNKNOWN gate missing'); -must(/forceDeep:true,correctUnknown:true/.test(s),'forced corrected Deep probe missing'); -must(/exportSmoke\.unresolved!==0/.test(s)&&/exportSmokeAA\.unresolved!==0/.test(s),'corrected export unresolved gate missing'); -console.log(JSON.stringify({status:'pass',checks:6,manualModes:true,panReferenceReuse:true},null,2)); diff --git a/tests/v24-baseline-idle-refinement-model.mjs b/tests/v24-baseline-idle-refinement-model.mjs deleted file mode 100644 index 109f9a5..0000000 --- a/tests/v24-baseline-idle-refinement-model.mjs +++ /dev/null @@ -1,46 +0,0 @@ -import fs from 'node:fs/promises'; -import vm from 'node:vm'; -import crypto from 'node:crypto'; - -const root=new URL('../',import.meta.url); -const script=await fs.readFile(new URL('script.js',root),'utf8'); -const kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; -const sha=s=>crypto.createHash('sha256').update(s).digest('hex'); -const ctx={globalThis:{}};vm.runInNewContext(kernels,ctx);const G=ctx.globalThis.MANDEL_WEBGPU_KERNELS; -const expected={ - DIRECT_F32_WGSL:'59301bf0c5ba3dda4a4055335b32d4a3f84a21991be30f8efc74deb5a5f11ccf', - FAST_PERTURB_WGSL:'03b45713b1bbb7b56e18bd8fdf0f2d8161d7ed25adf02482b41c1038dc16e3b8', - DEEP_PERTURB_WGSL:'2865a015d5c1a9b459085b7044e23563fcc5f0eec5362634249b6604a47e660a', - COLOR_WGSL:'cccac7bfb69c64093c46c3468ff9d685d680ba3a88de0d1ea57168d91a067acf' -}; -for(const [k,h] of Object.entries(expected))must(sha(G[k])===h,`${k} changed from v24.2.18 baseline`); - -const fallback=script.match(/function renderFallback\(.*?\nasync function recolor/s)?.[0]?.replace(/\nasync function recolor$/,''); -must(fallback,'fallback function not found'); -must(sha(fallback)==='689a6f56aedd416b5e512b17ab6a3ddebcf7c73494e0a8a600ce74a06d8073a2','fallback changed from v24.2.18 baseline'); -must(/function maxIter\(\)\{if\(!state\.adaptive\)return state\.baseIter;const z=zoomExp\(\),bonus=Math\.max\(0,Math\.floor\(70\*Math\.sqrt\(z\)\+15\*z\)\)/.test(script),'v24.2.18 maxIter policy changed'); -must(/scheduleIdleRefinement\(snap,iter,decision,token\);return;/.test(script),'fast idle refinement is not scheduled after primary completion'); -must(/presentFrame\(\{scaleX:1,scaleY:1,offsetX:0,offsetY:0\}\).*scheduleIdleRefinement\(snap,iter,decision,token\)/s.test(script),'accurate idle refinement is not scheduled after primary presentation'); -must(/ensureRefinePipelines\(\)/.test(script)&&/idleRefineTimer=setTimeout/.test(script),'refinement is not lazy/idle'); -const initBlock=script.slice(script.indexOf('async initPipelines(){'),script.indexOf('async ensureRefinePipelines(){')); -must(!/LIKELY_QUEUE_WGSL|FAST_LIKELY_REFINE_WGSL|DEEP_LIKELY_REFINE_WGSL/.test(initBlock),'refinement shaders are compiled on primary startup path'); -must(/if\(\(\(fieldMeta\[out\]>>28u\)&3u\)!=FIELD_INTERIOR_LIKELY\)\{return;\}/.test(G.DEEP_LIKELY_REFINE_WGSL),'deep refinement can overwrite non-likely pixels'); -must(/setRefineDeepContext/.test(script)&&/this\.refineDeepCtx\.refsB/.test(script),'idle refinement replaces the primary Deep reference context'); -must(/async function paintIdleRefinement/.test(script)&&/async recolor\(token,iter=state\.fieldView\?\.iter\?\?maxIter\(\)\)/.test(script),'idle refinement does not preserve fieldView baseline color normalization'); -must(!/readMeta\(|readFrameMeta|copyBufferToBuffer\(f\.meta/.test(script.slice(script.indexOf('async function runIdleRefinement'),script.indexOf('function renderFallback'))),'idle refinement performs full meta readback'); -must(/REFINE_STATS_BYTES=16/.test(script),'queue readback is not constrained to 16-byte stats'); -must(/\.0006\*dt/.test(script)&&/\.012\*dt/.test(script),'automatic color speed is not 1/10 of v24.2.18'); - -const baseIter=z=>350+Math.floor(70*Math.sqrt(z)+15*z); -const targetList=(z,base)=>{if(base>=8000)return[];const cap=Math.min(8000,Math.max(base*2,Math.round(1200+300*z))),raw=[Math.min(cap,base*2),Math.min(cap,base*4),cap],out=[];for(const v of raw){const n=Math.max(base+1,Math.floor(v));if(n>base&&(!out.length||n!==out.at(-1)))out.push(n)}return out}; -const batchPixels=(iter,budget)=>Math.max(64,Math.floor(Math.max(64,budget/Math.max(1,iter))/64)*64); -const cssW=1920,cssH=1080,pixelBudget=1572864,dpr=Math.sqrt(pixelBudget/(cssW*cssH)),w=Math.round(cssW*dpr),h=Math.round(cssH*dpr),pixels=w*h; -must(targetList(500,baseIter(500)).length===0,'idle refinement should stop once baseline iteration already exceeds 8000'); -const cases=[]; -for(const z of [6,8,12,20]){ - const base=baseIter(z),targets=targetList(z,base),row={zoomExp:z,baseIter:base,primaryWorstSampleIterations:pixels*base,targets,batches:{guardedIdle:[]}}; - for(const [name,budget] of [['guardedIdle',1800000]])for(const iter of targets){const px=batchPixels(iter,budget),work=px*iter;must(work<=budget,`${name} refinement batch exceeds budget at z${z}: ${work}>${budget}`);row.batches[name].push({iter,pixels:px,worstSampleIterations:work,budget})} - cases.push(row); -} -console.log(JSON.stringify({status:'pass',check:'v24.2.18-baseline-plus-idle-refinement',baselineShaderHashes:expected,representativeDisplay:{css:[cssW,cssH],internal:[w,h],pixels},cases,qualityInvariant:'primary output is v24.2.18; idle refinement always uses guarded Deep, re-checks FIELD_INTERIOR_LIKELY, and never overwrites resolved pixels'},null,2)); diff --git a/tests/v24-bla-model.mjs b/tests/v24-bla-model.mjs deleted file mode 100644 index 19730df..0000000 --- a/tests/v24-bla-model.mjs +++ /dev/null @@ -1,4 +0,0 @@ -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'); -const failures=[];if(/buildBla\(|BlaNode|useBla|blaLevels/.test(script+kernels))failures.push('BLA code remains in production');if(!/blaEnabled:false/.test(script))failures.push('sidecar/diagnostics do not declare BLA disabled');if(failures.length)throw new Error(failures.join('; ')); -console.log(JSON.stringify({status:'pass',kind:'bla-disabled-contract',reason:'dense swirly regression demonstrated unsafe f32-quantized BLA classifications',productionBla:false},null,2)); diff --git a/tests/v24-color-relative-model.mjs b/tests/v24-color-relative-model.mjs deleted file mode 100644 index d24efc9..0000000 --- a/tests/v24-color-relative-model.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import fs from 'node:fs/promises'; -const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; -const base=350,deltaCycle=0.001,relativeIteration=0.6; -function effectiveCycle(cycle,iter){return cycle*base/Math.max(base,iter)} -const changes=[350,3500,35000].map(iter=>relativeIteration*iter*(effectiveCycle(0.008+deltaCycle,iter)-effectiveCycle(0.008,iter))); -const spread=Math.max(...changes)-Math.min(...changes); -must(spread<1e-12,'zoom-relative cycle response is not invariant'); -const min=.001,max=.05,N=1000; -const sliderToCycle=v=>min*Math.pow(max/min,v/N); -const ratios=[100,500,900].map(v=>sliderToCycle(v+50)/sliderToCycle(v)); -must(Math.max(...ratios)-Math.min(...ratios)<1e-12,'log slider does not preserve equal multiplicative steps'); -must(/effectiveColorCycle\(iter/.test(script)&&/sliderToCycle/.test(script),'production relative color functions missing'); -console.log(JSON.stringify({status:'pass',check:'zoom-relative-color-cycle',phaseDelta:changes[0],sliderStepRatio:ratios[0]},null,2)); diff --git a/tests/v24-coordinate-format.mjs b/tests/v24-coordinate-format.mjs deleted file mode 100644 index 6642042..0000000 --- a/tests/v24-coordinate-format.mjs +++ /dev/null @@ -1,7 +0,0 @@ -function rd(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q} -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);if(neg)n=-n;return[n,10n**BigInt(places)]} -function fixed(t,b){const[n,d]=decRatio(t);return rd(n*(1n<>BigInt(bits);let s=q.toString().padStart(bits+1,'0');s=s.slice(0,-bits)+'.'+s.slice(-bits);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} -function parseFixed(s,bits){const[n,d]=decRatio(s);return n*(1n<[s.id,s]));map.set('period2-cusp-e400',{id:'period2-cusp-e400',re:'-0.75',im:'0',span:'1e-400'});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)} -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(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<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<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=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=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;yr.guardMismatch||r.falseEscaped||r.falseBounded))throw new Error('numeric classification failure '+JSON.stringify(report)); -const allUnknownStrict=report.filter(r=>r.mode==='strict'&&r.unknown===pts.length); -if(allUnknownStrict.length)throw new Error('strict mode became all UNKNOWN: '+JSON.stringify(allUnknownStrict)); -console.log(JSON.stringify({status:'pass',kind:'guarded-production-equation-cpu-f32-model-not-real-gpu',thresholds:{balanced:1e-3,strict:1e-4},report,denseRegression:dense},null,2)); diff --git a/tests/v24-deep-sparse-queue-model.mjs b/tests/v24-deep-sparse-queue-model.mjs deleted file mode 100644 index 9f3b0c0..0000000 --- a/tests/v24-deep-sparse-queue-model.mjs +++ /dev/null @@ -1,78 +0,0 @@ -import fs from 'node:fs/promises'; - -await import('../gpu-kernels.js'); -const G=globalThis.MANDEL_WEBGPU_KERNELS; -const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; - -must(!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('bucketState'),'production post-stats perturbation shader contains bucket state'); -must(!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('unknownQueue'),'production post-stats perturbation shader writes the UNKNOWN queue'); -must(!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('atomicAdd(&unresolved.remaining'),'production post-stats perturbation still performs per-UNKNOWN stats atomics'); -must(G.DEEP_BUCKET_HIST_STATS_WGSL.includes('localReasons'),'post-stats histogram lacks workgroup reason aggregation'); -must(G.DEEP_BUCKET_HIST_WGSL.includes('array,8>'),'bucket histogram lacks 8-bin workgroup aggregation'); -must(G.DEEP_BUCKET_HIST_WGSL.includes('(n*8u)/max(1u,p.maxIter)'),'bucket histogram does not use normalized UNKNOWN iteration'); -must(G.DEEP_BUCKET_PREFIX_WGSL.includes('atomicStore(&bucketState[8u+b],total)'),'bucket prefix does not initialize scatter cursors'); -must(G.DEEP_BUCKET_PREFIX_WGSL.includes('atomicStore(&bucketState[16u+b],total)'),'bucket prefix does not retain diagnostic offsets'); -must(G.DEEP_BUCKET_SCATTER_WGSL.includes('atomicAdd(&bucketState[8u+lane],c)'),'bucket scatter lacks workgroup range reservation'); -must(G.DEEP_BUCKET_SCATTER_WGSL.includes('atomicAdd(&localRanks[bucket],1u)'),'bucket scatter lacks workgroup-local rank allocation'); -must(G.DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)'),'queued correction is not a 1D 64-lane kernel'); -must(G.DEEP_CORRECT_QUEUE_WGSL.includes('let out=unknownQueue[qi]'),'queued correction does not consume queue indices'); -must(/this\.deepPostStats/.test(script)&&/this\.deepBucketHistStats/.test(script)&&/this\.deepBucketPrefix/.test(script)&&/this\.deepBucketScatter/.test(script)&&/this\.correctQueued/.test(script),'production post-stats/bucket pipelines missing'); -must(/encodeDeepPostStatsNumeric\(encoder,[\s\S]*encodeDeepBucketHistogramStats\(encoder,/.test(script),'production frame order is not post-stats primary -> histogram/stats'); -must(/correctUnknownFrame[\s\S]*encodeDeepBucketPrefix\(e,[\s\S]*encodeDeepBucketScatter\(e,[\s\S]*encodeQueuedDeepCorrection\(e,/.test(script),'production correction order is not prefix -> scatter -> indirect correction'); -must(!/correctUnknownFrame[\s\S]{0,1600}encodeDeepBucketHistogram\(e/.test(script),'production correction redundantly rescans the histogram'); -must((script.match(/dispatchWorkgroups\(Math\.ceil\(w\/64\),h\)/g)||[]).length>=2,'bucket histogram/scatter are not dispatched as 64-pixel row groups'); -must(/dispatchWorkgroupsIndirect\(indirect,0\)/.test(script),'production indirect correction dispatch missing'); -must(/encodeCorrectionNumeric/.test(script),'dense correction path for probes/exports was removed'); - -const FIELD_UNKNOWN=0,ITER_MASK=0x000fffff; -const packUnknown=n=>(n&ITER_MASK)>>>0; -const packKnown=n=>(((1<<28)|(n&ITER_MASK))>>>0); -const bucketOf=(meta,maxIter)=>Math.min(7,Math.floor(((meta&ITER_MASK)*8)/Math.max(1,maxIter))); -let seed=0x9e3779b9; -const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/0x100000000}; -const shuffle=a=>{for(let i=a.length-1;i>0;i--){const j=Math.floor(rnd()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}; -const cases=[]; -for(const [w,h,rate,maxIter] of [[1,1,1,350],[17,9,.01,413],[64,64,.05,521],[65,7,.2,613],[257,131,.2,721],[320,180,.8,721],[511,257,0,901]]){ - const n=w*h,meta=new Uint32Array(n); - for(let i=0;i[]); - for(let i=0;i>>28)===FIELD_UNKNOWN)expected[bucketOf(meta[i],maxIter)].push(i); - const counts=expected.map(x=>x.length),offsets=[];let total=0; - for(const c of counts){offsets.push(total);total+=c} - const cursors=[...offsets],queue=new Array(total),groups=[]; - for(let y=0;y[]); - for(let lane=0;lane<64;lane++){ - const x=gx*64+lane;if(x>=w)continue;const out=y*w+x; - if((meta[out]>>>28)===FIELD_UNKNOWN)local[bucketOf(meta[out],maxIter)].push(out); - } - for(let b=0;b<8;b++)if(local[b].length){ - globalReservations++; - const base=cursors[b];cursors[b]+=local[b].length; - const lanes=shuffle([...local[b]]); // workgroup atomic rank order is unspecified. - for(let j=0;ja+x.length,0),`total mismatch ${w}x${h}`); - must(queue.length===total,`queue length mismatch ${w}x${h}`); - must(new Set(queue).size===queue.length,`duplicate queue entry ${w}x${h}`); - for(let b=0;b<8;b++){ - must(cursors[b]===offsets[b]+counts[b],`cursor mismatch bucket ${b} ${w}x${h}`); - const got=queue.slice(offsets[b],offsets[b]+counts[b]).sort((a,c)=>a-c); - const exp=[...expected[b]].sort((a,c)=>a-c); - must(got.length===exp.length&&got.every((v,i)=>v===exp[i]),`bucket membership mismatch b${b} ${w}x${h}`); - must(got.every(i=>bucketOf(meta[i],maxIter)===b),`bucket range contamination b${b} ${w}x${h}`); - } - const workgroups=Math.ceil(total/64),launched=workgroups*64,visited=[]; - for(let qi=0;qis.id==='z0'); const F=Math.fround; -function rd(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q} -function rs(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q} -function dec(t,b){let s=String(t).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 dg=(i+f).replace(/^0+(?=\d)/,'')||'0',p=f.length-e;if(p<0){dg+='0'.repeat(-p);p=0}let n=BigInt(dg);if(neg)n=-n;return rd(n*(1n<bail)return n+1}return limit} -function direct(cr,ci,limit){cr=F(cr);ci=F(ci);const U=2**-24,y2=F(ci*ci),x=F(cr-.25),q=F(F(x*x)+y2),lhs=F(q*F(q+x)),rhs=F(.25*y2),margin=F(16*U*(Math.abs(lhs)+Math.abs(rhs)+1));if(lhs4)return n+1}return limit} -const bits=256,re=dec(scene.re,bits),im=dec(scene.im,bits),span=dec(scene.span,bits),W=33,H=21,limit=800;let falseEscaped=0,falseBounded=0,guardMismatch=0,countMismatch=0,tested=0; -for(let y=0;y{if(!ok)throw new Error(msg)}; -function f32Ulp(x){x=Math.abs(Math.fround(x));if(x===0)return 2**-149;if(x<2**-126)return 2**-149;return 2**(Math.floor(Math.log2(x))-23)} -function ratio(center,span,w){return (span/w)/f32Ulp(Math.max(Math.abs(center),span*.75,2**-126))} -must(ratio(-0.7436,3.4e-3,1600)>4,'shallow Direct should remain active'); -must(ratio(-0.7436,3.4e-5,1600)<4,'collapsed f32 coordinate regime should use fast extension'); -must(/fastReferenceBits\(snap\)/.test(script)&&/Math\.max\(96,Math\.min\(snap\.bits,need\)\)/.test(script),'adaptive reduced-precision reference policy missing'); -must(/FAST_PERTURB_WGSL/.test(kernels)&&/render_fast/.test(kernels),'fast perturbation shader missing'); -must(/fastExtended=!deep&&fastNeedsExtended/.test(script),'fast extension routing missing'); -console.log(JSON.stringify({status:'pass',check:'fast-precision-extension',shallowRatio:ratio(-0.7436,3.4e-3,1600),deepRatio:ratio(-0.7436,3.4e-5,1600)},null,2)); diff --git a/tests/v24-geometry-contract.mjs b/tests/v24-geometry-contract.mjs deleted file mode 100644 index 3a0e167..0000000 --- a/tests/v24-geometry-contract.mjs +++ /dev/null @@ -1,6 +0,0 @@ -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=32;must(newCounterBytes16384?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)); diff --git a/tests/v24-index-contract.mjs b/tests/v24-index-contract.mjs deleted file mode 100644 index 8aa2661..0000000 --- a/tests/v24-index-contract.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import fs from 'node:fs/promises'; -const h=await fs.readFile(new URL('../index.html',import.meta.url),'utf8'); -const req=['id="view"','id="renderMode"','>高速<','>正確<','id="processMode"','id="palette"','id="cycle"','id="shift"','id="colorAuto"','id="exportDialog"','mandelbrot-bundle','data-bundle="gpu-kernels"','data-bundle="app"']; -for(const x of req)if(!h.includes(x))throw new Error('missing '+x); -for(const banned of ['詳細設定・正確な座標','診断情報','id="backendBench"','id="sparseBench"','id="benchmarkDialog"','id="sparseDialog"','id="coordReInput"'])if(h.includes(banned))throw new Error('obsolete UI remains: '+banned); -if(/{if(!ok)throw new Error(msg)}; -must(/source:\{bits:snap\.bits,re:snap\.re,im:snap\.im,span:snap\.span,iter\}/.test(script),'reference source metadata is not retained'); -must(/newSpan!==oldSpan/.test(script),'span equality reuse guard missing'); -must(/src\.iter!==iter/.test(script),'iteration reuse guard missing'); -must(/pixel\.x<0\|\|pixel\.x>w\|\|pixel\.y<0\|\|pixel\.y>h/.test(script),'reference viewport reuse guard missing'); -must(/referencePixelForSource/.test(script),'selected reference pixel mapping missing'); -const W=800,H=500; -for(const [dx,dy] of [[0,0],[24,0],[-100,30],[399,-249]]){ - // Pure pan: new center = old center - dx*span/W (real), +dy*span/W (imag). - // Mapping old center into new view must recover center+pointer displacement. - const refX=W/2+dx,refY=H/2+dy; - if(refX<0||refX>W||refY<0||refY>H)throw new Error('test vector outside viewport'); -} -console.log(JSON.stringify({status:'pass',check:'pan-reference-reuse-model',vectors:4,policy:'same span + same iter + old reference inside new viewport'},null,2)); diff --git a/tests/v24-png-stream-model.mjs b/tests/v24-png-stream-model.mjs deleted file mode 100644 index f2d29b2..0000000 --- a/tests/v24-png-stream-model.mjs +++ /dev/null @@ -1,40 +0,0 @@ -import assert from 'node:assert/strict'; -import {inflateSync} from 'node:zlib'; - -const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})(); -function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0} -function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out} -class StreamingPng{ - constructor(w,h){this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()} - async rows(filteredRows){await this.writer.write(filteredRows)} - async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})} - async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}} -} - -assert.equal(typeof CompressionStream,'function','CompressionStream required for this model'); -const p=new StreamingPng(2,2); -const row0=Uint8Array.from([255,0,0,255, 0,255,0,255]); -const row1=Uint8Array.from([0,0,255,255, 255,255,255,128]); -const filtered=new Uint8Array(18);filtered.set(row0,1);filtered.set(row1,10);await p.rows(filtered); -const bytes=new Uint8Array(await (await p.finish()).arrayBuffer()); -assert.deepEqual(Array.from(bytes.subarray(0,8)),[137,80,78,71,13,10,26,10]); -let off=8, width=0, height=0, idats=[], seenIend=false; -while(off=1); assert.ok(seenIend); -const packed=Buffer.concat(idats.map(x=>Buffer.from(x))); -const raw=new Uint8Array(inflateSync(packed)); -assert.equal(raw.length,2*(1+2*4)); -assert.equal(raw[0],0); assert.deepEqual(Array.from(raw.subarray(1,9)),Array.from(row0)); -assert.equal(raw[9],0); assert.deepEqual(Array.from(raw.subarray(10,18)),Array.from(row1)); - -const aborted=new StreamingPng(1,1); await aborted.rows(Uint8Array.from([0,0,0,0,255])); await aborted.abort(new Error('cancelled')); -console.log(JSON.stringify({status:'pass',png:{width,height,idatChunks:idats.length,rawBytes:raw.length},abort:'settled'},null,2)); diff --git a/tests/v24-primary-post-stats-model.mjs b/tests/v24-primary-post-stats-model.mjs deleted file mode 100644 index 589c40a..0000000 --- a/tests/v24-primary-post-stats-model.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import fs from 'node:fs/promises'; -await import('../gpu-kernels.js'); -const G=globalThis.MANDEL_WEBGPU_KERNELS; -const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; -const ITER_MASK=0x000fffff, REASON_SHIFT=20, FIELD_ESCAPED=1; -const packUnknown=(n,r)=>((n&ITER_MASK)|((r&0xff)<>>0; -const packKnown=n=>((FIELD_ESCAPED<<28)|(n&ITER_MASK))>>>0; -const bucket=(m,maxIter)=>Math.min(7,Math.floor(((m&ITER_MASK)*8)/Math.max(1,maxIter))); -let seed=0x31415926; const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/0x100000000}; -const cases=[]; -for(const [w,h,maxIter,rate] of [[1,1,350,1],[65,7,611,.14],[320,180,667,.18],[257,131,721,.03],[511,23,901,.65]]){ - const meta=new Uint32Array(w*h),direct=new Uint32Array(8),wg=new Uint32Array(8),directBuckets=new Uint32Array(8),wgBuckets=new Uint32Array(8); - for(let i=0;i=w)continue;const m=meta[y*w+x];if((m>>>28)===0){localReasons[0]++;const r=(m>>>REASON_SHIFT)&0xff;if(r>=1&&r<=6)localReasons[r]++;localBuckets[bucket(m,maxIter)]++;}} - for(let i=0;i<8;i++){wg[i]+=localReasons[i];wgBuckets[i]+=localBuckets[i];} - } - must([...direct].every((v,i)=>v===wg[i]),`reason stats mismatch ${w}x${h}`); - must([...directBuckets].every((v,i)=>v===wgBuckets[i]),`bucket stats mismatch ${w}x${h}`); - cases.push({w,h,maxIter,unknown:direct[0],reasons:Array.from(direct.slice(1,7)),buckets:Array.from(directBuckets)}); -} -must(G.DEEP_PERTURB_POSTSTATS_WGSL.includes('fieldMeta[out]=pack_unknown')&&!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('atomicAdd(&unresolved.remaining'),'post-stats primary contract broken'); -must(G.DEEP_BUCKET_HIST_STATS_WGSL.includes('localReasons')&&G.DEEP_BUCKET_HIST_STATS_WGSL.includes('unresolvedStats'),'histogram stats aggregation contract broken'); -must(/ensureDeepBucketState/.test(script)&&/encodeDeepBucketHistogramStats/.test(script),'production integration contract broken'); -console.log(JSON.stringify({status:'pass',check:'primary-post-stats-workgroup-model',workgroupSize:64,cases},null,2)); diff --git a/tests/v24-reference-recovery-model.mjs b/tests/v24-reference-recovery-model.mjs deleted file mode 100644 index 04c2a07..0000000 --- a/tests/v24-reference-recovery-model.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import fs from 'node:fs/promises';import vm from 'node:vm';import {performance} from 'node:perf_hooks'; -const source=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; -function extract(prefix,suffix){const start=source.indexOf(prefix)+prefix.length,end=source.indexOf(suffix,start);if(start{message=m},onmessage:null};const ctx={self,postMessage:(...a)=>self.postMessage(...a),performance,Math,Number,BigInt,ArrayBuffer,DataView,Float32Array,Float64Array,Uint32Array,Set,String,Error};vm.createContext(ctx);vm.runInContext(workerSource,ctx,{timeout:5000});self.onmessage({data});if(!message)throw new Error('worker produced no message');if(message.type==='error')throw new Error(message.error);return message} -const bits=256,ONE=1n<<256n,re=ONE/2n,im=0n,span=34n*ONE/10n,iter=700,width=800,height=600; -const deep=runWorker(extract("function referenceWorkerSource(){return String.raw`","`}\nclass ReferenceService"),{type:'build',id:1,key:'deep-select',bits,re:re.toString(),im:im.toString(),span:span.toString(),width,height,iter}); -must(BigInt(deep.referenceRe)!==re,'Deep candidate selection stayed on short-lived center'); -must(deep.selectionEscape===0,'Deep candidate selection did not find a full-length reference'); -must(deep.refLen===iter,'Deep selected reference is not full-length'); -const fast=runWorker(extract("function fastReferenceWorkerSource(){return String.raw`","`}\nclass FastReferenceService"),{type:'build',id:2,key:'fast-select',sourceBits:bits,targetBits:128,re:re.toString(),im:im.toString(),span:span.toString(),width,height,iter}); -must(BigInt(fast.referenceRe)!==re,'Fast candidate selection stayed on short-lived center'); -must(fast.selectionEscape===0,'Fast candidate selection did not find a full-length reference'); -must(fast.refLen===iter,'Fast selected reference is not full-length'); -console.log(JSON.stringify({status:'pass',check:'reference-recovery-selection',deepReference:deep.referenceRe,fastReference:fast.referenceRe,iter},null,2)); diff --git a/tests/v24-reference-worker.mjs b/tests/v24-reference-worker.mjs deleted file mode 100644 index bcc6cb6..0000000 --- a/tests/v24-reference-worker.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import fs from 'node:fs/promises';import vm from 'node:vm';import {performance} from 'node:perf_hooks'; -const source=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'),prefix="function referenceWorkerSource(){return String.raw`",start=source.indexOf(prefix)+prefix.length,end=source.indexOf("`}\nclass ReferenceService",start);if(start{message=m},onmessage:null};const ctx={self,postMessage:(...a)=>self.postMessage(...a),performance,Math,Number,BigInt,ArrayBuffer,DataView,Float32Array,Float64Array,Uint32Array,Set,String,Error};vm.createContext(ctx);vm.runInContext(workerSource,ctx,{timeout:5000});if(typeof self.onmessage!=='function')throw new Error('worker handler missing'); -self.onmessage({data:{type:'build',id:1,key:'test',bits:256,re:(-3n*(1n<<256n)/4n).toString(),im:'0',iter:800}});if(!message)throw new Error('worker produced no message');if(message.type==='error')throw new Error(message.error);if(message.checkpointMismatch)throw new Error('reference +64 guard checkpoint mismatch');if(message.refLen<3||message.refs.byteLength!==(message.refLen+1)*16)throw new Error('reference packing invalid');for(const forbidden of ['nodes','levels','nodeCount','levelCount','invalidNodes'])if(forbidden in message)throw new Error('BLA payload leaked from quarantined reference worker: '+forbidden);const primary=message;message=null;const b=1536,R=(-3n*(1n<{if(!ok)throw new Error(msg)}; -must(/renderMode:'fast'/.test(script)&&/manual-accurate/.test(script)&&/manual-fast/.test(script),'manual fast/accurate backend policy missing'); -must(/\$\('#renderMode'\)\.onchange/.test(script)&&/mandelbrot\.renderMode/.test(script),'manual render mode control/persistence missing'); -must(!/ensureBackendProbe|scheduleBackendProbeAfterPaint|backendProfiles|backendViewProbes|runCrossoverBenchmark|runFixed96Experiment|__MANDEL_DIAG__/.test(script),'obsolete diagnostic/router system remains'); -must(!/DIRECT_DS_WGSL|DIRECT_DS_GUARDED_WGSL|DIRECT_FIXED96/.test(kernels),'diagnostic Direct DS/Fixed96 kernels remain'); -must(/正確 perturbation|DEEP_PERTURB_WGSL/.test(script+kernels)&&/DEEP_CORRECT_WGSL/.test(kernels)&&/correctUnknownFrame/.test(script),'accurate perturbation + sparse DS correction path missing'); -must(/DEEP_BUCKET_HIST_WGSL/.test(kernels)&&/DEEP_BUCKET_PREFIX_WGSL/.test(kernels)&&/DEEP_BUCKET_SCATTER_WGSL/.test(kernels)&&/DEEP_CORRECT_QUEUE_WGSL/.test(kernels),'production bucketed Deep queue path missing'); -must(/DEEP_PERTURB_POSTSTATS_WGSL/.test(kernels)&&/DEEP_BUCKET_HIST_STATS_WGSL/.test(kernels)&&/encodeDeepPostStatsNumeric/.test(script),'post-stats Deep primary missing'); -must(/presentTransform/.test(script)&&/前フレーム再利用/.test(script),'pan frame reprojection reuse missing'); -must(/FAST_PERTURB_WGSL/.test(kernels)&&/fastNeedsExtended/.test(script)&&/FastReferenceService/.test(script)&&/manual-fast-extended/.test(script),'fast deep-coordinate precision extension missing'); -must(/reusableDeepReference/.test(script)&&/referencePixel/.test(script)&&/reference再利用/.test(script),'Deep pan reference reuse missing'); -must(/colorAutoStep/.test(script)&&/colorCycleDir/.test(script)&&/colorShiftDir/.test(script)&&/state\.cycle>=cmax/.test(script)&&/state\.shift>=smax/.test(script),'bouncing color auto-slide missing'); -must(/effectiveColorCycle/.test(script)&&/state\.cycle\*base\/effective/.test(script)&&/sliderToCycle/.test(script)&&/cycleToSlider/.test(script),'zoom-relative/logarithmic color-period control missing'); -must(/mark_fast_unresolved/.test(kernels)&&/REASON_REFERENCE_END/.test(kernels)&&/REASON_REBASE_GAP/.test(kernels)&&/@group\(0\) @binding\(4\) var unresolved/.test(kernels),'Fast unresolved accounting missing'); -must(/provisional_unknown/.test(kernels)&&/fillCount>0\.0/.test(kernels),'UNKNOWN visual neighbor fill missing'); -must(/if\(!deep\)\{const cbg/.test(script)&&/if\(!deep\)\[f\.front,f\.back\]/.test(script)&&/const painted=await r\.recolor\(token,iter\)/.test(script),'deferred Accurate color/commit missing'); -must(/chooseReference/.test(script)&&/referenceCandidates/.test(script)&&/selectionEscape/.test(script),'long-lived reference selection missing'); -must(/max-height:calc\(100dvh - 20px\)/.test(dev)&&/overflow-y:auto/.test(dev),'scrollable bottom panel missing'); -must(/select option\{background:#000;color:#fff\}/.test(dev),'black/white dropdown styling missing'); -must(!/詳細設定・正確な座標|診断情報|backendBench|sparseBench/.test(dev),'removed settings/diagnostics UI remains'); -must(/referenceWorkerSource/.test(script)&&/BigInt\(d\.re\)/.test(script),'BigInt reference worker missing'); -must(/AA_RESOLVE_WGSL/.test(kernels)&&/renderTileRGBA2x/.test(script),'GPU 2x2 export resolve missing'); -must(/class StreamingPng/.test(script)&&/CompressionStream\('deflate'\)/.test(script),'streaming PNG export missing'); -console.log(JSON.stringify({status:'pass',shaderVersion:'24.2.26-baseline-idle-refinement',checks:22},null,2)); diff --git a/tests/v24-sparse-correction-model.mjs b/tests/v24-sparse-correction-model.mjs deleted file mode 100644 index 5086afe..0000000 --- a/tests/v24-sparse-correction-model.mjs +++ /dev/null @@ -1,45 +0,0 @@ -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)} -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(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<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<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=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[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{if(!ok)throw new Error(msg)}; -must(!/高速<\//.test(h)&&/>正確<\//.test(h)&&!/高速(標準)|正確(深部)/.test(h)&&/id="colorAuto"/.test(h),'manual render labels/color auto UI contract failed'); -must(!/診断情報|benchmarkDialog|sparseDialog|coordReInput/.test(h),'removed diagnostic/exact-coordinate UI still embedded'); -must(/ diff --git a/tests/webgpu-acceptance.js b/tests/webgpu-acceptance.js deleted file mode 100644 index 483df56..0000000 --- a/tests/webgpu-acceptance.js +++ /dev/null @@ -1,21 +0,0 @@ -(()=>{'use strict'; -const out=document.querySelector('#out'),frame=document.querySelector('#app'); -function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q} -function align(v,a,b){const d=b-a;return d===0?v:d>0?v<>BigInt(-d)} -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 pixel(st,x,y,bits=st.bits){const re=align(BigInt(st.re),st.bits,bits),im=align(BigInt(st.im),st.bits,bits),span=align(BigInt(st.span),st.bits,bits),den=BigInt(2*st.width);return[re+roundDiv(span*BigInt(2*x+1-st.width),den),im+roundDiv(span*BigInt(st.height-2*y-1),den)]} -function orbit(cr,ci,bits,limit){const bail=4n<bail)return n+1}return limit} -function points(w,h){const xs=[.15,.35,.5,.65,.85].map(t=>Math.min(w-1,Math.max(0,Math.round(t*(w-1))))),ys=[.15,.35,.5,.65,.85].map(t=>Math.min(h-1,Math.max(0,Math.round(t*(h-1)))));return ys.flatMap(y=>xs.map(x=>[x,y]))} -async function wait(ms){return new Promise(r=>setTimeout(r,ms))} -async function waitApp(){if(frame.contentWindow&&frame.contentWindow.__MANDEL_TEST__)return;await new Promise(r=>frame.addEventListener('load',r,{once:true}));const t=performance.now();while(!frame.contentWindow.__MANDEL_TEST__&&performance.now()-t<15000)await wait(50);if(!frame.contentWindow.__MANDEL_TEST__)throw new Error('test hook not available')} -async function numericDeep(api,id,re,im,span,iter){await api.setView({re,im,span,baseIter:iter,adaptive:false,processMode:'standard',renderMode:'accurate'});const st=api.state();if(st.renderMode!=='accurate'||!/正確 reference自動選択|正確 reference再利用/.test(st.engine))throw new Error('accurate mode did not select Deep: '+JSON.stringify(st));const ps=points(st.width,st.height),meta=await api.sampleMeta(ps);let guardMismatch=0,falseEscaped=0,falseBounded=0,unknown=0;for(let i=0;i>>28)&3;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a((m>>>28)&3)===0))throw new Error('forced corrected Deep probe left UNKNOWN'); - await api.setView({re:'-0.75',im:'0',span:'3.4e-20',baseIter:1000,adaptive:false,processMode:'standard',renderMode:'accurate'});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 result={status:'pass',kind:'real-webgpu-acceptance',date:new Date().toISOString(),manualModes:true,fastPrecisionExtension:true,panReferenceReuse:true,exportSmoke,exportSmokeAA,report};out.textContent=JSON.stringify(result,null,2);document.title='PASS v24.2.26 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.26 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result}); -})();