This commit is contained in:
33333-33333 2026-08-25 14:03:13 +09:00
commit b653d77d4e
49 changed files with 1636 additions and 4724 deletions

View file

@ -1,3 +0,0 @@
/*
X-Content-Type-Options: nosniff
Referrer-Policy: no-referrer

View file

@ -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<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->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<f32>, delta:vec2<f32>)->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<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(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<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
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<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
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<f32>, lo:vec2<f32> };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:array<atomic<u32>>;
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<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(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<dot(delta,delta)){
if(p.spanExp < -126){mark_fast_unresolved(out,n,REASON_REBASE_GAP);return;}
w=z;
d=scaled_to_f32(vec2<f32>(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<u32>){
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<f32>, lo:vec2<f32> };
struct UnresolvedHead{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> 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<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(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<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(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<u32>){
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<storage,read_write> unresolved:UnresolvedHead;',
'@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var<storage,read> refineQueue:array<u32>;\n@group(1) @binding(1) var<uniform> refineBatch:RefineBatch;'
)+String.raw`
@compute @workgroup_size(64)
fn refine_likely(@builtin(global_invocation_id) gid:vec3<u32>){
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<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
var<workgroup> localCounts:array<atomic<u32>,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<u32>,
@builtin(workgroup_id) wid:vec3<u32>
){
let lane=lid3.x;
if(lane<8u){atomicStore(&localCounts[lane],0u);}
workgroupBarrier();
let lx=wid.x*64u+lane;
let ly=wid.y;
if(lx<p.tileW && ly<p.tileH){
let out=p.outputBase+ly*p.outputStride+lx;
let packed=fieldMeta[out];
if(((packed>>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<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
};
struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 };
@group(0) @binding(0) var<storage,read_write> bucketState:array<atomic<u32>>;
@group(0) @binding(1) var<storage,read_write> sparseQueueStats:SparseQueueStats;
@group(0) @binding(2) var<storage,read_write> 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<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
@group(0) @binding(3) var<storage,read_write> sparseQueueStats:SparseQueueStats;
@group(0) @binding(4) var<storage,read_write> unknownQueue:array<u32>;
var<workgroup> localCounts:array<atomic<u32>,8>;
var<workgroup> localRanks:array<atomic<u32>,8>;
var<workgroup> groupBase:array<u32,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<u32>,
@builtin(workgroup_id) wid:vec3<u32>
){
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<p.tileW && ly<p.tileH){
out=p.outputBase+ly*p.outputStride+lx;
let packed=fieldMeta[out];
if(((packed>>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<capacity){unknownQueue[qi]=out;}
else{atomicAdd(&sparseQueueStats.overflow,1u);}
}
}
`;
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
// perturbation. It is a visual-quality pass, not a membership certificate.
const DEEP_CORRECT_WGSL=COMMON+String.raw`
const CORRECTION_MARK:u32=128u;
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<f32>, lo:vec2<f32> };
struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> 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.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.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<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
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;}
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<u32>){
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<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};`,
`struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct SparseQueueStats{
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
};`)
.replace(
`@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;`,
`@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
@group(0) @binding(5) var<storage,read_write> sparseQueueStats:SparseQueueStats;
@group(0) @binding(6) var<storage,read> unknownQueue:array<u32>;`)
.replace(
`@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
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<u32>){
let qi=gid.x;
let queued=atomicLoad(&sparseQueueStats.dispatchCount);
if(qi>=queued){return;}
let out=unknownQueue[qi];
atomicAdd(&sparseQueueStats.processed,1u);
if(out<p.outputBase){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;}
let local=out-p.outputBase;
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 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<u32>,overflow:atomic<u32>,_p0:atomic<u32>,_p1:atomic<u32>};
@group(0) @binding(0) var<uniform> p:QueueParams;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> queue:array<u32>;
@group(0) @binding(3) var<storage,read_write> 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<i32(p.width)&&yy<i32(p.height)){let cls=(fieldMeta[u32(yy)*p.width+u32(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<u32>){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<total){queue[slot]=i;}else{atomicAdd(&stats.overflow,1u);}}
`;
const COLOR_WGSL=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
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<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn palette_color(phase:f32)->vec3<f32>{
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<f32>(g);}return current_palette(phase);
}
fn escaped_color(m:u32,sm:f32)->vec3<f32>{
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<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv);
}
fn provisional_unknown(m:u32)->vec3<f32>{
let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3<f32>(6,10,22)/255.0,vec3<f32>(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52);
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3<f32>(0.0);}return escaped_color(m,fieldSmooth[i]);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
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<f32>(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<i32>(gid.xy),vec4<f32>(c,1.0));
}
`
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
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<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(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<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(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<f32>(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<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(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<storage,read_write> bucketState:array<atomic<u32>>;', '@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;\n@group(0) @binding(3) var<storage,read_write> unresolvedStats:array<atomic<u32>>;')
.replace('var<workgroup> localCounts:array<atomic<u32>,8>;', 'var<workgroup> localCounts:array<atomic<u32>,8>;\nvar<workgroup> localReasons:array<atomic<u32>,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
});
})();

View file

@ -1,62 +0,0 @@
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#050813">
<title>Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement</title>
<style>
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));max-height:calc(100dvh - 20px);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.18);background:#000;color:#fff;min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}select option{background:#000;color:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord"></span></div><div class="row"><span class="muted">倍率</span> <span id="zoom"></span> <span class="muted">表示幅</span> <span id="span"></span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render"></span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大"></button><button id="zout" aria-label="中心を縮小"></button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
<div class="line"><label for="renderMode">描画方式</label><select id="renderMode"><option value="fast" selected>高速</option><option value="accurate">正確</option></select><output></output></div>
<div class="line"><label for="processMode">負荷設定</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0" max="1000" step="1" value="532" aria-label="色周期(対数スケール)"><output id="cycleO" for="cycle">0.0080</output></div>
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
<div class="mini-actions"><button id="colorAuto" type="button" aria-pressed="false">色を自動変化</button></div>
</div>
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
<div class="hint">ドラッグ中は直前フレームを再投影して再利用。ホイール / ピンチでズーム、HキーでUI表示を切り替え。</div>
</div>
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI</button>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="exportDialog" aria-labelledby="exportTitle">
<h2 id="exportTitle">高解像度 PNG 出力</h2>
<div class="export-grid">
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
<label for="exportWidth">px</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×高速</option><option value="2">2×2 AA</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="strict">保守的 (Strict)</option></select>
</div>
<progress id="exportProgress" max="1" value="0" hidden></progress>
<div id="exportStatus" role="status" aria-live="polite"></div>
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
</dialog>
<script src="gpu-kernels.js"></script>
<script src="script.js"></script>
</body>
</html>

371
dist/hosted/script.js vendored
View file

@ -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<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):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(sh):roundDivSigned(m,1n<<BigInt(-sh));return neg?-q:q;
}
function splitFixedF32(v,bits,count=3){const out=[];let r=v;for(let i=0;i<count;i++){const x=Math.fround(fixedNum(r,bits));out.push(x);if(x===0)continue;r-=f32ToFixed(x,bits)}return out}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>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<Math.pow(2,-126))return Math.pow(2,-149);return Math.pow(2,Math.floor(Math.log2(x))-23)}
function directPixelRatio(snap,w){const cr=Math.abs(fixedNum(snap.re,snap.bits)),ci=Math.abs(fixedNum(snap.im,snap.bits)),sp=Math.abs(fixedNum(snap.span,snap.bits)),step=sp/Math.max(1,w),scale=Math.max(cr,ci,sp*.75,Math.pow(2,-126)),ulp=f32Ulp(scale);return step/ulp}
function fastNeedsExtended(snap,w){return !(directPixelRatio(snap,w)>=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<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>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<<B,BAIL=16n*ONE;let zr=0n,zi=0n;for(let n=0;n<iter;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>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;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.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<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>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<<BigInt(d):roundShift(v,-d)}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>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<<B,BAIL=16n*ONE;let zr=0n,zi=0n;for(let n=0;n<iter;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>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;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.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;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
async readUnresolved(){return(await this.readUnresolvedStats()).total}
async correctUnknownFrame(snap,iter,token,referencePixel=null){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');this.ensureDeepQueueWorkspace(f);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.deepQueueStats,0,new Uint32Array(SPARSE_QUEUE_STATS_BYTES/4));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array(SPARSE_INDIRECT_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,1,f.w,0));const e=d.createCommandEncoder({label:'deep-bucketed-correction'});this.encodeDeepBucketPrefix(e,{bucketState:f.deepBucketState,queueStats:f.deepQueueStats,indirect:f.deepIndirect});this.encodeDeepBucketScatter(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,queueStats:f.deepQueueStats,queue:f.deepQueue,w:f.w,h:f.h});this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
async renderTileMeta({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
await this.ready;const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});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 read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
}
async renderTileRGBA({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||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;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes,UNRESOLVED_BYTES/4),unresolvedCount=stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
async renderTileRGBA2x({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||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<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount=0,corrected=0;for(let si=0;si<4;si++){const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4);unresolvedCount+=stats[0]||0;corrected+=stats[7]||0}ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyRefineDeepContext();this.destroyDeepContext();this.destroyFastContext()}
}
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<targets.length;stage++){if(!canIdleRefine(token,snap,baseIter))return;const targetIter=targets[stage];state.refinementStage=stage+1;state.refinedIter=targetIter;const deepCtx=await refs.request(snap,targetIter,canvas.width,canvas.height);if(!canIdleRefine(token,snap,baseIter))return;if(deepCtx.checkpointMismatch)throw new Error('idle refinement reference guard checkpoint mismatch');const policy=idleRefinementQueuePolicy(stage),q=await renderer.buildLikelyRefineQueue(token,policy.mode,policy.stride);if(!canIdleRefine(token,snap,baseIter)||q.cancelled)return;state.refinementQueue=q.count;runtime.idleRefineQueued+=q.count;updateStats();if(q.count<=0)continue;const budget=idleRefinementBatchBudget(),batchPixels=Math.max(64,Math.floor(Math.max(64,budget/Math.max(1,targetIter))/64)*64);let base=0,lastPaint=performance.now();while(base<q.count){if(!canIdleRefine(token,snap,baseIter))return;const end=Math.min(q.count,base+batchPixels),ok=await renderer.refineLikelyBatch({snap,iter:targetIter,deepContext:deepCtx,token,base,end,forceStrict:state.processMode==='validate'});if(!ok||!canIdleRefine(token,snap,baseIter))return;base=end;state.refinementQueue=q.count-base;const now=performance.now();if(now-lastPaint>=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<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=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<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback正確モード非対応';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{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<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':fastExtended?'reduced-reference + fast-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'sidecar参照':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#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}
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();
})();

File diff suppressed because it is too large Load diff

View file

@ -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予算・品質単調性の専用テストを追加。

View file

@ -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 より遅くしない。 ## v24.2.55の契約
2. 補足精密化を停止しても v24.2.18 より品質を下げない。 1. 数値UNKNOWNを含むfrontはstable historyへ昇格しない。
3. 通常描画ループで fieldMeta 全体を GPU→CPU readback しない。 2. 通常のパン/ズーム範囲では、数値UNKNOWNを直前のstable frameから再投影して埋める。
4. 黒い `INTERIOR_LIKELY` の追加判定は一次表示後・アイドル時だけ行う。 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` classic Seahorse Valley相当 (654x690, span 3.4e-13, 2900 iter) のNode/V8 workerモデル:
- `FAST_PERTURB_WGSL` - 旧 CPU validate 64点: 約446.9 ms
- `DEEP_PERTURB_WGSL` - 新 table build only: 約60.0 ms
- `COLOR_WGSL` - 同期prep削減: 約386.9 ms
`maxIter()` も v24.2.18 と同じです BLAが不利なviewでは本体FAST計算時間そのものは残る。今回の変更では、数値補修Deep/multi-referenceを一次表示のクリティカルパスから外すことで、黒穴を出さずに操作から画像表示までの待ち時間を短縮する
```text ## 数値コア
baseIter + floor(70*sqrt(zoomExp) + 15*zoomExp) v24.2.54から変更したproduction WGSLはFAST_PERTURB_WGSLのみ。通常 `unknownOnly=0` の計算式・perturbation recurrenceは変更していない。追加分はcoverage repair用のearly guardである。
```
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モデル結果と実機測定は区別します。

View file

@ -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 | ## 3. coverage contract
|---|---| PASS:
| DIRECT_F32_WGSL | `59301bf0c5ba3dda4a4055335b32d4a3f84a21991be30f8efc74deb5a5f11ccf` | - tiled numeric開始時にmeta/smoothをzero clear
| FAST_PERTURB_WGSL | `03b45713b1bbb7b56e18bd8fdf0f2d8161d7ed25adf02482b41c1038dc16e3b8` | - color前にbaseline FAST coverage repair
| DEEP_PERTURB_WGSL | `2865a015d5c1a9b459085b7044e23563fcc5f0eec5362634249b6604a47e660a` | - coverage repairはreason=0 UNKNOWNだけをiteration
| COLOR_WGSL | `cccac7bfb69c64093c46c3468ff9d685d680ba3a88de0d1ea57168d91a067acf` | - 既計算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 | ## 8. browser smoke
|---:|---:|---:| Chromiumでlocalhostからロード:
| 10^6 | 611 | 961,318,072 | - page JavaScript error: 0
| 10^8 | 667 | 1,049,425,784 | - kernelVersion: `24.2.55-no-black-hole-async-recovery`
| 10^12 | 772 | 1,214,627,744 |
| 10^20 | 963 | 1,515,137,976 |
れは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 | ## 残る性能課題
|---:|---:|---:|---:| BLA probeがOFFになるSeahorse/Misiurewicz型viewではbaseline FASTのpixel-iteration量が依然支配的。v24.2.55はこのケースのCPU prepと同期補修待ちを削るが、primary perturbation本体を別アルゴリズムで高速化する変更ではない。
| 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仕事量上限である。

View file

@ -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<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->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<f32>, delta:vec2<f32>)->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<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(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<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
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<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
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<f32>, lo:vec2<f32> };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:array<atomic<u32>>;
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<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(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<dot(delta,delta)){
if(p.spanExp < -126){mark_fast_unresolved(out,n,REASON_REBASE_GAP);return;}
w=z;
d=scaled_to_f32(vec2<f32>(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<u32>){
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<f32>, lo:vec2<f32> };
struct UnresolvedHead{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> 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<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(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<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(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<u32>){
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<storage,read_write> unresolved:UnresolvedHead;',
'@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var<storage,read> refineQueue:array<u32>;\n@group(1) @binding(1) var<uniform> refineBatch:RefineBatch;'
)+String.raw`
@compute @workgroup_size(64)
fn refine_likely(@builtin(global_invocation_id) gid:vec3<u32>){
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<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
var<workgroup> localCounts:array<atomic<u32>,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<u32>,
@builtin(workgroup_id) wid:vec3<u32>
){
let lane=lid3.x;
if(lane<8u){atomicStore(&localCounts[lane],0u);}
workgroupBarrier();
let lx=wid.x*64u+lane;
let ly=wid.y;
if(lx<p.tileW && ly<p.tileH){
let out=p.outputBase+ly*p.outputStride+lx;
let packed=fieldMeta[out];
if(((packed>>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<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
};
struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 };
@group(0) @binding(0) var<storage,read_write> bucketState:array<atomic<u32>>;
@group(0) @binding(1) var<storage,read_write> sparseQueueStats:SparseQueueStats;
@group(0) @binding(2) var<storage,read_write> 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<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
@group(0) @binding(3) var<storage,read_write> sparseQueueStats:SparseQueueStats;
@group(0) @binding(4) var<storage,read_write> unknownQueue:array<u32>;
var<workgroup> localCounts:array<atomic<u32>,8>;
var<workgroup> localRanks:array<atomic<u32>,8>;
var<workgroup> groupBase:array<u32,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<u32>,
@builtin(workgroup_id) wid:vec3<u32>
){
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<p.tileW && ly<p.tileH){
out=p.outputBase+ly*p.outputStride+lx;
let packed=fieldMeta[out];
if(((packed>>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<capacity){unknownQueue[qi]=out;}
else{atomicAdd(&sparseQueueStats.overflow,1u);}
}
}
`;
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
// perturbation. It is a visual-quality pass, not a membership certificate.
const DEEP_CORRECT_WGSL=COMMON+String.raw`
const CORRECTION_MARK:u32=128u;
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<f32>, lo:vec2<f32> };
struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> 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.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.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<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
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;}
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<u32>){
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<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};`,
`struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct SparseQueueStats{
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
};`)
.replace(
`@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;`,
`@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
@group(0) @binding(5) var<storage,read_write> sparseQueueStats:SparseQueueStats;
@group(0) @binding(6) var<storage,read> unknownQueue:array<u32>;`)
.replace(
`@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
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<u32>){
let qi=gid.x;
let queued=atomicLoad(&sparseQueueStats.dispatchCount);
if(qi>=queued){return;}
let out=unknownQueue[qi];
atomicAdd(&sparseQueueStats.processed,1u);
if(out<p.outputBase){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;}
let local=out-p.outputBase;
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 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<u32>,overflow:atomic<u32>,_p0:atomic<u32>,_p1:atomic<u32>};
@group(0) @binding(0) var<uniform> p:QueueParams;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> queue:array<u32>;
@group(0) @binding(3) var<storage,read_write> 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<i32(p.width)&&yy<i32(p.height)){let cls=(fieldMeta[u32(yy)*p.width+u32(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<u32>){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<total){queue[slot]=i;}else{atomicAdd(&stats.overflow,1u);}}
`;
const COLOR_WGSL=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
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<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn palette_color(phase:f32)->vec3<f32>{
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<f32>(g);}return current_palette(phase);
}
fn escaped_color(m:u32,sm:f32)->vec3<f32>{
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<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv);
}
fn provisional_unknown(m:u32)->vec3<f32>{
let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3<f32>(6,10,22)/255.0,vec3<f32>(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52);
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3<f32>(0.0);}return escaped_color(m,fieldSmooth[i]);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
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<f32>(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<i32>(gid.xy),vec4<f32>(c,1.0));
}
`
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
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<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(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<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(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<f32>(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<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(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<storage,read_write> bucketState:array<atomic<u32>>;', '@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;\n@group(0) @binding(3) var<storage,read_write> unresolvedStats:array<atomic<u32>>;')
.replace('var<workgroup> localCounts:array<atomic<u32>,8>;', 'var<workgroup> localCounts:array<atomic<u32>,8>;\nvar<workgroup> localReasons:array<atomic<u32>,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
});
})();

View file

@ -1,62 +0,0 @@
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#050813">
<title>Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement</title>
<style>
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));max-height:calc(100dvh - 20px);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.18);background:#000;color:#fff;min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}select option{background:#000;color:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord"></span></div><div class="row"><span class="muted">倍率</span> <span id="zoom"></span> <span class="muted">表示幅</span> <span id="span"></span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render"></span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大"></button><button id="zout" aria-label="中心を縮小"></button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
<div class="line"><label for="renderMode">描画方式</label><select id="renderMode"><option value="fast" selected>高速</option><option value="accurate">正確</option></select><output></output></div>
<div class="line"><label for="processMode">負荷設定</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0" max="1000" step="1" value="532" aria-label="色周期(対数スケール)"><output id="cycleO" for="cycle">0.0080</output></div>
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
<div class="mini-actions"><button id="colorAuto" type="button" aria-pressed="false">色を自動変化</button></div>
</div>
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
<div class="hint">ドラッグ中は直前フレームを再投影して再利用。ホイール / ピンチでズーム、HキーでUI表示を切り替え。</div>
</div>
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI</button>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="exportDialog" aria-labelledby="exportTitle">
<h2 id="exportTitle">高解像度 PNG 出力</h2>
<div class="export-grid">
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
<label for="exportWidth">px</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×高速</option><option value="2">2×2 AA</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="strict">保守的 (Strict)</option></select>
</div>
<progress id="exportProgress" max="1" value="0" hidden></progress>
<div id="exportStatus" role="status" aria-live="polite"></div>
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
</dialog>
<script src="gpu-kernels.js"></script>
<script src="script.js"></script>
</body>
</html>

1578
index.html

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

371
script.js
View file

@ -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<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):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(sh):roundDivSigned(m,1n<<BigInt(-sh));return neg?-q:q;
}
function splitFixedF32(v,bits,count=3){const out=[];let r=v;for(let i=0;i<count;i++){const x=Math.fround(fixedNum(r,bits));out.push(x);if(x===0)continue;r-=f32ToFixed(x,bits)}return out}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>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<Math.pow(2,-126))return Math.pow(2,-149);return Math.pow(2,Math.floor(Math.log2(x))-23)}
function directPixelRatio(snap,w){const cr=Math.abs(fixedNum(snap.re,snap.bits)),ci=Math.abs(fixedNum(snap.im,snap.bits)),sp=Math.abs(fixedNum(snap.span,snap.bits)),step=sp/Math.max(1,w),scale=Math.max(cr,ci,sp*.75,Math.pow(2,-126)),ulp=f32Ulp(scale);return step/ulp}
function fastNeedsExtended(snap,w){return !(directPixelRatio(snap,w)>=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<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>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<<B,BAIL=16n*ONE;let zr=0n,zi=0n;for(let n=0;n<iter;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>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;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.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<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>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<<BigInt(d):roundShift(v,-d)}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>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<<B,BAIL=16n*ONE;let zr=0n,zi=0n;for(let n=0;n<iter;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>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;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.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;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
async readUnresolved(){return(await this.readUnresolvedStats()).total}
async correctUnknownFrame(snap,iter,token,referencePixel=null){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');this.ensureDeepQueueWorkspace(f);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.deepQueueStats,0,new Uint32Array(SPARSE_QUEUE_STATS_BYTES/4));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array(SPARSE_INDIRECT_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,1,f.w,0));const e=d.createCommandEncoder({label:'deep-bucketed-correction'});this.encodeDeepBucketPrefix(e,{bucketState:f.deepBucketState,queueStats:f.deepQueueStats,indirect:f.deepIndirect});this.encodeDeepBucketScatter(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,queueStats:f.deepQueueStats,queue:f.deepQueue,w:f.w,h:f.h});this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
async renderTileMeta({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
await this.ready;const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});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 read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
}
async renderTileRGBA({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||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;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes,UNRESOLVED_BYTES/4),unresolvedCount=stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
async renderTileRGBA2x({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||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<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount=0,corrected=0;for(let si=0;si<4;si++){const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4);unresolvedCount+=stats[0]||0;corrected+=stats[7]||0}ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyRefineDeepContext();this.destroyDeepContext();this.destroyFastContext()}
}
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<targets.length;stage++){if(!canIdleRefine(token,snap,baseIter))return;const targetIter=targets[stage];state.refinementStage=stage+1;state.refinedIter=targetIter;const deepCtx=await refs.request(snap,targetIter,canvas.width,canvas.height);if(!canIdleRefine(token,snap,baseIter))return;if(deepCtx.checkpointMismatch)throw new Error('idle refinement reference guard checkpoint mismatch');const policy=idleRefinementQueuePolicy(stage),q=await renderer.buildLikelyRefineQueue(token,policy.mode,policy.stride);if(!canIdleRefine(token,snap,baseIter)||q.cancelled)return;state.refinementQueue=q.count;runtime.idleRefineQueued+=q.count;updateStats();if(q.count<=0)continue;const budget=idleRefinementBatchBudget(),batchPixels=Math.max(64,Math.floor(Math.max(64,budget/Math.max(1,targetIter))/64)*64);let base=0,lastPaint=performance.now();while(base<q.count){if(!canIdleRefine(token,snap,baseIter))return;const end=Math.min(q.count,base+batchPixels),ok=await renderer.refineLikelyBatch({snap,iter:targetIter,deepContext:deepCtx,token,base,end,forceStrict:state.processMode==='validate'});if(!ok||!canIdleRefine(token,snap,baseIter))return;base=end;state.refinementQueue=q.count-base;const now=performance.now();if(now-lastPaint>=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<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=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<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback正確モード非対応';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{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<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':fastExtended?'reduced-reference + fast-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'sidecar参照':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#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}
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();
})();

View file

@ -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('<script src="gpu-kernels.js"></script>',`<script data-bundle="gpu-kernels">\n${safe(kernels)}\n</script>`)
.replace('<script src="script.js"></script>',`<script data-bundle="app">\n${safe(script)}\n</script>`)
.replace('<head>','<head>\n<meta name="mandelbrot-bundle" content="single-file-v24.2.26">');
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));

View file

@ -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));

View file

@ -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."
}

View file

@ -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
}

View file

@ -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": []
}
}

View file

@ -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"
}

View file

@ -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."
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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
}
]

View file

@ -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
}
}

View file

@ -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}
]
}

View file

@ -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'<script[^>]*>(.*?)</script>',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')

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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));

View file

@ -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(b)),d)}
function fmt(v,bits){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(bits),q=v*scale>>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<<BigInt(bits))/d}
for(const [text,bits] of [['1e-400',1569],['1e-1000',3562],['-0.75',1569]]){const v=fixed(text,bits),s=fmt(v,bits);if(s==='0'||s==='-0')throw new Error('deep formatter collapsed '+text);const r=parseFixed(s,bits);if(r!==v)throw new Error('exact decimal roundtrip failed '+text);}
console.log(JSON.stringify({status:'pass',kind:'exact-fixed-decimal-format',deep:['1e-400','1e-1000']},null,2));

View file

@ -1,47 +0,0 @@
import fs from 'node:fs/promises';
const corpus=JSON.parse(await fs.readFile(new URL('./scenes.json',import.meta.url),'utf8'));const map=new Map(corpus.scenes.map(s=>[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):v>>BigInt(-d)}
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}function bitsForSpan(t){const s=String(t).toLowerCase(),a=s.split('e'),m=Math.abs(Number(a[0])),e=a[1]?Number(a[1]):0,l2=Math.log2(m)+e*Math.log2(10);return Math.max(256,Math.ceil(-l2)+240)}function snap(scene){const bits=bitsForSpan(scene.span);return{bits,re:decFixed(scene.re,bits),im:decFixed(scene.im,bits),span:decFixed(scene.span,bits)}}
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}function log2FixedAt(v,b){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-b}function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:F(Math.pow(2,l-exp)),exp}}
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
function direct(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<<BigInt(b);let zr=0n,zi=0n,refLen=limit;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;const mag=roundShift(zr*zr,b)+roundShift(zi*zi,b);if(mag>bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[refLen*2]=hr1;hi[refLen*2+1]=hi1;lo[refLen*2]=F(rr1-hr1);lo[refLen*2+1]=F(ri1-hi1);break}}return{hi,lo,refLen}}
function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))}
function perturb(ref,s,w,h,x,y,limit,strict=false){const se=spanME(s),dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w),U=2**-24;let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=1*U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+1*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30),threshold=strict?1e-4:1e-3;return ea<=threshold?{kind:'bounded',n,err:ea}:{kind:'unknown',n,reason:'error-bound',err:ea}}if(m>ref.refLen)return{kind:'unknown',n,reason:'ref'};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+1*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n,err:ea};return{kind:'unknown',n,reason:'escape-uncertain',err:ea}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n,reason:'rebase-gap'};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=1*U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n,reason:'ref-end'};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=1*U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n,reason:'range'};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n,reason:'guard'}}}
const cfg=[['period2-cusp-z14',1200],['swirly-seahorses-z12',2000],['period2-cusp-z20',1800],['period2-cusp-z100',2400],['period3-interior',1200],['period2-cusp-e280',2400],['period2-cusp-e400',1600]],w=17,h=11,pts=[];
for(let y=0;y<h;y++)for(let x=0;x<w;x++)pts.push([x,y]);
const report=[];
for(const mode of [{name:'balanced',strict:false},{name:'strict',strict:true}]){
for(const[id,limit]of cfg){
const sc=map.get(id),s=snap(sc),ref=buildRef(s,limit);
let guardMismatch=0,falseEscaped=0,falseBounded=0,unknown=0,exactEscape=0;
const mismatch=[];
for(const[x,y]of pts){
const p=pixel(s,w,h,x,y),g=pixel(s,w,h,x,y,s.bits+64),a=direct(p[0],p[1],s.bits,limit),ag=direct(g[0],g[1],s.bits+64,limit);
if(a!==ag){guardMismatch++;continue}
const r=perturb(ref,s,w,h,x,y,limit,mode.strict);
if(r.kind==='unknown'){unknown++;continue}
if(r.kind==='escaped'&&a===limit)falseEscaped++;
if(r.kind==='bounded'&&a<limit)falseBounded++;
if(r.kind==='escaped'&&a<limit&&r.n===a)exactEscape++;
if(((r.kind==='escaped'&&a<limit&&r.n!==a)||(r.kind==='bounded'&&a<limit))&&mismatch.length<5)mismatch.push({x,y,oracle:a,gpu:r});
}
report.push({mode:mode.name,id,bits:s.bits,limit,guardMismatch,falseEscaped,falseBounded,unknown,known:pts.length-unknown,exactEscape,mismatch});
}
}
const denseScene=map.get('swirly-seahorses-z12'),denseS=snap(denseScene),denseRef=buildRef(denseS,2000),densePoints=[[25,12],[26,15],[27,25]],dense=[];
for(const[x,y]of densePoints){
const p=pixel(denseS,61,39,x,y),g=pixel(denseS,61,39,x,y,denseS.bits+64),a=direct(p[0],p[1],denseS.bits,2000),ag=direct(g[0],g[1],denseS.bits+64,2000);
if(a!==ag)throw new Error('dense oracle guard mismatch '+x+','+y);
for(const mode of [{name:'balanced',strict:false},{name:'strict',strict:true}]){
const r=perturb(denseRef,denseS,61,39,x,y,2000,mode.strict);
if(r.kind==='escaped'&&a===2000)throw new Error('dense false escape '+JSON.stringify({mode:mode.name,x,y,a,r}));
if(r.kind==='bounded'&&a<2000)throw new Error('dense false bounded '+JSON.stringify({mode:mode.name,x,y,a,r}));
dense.push({mode:mode.name,x,y,oracle:a,gpu:r});
}
}
if(report.some(r=>r.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));

View file

@ -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<atomic<u32>,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<n;i++){
const it=Math.min(maxIter,Math.floor(rnd()*(maxIter+1)));
meta[i]=rnd()<rate?packUnknown(it):packKnown(it);
}
const expected=Array.from({length:8},()=>[]);
for(let i=0;i<n;i++)if((meta[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<h;y++)for(let gx=0;gx<Math.ceil(w/64);gx++)groups.push([y,gx]);
shuffle(groups);
let globalReservations=0;
for(const [y,gx] of groups){
const local=Array.from({length:8},()=>[]);
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;j<lanes.length;j++)queue[base+j]=lanes[j];
}
}
must(total===expected.reduce((a,x)=>a+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;qi<launched;qi++)if(qi<total)visited.push(queue[qi]);
must(visited.length===total,`queued dispatch count mismatch ${w}x${h}`);
cases.push({w,h,pixels:n,maxIter,unknown:total,rate:total/Math.max(1,n),counts,offsets,histogramWorkgroups:Math.ceil(w/64)*h,scatterWorkgroups:Math.ceil(w/64)*h,globalReservations,correctionWorkgroups:workgroups});
}
console.log(JSON.stringify({status:'pass',check:'deep-8-bucket-correction-queue-model',bucketCount:8,workgroupSize:64,cases},null,2));

View file

@ -1,13 +0,0 @@
import fs from 'node:fs/promises';
const corpus=JSON.parse(await fs.readFile(new URL('./scenes.json',import.meta.url),'utf8'));
const scene=corpus.scenes.find(s=>s.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<<BigInt(b)),10n**BigInt(p))}
function pixel(re,im,span,b,w,h,x,y){const den=BigInt(2*w);return[re+rd(span*BigInt(2*x+1-w),den),im+rd(span*BigInt(h-2*y-1),den)]}
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const a=rs(zr*zr,b),c=rs(zi*zi,b);zi=rs(2n*zr*zi,b)+ci;zr=a-c+cr;if(rs(zr*zr,b)+rs(zi*zi,b)>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(lhs<F(rhs-margin))return limit;const x2=F(cr+1),bulb=F(F(x2*x2)+y2),bulbMargin=F(16*U*(Math.abs(bulb)+.0625+1));if(bulb<F(.0625-bulbMargin))return limit;let zr=0,zi=0;for(let n=0;n<limit;n++){const a=F(zr*zr),c=F(zi*zi);zi=F(F(F(2*zr)*zi)+ci);zr=F(F(a-c)+cr);if(F(F(zr*zr)+F(zi*zi))>4)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<H;y++)for(let x=0;x<W;x++){const [cr,ci]=pixel(re,im,span,bits,W,H,x,y),[gr,gi]=pixel(re<<64n,im<<64n,span<<64n,bits+64,W,H,x,y),a=oracle(cr,ci,bits,limit),g=oracle(gr,gi,bits+64,limit);if(a!==g){guardMismatch++;continue}const rr=Number(cr)/2**bits,ii=Number(ci)/2**bits,r=direct(rr,ii,limit);tested++;if(r<limit&&a===limit)falseEscaped++;if(r===limit&&a<limit)falseBounded++;if(r<limit&&a<limit&&r!==a)countMismatch++}
if(falseEscaped||falseBounded)throw new Error(JSON.stringify({falseEscaped,falseBounded,guardMismatch,countMismatch}));
console.log(JSON.stringify({status:'pass',kind:'direct-f32-cpu-model-not-real-gpu',tested,guardMismatch,falseEscaped,falseBounded,countMismatch},null,2));

View file

@ -1,11 +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 must=(ok,msg)=>{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));

View file

@ -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(newCounterBytes<oldQueueBytes/100000,'unresolved queue was not reduced to a counter');
for(const [cw,ch] of [[390,844],[1920,1080],[1080,1920]]){const aspect=ch/cw,w0=16384,h0=Math.round(w0*aspect),h=Math.min(16384,h0),w=h0>16384?Math.max(64,Math.round(h/aspect)):w0;must(w<=16384&&h<=16384,'export dimension clamp failed')}
console.log(JSON.stringify({status:'pass',pixelContract:'centered',exportTile:512,strictRetryPass:false,unresolvedCounterBytes:newCounterBytes,maxExportSide:16384},null,2));

View file

@ -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(/<script\s+src=/.test(h))throw new Error('standalone index still depends on external scripts');
if(!/select option\{background:#000;color:#fff\}/.test(h))throw new Error('dropdown theme not embedded');
if(!/overflow-y:auto/.test(h))throw new Error('panel scroll fix not embedded');
console.log(JSON.stringify({status:'pass',checks:req.length+9,singleFile:true},null,2));

View file

@ -1,16 +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)};
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));

View file

@ -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<bytes.length){
const dv=new DataView(bytes.buffer,bytes.byteOffset+off);
const len=dv.getUint32(0,false); const type=new TextDecoder().decode(bytes.subarray(off+4,off+8));
const data=bytes.subarray(off+8,off+8+len); const got=dv.getUint32(8+len,false);
assert.equal(got,crc32Parts([bytes.subarray(off+4,off+8),data]),`CRC ${type}`);
if(type==='IHDR'){const h=new DataView(data.buffer,data.byteOffset,data.byteLength);width=h.getUint32(0,false);height=h.getUint32(4,false);assert.equal(data[8],8);assert.equal(data[9],6)}
if(type==='IDAT')idats.push(data);
if(type==='IEND')seenIend=true;
off += 12+len;
}
assert.equal(width,2); assert.equal(height,2); assert.ok(idats.length>=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));

View file

@ -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)<<REASON_SHIFT))>>>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<meta.length;i++){
if(rnd()<rate){const reason=1+Math.floor(rnd()*6),n=Math.floor(rnd()*(maxIter+1));meta[i]=packUnknown(n,reason);direct[0]++;direct[reason]++;directBuckets[bucket(meta[i],maxIter)]++;}
else meta[i]=packKnown(Math.floor(rnd()*(maxIter+1)));
}
for(let y=0;y<h;y++)for(let gx=0;gx<Math.ceil(w/64);gx++){
const localReasons=new Uint32Array(8),localBuckets=new Uint32Array(8);
for(let lane=0;lane<64;lane++){const x=gx*64+lane;if(x>=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));

View file

@ -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<prefix.length||end<0)throw new Error('worker source not found');return source.slice(start,end)}
function runWorker(workerSource,data){let message=null;const self={postMessage:m=>{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));

View file

@ -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<prefix.length||end<0)throw new Error('worker source not found');const workerSource=source.slice(start,end);let message=null;const self={postMessage:m=>{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<<BigInt(b))/4n).toString();self.onmessage({data:{type:'build',id:2,key:'e400',bits:b,re:R,im:'0',iter:120}});if(!message||message.type==='error')throw new Error(message?.error||'e400 worker produced no message');if(message.checkpointMismatch||message.refs.byteLength!==(message.refLen+1)*16)throw new Error('e400 reference contract failed');console.log(JSON.stringify({status:'pass',refLen:primary.refLen,precisionBits:primary.precisionBits,checkpointCount:primary.checkpointCount,buildMs:primary.buildMs,productionBla:false,e400:{checkpointMismatch:message.checkpointMismatch,refLen:message.refLen}},null,2));

View file

@ -1,26 +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'),dev=await fs.readFile(new URL('index.external.html',root),'utf8');
const must=(ok,msg)=>{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<storage,read_write> 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));

View file

@ -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):v>>BigInt(-d)}
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
function bitsForSpan(t){const s=String(t).toLowerCase(),a=s.split('e'),m=Math.abs(Number(a[0])),e=a[1]?Number(a[1]):0,l2=Math.log2(m)+e*Math.log2(10);return Math.max(256,Math.ceil(-l2)+240)}
function snap(scene){const bits=bitsForSpan(scene.span);return{bits,re:decFixed(scene.re,bits),im:decFixed(scene.im,bits),span:decFixed(scene.span,bits)}}
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
function log2FixedAt(v,b){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-b}
function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:Math.pow(2,l-exp),exp}}
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
function direct(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<<BigInt(b);let zr=0n,zi=0n,refLen=limit;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;const mag=roundShift(zr*zr,b)+roundShift(zi*zi,b);if(mag>bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[2*refLen]=hr1;hi[2*refLen+1]=hi1;lo[2*refLen]=F(rr1-hr1);lo[2*refLen+1]=F(ri1-hi1);break}}return{hi,lo,refLen}}
function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}
function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))}
function primary(ref,s,w,h,x,y,limit){const se0=spanME(s),se={mant:F(se0.mant),exp:se0.exp},dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w);let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);return ea<=1e-3?{kind:'bounded',n}:{kind:'unknown',n}}if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n};return{kind:'unknown',n}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
function q(a,b){const x=F(a+b),e=F(b-F(x-a));return[x,e]}
function sum(a,b){const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
function prod(a,b){const x=F(a*b),ca=F(4097*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(4097*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(a[1]+b[1])))}
function neg(a){return[F(-a[0]),F(-a[1])]}
function sub(a,b){return add(a,neg(b))}
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(a[0]*b[1]));e=F(e+F(a[1]*b[0]));e=F(e+F(a[1]*b[1]));return q(t[0],e)}
function scaleDS(a,b){const t=prod(a[0],b);return q(t[0],F(t[1]+F(a[1]*b)))}
function pow2DS(a,e){if(e<-126)return[0,0];if(e>126)return[F(8.507059e37),0];return[F(a[0]*2**e),F(a[1]*2**e)]}
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]}
function cmulDS(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
function cscale(a,b){return[scaleDS(a[0],b),scaleDS(a[1],b)]}
function cpow2(a,e){return[pow2DS(a[0],e),pow2DS(a[1],e)]}
function mag2(a){return add(mul(a[0],a[0]),mul(a[1],a[1]))}
function val(a){return F(a[0]+a[1])}
function maxabsDS(a){return Math.max(Math.abs(val(a[0])),Math.abs(val(a[1])))}
function correction(ref,s,w,h,x,y,limit){const se=spanME(s),mh=F(se.mant),sm=[mh,F(se.mant-mh)],iw=1/w,iwh=F(iw),inv=[iwh,F(iw-iwh)],ox=F(x+.5-.5*w),oy=F(.5*h-y-.5),dx=scaleDS(inv,ox),dy=scaleDS(inv,oy),d0=[mul(sm,dx),mul(sm,dy)];let d=d0,wv=[[0,0],[0,0]],e=se.exp,n=0,m=0,ops=0;while(true){if(n>=limit)return{kind:'bounded',n};if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,r=[[ref.hi[rp],ref.lo[rp]],[ref.hi[rp+1],ref.lo[rp+1]]],delta=cpow2(wv,e),z=cadd(r,delta),mg=mag2(z);if(cmp(mg,[4,0])>0)return{kind:'escaped',n};const dm=mag2(delta);if(m>0&&cmp(dm,[0,0])>0&&cmp(mg,dm)<0){if(se.exp-e<-96)return{kind:'unknown',n};wv=z;d=cpow2(d0,se.exp);e=0;m=0;continue}if(m>=ref.refLen)return{kind:'unknown',n};const linear=cscale(cmulDS(r,wv),2),sq=cpow2(cmulDS(wv,wv),e);wv=cadd(cadd(linear,sq),d);m++;n++;ops++;const mm=Math.max(maxabsDS(wv),maxabsDS(d));if(mm>=1e30||!Number.isFinite(mm))return{kind:'unknown',n};if(mm>65536){wv=cscale(wv,1/65536);d=cscale(d,1/65536);e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wv=cscale(wv,65536);d=cscale(d,65536);e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
const s=snap(sc),w=61,h=39,limit=2000,ref=buildRef(s,limit);let guardMismatch=0,primaryUnknown=0,primaryFalseEscaped=0,primaryFalseBounded=0,corrected=0,remaining=0,correctionFalseEscaped=0,correctionFalseBounded=0,countMismatch=0,maxCountDiff=0;
for(let y=0;y<h;y++)for(let x=0;x<w;x++){const p=pixel(s,w,h,x,y),g=pixel(s,w,h,x,y,s.bits+64),oracle=direct(p[0],p[1],s.bits,limit),guard=direct(g[0],g[1],s.bits+64,limit);if(oracle!==guard){guardMismatch++;continue}const a=primary(ref,s,w,h,x,y,limit);if(a.kind==='escaped'&&oracle===limit)primaryFalseEscaped++;if(a.kind==='bounded'&&oracle<limit)primaryFalseBounded++;if(a.kind!=='unknown')continue;primaryUnknown++;const c=correction(ref,s,w,h,x,y,limit);if(c.kind==='unknown'){remaining++;continue}corrected++;if(c.kind==='escaped'&&oracle===limit)correctionFalseEscaped++;if(c.kind==='bounded'&&oracle<limit)correctionFalseBounded++;if(c.kind==='escaped'&&oracle<limit&&c.n!==oracle){countMismatch++;maxCountDiff=Math.max(maxCountDiff,Math.abs(c.n-oracle))}}
const report={status:'pass',kind:'sparse-double-single-correction-cpu-model-not-real-gpu',scene:sc.id,grid:[w,h],limit,referenceLength:ref.refLen,guardMismatch,primary:{unknown:primaryUnknown,falseEscaped:primaryFalseEscaped,falseBounded:primaryFalseBounded},correction:{corrected,remaining,falseEscaped:correctionFalseEscaped,falseBounded:correctionFalseBounded,escapeIterationMismatch:countMismatch,maxEscapeIterationDifference:maxCountDiff},note:'DS correction is a visual-quality recovery pass; escape iteration equality is not certified.'};
if(guardMismatch||primaryFalseEscaped||primaryFalseBounded||correctionFalseEscaped||correctionFalseBounded||remaining)throw new Error(JSON.stringify(report));
console.log(JSON.stringify(report,null,2));

View file

@ -1,10 +0,0 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url),h=await fs.readFile(new URL('index.html',root),'utf8'),dev=await fs.readFile(new URL('index.external.html',root),'utf8'),script=await fs.readFile(new URL('script.js',root),'utf8');
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
must(!/<script\s+src=/.test(h),'root standalone HTML has external script dependency');
must(/data-bundle="gpu-kernels"/.test(h)&&/data-bundle="app"/.test(h),'inline bundle markers missing');
must(/id="renderMode"/.test(h)&&/>高速<\//.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(/<script src="gpu-kernels\.js"><\/script>/.test(dev)&&/<script src="script\.js"><\/script>/.test(dev),'hosted/dev template no longer has explicit source scripts');
must(/\$\('#renderMode'\)\.onchange/.test(script)&&/colorAutoStep/.test(script),'source UI handlers missing');
console.log(JSON.stringify({status:'pass',check:'single-file-ui-contract',checks:6},null,2));

View file

@ -1,5 +0,0 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url);const exists=async p=>{try{await fs.stat(new URL(p,root));return true}catch{return false}};
for(const banned of ['kernels.js','src/deep_kernel.c','src/bla_kernel_v18.c','src/color_kernel.c','src/shallow_kernel.c','build/wasm-v23'])if(await exists(banned))throw new Error('legacy renderer asset remains: '+banned);
for(const required of ['index.html','script.js','gpu-kernels.js','tests/webgpu-acceptance.html','tests/webgpu-acceptance.js'])if(!await exists(required))throw new Error('required v24 asset missing: '+required);
console.log(JSON.stringify({status:'pass',legacyDeepAssets:0,requiredAssets:5},null,2));

View file

@ -1,21 +0,0 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
const root=new URL('../',import.meta.url);
const src=await fs.readFile(new URL('gpu-kernels.js',root),'utf8');
const context={};context.globalThis=context;vm.runInNewContext(src,context,{filename:'gpu-kernels.js'});
const kernels=context.MANDEL_WEBGPU_KERNELS;
if(!kernels)throw new Error('kernel bundle did not initialize');
// WGSL 16.2 Reserved Words. A module must not contain one of these tokens.
const reserved=`NULL Self abstract active alignas alignof as asm asm_fragment async attribute auto await become cast catch class co_await co_return co_yield coherent column_major common compile compile_fragment concept const_cast consteval constexpr constinit crate debugger decltype delete demote demote_to_helper do dynamic_cast enum explicit export extends extern external fallthrough filter final finally friend from fxgroup get goto groupshared highp impl implements import inline instanceof interface layout lowp macro macro_rules match mediump meta mod module move mut mutable namespace new nil noexcept noinline nointerpolation non_coherent noncoherent noperspective null nullptr of operator package packoffset partition pass patch pixelfragment precise precision premerge priv protected pub public readonly ref regardless register reinterpret_cast require resource restrict self set shared sizeof smooth snorm static static_assert static_cast std subroutine super target template this thread_local throw trait try type typedef typeid typename typeof union unless unorm unsafe unsized use using varying virtual volatile wgsl where with writeonly yield`.split(/\s+/);
const strip=s=>s.replace(/\/\*[\s\S]*?\*\//g,' ').replace(/\/\/.*$/gm,' ');
const failures=[];
for(const [name,code] of Object.entries(kernels)){
if(typeof code!=='string'||!name.endsWith('_WGSL'))continue;
const clean=strip(code);
for(const word of reserved){
const re=new RegExp(`\\b${word}\\b`);
if(re.test(clean))failures.push(`${name}: reserved token ${word}`);
}
}
if(failures.length)throw new Error(failures.join('\n'));
console.log(JSON.stringify({status:'pass',check:'wgsl-reserved-words',shaderVersion:kernels.version,kernels:Object.keys(kernels).filter(k=>k.endsWith('_WGSL')).length},null,2));

View file

@ -1,4 +0,0 @@
<!doctype html><meta charset="utf-8"><title>v24 WebGPU acceptance</title>
<style>body{font:13px ui-monospace,monospace;background:#09101d;color:#e9efff;margin:16px}iframe{width:320px;height:200px;border:1px solid #445}pre{white-space:pre-wrap}</style>
<h1>v24 WebGPU acceptance</h1><iframe id="app" src="../index.html"></iframe><pre id="out">starting…</pre>
<script src="webgpu-acceptance.js"></script>

View file

@ -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):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<<BigInt(bits);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,bits)+roundShift(zi*zi,bits)>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<ps.length;i++){const[x,y]=ps[i],p=pixel(st,x,y),pg=pixel(st,x,y,st.bits+64),a=orbit(p[0],p[1],st.bits,st.iter),g=orbit(pg[0],pg[1],st.bits+64,st.iter);if(a!==g){guardMismatch++;continue}const m=meta[i],cls=(m>>>28)&3;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a<st.iter)falseBounded++}const row={id,guardMismatch,falseEscaped,falseBounded,unknown};if(guardMismatch||falseEscaped||falseBounded)throw new Error(id+' numeric gate failed: '+JSON.stringify(row));if(unknown!==0)throw new Error(id+': corrected production frame produced UNKNOWN: '+JSON.stringify(row));return row}
async function run(){await waitApp();const api=frame.contentWindow.__MANDEL_TEST__,report=[];
await api.setView({re:'-0.5',im:'0',span:'3.4',baseIter:350,adaptive:false,processMode:'standard',renderMode:'fast'});let st=api.state();if(st.renderMode!=='fast'||st.backend!=='direct')throw new Error('shallow fast mode did not select Direct: '+JSON.stringify(st));
await api.setView({re:'-0.7453983606667815',im:'0.1125046349959942',span:'3.52e-8',baseIter:1200,adaptive:false,processMode:'standard',renderMode:'fast'});st=api.state();if(st.renderMode!=='fast'||st.backend!=='fast-extended'||!st.fastExtended||!/高速拡張/.test(st.engine))throw new Error('deep fast mode did not select precision extension: '+JSON.stringify(st));const fm=await api.sampleMeta(points(st.width,st.height));if(new Set(fm).size<2)throw new Error('fast precision extension collapsed sampled field');
report.push(await numericDeep(api,'seahorse-z12','-0.7453983606667815','0.1125046349959942','3.52e-12',2000));
const before=api.state();await api.panPixels(24,0);st=api.state();if(!st.referenceReused)throw new Error('pan did not reuse Deep reference: '+JSON.stringify({before,after:st}));
const denseMeta=await api.probeMeta({w:61,h:39,strict:true,forceDeep:true,correctUnknown:true});if(denseMeta.some(m=>((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});
})();