food_chain/docs/reported-view-bugfix/index.before.html

1970 lines
219 KiB
HTML
Raw Permalink Normal View History

2026-09-29 23:45:12 +09:00
<!doctype html>
<html lang="ja">
<head>
<meta name="mandelbrot-bundle" content="single-file-v24.6.0-stable-reuse">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#232a38">
<title>Mandelbrot Deep Zoom v24.6.0</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:#000;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:#000;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,.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-return{display:block;margin-top:3px;color:var(--muted);font-size:10px;font-weight:650;letter-spacing:0;text-decoration:none}.brand-return:hover{text-decoration:underline;color:var(--text)}.brand-return:focus-visible{outline:2px solid #fff;outline-offset:2px;border-radius:3px}
.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)}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.render-info{margin-top:8px;padding:8px 9px;border:1px solid rgba(255,255,255,.09);border-radius:12px;background:rgba(255,255,255,.045);font:10.5px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;color:#d9dfed;overflow:hidden}.info-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-main{display:flex;justify-content:space-between;gap:10px}.info-main #badge{min-width:0;overflow:hidden;text-overflow:ellipsis}.info-main #render{flex:none;color:var(--muted)}.info-key{color:var(--muted)}
.bottom{display:flex;justify-content:flex-end}.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)}
@media(max-width:700px){.brand-return{font-size:9.5px}.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}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.panel,.toast,#uiToggle{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"><span>MANDELBROT DEEP ZOOM</span><a class="brand-return" href="https://2012r2.nishi.boats/~333/">333の部屋に戻る</a></div></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="quality">画質</label><select id="quality"><option value="fast">高速</option><option value="standard" selected>標準</option><option value="high">高画質</option></select></div>
<div class="line"><label for="palette">彩色</label><select id="palette" aria-label="彩色プリセット"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option><option value="3">インフェルノ</option><option value="5">氷晶</option><option value="7">オーロラ</option><option value="10">黄金</option><option value="13">電光</option><option value="15">蒼橙</option><option value="19">ピンクと黒</option></select></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" title="色周期は固定し、色相位置だけを動かします">色相変化</button></div>
<div class="render-info" aria-label="描画情報">
<div class="info-line info-main"><span id="badge">起動中</span><span id="render">…</span></div>
<div class="info-line"><span class="info-key">中心</span> <span id="coord">…</span></div>
<div class="info-line"><span class="info-key">倍率</span> <span id="zoom">…</span> · <span class="info-key">幅</span> <span id="span">…</span> · <span id="engine">起動中…</span></div>
</div>
</div>
<div class="group bottom"><button id="share">URL共有</button></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 data-bundle="gpu-kernels">
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_HEURISTIC: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 scale_scalar(v:f32,e:i32)->f32{
// ldexp rounds the combined v*2^e result. Do not reject solely because e
// is below the normal f32 exponent: a large mantissa can still produce a
// perfectly normal representable result (critical around 10^38.461 zoom).
return ldexp(v,e);
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
return ldexp(v,vec2<i32>(e));
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(scale_scalar(errScaled,scaleExp));
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
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, outputStride:u32, outputBase:u32, _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=p.outputBase+gid.y*p.outputStride+gid.x;
if(((fieldMeta[out]>>28u)&3u)==FIELD_INTERIOR_PROVEN){return;}
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;
// Brent candidate search followed by two attracting-cycle confirmations.
// Class 2 stays heuristic and is disabled by strict/export rendering.
var cycleR=0.0; var cycleI=0.0; var cyclePower=1u; var cycleLam=0u;
var candPeriod=0u; var candAge=0u; var candR=0.0; var candI=0.0;
var candMul=vec2<f32>(1.0,0.0); var candRepeats=0u;
loop{
if(n>=p.maxIter){break;}
let prev=vec2<f32>(zr,zi);
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;}
if(p.strict==0u && candPeriod>0u){
candMul=cmul(candMul,2.0*prev);candAge+=1u;
if(candAge>=candPeriod){
let tol=4.0*F32_U*(1.0+max(abs(zr),abs(zi)));
let close=max(abs(zr-candR),abs(zi-candI))<=tol;
let attracting=dot(candMul,candMul)<0.9025;
if(close&&attracting){candRepeats+=1u;if(candRepeats>=2u){fieldMeta[out]=pack_meta(n,FIELD_INTERIOR_HEURISTIC);fieldSmooth[out]=0.0;return;}candR=zr;candI=zi;candAge=0u;candMul=vec2<f32>(1.0,0.0);}else{candPeriod=0u;candRepeats=0u;}
}
}
if(p.strict==0u && candPeriod==0u && n>=64u){
let tol=4.0*F32_U*(1.0+max(abs(zr),abs(zi)));
if(max(abs(zr-cycleR),abs(zi-cycleI))<=tol){let period=cycleLam+1u;if(period<=2048u){candPeriod=period;candAge=0u;candR=zr;candI=zi;candMul=vec2<f32>(1.0,0.0);candRepeats=0u;}}
}
cycleLam+=1u;
if(cycleLam>=cyclePower){cycleR=zr;cycleI=zi;cyclePower=min(65536u,cyclePower*2u);cycleLam=0u;}
}
fieldMeta[out]=pack_unknown(p.maxIter,REASON_OPERATION_LIMIT); fieldSmooth[out]=0.0;
}
`;
// Continuation queues are compacted after each bounded chunk. State is
// indexed by pixel so queue order cannot change the arithmetic sequence.
const ACTIVE_PREPARE_WGSL=String.raw`
@group(0) @binding(0) var<storage,read_write> activeCount:array<atomic<u32>>;
@group(0) @binding(1) var<storage,read_write> indirect:array<u32>;
@compute @workgroup_size(1) fn main(){let n=atomicLoad(&activeCount[0]);indirect[0]=(n+63u)/64u;indirect[1]=1u;indirect[2]=1u;}
`;
const OPERATION_LIMIT_QUEUE_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,targetIter:u32,chunkSize:u32,_numeric2:u32};
struct DeepState{d:vec2<f32>,w:vec2<f32>,scaleExp:i32,n:u32,m:u32,errScaled: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> states:array<DeepState>;
@group(0) @binding(3) var<storage,read_write> queue:array<u32>;
@group(0) @binding(4) var<storage,read_write> count:array<atomic<u32>>;
@group(0) @binding(5) var<storage,read_write> activeMark:array<atomic<u32>>;
@group(0) @binding(6) var<storage,read_write> pixelMap:array<u32>;
fn claim(out:u32)->bool{let wi=out>>5u;let bit=1u<<(out&31u);let old=atomicOr(&activeMark[wi],bit);return (old&bit)==0u;}
fn release(out:u32){let wi=out>>5u;let bit=1u<<(out&31u);let _old=atomicAnd(&activeMark[wi],~bit);}
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let out=gid.x;if(out>=p.fullW*p.fullH){return;}
let packed=fieldMeta[out];
if(((packed>>28u)&3u)!=FIELD_UNKNOWN||((packed&REASON_MASK)>>REASON_SHIFT)!=REASON_OPERATION_LIMIT){return;}
if(!claim(out)){return;}
let q=atomicAdd(&count[0],1u);if(q>=p.unknownOnly){release(out);return;}
let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;
let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW);
let d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
states[q]=DeepState(d,vec2<f32>(0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));pixelMap[q]=out;queue[q]=q;
}
`;
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>>;
struct SeriesData{jump:u32,enabled:u32,_pad0:u32,_pad1:u32,a1:vec2<f32>,a2:vec2<f32>,a3:vec2<f32>,a4:vec2<f32>};
@group(0) @binding(5) var<storage,read> series:SeriesData;
fn series_start(d:vec2<f32>,spanExp:i32)->vec2<f32>{
let d2=cmul(d,d);let d3=cmul(d2,d);let d4=cmul(d2,d2);
var w=cmul(series.a1,d);
w+=ldexp(cmul(series.a2,d2),vec2<i32>(spanExp));
w+=ldexp(cmul(series.a3,d3),vec2<i32>(2*spanExp));
w+=ldexp(cmul(series.a4,d4),vec2<i32>(3*spanExp));
return w;
}
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;
if(p.strict==0u && series.enabled!=0u && series.jump>0u && series.jump<=p.refLen && series.jump<p.maxIter){
w=series_start(d,p.spanExp);n=series.jump;m=series.jump;
}
loop{
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;}
if(n>=p.maxIter){mark_fast_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);return;}
// Reference-near-zero episodes are not an automatic failure. Continue the
// valid perturbation recurrence; explicit range/reference guards remain.
if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=rp;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
let sq=scaled_to_f32(cmul(w,w),scaleExp);
w=linear+sq+d; m+=1u; n+=1u;
let mm=max(maxabs(w),maxabs(d));
if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;}
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;
if(((fieldMeta[out]>>28u)&3u)==FIELD_INTERIOR_PROVEN){return;}
if(p.unknownOnly!=0u){
let prior=fieldMeta[out];let cls=(prior>>28u)&3u;
if(cls!=FIELD_UNKNOWN){return;}
}
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_ACTIVE_RESUME_INIT_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,targetIter:u32,chunkSize:u32,_numeric2:u32};
struct DeepState{d:vec2<f32>,w:vec2<f32>,scaleExp:i32,n:u32,m:u32,errScaled:f32};
@group(0) @binding(0) var<uniform> p:Params;@group(0) @binding(1) var<storage,read_write> states:array<DeepState>;@group(0) @binding(2) var<storage,read> queue:array<u32>;@group(0) @binding(3) var<storage,read_write> activeMark:array<atomic<u32>>;@group(0) @binding(4) var<storage,read> pixelMap:array<u32>;
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){let qi=gid.x;if(qi>=p.unknownOnly){return;}let slot=queue[qi];let out=pixelMap[slot];if(out>=p.fullW*p.fullH){return;}let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW);let d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);states[slot]=DeepState(d,vec2<f32>(0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));}
`;
const DEEP_ACTIVE_CONTINUE_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,targetIter:u32,chunkSize:u32,_numeric2:u32};
struct RefPoint{hi:vec2<f32>,lo:vec2<f32>};struct DeepState{d:vec2<f32>,w:vec2<f32>,scaleExp:i32,n:u32,m:u32,errScaled: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> states:array<DeepState>;@group(0) @binding(3) var<storage,read> queueIn:array<u32>;@group(0) @binding(4) var<storage,read_write> countIn:array<atomic<u32>>;@group(0) @binding(5) var<storage,read_write> queueOut:array<u32>;@group(0) @binding(6) var<storage,read_write> countOut:array<atomic<u32>>;@group(0) @binding(7) var<storage,read_write> fieldMeta:array<u32>;@group(0) @binding(8) var<storage,read_write> fieldSmooth:array<f32>;@group(0) @binding(9) var<storage,read_write> activeMark:array<atomic<u32>>;@group(0) @binding(10) var<storage,read> pixelMap:array<u32>;
fn clear_active(out:u32){let wi=out>>5u;let bit=1u<<(out&31u);let _old=atomicAnd(&activeMark[wi],~bit);}
fn fail(out:u32,n:u32,reason:u32){fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;clear_active(out);}
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){if(gid.x>=atomicLoad(&countIn[0])){return;}let slot=queueIn[gid.x];let out=pixelMap[slot];let cls=(fieldMeta[out]>>28u)&3u;if(cls!=FIELD_UNKNOWN){clear_active(out);return;}var s=states[slot];let stop=min(p.targetIter,s.n+max(1u,p.chunkSize));loop{if(s.n>=stop){break;}if(s.m>p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}let rp=refs[s.m];let delta=scaled_to_f32(s.w,s.scaleExp);let z=rp.hi+(rp.lo+delta);let mag=dot(z,z);if(mag>4.0){let errAbs=safe_abs_error(s.errScaled,s.scaleExp,z,delta);if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(s.n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(s.n,mag);clear_active(out);return;}fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;}if(s.m>=p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}let refAbs=maxabs(rp.hi)+maxabs(rp.lo);let wAbs=maxabs(s.w);let dAbs=maxabs(s.d);let deltaAbs=maxabs(delta);let sq=scaled_to_f32(cmul(s.w,s.w),s.scaleExp);let sqAbs=maxabs(sq);let gain=2.0*refAbs+2.0*deltaAbs;let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30);s.errScaled=gain*s.errScaled+roundErr;let linear=2.0*(cmul(rp.hi,s.w)+cmul(rp.lo,s.w));s.w=linear+sq+s.d;s.m++;s.n++;if(maxabs(s.w)>=1.0e30||maxabs(s.d)>=1.0e30){fail(out,s.n,REASON_RANGE);return;}let mm=max(maxabs(s.w),maxabs(s.d));if(mm>65536.0){s.w*=0.0000152587890625;s.d*=0.0000152587890625;s.errScaled*=0.0000152587890625;s.scaleExp+=16;}else if(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp){s.w*=65536.0;s.d*=65536.0;s.errScaled*=65536.0;s.scaleExp-=16;}if(s.scaleExp>126||s.errScaled!=s.errScaled||s.errScaled>1.0e35){fail(out,s.n,REASON_RANGE);return;}}
if(s.n>=p.targetIter){
if(s.m>p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}
let rpEnd=refs[s.m];let deltaEnd=scaled_to_f32(s.w,s.scaleExp);let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);let mag=dot(zEnd,zEnd);let errAbs=safe_abs_error(s.errScaled,s.scaleExp,zEnd,deltaEnd);
if(mag!=mag||errAbs!=errAbs){fail(out,s.n,REASON_RANGE);return;}
if(mag>4.0){if(length(zEnd)-errAbs>2.0){fieldMeta[out]=pack_meta(s.n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(s.n,mag);clear_active(out);return;}fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;}
if(length(zEnd)+errAbs>2.0){fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;}let limit=select(1.0e-3,1.0e-4,p.strict!=0u);if(errAbs>limit){fail(out,s.n,REASON_ERROR_BOUND);return;}fieldMeta[out]=pack_unknown(s.n,REASON_OPERATION_LIMIT);fieldSmooth[out]=0.0;
}
states[slot]=s;let q=atomicAdd(&countOut[0],1u);queueOut[q]=slot;}
`;
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));
}
fn selected_unknown(packed:u32)->bool{
if(((packed>>28u)&3u)!=FIELD_UNKNOWN){return false;}
if(p.unknownOnly==2u){let reason=(packed&REASON_MASK)>>REASON_SHIFT;return reason>=1u && reason<=5u;}
return true;
}
@compute @workgroup_size(64)
fn main(
@builtin(local_invocation_id) lid3:vec3<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(selected_unknown(packed)){
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));
}
fn selected_unknown(packed:u32)->bool{
if(((packed>>28u)&3u)!=FIELD_UNKNOWN){return false;}
if(p.unknownOnly==2u){let reason=(packed&REASON_MASK)>>REASON_SHIFT;return reason>=1u && reason<=5u;}
return true;
}
@compute @workgroup_size(64)
fn main(
@builtin(local_invocation_id) lid3:vec3<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(selected_unknown(packed)){
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,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
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{
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{
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;
}
if(n>=p.maxIter){count_remaining(out,p.maxIter,REASON_OPERATION_LIMIT);return;}
// DS correction follows the same continuous perturbation recurrence.
if(m>=p.refLen){count_remaining(out,n,REASON_REFERENCE_END);return;}
let linear=cds_scale(cds_mul(r,w),2.0);
let sq=cds_pow2(cds_mul(w,w),scaleExp);
w=cds_add(cds_add(linear,sq),d);
m+=1u; n+=1u;
let mm=max(cds_maxabs(w),cds_maxabs(d));
if(mm>=1.0e30 || mm!=mm){count_remaining(out,n,REASON_RANGE);return;}
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
if(scaleExp>126){count_remaining(out,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 prior=fieldMeta[out];
if(((prior>>28u)&3u)!=FIELD_UNKNOWN){return;}
if(p.unknownOnly==2u){
let reason=(prior&REASON_MASK)>>REASON_SHIFT;
if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;}
}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
// 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;
let prior=fieldMeta[out];
if(((prior>>28u)&3u)!=FIELD_UNKNOWN){return;}
if(p.unknownOnly==2u){
let reason=(prior&REASON_MASK)>>REASON_SHIFT;
if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;}
}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}`,
`@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let localQi=gid.x;
let queued=atomicLoad(&sparseQueueStats.dispatchCount);
if(localQi>=queued){return;}
let qi=bitcast<u32>(p._numeric2)+localQi;
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;}
let prior=fieldMeta[out];
if(((prior>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;}
if(p.unknownOnly==2u){
let reason=(prior&REASON_MASK)>>REASON_SHIFT;
if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;}
}
let gx=f32(p.tileX+lx)+p.sampleX;
let gy=f32(p.tileY+ly)+p.sampleY;
correct_pixel(out,gx,gy);
}`);
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 INTERIOR_MASK_WGSL=String.raw`
struct Mask{width:u32,height:u32,tile:u32,columns:u32,flags:array<u32>};
@group(0) @binding(0) var<storage,read> mask:Mask;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=mask.width||gid.y>=mask.height){return;}
if(mask.flags[(gid.y/mask.tile)*mask.columns+gid.x/mask.tile]==0u){return;}
let i=gid.y*mask.width+gid.x;fieldMeta[i]=3u<<28u;fieldSmooth[i]=0.0;
}
`;
const PRECISION_SCATTER_WGSL=String.raw`
struct Result{index:u32,packed:u32,smoothed:f32,accepted:u32};
@group(0) @binding(0) var<storage,read> results:array<Result>;
@group(0) @binding(1) var<storage,read_write> packed:array<u32>;
@group(0) @binding(2) var<storage,read_write> smoothed:array<f32>;
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=arrayLength(&results)){return;}let r=results[gid.x];
if(r.accepted==0u||r.index>=arrayLength(&packed)){return;}
let prior=packed[r.index];let reason=(prior>>20u)&255u;
if(((prior>>28u)&3u)!=0u||reason<1u||reason>5u){return;}
packed[r.index]=r.packed;smoothed[r.index]=r.smoothed;
}
`;
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 rgb8(r:f32,g:f32,b:f32)->vec3<f32>{return vec3<f32>(r,g,b)/255.0;}
fn tri_phase(t0:f32)->f32{return 1.0-abs(2.0*fract(t0)-1.0);}
fn ramp5(t:f32,c0:vec3<f32>,c1:vec3<f32>,c2:vec3<f32>,c3:vec3<f32>,c4:vec3<f32>)->vec3<f32>{
if(t<0.25){return mix(c0,c1,smoothstep(0.0,0.25,t));}
if(t<0.50){return mix(c1,c2,smoothstep(0.25,0.50,t));}
if(t<0.75){return mix(c2,c3,smoothstep(0.50,0.75,t));}
return mix(c3,c4,smoothstep(0.75,1.0,t));
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(rgb8(4.0,10.0,27.0),rgb8(12.0,53.0,79.0),smoothstep(0.0,0.11,t));}
if(t<0.25){return mix(rgb8(12.0,53.0,79.0),rgb8(31.0,156.0,184.0),smoothstep(0.11,0.25,t));}
if(t<0.38){return mix(rgb8(31.0,156.0,184.0),rgb8(91.0,226.0,234.0),smoothstep(0.25,0.38,t));}
if(t<0.50){return mix(rgb8(91.0,226.0,234.0),rgb8(66.0,53.0,151.0),smoothstep(0.38,0.50,t));}
if(t<0.62){return mix(rgb8(66.0,53.0,151.0),rgb8(139.0,49.0,170.0),smoothstep(0.50,0.62,t));}
if(t<0.73){return mix(rgb8(139.0,49.0,170.0),rgb8(232.0,72.0,145.0),smoothstep(0.62,0.73,t));}
if(t<0.84){return mix(rgb8(232.0,72.0,145.0),rgb8(255.0,137.0,64.0),smoothstep(0.73,0.84,t));}
if(t<0.93){return mix(rgb8(255.0,137.0,64.0),rgb8(255.0,211.0,99.0),smoothstep(0.84,0.93,t));}
return mix(rgb8(255.0,211.0,99.0),rgb8(255.0,250.0,223.0),smoothstep(0.93,1.0,t));
}
fn palette_color(phase:f32)->vec3<f32>{
if(p.palette==0u){return current_palette(phase);}
if(p.palette==1u){return hsv(phase,0.92,1.0);}
if(p.palette==2u){let g=(18.0+237.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3<f32>(g);}
let t=tri_phase(phase);
if(p.palette==19u){return ramp5(t,rgb8(20.0,14.0,26.0),rgb8(72.0,47.0,83.0),rgb8(161.0,92.0,130.0),rgb8(224.0,130.0,164.0),rgb8(249.0,193.0,205.0));}
if(p.palette==3u){return ramp5(t,rgb8(0.0,0.0,2.0),rgb8(40.0,8.0,84.0),rgb8(141.0,36.0,101.0),rgb8(228.0,88.0,48.0),rgb8(252.0,255.0,166.0));}
if(p.palette==5u){return ramp5(t,rgb8(1.0,4.0,14.0),rgb8(7.0,36.0,92.0),rgb8(0.0,132.0,178.0),rgb8(116.0,232.0,240.0),rgb8(242.0,254.0,255.0));}
if(p.palette==7u){return ramp5(t,rgb8(3.0,6.0,20.0),rgb8(63.0,26.0,132.0),rgb8(10.0,146.0,174.0),rgb8(48.0,224.0,140.0),rgb8(229.0,255.0,184.0));}
if(p.palette==10u){return ramp5(t,rgb8(5.0,4.0,2.0),rgb8(44.0,24.0,4.0),rgb8(132.0,83.0,10.0),rgb8(228.0,171.0,46.0),rgb8(255.0,245.0,198.0));}
if(p.palette==13u){return ramp5(t,rgb8(0.0,0.0,14.0),rgb8(21.0,16.0,152.0),rgb8(0.0,188.0,255.0),rgb8(220.0,0.0,255.0),rgb8(255.0,255.0,255.0));}
if(p.palette==15u){return ramp5(t,rgb8(2.0,8.0,26.0),rgb8(18.0,85.0,210.0),rgb8(192.0,244.0,255.0),rgb8(255.0,134.0,36.0),rgb8(255.0,242.0,196.0));}
return current_palette(phase);
}
fn interior_color()->vec3<f32>{return select(vec3<f32>(0.0),rgb8(14.0,10.0,19.0),p.palette==19u);}
fn escaped_color(m:u32,sm:f32)->vec3<f32>{
// Direct phase mapping: no iteration-band/anchor compensation.
let phase=fract(p.shift+sm*p.cycle);
let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.30+0.70*pow(edge,0.40);let floorc=select(interior_color(),vec3<f32>(2.0/255.0),p.palette==2u);return mix(floorc,c,mixv);
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i];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;
if(cls==FIELD_UNKNOWN){
// Finite-budget survivors are unresolved membership, not certified interior.
// Keep alpha zero so stable history can remain visible without asserting black membership.
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(0.0));
return;
}
if(cls!=FIELD_ESCAPED){
// Certified / likely interior is real image content, not a temporal hole.
// Keep it opaque so a reprojected history frame can never leak exterior
// colour into an already-known Mandelbrot interior pixel.
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(interior_color(),1.0));
return;
}
var c=escaped_color(m,fieldSmooth[i]);
if(p.edgeAA!=0u){
let x=i32(gid.x);let y=i32(gid.y);var sum=linearize(c);var cnt=1.0;
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){
if(ox==0&&oy==0){continue;}
let xx=x+ox;let yy=y+oy;
if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}
let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];
if(((mj>>28u)&3u)==FIELD_ESCAPED){sum+=linearize(escaped_color(mj,fieldSmooth[j]));cnt+=1.0;}
}}
c=delinearize(sum/cnt);
}
textureStore(outTex,vec2<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 va=textureLoad(a,q,0);let vb=textureLoad(b,q,0);let vc=textureLoad(c,q,0);let vd=textureLoad(d,q,0);
let alpha=(va.a+vb.a+vc.a+vd.a)*0.25;
let premulLin=(lin3(va.rgb)*va.a+lin3(vb.rgb)*vb.a+lin3(vc.rgb)*vc.a+lin3(vd.rgb)*vd.a)*0.25;
if(alpha<=0.00001){textureStore(outTex,q,vec4<f32>(0.0));return;}
let straightLin=premulLin/alpha;
textureStore(outTex,q,vec4<f32>(srgb3(straightLin)*alpha,alpha));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{
frontScaleX:f32,frontScaleY:f32,frontOffsetX:f32,frontOffsetY:f32,
histScaleX:f32,histScaleY:f32,histOffsetX:f32,histOffsetY:f32,
historyEnabled:f32,_p0:f32,_p1:f32,_p2:f32,
};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var frontTex:texture_2d<f32>;
@group(0) @binding(2) var historyTex:texture_2d<f32>;
@group(0) @binding(3) 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;
}
fn mapped_uv(uv:vec2<f32>,scale:vec2<f32>,offset:vec2<f32>)->vec2<f32>{
return vec2<f32>(0.5)+(uv-vec2<f32>(0.5))*scale+offset;
}
fn inside(uv:vec2<f32>)->bool{return all(uv>=vec2<f32>(0.0))&&all(uv<=vec2<f32>(1.0));}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let fuv=mapped_uv(in.uv,vec2<f32>(p.frontScaleX,p.frontScaleY),vec2<f32>(p.frontOffsetX,p.frontOffsetY));
var front=vec4<f32>(0.0);
if(inside(fuv)){front=textureSampleLevel(frontTex,samp,fuv,0.0);}
if(p.historyEnabled<0.5||front.a>=0.999){return front;}
let huv=mapped_uv(in.uv,vec2<f32>(p.histScaleX,p.histScaleY),vec2<f32>(p.histOffsetX,p.histOffsetY));
var hist=vec4<f32>(0.0);
if(inside(huv)){hist=textureSampleLevel(historyTex,samp,huv,0.0);}
return front+hist*(1.0-front.a);
}
`;
const FAST_PERTURB_POSTSTATS_WGSL=FAST_PERTURB_WGSL.replace('fn mark_fast_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u);\n if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);}\n}','fn mark_fast_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;\n}');
if(FAST_PERTURB_POSTSTATS_WGSL===FAST_PERTURB_WGSL)throw new Error('screen post-stats shader derivation failed');
// One O(pixels) reduction is shared by Direct, Accurate DS, FAST and Deep.
// Workgroup-local atomics replace up to two contended global atomics for
// every UNKNOWN pixel, and reason zero remains visible in the total count.
const UNKNOWN_STATS_WGSL=COMMON+String.raw`
struct Params{pixelCount:u32,_p0:u32,_p1:u32,_p2: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> unresolvedStats:array<atomic<u32>>;
var<workgroup> localReasons:array<atomic<u32>,9>;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) gid:vec3<u32>,@builtin(local_invocation_id) lid:vec3<u32>){
if(lid.x<9u){atomicStore(&localReasons[lid.x],0u);}
workgroupBarrier();
if(gid.x<p.pixelCount){
let packed=fieldMeta[gid.x];let cls=(packed>>28u)&3u;
if(cls==FIELD_UNKNOWN){
atomicAdd(&localReasons[0],1u);
let reason=(packed&REASON_MASK)>>REASON_SHIFT;
if(reason>=1u && reason<=6u){atomicAdd(&localReasons[reason],1u);}
}else if(cls==FIELD_INTERIOR_HEURISTIC){atomicAdd(&localReasons[8],1u);}
}
workgroupBarrier();
if(lid.x<9u){let count=atomicLoad(&localReasons[lid.x]);if(count>0u){atomicAdd(&unresolvedStats[lid.x],count);}}
}
`;
const SYMMETRY_COPY_WGSL=String.raw`
struct Params{width:u32,height:u32,_p0:u32,_p1:u32};
@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>;
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let half=p.height/2u;if(gid.x>=p.width||gid.y>=half){return;}
let src=gid.y*p.width+gid.x;let dst=(p.height-1u-gid.y)*p.width+gid.x;
fieldMeta[dst]=fieldMeta[src];fieldSmooth[dst]=fieldSmooth[src];
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.7.0-clean-finite',INTERIOR_MASK_WGSL,PRECISION_SCATTER_WGSL,DIRECT_F32_WGSL,ACTIVE_PREPARE_WGSL,OPERATION_LIMIT_QUEUE_WGSL,FAST_PERTURB_WGSL,FAST_PERTURB_POSTSTATS_WGSL,DEEP_ACTIVE_RESUME_INIT_WGSL,DEEP_ACTIVE_CONTINUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,UNKNOWN_STATS_WGSL,SYMMETRY_COPY_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
});
})();
</script>
<script data-bundle="app">
(()=>{'use strict';
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
const $=id=>document.getElementById(id),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,PALETTE_MAX=19,VALID_PALETTE_IDS=new Set([0,1,2,3,5,7,10,13,15,19]);
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_HEURISTIC=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=36,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96;
const PIXEL_FRONTIER_WORK=4000000,ACTIVE_CONTINUATION_WORK=8000000,PIXEL_FRONTIER_MAX_ITER=150000;
const QUALITY_PRESETS={
fast:{scale:.75,pixels:524288,iterations:4096},
standard:{scale:1,pixels:1048576,iterations:16384},
high:{scale:2,pixels:4194304,iterations:32768}
};
function normalizeQuality(value){return value==='fast'||value==='power'?'fast':value==='high'||value==='fine'||value==='validate'?'high':'standard'}
function applyQuality(value){state.quality=normalizeQuality(value);state.continuationBudget=QUALITY_PRESETS[state.quality].iterations}
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,continuationBudget:16384,quality:'standard',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,renderClock:null,drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,unresolved:0,unknownReasons:null,gpuError:'',gpuInitFailed:false,lastInteraction:performance.now(),uiHidden:false,temporalFill:false,deferNumericPublish:false,heuristicInterior:0};
let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
// ── 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 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 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 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 fastExtended=fastNeedsExtended(snap,w);return{backend:fastExtended?'fast-extended':'direct',fastExtended,reason:fastExtended?'auto-fast-extended':'auto-direct'}}
function pixelPrecisionBits(snap=snapshot(),w=canvas.width,guard=32){const pixelLog2=log2FixedAt(snap.span,snap.bits)-Math.log2(Math.max(1,w)),need=Math.ceil(Math.max(0,-pixelLog2))+Math.max(16,guard|0);return Math.max(96,Math.min(snap.bits+64,need))}
function numericViewKey(){return[state.bits,state.re,state.im,state.span,state.baseIter,state.adaptive,state.continuationBudget,state.quality].join(':')}
// ── iteration / quality policy ───────────────────────────────────────────
const ADAPTIVE_ITER_QUANTUM=64,ADAPTIVE_PROBE_COLS=4,ADAPTIVE_PROBE_ROWS=3;
const ADAPTIVE_PROBE_CHOICES=[512,768,1024,1536,2048];
function adaptiveIterationBonus(screenBits,q,quantum=ADAPTIVE_ITER_QUANTUM){const raw=48*Math.max(0,Number(q)||0)*Math.max(0,Number(screenBits)||0);return raw>0?Math.ceil(raw/quantum)*quantum:0}
function maxIter(){if(!state.adaptive)return Math.min(state.baseIter,state.continuationBudget||state.baseIter);const z=zoomExp(),w=Math.max(1,canvas.width||innerWidth||1),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),bonus=adaptiveIterationBonus(screenBits,1);return Math.min(state.continuationBudget||12000,12000,Math.max(state.baseIter,state.baseIter+bonus))}
function canUseStableReprojection(){
if(!renderer?.historyReady||!state.frameView)return false;
// Covers normal wheel/pinch, one 2x toolbar zoom, and ordinary pans while
// avoiding grotesque reuse after a reset or a distant shared-URL jump.
return reprojectionSafe(renderer.presentTransform(),2.25,.45);
}
function sameViewSnapshot(a,b){return !!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span}
function canUseRealAxisSymmetry(snap){return !!snap&&snap.im===0n}
function reprojectionSafe(t,maxZoom=1.30,maxShift=.18){const s=Math.abs(Number(t?.scaleX));if(!Number.isFinite(s)||s<=0)return false;const z=Math.max(s,1/s),shift=Math.hypot(Number(t?.offsetX)||0,Number(t?.offsetY)||0);return z<=maxZoom&&shift<=maxShift}
function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))}
function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)}
function fract01(x){return x-Math.floor(x)}
function colorStyleSnapshot(){return{palette:state.palette,cycle:state.cycle,shift:state.shift}}
function colorStyleKey(style=colorStyleSnapshot()){return style.palette+':'+style.cycle+':'+style.shift}
function pixelBudget(){const preset=QUALITY_PRESETS[state.quality],low=Number(navigator.deviceMemory||8)<=4,limit=renderer?.device?.limits?.maxStorageBufferBindingSize||134217728;return Math.max(4096,Math.min(Math.floor(preset.pixels*(low?.5:1)),Math.floor(limit/40)))}
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*QUALITY_PRESETS[state.quality].scale,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));if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function fixedWorkerCommonSource(){return String.raw`
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
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 fixedLog2Abs(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 cadd(a,b){return[a[0]+b[0],a[1]+b[1]]}
function cmuln(a,b){return[a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]]}
function cscale(a,k){return[a[0]*k,a[1]*k]}
function clog2(a){const m=Math.hypot(a[0],a[1]);return m>0?Math.log2(m):-Infinity}
function seriesForOrbit(rr,ri,refLen,span,bits,w,h){
if(!span||refLen<16)return{jump:0,coeffs:Array(8).fill(0),errorLog2:0};
const aspect=Math.max(1e-9,h/Math.max(1,w)),logDelta=fixedLog2Abs(BigInt(span),bits)+Math.log2(Math.hypot(1,aspect));
let a=Array.from({length:7},()=>[0,0]),best={jump:0,coeffs:Array(8).fill(0),errorLog2:0},limit=Math.min(refLen,4096);
for(let n=0;n<limit;n++){
const z=[rr[n],ri[n]],old=a.map(v=>[v[0],v[1]]),next=Array.from({length:7},()=>[0,0]);
for(let k=1;k<=6;k++){
let v=cscale(cmuln(z,old[k]),2);if(k===1)v=cadd(v,[1,0]);
for(let j=1;j<k;j++)v=cadd(v,cmuln(old[j],old[k-j]));
next[k]=v;
}
a=next;const jump=n+1;if(jump<16)continue;
if(a.slice(1).some(v=>!Number.isFinite(v[0])||!Number.isFinite(v[1])))break;
let maxMain=-Infinity;for(let k=1;k<=4;k++)maxMain=Math.max(maxMain,clog2(a[k])+k*logDelta);
const l5=clog2(a[5])+5*logDelta,l6=clog2(a[6])+6*logDelta;
const coeffs=[];let packable=true;for(let k=1;k<=4;k++){const r=Math.fround(a[k][0]),i=Math.fround(a[k][1]);if(!Number.isFinite(r)||!Number.isFinite(i))packable=false;coeffs.push(r,i)}
if(packable&&maxMain<-0.25&&l5<-22&&(l6<l5-1||l6<-28))best={jump,coeffs,errorLog2:l5};
}
return best;
}
`}
function referenceWorkerSource(){return fixedWorkerCommonSource()+String.raw`
'use strict';
const MAX_REF=150001;
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}
// Base and verifier have independent exact fixed-point terminal states.
const orbitCache=new Map();
function orbit(bits,re,im,iter){
const key=[bits,re,im].join(':'),old=orbitCache.get(key);
let r=old||{rr:new Float64Array(iter+1),ri:new Float64Array(iter+1),zr:0n,zi:0n,zr2:0n,zi2:0n,n:0,escape:0,terminalEscape:0,verifiedTo:-1};
const previousLen=r.n;
if(r.rr.length<=iter){const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);rr.set(r.rr);ri.set(r.ri);r.rr=rr;r.ri=ri;}
const B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},bail=16n*(1n<<B);
while(r.n<iter&&!r.terminalEscape){
r.zi=round(2n*r.zr*r.zi)+im;r.zr=r.zr2-r.zi2+re;
r.zr2=(r.zr*r.zr+half)>>B;r.zi2=(r.zi*r.zi+half)>>B;r.n++;
r.rr[r.n]=fixedNum(r.zr,bits);r.ri[r.n]=fixedNum(r.zi,bits);
if(r.zr2+r.zi2>bail)r.terminalEscape=r.n;
}
r.escape=r.terminalEscape<=iter?r.terminalEscape:0;r.refLen=Math.min(r.n,iter);r.previousLen=Math.min(previousLen,r.refLen);
orbitCache.delete(key);orbitCache.set(key,r);
while(orbitCache.size>4)orbitCache.delete(orbitCache.keys().next().value);
return r;
}
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},BAIL=16n*(1n<<B);let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<iter;n++){zi=round(2n*zr*zi)+im;zr=zr2-zi2+re;zr2=(zr*zr+half)>>B;zi2=(zi*zi+half)>>B;if(zr2+zi2>BAIL)return n+1}return 0}
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0,probeBits=bits){
let best={re,im,escape:centerEscape,score:centerEscape||iter};
if(centerEscape===0||span<=0n||w<=0||h<=0)return best;
// Score candidates at the packed-reference precision used by the orbit.
const pb=Math.max(64,Math.min(bits,probeBits|0)),bw=BigInt(w),seen=new Set([re+':'+im]),goal=Math.max(4096,Math.floor(iter/3));
const probe=(sr,si)=>{const key=sr+':'+si;if(seen.has(key))return false;seen.add(key);const rr=requant(sr,bits,pb),ii=requant(si,bits,pb),escape=orbitEscape(pb,rr,ii,iter),score=escape||iter;if(score>best.score)best={re:sr,im:si,escape,score};return escape===0||score>=goal};
// Sample the actual pixel-centre domain with an 8x6 checkerboard.
const div=(v,d)=>{const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q},den=2n*bw;
for(let jy=0;jy<6;jy++)for(let ix=0;ix<8;ix++){if(((ix+jy)&1)!==1)continue;const x=Math.floor((ix+.5)*w/8),y=Math.floor((jy+.5)*h/6),sr=re+div(span*(2n*BigInt(x)+1n-bw),den),si=im+div(span*(BigInt(h)-2n*BigInt(y)-1n),den);if(probe(sr,si))return best}
return best
}
function samePacked(a,b){const ah=Math.fround(a),bh=Math.fround(b);return Object.is(ah,bh)&&Object.is(Math.fround(a-ah),Math.fround(b-bh))}
function verify(baseBits,re,im,ref,refLen){
const bits=baseBits+32,B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},BAIL=16n*(1n<<B),hre=re<<32n,him=im<<32n,from=Math.max(0,ref.verifiedTo+1);
let zr=0n,zi=0n,zr2=0n,zi2=0n,n=0,escape=0,mismatch=false,checked=0;
const check=()=>{if(n<from||n!==refLen&&n%256!==0&&(n&(n-1))!==0)return;checked++;if(!samePacked(fixedNum(zr,bits),ref.rr[n])||!samePacked(fixedNum(zi,bits),ref.ri[n]))mismatch=true};
check();
while(n<refLen&&!escape&&!mismatch){zi=round(2n*zr*zi)+him;zr=zr2-zi2+hre;zr2=(zr*zr+half)>>B;zi2=(zi*zi+half)>>B;n++;check();if(zr2+zi2>BAIL)escape=n}
if(n<refLen)mismatch=true;
if(!mismatch)ref.verifiedTo=Math.max(ref.verifiedTo,refLen);
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)return;const t0=performance.now();try{
if(d.type==='score'){
const sourceBits=d.bits,bits=d.targetBits,limit=Math.min(MAX_REF-1,d.iter),candidates=Array.isArray(d.candidates)?d.candidates:[];
let winner=-1,bestScore=-1,bestEscape=0;const scores=new Uint32Array(candidates.length),escapes=new Uint32Array(candidates.length);
for(let i=0;i<candidates.length;i++){
const sr=BigInt(candidates[i].re),si=BigInt(candidates[i].im),re=requant(sr,sourceBits,bits),im=requant(si,sourceBits,bits),escape=orbitEscape(bits,re,im,limit),score=escape||limit;
scores[i]=score;escapes[i]=escape;if(score>bestScore){winner=i;bestScore=score;bestEscape=escape}
}
postMessage({type:'scored',id:d.id,winner,bestScore:Math.max(0,bestScore),bestEscape,precisionBits:bits,scoreMs:performance.now()-t0,scores:Array.from(scores),escapes:Array.from(escapes)});return;
}
if(d.type!=='build')return;
const sourceBits=d.bits,sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,fixed=d.fixedRe!=null&&d.fixedIm!=null,baseRe=fixed?BigInt(d.fixedRe):sourceRe,baseIm=fixed?BigInt(d.fixedIm):sourceIm,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),limit=Math.min(MAX_REF-1,d.iter),bits=d.targetBits,centerRe=requant(baseRe,sourceBits,bits),centerIm=requant(baseIm,sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,limit),chosen=fixed?{re:baseRe,im:baseIm,escape:centerRef.escape,score:centerRef.escape||limit}:chooseReference(sourceBits,sourceRe,sourceIm,sourceSpan,w,h,limit,centerRef.escape,bits),re=requant(chosen.re,sourceBits,bits),im=requant(chosen.im,sourceBits,bits),ref=(chosen.re===baseRe&&chosen.im===baseIm)?centerRef:orbit(bits,re,im,limit),v=verify(bits,re,im,ref,ref.refLen),series=seriesForOrbit(ref.rr,ref.ri,ref.refLen,sourceSpan,sourceBits,w,h),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,referenceId:[bits,chosen.re,chosen.im,sourceBits,'guarded-v2-longref-failure-guided'].join(':'),extendedFrom:ref.previousLen,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,series,refs},[refs])
}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class ReferenceService{
constructor(maxCacheBytes=8*1024*1024){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=8;this.maxCacheBytes=maxCacheBytes}
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;
if(d.type==='scored'){this.pending.delete(d.id);p.resolve(d);return}
if(d.type!=='error'&&d.checkpointMismatch&&p.message?.type==='build'&&p.attempt<3){p.attempt++;p.message.targetBits+=32;this.worker.postMessage(p.message);return}
this.pending.delete(d.id);if(d.type==='error'){p.reject(new Error(d.error));return}
d.source={...p.source,precisionBits:d.precisionBits,referenceId:d.referenceId,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>1&&(this.cache.size>this.maxCache||Array.from(this.cache.values()).reduce((n,c)=>n+c.refs.byteLength,0)>this.maxCacheBytes))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){const targetBits=pixelPrecisionBits(snap,width,40),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'pixel-guarded-perturb-v24.5-longref-guided'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail}),message={type:'build',id,key,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter};this.pending.set(id,{resolve,reject,key,promise,message,attempt:0,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage(message);return promise}
requestFixed(snap,iter,width,height,fixedSource){if(!fixedSource)return this.request(snap,iter,width,height);const targetBits=Math.max(pixelPrecisionBits(snap,width,40),fixedSource.precisionBits||0),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,fixedSource.re,fixedSource.im,'pixel-fixed-reference-v24.5-guided'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded fixed reference request');if(!this.ensure())return Promise.reject(new Error('Reference Worker unavailable'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail}),message={type:'build',id,key,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter,fixedRe:fixedSource.re.toString(),fixedIm:fixedSource.im.toString()};this.pending.set(id,{resolve,reject,key,promise,message,attempt:0,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage(message);return promise}
scoreFixedCandidates(snap,iter,width,height,candidates){
if(!candidates?.length)return Promise.resolve({type:'scored',winner:-1,bestScore:0,bestEscape:0,scoreMs:0,scores:[],escapes:[]});
const targetBits=Math.max(pixelPrecisionBits(snap,width,40)+32,...candidates.map(c=>c.precisionBits||0));if(this.pending.size)this.cancelPending('superseded reference score');if(!this.ensure())return Promise.reject(new Error('Reference Worker unavailable'));
const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail}),message={type:'score',id,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter,candidates:candidates.map(c=>({re:c.re.toString(),im:c.im.toString()}))};
this.pending.set(id,{resolve,reject,key:null,promise,message,attempt:0,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage(message);return promise
}
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();
const referenceScorer=new ReferenceService(2*1024*1024);
function adaptiveProbeSources(snap,width,height){
const out=[];for(let jy=0;jy<ADAPTIVE_PROBE_ROWS;jy++)for(let ix=0;ix<ADAPTIVE_PROBE_COLS;ix++){
const x=Math.min(width-1,Math.floor((ix+.5)*width/ADAPTIVE_PROBE_COLS)),y=Math.min(height-1,Math.floor((jy+.5)*height/ADAPTIVE_PROBE_ROWS));
out.push(fixedSourceForPixel(y*width+x,snap,width,height));
}return out;
}
function adaptiveProbeChoice(escapes,adaptiveIter,baseIter=state.baseIter){
const options=[...new Set([baseIter,...ADAPTIVE_PROBE_CHOICES.filter(x=>x>=baseIter&&x<adaptiveIter),adaptiveIter])].sort((a,b)=>a-b);
let best=adaptiveIter,bestWork=Infinity;const work=[];
for(const initial of options){let total=0;for(const e of escapes){if(e&&e<=initial)total+=e;else{total+=initial;total+=(e||adaptiveIter)}}work.push({initial,total});if(total<bestWork||(total===bestWork&&initial<best)){bestWork=total;best=initial}}
return{iter:best,work};
}
async function selectAdaptiveInitialIter(snap,adaptiveIter,width,height,token=state.token){
if(!state.adaptive||adaptiveIter<=state.baseIter||width<=0||height<=0)return{iter:adaptiveIter,adaptiveIter,probeMs:0,samples:0,escapes:[],work:[]};
const sources=adaptiveProbeSources(snap,width,height),started=performance.now();
try{
const scored=await referenceScorer.scoreFixedCandidates(snap,adaptiveIter,width,height,sources);if(token!==state.token)return null;
const selected=adaptiveProbeChoice(scored.escapes||[],adaptiveIter,state.baseIter),probeMs=performance.now()-started;
return{...selected,adaptiveIter,probeMs,samples:sources.length,escapes:scored.escapes||[]};
}catch(error){
if(token!==state.token)return null;
console.warn('adaptive initial-iteration probe skipped:',error);return{iter:adaptiveIter,adaptiveIter,probeMs:performance.now()-started,samples:0,escapes:[],work:[],fallback:true};
}
}
function fastReferenceBits(snap,w=canvas.width){return pixelPrecisionBits(snap,w,32)}
function fastReferenceWorkerSource(){return fixedWorkerCommonSource()+String.raw`
'use strict';
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 orbit(bits,re,im,iter){const B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},BAIL=16n*(1n<<B);let zr=0n,zi=0n,zr2=0n,zi2=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);zi=round(2n*zr*zi)+im;zr=zr2-zi2+re;zr2=(zr*zr+half)>>B;zi2=(zi*zi+half)>>B;if(zr2+zi2>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),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},BAIL=16n*(1n<<B);let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<iter;n++){zi=round(2n*zr*zi)+im;zr=zr2-zi2+re;zr2=(zr*zr+half)>>B;zi2=(zi*zi+half)>>B;if(zr2+zi2>BAIL)return n+1}return 0}
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0,probeBits=bits){
let best={re,im,escape:centerEscape,score:centerEscape||iter};
if(centerEscape===0||span<=0n||w<=0||h<=0)return best;
// Score candidates at the packed-reference precision used by the orbit.
const pb=Math.max(64,Math.min(bits,probeBits|0)),bw=BigInt(w),seen=new Set([re+':'+im]),goal=Math.max(4096,Math.floor(iter/3));
const probe=(sr,si)=>{const key=sr+':'+si;if(seen.has(key))return false;seen.add(key);const rr=requant(sr,bits,pb),ii=requant(si,bits,pb),escape=orbitEscape(pb,rr,ii,iter),score=escape||iter;if(score>best.score)best={re:sr,im:si,escape,score};return escape===0||score>=goal};
// Sample the actual pixel-centre domain with an 8x6 checkerboard.
const div=(v,d)=>{const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q},den=2n*bw;
for(let jy=0;jy<6;jy++)for(let ix=0;ix<8;ix++){if(((ix+jy)&1)!==1)continue;const x=Math.floor((ix+.5)*w/8),y=Math.floor((jy+.5)*h/6),sr=re+div(span*(2n*BigInt(x)+1n-bw),den),si=im+div(span*(BigInt(h)-2n*BigInt(y)-1n),den);if(probe(sr,si))return best}
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,bits),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),series=seriesForOrbit(ref.rr,ref.ri,ref.refLen,sourceSpan,d.sourceBits,w,h),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,series,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=8;this.maxCacheBytes=8*1024*1024}
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>1&&(this.cache.size>this.maxCache||Array.from(this.cache.values()).reduce((n,c)=>n+c.refs.byteLength,0)>this.maxCacheBytes))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,width),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.4-longref'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail});this.pending.set(id,{resolve,reject,key,promise,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm: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});return promise}
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 fastRefs=new FastReferenceService();
function fastSeriesBufferData(series){const out=new ArrayBuffer(64),d=new DataView(out),c=Array.isArray(series?.coeffs)?series.coeffs:[];d.setUint32(0,Math.max(0,series?.jump|0),true);d.setUint32(4,series?.jump>0?1:0,true);for(let i=0;i<8;i++)d.setFloat32(16+i*4,Math.fround(Number(c[i])||0),true);return out}
function precisionFallbackWorkerSource(){return fixedWorkerCommonSource()+String.raw`
'use strict';
let orbitCount=0;
const ITER_MASK=0x000fffff,FIELD_ESCAPED=1,REASON_SHIFT=20;
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,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function smooth(n,mag,bits){const m=Math.max(4.0000005,fixedNum(mag,bits)),u=Math.log2(m);return Math.fround(n+1-Math.log2(Math.max(1e-20,.5*u)))}
// The rounding width is constant for this orbit, including negative ties.
function pixel(s,index,bits){orbitCount++;const B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},ONE=1n<<B,w=BigInt(s.width),x=BigInt(index%s.width),y=BigInt(Math.floor(index/s.width)),den=2n*w,re=requant(BigInt(s.re),s.bits,bits),im=requant(BigInt(s.im),s.bits,bits),span=requant(BigInt(s.span),s.bits,bits),cr=re+roundDiv(span*(2n*x+1n-w),den),ci=im+roundDiv(span*(BigInt(s.height)-2n*y-1n),den),bail=4n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<s.iter;n++){zi=round(2n*zr*zi)+ci;zr=zr2-zi2+cr;zr2=(zr*zr+half)>>B;zi2=(zi*zi+half)>>B;const mag=zr2+zi2;if(mag>bail)return{escape:n+1,smooth:smooth(n+1,mag,bits)}}return{escape:0,smooth:0}}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='solve')return;orbitCount=0;try{const indices=new Uint32Array(d.indices),meta=new Uint32Array(indices.length),smoothOut=new Float32Array(indices.length),accepted=new Uint8Array(indices.length),s={bits:d.bits,re:d.re,im:d.im,span:d.span,width:d.width,height:d.height,iter:d.iter},baseBits=d.targetBits;for(let i=0;i<indices.length;i++){let a=pixel(s,indices[i],baseBits);for(let attempt=0;attempt<4&&!accepted[i];attempt++){const bits=baseBits+attempt*32,b=pixel(s,indices[i],bits+32);if(a.escape&&a.escape===b.escape&&Object.is(a.smooth,b.smooth)){meta[i]=(a.escape&ITER_MASK)|(FIELD_ESCAPED<<28);smoothOut[i]=a.smooth;accepted[i]=1}else if(!a.escape&&!b.escape){meta[i]=d.terminalClass>=0?((d.iter&ITER_MASK)|((d.terminalClass&3)<<28)):((d.iter&ITER_MASK)|(6<<REASON_SHIFT));accepted[i]=1}a=b}}postMessage({type:'solved',id:d.id,orbitCount,meta:meta.buffer,smooth:smoothOut.buffer,accepted:accepted.buffer},[meta.buffer,smoothOut.buffer,accepted.buffer])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class PrecisionFallbackService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.failed=false}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([precisionFallbackWorkerSource()],{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 p.resolve({indices:p.indices,orbitCount:d.orbitCount||0,meta:new Uint32Array(d.meta),smooth:new Float32Array(d.smooth),accepted:new Uint8Array(d.accepted)})};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'precision fallback worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter,width,height,indices,terminalClass=-1){if(this.pending.size)this.cancel('superseded precision fallback');if(!this.ensure())return Promise.reject(new Error('precision fallback Workerを作成できません'));const id=++this.serial,copy=Uint32Array.from(indices),targetBits=pixelPrecisionBits(snap,width,40);return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,indices:Uint32Array.from(indices)});this.worker.postMessage({type:'solve',id,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter,terminalClass,indices:copy.buffer},[copy.buffer])})}
cancel(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.cancel('destroyed');if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
}
function precisionFallbackWorkerCount(pixelCount){const cap=Math.max(1,Math.min(4,(Number(navigator.hardwareConcurrency)||4)-1));return pixelCount>=128?cap:Math.min(2,cap)}
const precisionFallbackPool=Array.from({length:Math.max(1,Math.min(4,(Number(navigator.hardwareConcurrency)||4)-1))},()=>new PrecisionFallbackService());
// ── 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}
function numericHistoryScaleEligible(w,h){
const cw=Math.max(1,Math.round(canvas.clientWidth||w)),ch=Math.max(1,Math.round(canvas.clientHeight||h));
const gcd=(a,b)=>{a=Math.abs(a);b=Math.abs(b);while(b){const t=a%b;a=b;b=t}return a||1};
return cw/gcd(w,cw)<=8&&ch/gcd(h,ch)<=8;
}
class WebGpuRenderer{
constructor(adapter,device){
this.device=device;
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.exportWs=[];this.lossReason='';this.activeGpuStage='';this.lastCompletedGpuStage='';this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false;this.numericHistoryView=null;this.numericHistoryIter=0;this.numericHistoryReady=false;this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});this.reprojectSampler=device.createSampler({magFilter:'nearest',minFilter:'nearest'});
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';const where=this.activeGpuStage||this.lastCompletedGpuStage||'unknown-stage';state.gpuError='device lost @ '+where+': '+this.lossReason;if(renderer===this){renderer=null;state.gpuInitFailed=true;markDirty(false);setTimeout(()=>{state.gpuInitFailed=false;initRenderer().then(()=>markDirty(false))},1200)}});
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'premultiplied'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo(),errs=info.messages.filter(x=>x.type==='error');if(errs.length)throw new Error(label+': '+errs.map(x=>'line '+x.lineNum+':'+x.linePos+' '+x.message).join('\n'))}return m}
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,um,sm,cm,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('unknown-stats',G.UNKNOWN_STATS_WGSL),this.module('real-axis-symmetry-copy',G.SYMMETRY_COPY_WGSL),this.module('color',G.COLOR_WGSL),this.module('present',G.PRESENT_WGSL)]);
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
this.unknownStats=this.device.createComputePipeline({layout:'auto',compute:{module:um,entryPoint:'main'}});
this.symmetryCopy=this.device.createComputePipeline({layout:'auto',compute:{module:sm,entryPoint:'main'}});
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,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を取得できません');
this.configure();
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
makeCompute(module){return this.device.createComputePipelineAsync?this.device.createComputePipelineAsync({layout:'auto',compute:{module,entryPoint:'main'}}):this.device.createComputePipeline({layout:'auto',compute:{module,entryPoint:'main'}})}
async ensureFastPipelines(){if(this.fast&&this.fastPostStats)return;if(this.fastPipelinePromise)return this.fastPipelinePromise;this.fastPipelinePromise=(async()=>{const [base,screen]=await Promise.all([this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('fast-perturb-post-stats',G.FAST_PERTURB_POSTSTATS_WGSL)]);[this.fast,this.fastPostStats]=await Promise.all([this.makeCompute(base),this.makeCompute(screen)])})();try{return await this.fastPipelinePromise}finally{this.fastPipelinePromise=null}}
async ensureCorrectionPipelines(){if(this.correct&&this.correctQueued&&this.deepBucketHist&&this.deepBucketPrefix&&this.deepBucketScatter)return;if(this.correctionPipelinePromise)return this.correctionPipelinePromise;this.correctionPipelinePromise=(async()=>{const modules=await Promise.all([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.deepBucketHist,this.deepBucketPrefix,this.deepBucketScatter,this.correct,this.correctQueued]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.correctionPipelinePromise}finally{this.correctionPipelinePromise=null}}
async ensureAaPipeline(){if(this.aaResolve)return;if(this.aaPipelinePromise)return this.aaPipelinePromise;this.aaPipelinePromise=(async()=>{this.aaResolve=await this.makeCompute(await this.module('aa-resolve',G.AA_RESOLVE_WGSL))})();try{return await this.aaPipelinePromise}finally{this.aaPipelinePromise=null}}
async ensureDeepActivePipelines(){if(this.activePrepare&&this.operationLimitQueue&&this.deepActiveResumeInit&&this.deepActiveContinue)return;if(this.deepActivePipelinePromise)return this.deepActivePipelinePromise;this.deepActivePipelinePromise=(async()=>{const modules=await Promise.all([this.module('active-prepare-production',G.ACTIVE_PREPARE_WGSL),this.module('operation-limit-queue-production',G.OPERATION_LIMIT_QUEUE_WGSL),this.module('deep-active-resume-init-production',G.DEEP_ACTIVE_RESUME_INIT_WGSL),this.module('deep-active-continue-production',G.DEEP_ACTIVE_CONTINUE_WGSL)]);[this.activePrepare,this.operationLimitQueue,this.deepActiveResumeInit,this.deepActiveContinue]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.deepActivePipelinePromise}finally{this.deepActivePipelinePromise=null}}
clearBindGroupCache(){if(this.bindGroupCache)this.bindGroupCache.clear()}
bindResourceId(resource){const object=resource?.buffer||resource;if(!object||typeof object!=='object')return String(object);if(!this.bindResourceIds){this.bindResourceIds=new WeakMap();this.nextBindResourceId=1}let id=this.bindResourceIds.get(object);if(!id){id=this.nextBindResourceId++;this.bindResourceIds.set(object,id)}return id}
cachedTextureView(texture){if(!this.textureViews)this.textureViews=new WeakMap();let view=this.textureViews.get(texture);if(!view){view=texture.createView();this.textureViews.set(texture,view)}return view}
colorTarget(f=this.frame){if(!f)throw new Error('color target requested without a frame');if(f.back===f.history){const old=f.history;f.back=f.spare;f.spare=old}return f.back}
cachedBindGroup(pipeline,entries){if(!this.bindGroupCache)this.bindGroupCache=new Map();const key=this.bindResourceId(pipeline)+'|'+entries.map(e=>e.binding+':'+this.bindResourceId(e.resource)+':'+(e.resource?.offset||0)+':'+(e.resource?.size||0)).join('|');let bg=this.bindGroupCache.get(key);if(!bg){bg=this.device.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries});this.bindGroupCache.set(key,bg)}return bg}
frameDestroy(){this.releaseColorSource();if(!this.frame)return;this.clearBindGroupCache();for(const ring of Object.values(this.frame.readbackRings||{}))for(const b of ring.buffers)destroy(b);for(const slot of this.frame.precisionUploadRing?.slots||[])destroy(slot.buffer);for(const k of ['meta','smooth','historyMeta','historySmooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead','deepActiveMark','deepActivePixel','numericParams','statsParams','symmetryParams','colorParams','presentParams'])destroy(this.frame[k]);for(const t of new Set([this.frame.front,this.frame.back,this.frame.history,this.frame.spare]))destroy(t);this.frame=null;this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false;this.numericHistoryView=null;this.numericHistoryIter=0;this.numericHistoryReady=false}
ensureFrame(w,h){
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,colorUsage=T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,front=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'front-color'}),back=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'back-color'}),history=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'stable-history'});
const keepNumericHistory=numericHistoryScaleEligible(w,h);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'),historyMeta:keepNumericHistory?buf(d,n*4,B.COPY_SRC|B.COPY_DST,'stable-history-meta'):null,historySmooth:keepNumericHistory?buf(d,n*4,B.COPY_SRC|B.COPY_DST,'stable-history-smooth'):null,unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,deepActiveCapacity:0,deepActiveState:null,deepActiveQueueA:null,deepActiveQueueB:null,deepActiveCountA:null,deepActiveCountB:null,deepActiveIndirect:null,deepActiveRead:null,deepActiveMark:null,deepActivePixel:null,readbackRings:{},numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),statsParams:buf(d,16,B.UNIFORM|B.COPY_DST,'unknown-stats-params'),symmetryParams:buf(d,16,B.UNIFORM|B.COPY_DST,'symmetry-copy-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,48,B.UNIFORM|B.COPY_DST,'present-params'),front,back,history,spare:history};return this.frame;
}
frameReadback(kind,size){const f=this.frame;if(!f)throw new Error('frame readback requested without a frame');let ring=f.readbackRings[kind];if(!ring){const B=GPUBufferUsage;ring=f.readbackRings[kind]={cursor:0,buffers:Array.from({length:3},(_,i)=>buf(this.device,size,B.COPY_DST|B.MAP_READ,kind+'-readback-'+i))}}for(let tries=0;tries<ring.buffers.length;tries++){const i=ring.cursor++%ring.buffers.length,b=ring.buffers[i];if(!b.mapState||b.mapState==='unmapped')return b}throw new Error(kind+' readback ring is saturated')}
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}
ensureDeepActiveWorkspace(capacity,f=this.frame,stride=32){if(!f)return null;capacity=Math.max(1,Math.min(f.n,Math.ceil(capacity)));if(f.deepActiveCapacity>=capacity&&capacity>=f.deepActiveCapacity/4&&f.deepActiveState&&f.deepActiveMark&&f.deepActivePixel&&f.deepActiveStride===stride)return f;this.clearBindGroupCache();for(const k of ['deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead','deepActiveMark','deepActivePixel'])destroy(f[k]);const d=this.device,B=GPUBufferUsage;f.deepActiveCapacity=capacity;f.deepActiveStride=stride;f.deepActiveState=buf(d,capacity*stride,B.STORAGE|B.COPY_DST,'deep-active-state');f.deepActiveQueueA=buf(d,capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-queue-a');f.deepActiveQueueB=buf(d,capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-queue-b');f.deepActivePixel=buf(d,capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-pixel-map');f.deepActiveCountA=buf(d,4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-count-a');f.deepActiveCountB=buf(d,4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-count-b');f.deepActiveIndirect=buf(d,12,B.STORAGE|B.INDIRECT|B.COPY_DST,'deep-active-indirect');f.deepActiveRead=buf(d,4,B.COPY_DST|B.MAP_READ,'deep-active-count-readback');f.deepActiveMark=buf(d,Math.max(4,Math.ceil(f.n/32)*4),B.STORAGE|B.COPY_DST,'deep-active-membership-bitset');return f}
exportTileSize(){const l=this.device.limits||{},memory=Number(navigator.deviceMemory||8),storage=Number(l.maxStorageBufferBindingSize||0),maxBuffer=Number(l.maxBufferSize||0);return memory<=4||(storage&&storage<2*1024*1024)||(maxBuffer&&maxBuffer<8*1024*1024)?256:512}
exportRingDepth(tile=this.exportTileSize()){const memory=Number(navigator.deviceMemory||8),limit=Number(this.device.limits?.maxBufferSize||0);return memory<=4||(limit&&limit<32*1024*1024)||tile<512?2:3}
destroyExportWorkspace(ws){if(!ws)return;this.clearBindGroupCache();for(const k of ['meta','smooth','cbuf','tex','read'])destroy(ws[k]);for(const b of ws.unresolveds)destroy(b);for(const b of ws.pbufs)destroy(b);for(const t of ws.samples)destroy(t)}
ensureExportWorkspace(slot=0,S=this.exportTileSize()){
const old=this.exportWs[slot];if(old&&old.size===S)return old;if(old)this.destroyExportWorkspace(old);const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,n=S*S,bpr=S*4,pixelBytes=bpr*S,label='export-'+slot+'-';
const ws={slot,size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,label+'unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,label+'numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,label+'color'),samples:[],tex:d.createTexture({label:label+'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,label+'readback')};this.exportWs[slot]=ws;return ws;
}
ensureExportAaSamples(ws){if(ws.samples.length===4)return ws.samples;const d=this.device,T=GPUTextureUsage;for(let i=0;i<4;i++)ws.samples.push(d.createTexture({label:'export-'+ws.slot+'-sample-'+i,size:[ws.size,ws.size],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING}));return ws.samples}
exportWorkspaceDestroy(){for(const ws of this.exportWs)this.destroyExportWorkspace(ws);this.exportWs=[]}
setDeepContext(ctx){
if(this.deepCtx?.key===ctx.key&&this.deepCtx.refLen>=ctx.refLen)return;
const d=this.device,B=GPUBufferUsage,id=ctx.referenceId||ctx.key;if(!this.deepContexts)this.deepContexts=new Map();
const old=this.deepContexts.get(id),bytes=ctx.refs.byteLength,oldBytes=old?.uploadedBytes||0;let refsB=old?.refsB;
if(!refsB||refsB.size<bytes){const capacity=Math.min((PIXEL_FRONTIER_MAX_ITER+1)*16,Math.max(bytes,old?old.refsB.size*2:bytes));refsB=buf(d,capacity,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);if(old){const retired=old.refsB;d.queue.onSubmittedWorkDone().then(()=>destroy(retired),()=>destroy(retired));this.clearBindGroupCache()}}
else if(bytes>oldBytes)d.queue.writeBuffer(refsB,oldBytes,ctx.refs,oldBytes,bytes-oldBytes);
const chosen=old&&old.refLen>ctx.refLen?old:ctx,entry={...chosen,refsB,uploadedBytes:Math.max(oldBytes,bytes)};delete entry.refs;
this.deepContexts.delete(id);this.deepContexts.set(id,entry);this.deepCtx=entry;
if(referenceScorer.cache.get(ctx.key)===ctx)referenceScorer.cache.delete(ctx.key);
while(this.deepContexts.size>3){const key=this.deepContexts.keys().next().value,retired=this.deepContexts.get(key);this.deepContexts.delete(key);d.queue.onSubmittedWorkDone().then(()=>destroy(retired.refsB),()=>destroy(retired.refsB));this.clearBindGroupCache()}
}
destroyDeepContext(){
for(const ctx of this.deepContexts?.values()||[])destroy(ctx.refsB);
this.deepContexts?.clear();this.deepCtx=null;this.clearBindGroupCache();
}
setFastContext(ctx){
if(this.fastCtx&&this.fastCtx.key===ctx.key&&this.fastCtx.refLen>=ctx.refLen)return;this.destroyFastContext();
const d=this.device,B=GPUBufferUsage,seriesB=buf(d,64,B.STORAGE|B.COPY_DST,'fast-series');d.queue.writeBuffer(seriesB,0,fastSeriesBufferData(ctx.series));
if(ctx.refsB){this.fastCtx={...ctx,seriesB,borrowedRefs:true};return}
const 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,seriesB,borrowedRefs:false}
}
destroyFastContext(){if(this.fastCtx){if(!this.fastCtx.borrowedRefs)destroy(this.fastCtx.refsB);destroy(this.fastCtx.seriesB)}this.fastCtx=null;this.clearBindGroupCache()}
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,outputStride=w,outputBase=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);d.setUint32(52,outputStride,true);d.setUint32(56,outputBase,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,queueOffset=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||0,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);d.setUint32(92,queueOffset,true)})}
deepActiveParams(w,h,iter,snap,refPixel,targetIter=iter,chunkSize=128,strict=0,selectionCount=0){const data=this.deepParams(w,h,w,h,0,0,iter,snap,.5,.5,strict,refPixel.x,refPixel.y,selectionCount,w,0);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(84,targetIter,true);d.setUint32(88,chunkSize,true)})}
fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0,unknownOnly=0,strict=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,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)})}
colorParamsData(w,h,_iter=state.fieldView?.iter??maxIter(),style=colorStyleSnapshot()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,style.palette,true);d.setUint32(12,0,true);d.setFloat32(16,style.cycle,true);d.setFloat32(20,style.shift,true)})}
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const bg=this.cachedBindGroup(this.direct,[{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 bg=this.cachedBindGroup(this.fast,[{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}},{binding:5,resource:{buffer:this.fastCtx.seriesB}}]),pass=encoder.beginComputePass();pass.setPipeline(this.fast);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeFastPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const bg=this.cachedBindGroup(this.fastPostStats,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:5,resource:{buffer:this.fastCtx.seriesB}}]),pass=encoder.beginComputePass();pass.setPipeline(this.fastPostStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeUnknownStats(encoder,{meta,unresolved,n,params=this.frame?.statsParams}){const d=this.device;if(!params)throw new Error('unknown stats params are missing');d.queue.writeBuffer(params,0,new Uint32Array([n,0,0,0]));encoder.clearBuffer(unresolved,0,UNRESOLVED_BYTES);const bg=this.cachedBindGroup(this.unknownStats,[{binding:0,resource:{buffer:params}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass({label:'unknown-post-stats'});pass.setPipeline(this.unknownStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(n/256));pass.end()}
encodeSymmetryCopy(encoder,{meta,smooth,w,h,params=this.frame?.symmetryParams}){if(h<2)return;const d=this.device;if(!params)throw new Error('symmetry params are missing');d.queue.writeBuffer(params,0,new Uint32Array([w,h,0,0]));const bg=this.cachedBindGroup(this.symmetryCopy,[{binding:0,resource:{buffer:params}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]),pass=encoder.beginComputePass({label:'real-axis-symmetry-copy'});pass.setPipeline(this.symmetryCopy);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(Math.floor(h/2)/8));pass.end()}
encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const bg=this.cachedBindGroup(this.deepBucketHist,[{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()}
encodeDeepBucketPrefix(encoder,{bucketState,queueStats,indirect}){const bg=this.cachedBindGroup(this.deepBucketPrefix,[{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 bg=this.cachedBindGroup(this.deepBucketScatter,[{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 bg=this.cachedBindGroup(this.correctQueued,[{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}){const pipeline=this.correct,bg=this.cachedBindGroup(pipeline,[{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()}
numericInFlightBudgetMs(){return state.pointerActive||state.wheelActive||performance.now()-state.lastInteraction<180?24:40}
numericBurstLimit(predictedStripMs){return Math.max(1,Math.min(8,Math.ceil(this.numericInFlightBudgetMs()/Math.max(1,Number(predictedStripMs)||20))))}
numericTileShape(w,iter){
const maxPixels=Math.floor(48000000/Math.max(1,iter));
const width=Math.max(1,Math.min(w,maxPixels));
return{width,rows:Math.max(1,Math.floor(maxPixels/width))}
}
initialNumericRows(w,iter){
const workBudget=48000000;return Math.max(1,Math.floor(workBudget/Math.max(1,w*iter)))
}
adaptNumericRows(rows,elapsedMs,remaining){
if(remaining<=0)return rows;const targetMs=22,ms=Math.max(.25,Number(elapsedMs)||targetMs);if(ms<=targetMs*1.25)return Math.max(1,Math.min(rows,remaining));const factor=Math.max(.5,Math.min(1,Math.sqrt(targetMs/ms)));return Math.max(1,Math.min(rows,remaining,Math.round(rows*factor)))
}
async submitStage(encoder,stage,token,counter='numeric'){
this.activeGpuStage=stage;
const t0=performance.now();this.device.queue.submit([encoder.finish()]);
await this.device.queue.onSubmittedWorkDone();
const ms=performance.now()-t0;this.lastCompletedGpuStage=stage;this.activeGpuStage='';
return token===undefined||token===state.token?ms:null;
}
submitStageDeferred(encoder,stage,counter='numeric'){this.activeGpuStage=stage;this.device.queue.submit([encoder.finish()]);return stage}
async waitStageBurst(stage,token,t0,count=1){await this.device.queue.onSubmittedWorkDone();const ms=(performance.now()-t0)/Math.max(1,count);this.lastCompletedGpuStage=stage;this.activeGpuStage='';return token===undefined||token===state.token?ms:null}
encodeDeepActiveResumeInit(e,{pbuf,stateB,queue,count,mark=this.frame?.deepActiveMark,pixelMap=this.frame?.deepActivePixel,pipeline=this.deepActiveResumeInit}){const d=this.device,bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:stateB}},{binding:2,resource:{buffer:queue}},{binding:3,resource:{buffer:mark}},{binding:4,resource:{buffer:pixelMap}}]),pass=e.beginComputePass({label:'deep-active-resume-init-production'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(count/64));pass.end()}
async readDeepActiveCount(buffer){await buffer.mapAsync(GPUMapMode.READ,0,4);const n=new Uint32Array(buffer.getMappedRange(0,4))[0]||0;buffer.unmap();return n}
async certifyInterior(snap,token,clear=false,proof=null,historySeed=null){
if(token!==state.token)return 0;
const f=this.ensureFrame(canvas.width,canvas.height),result=proof||certifiedInteriorTiles(snap,f.w,f.h),{mask,pixels}=result,hasSeed=!!historySeed;if(!pixels&&!hasSeed)return 0;
if(pixels&&!this.interiorMask)this.interiorMask=await this.makeCompute(await this.module('exact-interior-mask',G.INTERIOR_MASK_WGSL));
if(token!==state.token)return 0;
const d=this.device,input=pixels?buf(d,mask.byteLength,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST,'interior-mask'):null;
try{
if(input)d.queue.writeBuffer(input,0,mask);const e=d.createCommandEncoder({label:'history-seed-and-exact-interior'});if(historySeed)this.encodeNumericHistorySeed(e,historySeed);else if(clear){e.clearBuffer(f.meta);e.clearBuffer(f.smooth)}
if(pixels){const bg=d.createBindGroup({layout:this.interiorMask.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:input}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]});
const pass=e.beginComputePass();pass.setPipeline(this.interiorMask);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end()}
return await this.submitStage(e,historySeed?'numeric history + exposed interior':'exact interior before primary',token,'correction')===null?0:pixels;
}finally{destroy(input)}
}
async beginGpuOperationLimitContinuation(snap,token,refPixel={x:canvas.width*.5,y:canvas.height*.5},strict=false,expectedActive=0){
await this.ready;await this.ensureDeepActivePipelines();if(token!==state.token)return null;const f=this.ensureFrame(canvas.width,canvas.height),expected=Math.max(0,Math.min(f.n,Math.floor(expectedActive||0))),capacity=expected?Math.min(f.n,Math.max(expected,Math.ceil(expected*1.25)+256)):f.n,limit=Math.min(Number(this.device.limits?.maxStorageBufferBindingSize||this.adapterLimits.maxStorageBufferBindingSize||Infinity),Number(this.device.limits?.maxBufferSize||this.adapterLimits.maxBufferSize||Infinity)),stateBytes=capacity*32;if(stateBytes>limit)throw new Error('GPU-resident active state exceeds adapter buffer limit: '+stateBytes+' > '+limit);this.ensureDeepActiveWorkspace(capacity,f,32);const d=this.device,queues=[f.deepActiveQueueA,f.deepActiveQueueB],counts=[f.deepActiveCountA,f.deepActiveCountB];d.queue.writeBuffer(f.numericParams,0,this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER+1,snap,refPixel,0,1,strict?1:0,f.deepActiveCapacity));const e=d.createCommandEncoder({label:'gpu-resident-frontier-init'});e.clearBuffer(f.deepActiveMark);e.clearBuffer(counts[0]);e.clearBuffer(counts[1]);this.encodeOperationLimitQueue(e,{pbuf:f.numericParams,meta:f.meta,stateB:f.deepActiveState,queue:queues[0],count:counts[0],mark:f.deepActiveMark,pixelMap:f.deepActivePixel,n:f.n});e.copyBufferToBuffer(counts[0],0,f.deepActiveRead,0,4);this.submitStageDeferred(e,'GPU-resident frontier init','correction');const count=await this.readDeepActiveCount(f.deepActiveRead);if(token!==state.token)return null;if(count>f.deepActiveCapacity)return this.beginGpuOperationLimitContinuation(snap,token,refPixel,strict,count);return{f,queues,counts,compact:true,mapping:f.deepActivePixel,pipeline:this.deepActiveContinue,input:0,active:count,initial:count,progress:0,maxDispatchWork:0,dispatches:0,pixelIterations:0,strict:!!strict,batchMsPerPass:8,referenceId:null,capacity:f.deepActiveCapacity};
}
async reinitializeActiveSession(session,snap,token,refPixel){if(!session?.active||token!==state.token)return session;const {f}=session,d=this.device;d.queue.writeBuffer(f.numericParams,0,this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER+1,snap,refPixel,0,1,session.strict?1:0,session.active));const e=d.createCommandEncoder({label:'active-reference-reinit'});this.encodeDeepActiveResumeInit(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queue:session.queues[session.input],count:session.active,mark:f.deepActiveMark,pixelMap:f.deepActivePixel,pipeline:this.deepActiveResumeInit});const ms=await this.submitStage(e,'active reference reinit '+session.active,token,'correction');if(ms===null)return null;session.progress=0;return session}
async appendRecoveredOperationLimits(session,snap,token,refPixel,expectedTotal=0){if(!session||token!==state.token)return null;const before=session.active;const rebuilt=await this.beginGpuOperationLimitContinuation(snap,token,refPixel,session.strict,Math.max(expectedTotal,before));if(!rebuilt||token!==state.token)return null;Object.assign(session,rebuilt);const added=Math.max(0,session.active-before);if(added)session.progress=0;return added}
async sampleActiveIndices(session,maxSamples=12){if(!session?.active)return new Uint32Array();const count=Math.min(session.active,Math.max(1,maxSamples|0)),d=this.device,B=GPUBufferUsage,slotRead=buf(d,count*4,B.COPY_DST|B.MAP_READ,'active-sample-slots'),e=d.createCommandEncoder({label:'active-sample-slots'});for(let i=0;i<count;i++){const pos=count===1?0:Math.round(i*(session.active-1)/(count-1));e.copyBufferToBuffer(session.queues[session.input],pos*4,slotRead,i*4,4)}d.queue.submit([e.finish()]);await slotRead.mapAsync(GPUMapMode.READ);const slots=new Uint32Array(slotRead.getMappedRange().slice(0));slotRead.unmap();destroy(slotRead);const read=buf(d,count*4,B.COPY_DST|B.MAP_READ,'active-samples'),g=d.createCommandEncoder({label:'active-sample-pixels'});for(let i=0;i<count;i++)g.copyBufferToBuffer(session.f.deepActivePixel,slots[i]*4,read,i*4,4);d.queue.submit([g.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();destroy(read);return out}
encodeOperationLimitQueue(e,{pbuf,meta,stateB,queue,count,mark,pixelMap=this.frame?.deepActivePixel,n}){const pipeline=this.operationLimitQueue,bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:stateB}},{binding:3,resource:{buffer:queue}},{binding:4,resource:{buffer:count}},{binding:5,resource:{buffer:mark}},{binding:6,resource:{buffer:pixelMap}}]),pass=e.beginComputePass({label:'operation-limit-queue'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(n/256));pass.end()}
encodeDeepActiveChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input,passes,refsB=this.deepCtx.refsB,pipeline=this.deepActiveContinue,mark=this.frame?.deepActiveMark,pixelMap=this.frame?.deepActivePixel}){const d=this.device;for(let k=0;k<passes;k++){const output=1-input;e.clearBuffer(counts[output]);let bg=d.createBindGroup({layout:this.activePrepare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:counts[input]}},{binding:1,resource:{buffer:indirect}}]}),pass=e.beginComputePass({label:'deep-active-indirect-prepare'});pass.setPipeline(this.activePrepare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end();bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:refsB}},{binding:2,resource:{buffer:stateB}},{binding:3,resource:{buffer:queues[input]}},{binding:4,resource:{buffer:counts[input]}},{binding:5,resource:{buffer:queues[output]}},{binding:6,resource:{buffer:counts[output]}},{binding:7,resource:{buffer:meta}},{binding:8,resource:{buffer:smooth}},{binding:9,resource:{buffer:mark}},{binding:10,resource:{buffer:pixelMap}}]});pass=e.beginComputePass({label:'deep-active-chunk'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end();input=output}return input}
async continueOperationLimitActive(session,snap,target,token,ctx,refPixel){
if(!session||!ctx)throw new Error('active continuation session/context is missing');
if(token!==state.token)return null;
this.setDeepContext(ctx);const {f,queues,counts}=session,d=this.device,refsB=this.deepCtx.refsB;
while(session.active>0&&session.progress<target){
if(token!==state.token)return null;
const before=session.active,remaining=target-session.progress;
const chunk=Math.min(256,remaining,Math.floor(ACTIVE_CONTINUATION_WORK/before));
if(chunk<1)throw new Error('active workspace exceeds per-dispatch work capacity');
const budgetMs=this.numericInFlightBudgetMs(),seedMs=Math.max(.5,session.batchMsPerPass||session.batchPasses&&budgetMs/session.batchPasses||8),passCap=12,passes=Math.min(Math.ceil(remaining/chunk),passCap,Math.max(1,Math.floor(budgetMs/seedMs)));
const params=this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER,snap,refPixel,target,chunk,session.strict?1:0);new DataView(params).setUint32(28,ctx.refLen,true);d.queue.writeBuffer(f.numericParams,0,params);
const e=d.createCommandEncoder({label:'active-batch'}),started=performance.now();
session.input=this.encodeDeepActiveChunks(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queues,counts,indirect:f.deepActiveIndirect,meta:f.meta,smooth:f.smooth,input:session.input,passes,refsB,pipeline:session.pipeline});
e.copyBufferToBuffer(counts[session.input],0,f.deepActiveRead,0,4);
this.submitStageDeferred(e,'active batch @ '+target,'correction');
// Mapping this copy provides the completion fence; no second queue wait.
session.active=await this.readDeepActiveCount(f.deepActiveRead);
if(token!==state.token)return null;
if(session.active>before)throw new Error('active queue grew within a batch');
const elapsed=performance.now()-started,work=before*chunk;
session.progress=Math.min(target,session.progress+chunk*passes);
session.dispatches+=passes;session.pixelIterations+=work*passes;
session.maxDispatchWork=Math.max(session.maxDispatchWork,work);
const msPerPass=elapsed/Math.max(1,passes);session.batchMsPerPass=.65*seedMs+.35*Math.max(.25,msPerPass);session.batchPasses=passes;
this.lastCompletedGpuStage='active batch';this.activeGpuStage='';
}
if(token!==state.token)return null;
const e=d.createCommandEncoder({label:'active-post-stats'});
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
this.submitStageDeferred(e,'active post-stats','correction');
return session;
}
async computePerturbFrameTiled(snap,iter,token,referencePixel,colorStyle,colorKey,preserveInterior=false,preseededHistory=false,deferColor=false){
const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h,previous=state.fieldView,currentReferenceKey=this.fastCtx?.key||'',reusePriorField=!!previous&&sameViewSnapshot(previous,snap)&&previous.w===f.w&&previous.h===f.h&&previous.iter<iter&&previous.referenceKey===currentReferenceKey&&previous.fastExtended,historySeed=preseededHistory?null:this.numericHistorySeed(snap,iter,preserveInterior),reuseResolved=reusePriorField||preseededHistory||!!historySeed;
const tileWidth=this.numericTileShape(f.w,iter).width;let rows=this.initialNumericRows(tileWidth,iter),burstCount=0,burstStart=performance.now(),lastStage='',predictedStripMs=22,burstLimit=this.numericBurstLimit(predictedStripMs),burstPredictedMs=0,lastCompletedRows=rows;
const flushBurst=async remaining=>{if(!burstCount)return true;const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;rows=this.adaptNumericRows(lastCompletedRows,ms,remaining);predictedStripMs=.65*predictedStripMs+.35*ms;burstLimit=this.numericBurstLimit(predictedStripMs);burstCount=0;burstPredictedMs=0;burstStart=performance.now();return true};
for(let y=0;y<computeH;){if(token!==state.token)return false;const th=Math.min(rows,computeH-y);lastCompletedRows=th;for(let x=0;x<f.w;x+=tileWidth){if(token!==state.token)return false;const tw=Math.min(tileWidth,f.w-x),base=y*f.w+x,params=this.fastParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,refX,refY,f.w,base,reuseResolved?1:0);d.queue.writeBuffer(f.numericParams,0,params);const e=d.createCommandEncoder({label:'fast-numeric-strip-'+y});if(x===0&&y===0){if(historySeed)this.encodeNumericHistorySeed(e,historySeed);else if(!reusePriorField&&!preserveInterior){e.clearBuffer(f.meta);e.clearBuffer(f.smooth)}}this.encodeFastPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});lastStage='fast primary rows '+y+'..'+(y+th-1);this.submitStageDeferred(e,lastStage,'numeric');burstCount++;burstPredictedMs+=predictedStripMs;const budgetMs=this.numericInFlightBudgetMs();if(burstCount>=burstLimit||burstPredictedMs>=budgetMs||performance.now()-burstStart>=budgetMs){if(!await flushBurst(computeH-(y+th)))return false}}y+=th}
if(!await flushBurst(0))return false;if(!deferColor)d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder({label:deferColor?'fast-post-stats':'fast-post-stats-and-color'});if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});if(!deferColor){const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),cp=e.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end()}const ms=await this.submitStage(e,deferColor?'fast post-stats only':'fast post-stats + color',token,'numeric');if(ms===null)return false;if(!deferColor){[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey}return true
}
async computeFrame(snap,iter,token,referencePixel=null,fastExtended=false,fastContext=null,preserveInterior=false,preseededHistory=false,deferColor=false){
await this.ready;if(fastExtended)await this.ensureFastPipelines();if(token!==state.token)return false;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);
if(fastExtended){this.setFastContext(fastContext);return this.computePerturbFrameTiled(snap,iter,token,referencePixel,colorStyle,colorKey,preserveInterior,preseededHistory,deferColor)}
d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
const mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h,tiled=f.w*computeH*iter>48000000;
if(tiled){const shape=this.numericTileShape(f.w,iter);let burstCount=0,burstStart=performance.now(),predictedTileMs=8,burstLimit=this.numericBurstLimit(predictedTileMs),lastStage='';for(let y=0;y<computeH;y+=shape.rows)for(let x=0;x<f.w;x+=shape.width){
if(token!==state.token)return false;const tw=Math.min(shape.width,f.w-x),th=Math.min(shape.rows,computeH-y);
d.queue.writeBuffer(f.numericParams,0,this.directParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,0,f.w,y*f.w+x));
const e=d.createCommandEncoder({label:'direct-bounded-tile'});if(x===0&&y===0&&!preserveInterior){e.clearBuffer(f.meta);e.clearBuffer(f.smooth)}this.encodeDirectNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});lastStage='direct bounded tile';this.submitStageDeferred(e,lastStage,'numeric');burstCount++;
if(burstCount>=burstLimit){const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;predictedTileMs=.65*predictedTileMs+.35*ms;burstLimit=this.numericBurstLimit(predictedTileMs);burstCount=0;burstStart=performance.now()}
}if(burstCount&&await this.waitStageBurst(lastStage,token,burstStart,burstCount)===null)return false}else d.queue.writeBuffer(f.numericParams,0,this.directParams(f.w,computeH,f.w,f.h,0,0,iter,snap));
if(!deferColor)d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const encoder=d.createCommandEncoder({label:deferColor?'mandelbrot-direct-numeric':'mandelbrot-direct-frame'});
if(!tiled){if(!preserveInterior){encoder.clearBuffer(f.meta);encoder.clearBuffer(f.smooth)}this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:computeH})}
if(mirror)this.encodeSymmetryCopy(encoder,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});
this.encodeUnknownStats(encoder,{meta:f.meta,unresolved:f.unresolved,n:f.n});
if(!deferColor){const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end()}
const ms=await this.submitStage(encoder,deferColor?'direct numeric only':'direct primary',token,'numeric');if(ms===null)return false;if(!deferColor){[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey}return true;
}
releaseColorSource(){
const source=this.colorSource;this.colorSource=null;this.colorSourceVisible=false;
if(source){this.clearBindGroupCache();for(const k of ['meta','smooth','params','texture'])destroy(source[k])}
}
numericHistorySeed(snap,iter,preserveInterior=false,allowHigherIter=false){
const f=this.frame;if(!f||preserveInterior||!this.numericHistoryReady||!this.numericHistoryView||this.numericHistoryIter>iter&&(!allowHigherIter||this.numericHistoryIter>(state.continuationBudget||iter)))return null;
const shift=integerGridReuseShift(this.numericHistoryView,snap,f.w,f.h);if(!shift||shift.pixels<=0)return null;return shift;
}
encodeNumericHistorySeed(e,seed){
const f=this.frame;if(!f||!seed)return false;e.clearBuffer(f.meta);e.clearBuffer(f.smooth);
const bytes=seed.width*4;for(let row=0;row<seed.height;row++){const src=((seed.srcY+row)*f.w+seed.srcX)*4,dst=((seed.dstY+row)*f.w+seed.dstX)*4;e.copyBufferToBuffer(f.historyMeta,src,f.meta,dst,bytes);e.copyBufferToBuffer(f.historySmooth,src,f.smooth,dst,bytes)}
return true;
}
commitNumericHistory(view=state.frameView,iter=state.fieldView?.iter??maxIter()){
const f=this.frame;if(!f||!f.historyMeta||!f.historySmooth||!view||state.fieldView?.complete===false){this.numericHistoryReady=false;return false}const e=this.device.createCommandEncoder({label:'stable-numeric-history'});e.copyBufferToBuffer(f.meta,0,f.historyMeta,0,f.n*4);e.copyBufferToBuffer(f.smooth,0,f.historySmooth,0,f.n*4);this.device.queue.submit([e.finish()]);this.numericHistoryView={...view,w:f.w,h:f.h};this.numericHistoryIter=iter;this.numericHistoryReady=true;return true;
}
captureColorSource(view=state.frameView,needed=state.colorAuto||state.rendering){
this.colorSourceVisible=false;
if(!needed){this.releaseColorSource();return}
const f=this.frame;if(!f||!view)return;
const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
if(!this.colorSource){this.colorSource={
meta:buf(d,f.n*4,B.STORAGE|B.COPY_DST,'color-snapshot-meta'),
smooth:buf(d,f.n*4,B.STORAGE|B.COPY_DST,'color-snapshot-smooth'),
params:buf(d,32,B.UNIFORM|B.COPY_DST,'color-snapshot-params'),
texture:d.createTexture({size:[f.w,f.h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'animated-published-color'})
}}
const source=this.colorSource;source.view={...view};
const e=d.createCommandEncoder({label:'published-color-snapshot'});
e.copyBufferToBuffer(f.meta,0,source.meta,0,f.n*4);e.copyBufferToBuffer(f.smooth,0,source.smooth,0,f.n*4);d.queue.submit([e.finish()]);
}
recolorPublished(){
// Only published numerical data is sampled during in-flight refinement.
// This pass uses its own uniforms/texture and never swaps numeric front/back.
const source=this.colorSource,f=this.frame;if(!source||!f||this.publishedColorWork)return false;
const d=this.device;d.queue.writeBuffer(source.params,0,this.colorParamsData(f.w,f.h));
const bg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:source.params}},{binding:1,resource:{buffer:source.meta}},{binding:2,resource:{buffer:source.smooth}},{binding:3,resource:this.cachedTextureView(source.texture)}]);
const e=d.createCommandEncoder({label:'animate-published-color'}),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()]);
this.colorSourceVisible=true;this.presentFrame();
// Bound optional color work even when the GPU is slower than animation.
const work=d.queue.onSubmittedWorkDone();this.publishedColorWork=work;
const done=()=>{if(this.publishedColorWork===work)this.publishedColorWork=null};work.then(done,done);
return true;
}
async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(token!==state.token||!this.frame)return false;const f=this.frame,d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder(),bg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;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)}}
commitHistory(view=state.frameView,commitNumeric=true){if(!this.frame||!view||state.fieldView?.complete===false)return false;const f=this.frame;if(f.history!==f.front){f.history=f.front;this.clearBindGroupCache()}this.historyView=view;this.historyColorKey=this.frontColorKey;this.historyReady=true;if(commitNumeric)this.commitNumericHistory(view,state.fieldView?.iter??maxIter());return true}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const colored=this.colorSourceVisible&&this.colorSource;if(colored)transform=this.presentTransform(colored.view);const f=this.frame,d=this.device,pb=f.presentParams,interactive=state.pointerActive||state.wheelActive,finite=Number.isFinite(transform?.scaleX)&&Number.isFinite(transform?.scaleY)&&Number.isFinite(transform?.offsetX)&&Number.isFinite(transform?.offsetY)&&transform.scaleX>0&&transform.scaleY>0,ft=finite?transform:{scaleX:1,scaleY:1,offsetX:0,offsetY:0},historyCandidate=!!(!colored&&state.temporalFill&&this.historyReady&&this.historyView&&this.historyColorKey===colorStyleKey()),rawHistory=historyCandidate?this.presentTransform(this.historyView):{scaleX:1,scaleY:1,offsetX:0,offsetY:0},useHistory=!!(historyCandidate&&reprojectionSafe(rawHistory,2.25,.45)),ht=useHistory?rawHistory:{scaleX:1,scaleY:1,offsetX:0,offsetY:0};d.queue.writeBuffer(pb,0,new Float32Array([ft.scaleX,ft.scaleY,ft.offsetX,ft.offsetY,ht.scaleX,ht.scaleY,ht.offsetX,ht.offsetY,useHistory?1:0,0,0,0]));const bg=this.cachedBindGroup(this.present,[{binding:0,resource:interactive?this.reprojectSampler:this.sampler},{binding:1,resource:this.cachedTextureView(colored?colored.texture:f.front)},{binding:2,resource:this.cachedTextureView(f.history)},{binding:3,resource:{buffer:pb}}]),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:0,g:0,b:0,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<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 readNumericalIndices(token){
await this.ensureCorrectionPipelines();if(token!==state.token)return null;
const f=this.ensureDeepQueueWorkspace(this.frame),d=this.device;
const params=writeU32F32(NUMERIC_PARAM_BYTES,p=>{[f.w,f.h,f.w,f.h,0,0,PIXEL_FRONTIER_MAX_ITER,0,0,2,f.w,0].forEach((v,i)=>p.setUint32(i*4,v,true))});
d.queue.writeBuffer(f.numericParams,0,params);
let e=d.createCommandEncoder({label:'numerical-failure-index-build'});e.clearBuffer(f.deepBucketState);e.clearBuffer(f.deepQueueStats);e.clearBuffer(f.deepIndirect);
this.encodeDeepBucketHistogram(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,w:f.w,h:f.h});
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.submitStageDeferred(e,'numerical failure indices','correction');
const q=await this.readDeepQueueStats();if(token!==state.token)return null;
if(q.selected!==q.enqueued||q.overflow||q.invalidIndex||q.selected>f.n)throw new Error('numerical failure queue overflow or missing pixel');
if(!q.selected)return new Uint32Array();
const bytes=q.selected*4;const read=buf(d,bytes,GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ,'numerical-failure-indices');
try{e=d.createCommandEncoder();e.copyBufferToBuffer(f.deepQueue,0,read,0,bytes);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();return token===state.token?out:null;}finally{destroy(read)}
}
precisionUploadBuffer(bytes,f=this.frame){
if(!f)throw new Error('precision upload requested without a frame');
const d=this.device,B=GPUBufferUsage,capacity=Math.max(256,1<<Math.ceil(Math.log2(Math.max(16,bytes))));
if(!f.precisionUploadRing)f.precisionUploadRing={cursor:0,slots:Array.from({length:3},()=>({buffer:null,capacity:0}))};
const slot=f.precisionUploadRing.slots[f.precisionUploadRing.cursor++%f.precisionUploadRing.slots.length];
if(!slot.buffer||slot.capacity<bytes){destroy(slot.buffer);slot.buffer=buf(d,capacity,B.STORAGE|B.COPY_DST,'precision-results-ring');slot.capacity=capacity}
return slot.buffer;
}
async applyPrecisionFallback(result,token){
if(token!==state.token||!this.frame||!result)return null;
if(!this.precisionScatter)this.precisionScatter=await this.makeCompute(await this.module('precision-scatter',G.PRECISION_SCATTER_WGSL));
if(token!==state.token)return null;
const f=this.frame,d=this.device,{indices,meta,smooth,accepted}=result;
const data=new ArrayBuffer(indices.length*16),u=new Uint32Array(data),v=new Float32Array(data);let applied=0;
for(let i=0;i<indices.length;i++){u[i*4]=indices[i];u[i*4+1]=meta[i];v[i*4+2]=smooth[i];u[i*4+3]=accepted[i];applied+=accepted[i]?1:0;}
if(!indices.length)return {applied:0};
const input=this.precisionUploadBuffer(data.byteLength,f);
if(token!==state.token)return null;
d.queue.writeBuffer(input,0,data);
const e=d.createCommandEncoder({label:'precision-results-scatter'});
const bg=d.createBindGroup({layout:this.precisionScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:input}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]});
const p=e.beginComputePass();p.setPipeline(this.precisionScatter);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(indices.length/64));p.end();
// Aggregate once when statistics are consumed, after all queued scatters.
// Keep the flag on the frame so resize/cancellation cannot transfer it.
f.precisionStatsDirty=true;
const ms=await this.submitStage(e,'precision scatter',token,'correction');
return ms===null?null:{applied,ms};
}
async readUnresolvedStats(){
const f=this.frame;if(!f)return{total:0,corrected:0,reasons:{}};
const d=this.device,r=this.frameReadback('unknown-stats',UNRESOLVED_BYTES),e=d.createCommandEncoder();
if(f.precisionStatsDirty){this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});f.precisionStatsDirty=false}
e.copyBufferToBuffer(f.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();
return{total:a[0]||0,corrected:a[7]||0,heuristicInterior:a[8]||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 readDeepQueueStats(){if(!this.frame?.deepQueueStats)return{selected:0,overflow:0,enqueued:0,dispatchCount:0,invalidIndex:0,staleEntry:0,processed:0};const d=this.device,r=this.frameReadback('deep-queue-stats',SPARSE_QUEUE_STATS_BYTES),e=d.createCommandEncoder({label:'deep-queue-stats-readback'});e.copyBufferToBuffer(this.frame.deepQueueStats,0,r,0,SPARSE_QUEUE_STATS_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return{selected:a[0]||0,overflow:a[1]||0,enqueued:a[2]||0,dispatchCount:a[3]||0,invalidIndex:a[4]||0,staleEntry:a[5]||0,processed:a[6]||0}}
async correctUnknownFrame(snap,iter,token,referencePixel=null,numericCount=0,tile=null){
await this.ready;await this.ensureCorrectionPipelines();const f=this.ensureDeepQueueWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
const area=tile||{x:0,y:0,w:f.w,h:f.h};
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;
d.queue.writeBuffer(f.numericParams,0,this.deepParams(area.w,area.h,f.w,f.h,area.x,area.y,iter,snap,.5,.5,0,refX,refY,2,f.w,area.y*f.w+area.x));
let e=d.createCommandEncoder({label:'deep-sparse-queue-build'});e.clearBuffer(f.deepBucketState);e.clearBuffer(f.deepQueueStats);e.clearBuffer(f.deepIndirect);
this.encodeDeepBucketHistogram(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,w:area.w,h:area.h});
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:area.w,h:area.h});
let totalMs=await this.submitStage(e,'queued DS build',token,'correction');if(totalMs===null)return false;
const built=await this.readDeepQueueStats();if(token!==state.token)return false;
const buildValid=built.selected===built.enqueued&&built.overflow===0&&built.invalidIndex===0;if(!buildValid){console.warn('Sparse correction build invariant failed; using bounded full scan',built);return this.correctUnknownFrameScan(snap,iter,token,referencePixel)}
const maxBatch=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,iter)));let offset=0;
while(offset<built.selected){if(token!==state.token)return false;const count=Math.min(maxBatch,built.selected-offset),params=this.deepParams(area.w,area.h,f.w,f.h,area.x,area.y,iter,snap,.5,.5,0,refX,refY,2,f.w,area.y*f.w+area.x,offset);d.queue.writeBuffer(f.numericParams,0,params);d.queue.writeBuffer(f.deepQueueStats,12,new Uint32Array([count]));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array([Math.ceil(count/64),1,1]));e=d.createCommandEncoder({label:'deep-sparse-correction-'+offset});this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});const ms=await this.submitStage(e,'queued DS correction '+(offset+1)+'..'+(offset+count)+'/'+built.selected,token,'correction');if(ms===null)return false;totalMs+=ms;offset+=count}
d.queue.writeBuffer(f.deepQueueStats,12,new Uint32Array([built.selected]));e=d.createCommandEncoder({label:'deep-sparse-correction-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const statsMs=await this.submitStage(e,'queued DS post-stats',token,'correction');if(statsMs===null)return false;totalMs+=statsMs;
const q=await this.readDeepQueueStats();if(token!==state.token)return false;
const valid=q.selected===q.enqueued&&q.enqueued===q.dispatchCount&&q.dispatchCount===q.processed&&q.overflow===0&&q.invalidIndex===0&&q.staleEntry===0;
if(valid)return{mode:'queue',queue:q};
console.warn('Sparse correction invariant failed; using bounded full scan',q);
return this.correctUnknownFrameScan(snap,iter,token,referencePixel)
}
async correctUnknownFrameScan(snap,iter,token,referencePixel=null){const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,maxPixels=Math.max(1,Math.floor(4000000/Math.max(1,iter))),tileW=Math.min(f.w,maxPixels),rows=Math.max(1,Math.min(128,Math.floor(maxPixels/tileW)));for(let y=0;y<f.h;y+=rows){const th=Math.min(rows,f.h-y);for(let x=0;x<f.w;x+=tileW){if(token!==state.token)return false;const tw=Math.min(tileW,f.w-x),base=y*f.w+x;d.queue.writeBuffer(f.numericParams,0,this.deepParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,0,refX,refY,2,f.w,base));const e=d.createCommandEncoder({label:'deep-correction-fallback-'+x+'-'+y});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:tw,h:th});const ms=await this.submitStage(e,'DS fallback tile '+x+','+y+' '+tw+'x'+th,token,'correction');if(ms===null)return false}}const e=d.createCommandEncoder({label:'deep-correction-fallback-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms=await this.submitStage(e,'DS fallback post-stats',token,'correction');if(ms===null)return false;return{mode:'scan'}}
async readCompleteRgba(token=state.token){
if(token!==state.token||state.rendering||!numericFrameComplete())throw new Error('current frame is incomplete');
const f=this.frame,d=this.device,bpr=Math.ceil(f.w*4/256)*256,read=buf(d,bpr*f.h,GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ,'complete-frame-rgba');
try{const e=d.createCommandEncoder();e.copyTextureToBuffer({texture:f.front},{buffer:read,bytesPerRow:bpr,rowsPerImage:f.h},{width:f.w,height:f.h});d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=new Uint8Array(read.getMappedRange()),rgba=new Uint8Array(f.n*4);for(let y=0;y<f.h;y++)rgba.set(raw.subarray(y*bpr,y*bpr+f.w*4),y*f.w*4);read.unmap();if(token!==state.token)throw new Error('cancelled');return rgba;}finally{destroy(read)}
}
async renderTileRGBA({snap,iter,fastContext=null,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,strict=false,workspaceSlot=0,workspaceSize=this.exportTileSize()}){
if(w*h*Math.max(1,iter)>48000000){const shape=this.numericTileShape(w,iter),out=new Uint8ClampedArray(w*h*4),generation=state.token;let unresolved=0;for(let y=0;y<h;y+=shape.rows)for(let x=0;x<w;x+=shape.width){if(generation!==state.token||exportJob.cancelled&&exportJob.active)throw new Error('cancelled');const tw=Math.min(shape.width,w-x),th=Math.min(shape.rows,h-y),part=await this.renderTileRGBA({snap,iter,fastContext,fullW,fullH,sampleX,sampleY,edgeAA,strict,workspaceSlot,workspaceSize,tileX:tileX+x,tileY:tileY+y,w:tw,h:th});for(let row=0;row<th;row++)out.set(part.rgba.subarray(row*tw*4,(row+1)*tw*4),((y+row)*w+x)*4);unresolved+=part.unresolved}return{rgba:out,unresolved}}
await this.ready;const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!!fastContext;if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,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(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,w,0,0,strict?1:0));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,strict?1:0));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;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount}
}
async renderTileRGBA2x({snap,iter,fastContext=null,fullW,fullH,tileX,tileY,w,h,strict=false,workspaceSlot=0,workspaceSize=this.exportTileSize()}){
if(w*h*Math.max(1,iter)>48000000){const shape=this.numericTileShape(w,iter),out=new Uint8ClampedArray(w*h*4),generation=state.token;let unresolved=0;for(let y=0;y<h;y+=shape.rows)for(let x=0;x<w;x+=shape.width){if(generation!==state.token||exportJob.cancelled&&exportJob.active)throw new Error('cancelled');const tw=Math.min(shape.width,w-x),th=Math.min(shape.rows,h-y),part=await this.renderTileRGBA2x({snap,iter,fastContext,fullW,fullH,strict,workspaceSlot,workspaceSize,tileX:tileX+x,tileY:tileY+y,w:tw,h:th});for(let row=0;row<th;row++)out.set(part.rgba.subarray(row*tw*4,(row+1)*tw*4),((y+row)*w+x)*4);unresolved+=part.unresolved}return{rgba:out,unresolved}}
await this.ready;await this.ensureAaPipeline();const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);this.ensureExportAaSamples(ws);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!!fastContext;if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,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(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,w,0,0,strict?1:0));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,strict?1:0));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 unresolved=0;for(let si=0;si<4;si++)unresolved+=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4)[0]||0;ws.read.unmap();return{rgba:out,unresolved}
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext();this.destroyFastContext()}
}
function numericalFailureCount(stats){const r=stats&&stats.reasons||{};return (r.errorBound||0)+(r.escapeUncertain||0)+(r.referenceEnd||0)+(r.rebaseGap||0)+(r.range||0)}
// A broad failure is cheaper to normalize once with the full-frame Deep pass;
// sparse/moderate failures go directly to local references.
// Integer interval tests over exact rational pixel coordinates. No f32
// rounding, periodic-orbit guess, or agreement of two precisions is a proof.
function knownInteriorViewportMayOverlap(snap,width,height){
const unit=1n<<BigInt(snap.bits),bw=BigInt(Math.max(1,width)),bh=BigInt(Math.max(1,height));
const xLo=2n*snap.re-snap.span,xHi=2n*snap.re+snap.span,xDen=2n*unit;
const yLo=2n*bw*snap.im-snap.span*bh,yHi=2n*bw*snap.im+snap.span*bh,yDen=2n*bw*unit;
const axis=(lo,hi,den,lp,lq,hp,hq)=>hi*BigInt(lq)>=BigInt(lp)*den&&lo*BigInt(hq)<=BigInt(hp)*den;
const cardioid=axis(xLo,xHi,xDen,-3,4,3,8)&&axis(yLo,yHi,yDen,-2,3,2,3);
const bulb=axis(xLo,xHi,xDen,-5,4,-3,4)&&axis(yLo,yHi,yDen,-1,4,1,4);
return cardioid||bulb;
}
function certifiedInteriorTiles(snap,width,height,tile=32,historySeed=null){
const columns=Math.ceil(width/tile),rows=Math.ceil(height/tile),mask=new Uint32Array(4+columns*rows);
mask.set([width,height,tile,columns]);let pixels=0;
const unit=1n<<BigInt(snap.bits),den=2n*BigInt(width)*unit,den2=den*den;
const sq=(a,b)=>{const aa=a*a,bb=b*b;return[a<=0n&&b>=0n?0n:aa<bb?aa:bb,aa>bb?aa:bb]};
const productUpper=(a,b,c,d)=>{const p=[a*c,a*d,b*c,b*d];return p.reduce((v,x)=>x>v?x:v)};
const real=x=>2n*BigInt(width)*snap.re+snap.span*BigInt(2*x+1-width);
const imag=y=>2n*BigInt(width)*snap.im+snap.span*BigInt(height-2*y-1);
for(let y=0;y<height;y+=tile)for(let x=0;x<width;x+=tile){
const w=Math.min(tile,width-x),h=Math.min(tile,height-y);if(historySeed&&x>=historySeed.dstX&&y>=historySeed.dstY&&x+w<=historySeed.dstX+historySeed.width&&y+h<=historySeed.dstY+historySeed.height)continue;const xl=real(x),xh=real(x+w-1),yl=imag(y+h-1),yh=imag(y);
const y2=sq(yl,yh),bulb=sq(xl+den,xh+den);
let inside=16n*(bulb[1]+y2[1])<den2;
if(!inside){
const lo=xl-den/4n,hi=xh-den/4n,x2=sq(lo,hi),ql=x2[0]+y2[0],qh=x2[1]+y2[1];
inside=4n*productUpper(ql,qh,ql+lo*den,qh+hi*den)<y2[0]*den2;
}
if(inside){mask[4+(y/tile)*columns+x/tile]=1;pixels+=w*h;}
}
return{mask,pixels};
}
const FAILURE_GUIDED_REF_CANDIDATES=8,FAILURE_GUIDED_REF_PREFILTER=16,FAILURE_GUIDED_REF_DOMINANCE=.6,FAILURE_GUIDED_REF_MIN_EXTEND=256,FAILURE_GUIDED_REF_MAX_PASSES=3;
const OPERATION_LIMIT_REF_BATCH=12,OPERATION_LIMIT_REF_CANDIDATES=24,OPERATION_LIMIT_REF_MIN_ACTIVE=512,OPERATION_LIMIT_REF_MIN_EXTEND=64,OPERATION_LIMIT_REF_DOMINANCE=.75;
function failureGuidedCandidateIndices(indices,w,h,count=FAILURE_GUIDED_REF_CANDIDATES){
if(!indices?.length)return new Uint32Array();if(indices.length<=count)return Uint32Array.from(indices);
// Deterministic 4x2 spatial stratification over the already resident GPU
// failure queue. This avoids a full meta readback and prevents one dense
// black patch from consuming every probe slot.
const cols=4,rows=2,best=new Int32Array(cols*rows);best.fill(-1);const dist=new Float64Array(cols*rows);dist.fill(Infinity);
for(let k=0;k<indices.length;k++){const i=indices[k],x=i%w,y=Math.floor(i/w),bx=Math.min(cols-1,Math.floor(x*cols/Math.max(1,w))),by=Math.min(rows-1,Math.floor(y*rows/Math.max(1,h))),b=by*cols+bx,cx=(bx+.5)*w/cols,cy=(by+.5)*h/rows,d=(x+.5-cx)**2+(y+.5-cy)**2;if(d<dist[b]){dist[b]=d;best[b]=i}}
const out=[],seen=new Set();for(const i of best)if(i>=0&&!seen.has(i)){seen.add(i);out.push(i)}
for(let q=0;out.length<count&&q<count*2;q++){const pos=Math.min(indices.length-1,Math.floor((q+.5)*indices.length/(count*2))),i=indices[pos];if(!seen.has(i)){seen.add(i);out.push(i)}}
return Uint32Array.from(out.slice(0,count));
}
function failureGuidedReferenceEligible(stats,currentRefLen){const r=stats?.reasons||{},numeric=numericalFailureCount(stats),referenceEnd=r.referenceEnd||0;return numeric>=32&&referenceEnd>=Math.max(16,Math.ceil(numeric*FAILURE_GUIDED_REF_DOMINANCE))&&currentRefLen>0}
function operationLimitReferenceEligible(stats,currentRefLen,targetLen){const r=stats?.reasons||{},numeric=numericalFailureCount(stats),operationLimit=r.operationLimit||0,dominates=operationLimit>=Math.max(OPERATION_LIMIT_REF_MIN_ACTIVE,Math.ceil((numeric+operationLimit)*OPERATION_LIMIT_REF_DOMINANCE));return dominates&&currentRefLen>0&&currentRefLen+OPERATION_LIMIT_REF_MIN_EXTEND<targetLen}
async function tryFailureGuidedReference(r,snap,iter,token,stats,tried){
const current=r.deepCtx||r.fastCtx,currentLen=current?.refLen||0;if(!failureGuidedReferenceEligible(stats,currentLen))return{stats,used:false,recovered:0,attempted:0};
const all=await r.readNumericalIndices(token);if(!all||token!==state.token)return null;const pre=failureGuidedCandidateIndices(all,canvas.width,canvas.height,FAILURE_GUIDED_REF_PREFILTER);if(!pre.length)return{stats,used:false,recovered:0,attempted:0};
const preMeta=await r.readMeta(pre);if(token!==state.token)return null;const refEnd=[];for(let i=0;i<pre.length;i++){const packed=preMeta[i]>>>0;if(((packed>>>28)&3)===FIELD_UNKNOWN&&((packed>>>REASON_SHIFT)&255)===3)refEnd.push(pre[i])}const sampled=failureGuidedCandidateIndices(Uint32Array.from(refEnd),canvas.width,canvas.height,FAILURE_GUIDED_REF_CANDIDATES);if(!sampled.length)return{stats,used:false,recovered:0,attempted:pre.length};
const sources=Array.from(sampled,i=>fixedSourceForPixel(i,snap,canvas.width,canvas.height));const scorer=referenceScorer;
const scored=await scorer.scoreFixedCandidates(snap,iter,canvas.width,canvas.height,sources);if(token!==state.token)return null;
if(scored.winner<0)return{stats,used:false,recovered:0,attempted:sampled.length};const winner={...sources[scored.winner],precisionBits:(scored.precisionBits||pixelPrecisionBits(snap,canvas.width,40)+32)+32},minGain=Math.max(FAILURE_GUIDED_REF_MIN_EXTEND,Math.ceil(currentLen*.08));if(scored.bestScore<currentLen+minGain)return{stats,used:false,recovered:0,attempted:sampled.length};
const identity=[winner.re,winner.im,winner.bits,pixelPrecisionBits(snap,canvas.width,40),iter].join(':');if(tried?.has(identity))return{stats,used:false,recovered:0,attempted:sampled.length};tried?.add(identity);
const ctx=await scorer.requestFixed(snap,iter,canvas.width,canvas.height,winner);if(token!==state.token)return null;if(ctx.checkpointMismatch)throw new Error('failure-guided reference guard checkpoint mismatch');if(ctx.refLen<currentLen+minGain)return{stats,used:false,recovered:0,attempted:sampled.length};
r.setDeepContext(ctx);const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height),before=numericalFailureCount(stats),next=await r.correctUnknownFrame(snap,iter,token,refPixel);if(!next||token!==state.token)return null;const after=await r.readUnresolvedStats();if(token!==state.token)return null;const recovered=Math.max(0,before-numericalFailureCount(after));
return{stats:after,used:true,recovered,attempted:(next.queue?.selected||0),ctx,refLen:ctx.refLen,previousRefLen:currentLen,scoreMs:scored.scoreMs||0,buildMs:ctx.buildMs||0,candidates:sampled.length};
}
async function tryOperationLimitGuidedReference(r,session,snap,targetIter,token,stats,currentCtx,tried){
const currentLen=currentCtx?.refLen||0;if(!operationLimitReferenceEligible(stats,currentLen,targetIter))return{used:false,attempted:0};
const sampled=await r.sampleActiveIndices(session,OPERATION_LIMIT_REF_CANDIDATES);if(token!==state.token)return null;if(!sampled.length)return{used:false,attempted:0};
const unique=Array.from(new Set(sampled)),firstIndices=unique.filter((_,i)=>(i&1)===0).slice(0,OPERATION_LIMIT_REF_BATCH),secondIndices=unique.filter((_,i)=>(i&1)===1).slice(0,OPERATION_LIMIT_REF_BATCH),scorer=referenceScorer;
const scoreBatch=async indices=>{if(!indices.length)return null;const sources=indices.map(i=>fixedSourceForPixel(i,snap,canvas.width,canvas.height)),scored=await scorer.scoreFixedCandidates(snap,targetIter,canvas.width,canvas.height,sources);if(token!==state.token)return null;return{sources,scored}};
const first=await scoreBatch(firstIndices);if(!first||token!==state.token)return null;const minGain=Math.max(OPERATION_LIMIT_REF_MIN_EXTEND,Math.ceil(currentLen*.01));let selected=first,scoreMs=first.scored.scoreMs||0,scoredCandidates=first.sources.length,usedSecond=false;
if(first.scored.bestScore<targetIter&&secondIndices.length){const second=await scoreBatch(secondIndices);if(!second||token!==state.token)return null;scoreMs+=second.scored.scoreMs||0;scoredCandidates+=second.sources.length;usedSecond=true;if(second.scored.bestScore>first.scored.bestScore)selected=second}
const scored=selected.scored,sources=selected.sources;if(scored.winner<0)return{used:false,attempted:scoredCandidates};const winner={...sources[scored.winner],precisionBits:(scored.precisionBits||pixelPrecisionBits(snap,canvas.width,40)+32)+32},identity=[sources[scored.winner].re,sources[scored.winner].im,sources[scored.winner].bits,pixelPrecisionBits(snap,canvas.width,40),targetIter].join(':');
if(scored.bestScore<currentLen+minGain||tried?.has(identity))return{used:false,attempted:scoredCandidates};tried?.add(identity);
const ctx=await scorer.requestFixed(snap,targetIter,canvas.width,canvas.height,winner);if(token!==state.token)return null;if(ctx.checkpointMismatch)throw new Error('operation-limit guided reference guard checkpoint mismatch');if(ctx.refLen<currentLen+minGain)return{used:false,attempted:scoredCandidates};
return{used:true,attempted:scoredCandidates,ctx,refLen:ctx.refLen,previousRefLen:currentLen,scoreMs,buildMs:ctx.buildMs||0,candidates:scoredCandidates,usedSecond};
}
async function recoverNumericalGuided(r,snap,iter,token,initialStats){
const initialFailed=numericalFailureCount(initialStats);if(initialFailed<=0)return{stats:initialStats,passes:0,recovered:0,attempted:0,references:[]};
let stats=initialStats,passes=0,attempted=0,recovered=0;const tried=new Set(),references=[];
while(passes<FAILURE_GUIDED_REF_MAX_PASSES&&numericalFailureCount(stats)>0){
const before=numericalFailureCount(stats),guided=await tryFailureGuidedReference(r,snap,iter,token,stats,tried);if(!guided||token!==state.token)return null;attempted+=guided.attempted||0;if(!guided.used)break;
const gain=Math.max(0,before-numericalFailureCount(guided.stats||stats));stats=guided.stats||stats;recovered+=gain;passes++;references.push({refLen:guided.refLen||0,recovered:gain,candidates:guided.candidates||0,scoreMs:guided.scoreMs||0,buildMs:guided.buildMs||0});
if(gain<Math.max(8,Math.ceil(before*.02)))break;
}
state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.heuristicInterior=stats.heuristicInterior||0;return{stats,passes,recovered:Math.max(recovered,initialFailed-numericalFailureCount(stats)),attempted,references};
}
async function recoverNumericalCostOrdered(r,snap,iter,token,initialStats){
let stats=initialStats,attempted=0,applied=0,passes=0;if(numericalFailureCount(stats)<=0)return{stats,attempted,applied,passes,policy:'none'};
const guided=await recoverNumericalGuided(r,snap,iter,token,stats);if(!guided||token!==state.token)return null;stats=guided.stats;attempted+=guided.attempted||0;passes+=guided.passes||0;
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats;attempted+=pf.attempted||0;applied+=pf.applied||0}
return{stats,attempted,applied,passes,policy:'guided-precision'};
}
function precisionBatchSize(iter,bits){return Math.max(1,Math.min(64,Math.floor(500000/(Math.max(1,iter)*5*Math.max(1,bits/128)))))}
async function recoverResidualPrecision(r,snap,iter,token,initialStats,terminalClass=-1){
if(numericalFailureCount(initialStats)<=0)return{stats:initialStats,attempted:0,applied:0};
const indices=await r.readNumericalIndices(token);if(!indices||token!==state.token)return null;
const bits=pixelPrecisionBits(snap,canvas.width,40),workerCount=Math.min(precisionFallbackPool.length,precisionFallbackWorkerCount(indices.length)),maxBatchSize=precisionBatchSize(iter,bits);
const active=new Map(),completed=[];let completedCount=0,lastApply=performance.now();let next=0,attempted=0,appliedCount=0,batchSize=maxBatchSize,smoothedMs=0;
const launch=worker=>{
if(next>=indices.length||token!==state.token)return;
const end=Math.min(indices.length,next+batchSize),part=indices.slice(next,end),started=performance.now();next=end;
// Resolve failures into values so cancellation never leaves an unhandled job.
active.set(worker,precisionFallbackPool[worker].request(snap,iter,canvas.width,canvas.height,part,terminalClass)
.then(result=>({worker,result,ms:performance.now()-started}),error=>({worker,error})));
};
for(let i=0;i<workerCount;i++)launch(i);
try{
while(active.size){
const done=await Promise.race(active.values());active.delete(done.worker);
if(token!==state.token)return null;
if(done.error)throw done.error;
completed.push(done.result);completedCount+=done.result.indices.length;attempted+=done.result.indices.length;
smoothedMs=smoothedMs?smoothedMs*.7+done.ms*.3:done.ms;
if(smoothedMs>40)batchSize=Math.max(1,Math.floor(batchSize*.6));
else if(smoothedMs<20&&batchSize<maxBatchSize)batchSize=Math.min(maxBatchSize,batchSize+Math.max(1,Math.ceil(batchSize*.25)));
launch(done.worker);
if(completedCount>=64||performance.now()-lastApply>=32||!active.size){
const joined={indices:new Uint32Array(completedCount),meta:new Uint32Array(completedCount),smooth:new Float32Array(completedCount),accepted:new Uint8Array(completedCount)};let offset=0;
for(const part of completed){for(const key of ['indices','meta','smooth','accepted'])joined[key].set(part[key],offset);offset+=part.indices.length;}
const applied=await r.applyPrecisionFallback(joined,token);if(!applied||token!==state.token)return null;appliedCount+=applied.applied;
completed.length=0;completedCount=0;lastApply=performance.now();
}
}
const stats=await r.readUnresolvedStats();if(token!==state.token)return null;
return{stats,attempted,applied:appliedCount,workers:workerCount};
}finally{
for(const worker of active.keys())precisionFallbackPool[worker].cancel('cancelled precision batch');
}
}
async function continuationReferenceForActive(r,session,snap,target,token,preferredSource=null,stats=null){
let ctx=preferredSource?await refs.requestFixed(snap,target,canvas.width,canvas.height,preferredSource):await refs.request(snap,target,canvas.width,canvas.height);if(token!==state.token)return null;if(ctx.checkpointMismatch)throw new Error('active reference guard checkpoint mismatch');if(ctx.refLen>=target)return{ctx,source:ctx.source};
let best=ctx,bestSource=ctx.source;const tried=new Set();
if(bestSource)tried.add([bestSource.re,bestSource.im,bestSource.bits,pixelPrecisionBits(snap,canvas.width,40),target].join(':'));
const guided=await tryOperationLimitGuidedReference(r,session,snap,target,token,stats,best,tried);if(!guided||token!==state.token)return null;if(guided.used){best=guided.ctx;bestSource=guided.ctx.source;if(best.refLen>=target)return{ctx:best,source:bestSource}}
return{ctx:best,source:bestSource};
}
async function refinePixelFrontier(r,snap,initialIter,token,initialStats,certified=0){
if(!state.continuationBudget)throw new Error('production quality requires a finite continuation budget');
const maxIter=Math.max(initialIter,state.continuationBudget),minimumIter=maxIter;
let session=await r.beginGpuOperationLimitContinuation(snap,token,{x:canvas.width*.5,y:canvas.height*.5},false,initialStats?.reasons?.operationLimit||0);if(!session||token!==state.token)return null;let iter=initialIter,stats=initialStats,rounds=0,processed=0,retired=0,ctx=null,fixedSource=null,converged=session.active===0||iter>=maxIter;
while(!converged&&iter<maxIter){
if(token!==state.token)return null;let target=Math.min(maxIter,Math.max(iter+1,iter*2));
const selected=await continuationReferenceForActive(r,session,snap,target,token,fixedSource,stats);if(!selected||token!==state.token)return null;ctx=selected.ctx;if(ctx.refLen<=iter)throw new Error('active領域を前進できるreferenceを選択できません');if(ctx.refLen<target)target=Math.min(target,ctx.refLen);const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height),refId=ctx.referenceId||ctx.key;
r.setDeepContext(ctx);if(session.referenceId!==refId){session=await r.reinitializeActiveSession(session,snap,token,refPixel);if(!session||token!==state.token)return null;session.referenceId=refId;fixedSource=ctx.source}
const before=session.active,workBefore=session.pixelIterations,pass=await r.continueOperationLimitActive(session,snap,target,token,ctx,refPixel);if(!pass||token!==state.token)return null;processed+=session.pixelIterations-workBefore;retired+=Math.max(0,before-session.active);
stats=await r.readUnresolvedStats();if(token!==state.token)return null;let failed=numericalFailureCount(stats),recoveryWroteOutsideActive=false;
if(failed>0){r.setDeepContext(ctx);const corrected=await r.correctUnknownFrame(snap,target,token,refPixel);if(!corrected||token!==state.token)return null;recoveryWroteOutsideActive=(corrected.mode==='scan'||(corrected.queue?.processed||corrected.queue?.selected||0)>0);stats=await r.readUnresolvedStats();if(token!==state.token)return null}
if(numericalFailureCount(stats)>0){const guided=await recoverNumericalGuided(r,snap,target,token,stats);if(!guided||token!==state.token)return null;stats=guided.stats;recoveryWroteOutsideActive ||= guided.attempted>0;r.setDeepContext(ctx)}
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,target,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats;recoveryWroteOutsideActive ||= pf.applied>0;r.setDeepContext(ctx)}
if(recoveryWroteOutsideActive){const added=await r.appendRecoveredOperationLimits(session,snap,token,refPixel,stats?.reasons?.operationLimit||session.active);if(added===null||token!==state.token)return null;stats=await r.readUnresolvedStats();if(token!==state.token)return null;}
rounds++;iter=target;if(!await presentProvisional(r,snap,iter,token))return null;converged=session.active===0||iter>=maxIter;
}
stats=await r.readUnresolvedStats();if(token!==state.token)return null;if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats}
const membershipCertified=numericalFailureCount(stats)===0&&(stats.reasons?.operationLimit||0)===0&&(stats.heuristicInterior||0)===0;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.heuristicInterior=stats.heuristicInterior||0;return{stats,iter,minimumIter,rounds,processed,escaped:retired,converged,finiteBudgetComplete:converged,policy:'explicit-gpu-resident-finite-budget',membershipCertified,applied:rounds>0||certified>0};
}
function fixedSourceForPixel(index,snap,w,h){const x=BigInt(index%w),y=BigInt(Math.floor(index/w)),bw=BigInt(w),den=2n*bw;return{bits:snap.bits,re:snap.re+roundDivSigned(snap.span*(2n*x+1n-bw),den),im:snap.im+roundDivSigned(snap.span*(BigInt(h)-2n*y-1n),den),viewRe:snap.re,viewIm:snap.im,span:snap.span}}
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 referenceOriginMatchesView(src,snap){return !!src&&!!snap&&src.bits===snap.bits&&(src.viewRe??src.re)===snap.re&&(src.viewIm??src.im)===snap.im&&src.span===snap.span}
function referenceSourceView(src){return src?{bits:src.bits,re:src.viewRe??src.re,im:src.viewIm??src.im,span:src.span}:null}
function nearbyReferenceViewSafe(src,snap){
const old=referenceSourceView(src);if(!old||!snap||old.span<=0n||snap.span<=0n)return false;
const b=Math.max(old.bits,snap.bits),os=align(old.span,old.bits,b),ns=align(snap.span,snap.bits,b),dr=align(snap.re,snap.bits,b)-align(old.re,old.bits,b),di=align(snap.im,snap.bits,b)-align(old.im,old.bits,b),scale=fixedRatio(ns,os),shift=Math.hypot(fixedRatio(dr,os),fixedRatio(di,os));
if(!Number.isFinite(scale)||scale<=0||!Number.isFinite(shift))return false;
const z=Math.max(scale,1/scale);return z<=2.25&&shift<=.45;
}
function reusableReferencePixel(ctx,snap,iter,w,h,requiredBits,allowNearby=false){
const src=ctx&&ctx.source;if(!ctx||!src||ctx.refLen<iter||(ctx.precisionBits||0)<requiredBits||ctx.checkpointMismatch)return null;
const exact=referenceOriginMatchesView(src,snap);if(!exact&&(!allowNearby||!nearbyReferenceViewSafe(src,snap)))return null;
const pixel=referencePixelForSource(src,snap,w,h),margin=(exact?.125:.10)*Math.max(w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x < -margin||pixel.x>w+margin||pixel.y < -margin||pixel.y>h+margin)return null;
const dist=Math.hypot(pixel.x-w*.5,pixel.y-h*.5)/Math.max(1,Math.max(w,h));return{ctx,pixel,exact,dist};
}
function nearbyReferenceCandidates(){
const out=[],seen=new Set(),push=ctx=>{if(!ctx)return;const id=ctx.referenceId||ctx.key;if(seen.has(id))return;seen.add(id);out.push(ctx)};
push(renderer&&renderer.fastCtx);for(const ctx of fastRefs.cache.values())push(ctx);for(const ctx of refs.cache.values())push(ctx);for(const ctx of renderer?.deepContexts?.values()||[])push(ctx);return out;
}
function reusableFastReference(snap,iter,w,h){
const required=fastReferenceBits(snap,w),requiredLen=Math.min(iter,Math.max(4096,Math.floor(iter/3)));let best=null;for(const ctx of nearbyReferenceCandidates()){const item=reusableReferencePixel(ctx,snap,requiredLen,w,h,required,true);if(!item)continue;const score=(item.exact?1e9:0)+Math.min(ctx.refLen,iter*4)-item.dist*iter*.25+(ctx.precisionBits||0)*.01;if(!best||score>best.score)best={...item,score}}
if(best&&!best.exact){}
return best;
}
function integerGridReuseShift(oldView,newView,w,h){
if(!oldView||!newView||oldView.w!==w||oldView.h!==h||oldView.span<=0n||newView.span<=0n)return null;
const b=Math.max(oldView.bits,newView.bits),os=align(oldView.span,oldView.bits,b),ns=align(newView.span,newView.bits,b);if(os!==ns)return null;
const bw=BigInt(w),dxNum=(align(newView.re,newView.bits,b)-align(oldView.re,oldView.bits,b))*bw,dyNum=(align(newView.im,newView.bits,b)-align(oldView.im,oldView.bits,b))*bw,qx=roundDivSigned(dxNum,os),qy=roundDivSigned(dyNum,os),abs=v=>v<0n?-v:v,tol=(()=>{const a=abs(os),rel=a>>64n;return rel>bw?rel:bw})();if(abs(dxNum-qx*os)>tol||abs(dyNum-qy*os)>tol)return null;
const sx=Number(qx),sy=-Number(qy);if(!Number.isSafeInteger(sx)||!Number.isSafeInteger(sy)||Math.abs(sx)>=w||Math.abs(sy)>=h)return null;
const dstX=Math.max(0,-sx),dstY=Math.max(0,-sy),srcX=dstX+sx,srcY=dstY+sy,width=w-Math.abs(sx),height=h-Math.abs(sy);if(width<=0||height<=0)return null;
return{sx,sy,dstX,dstY,srcX,srcY,width,height,pixels:width*height};
}
// ── CPU fallback (WebGPU unavailable) ───────────────────────────────────
function cpuFallbackWorkerSource(){return String.raw`
'use strict';
function fract(x){return x-Math.floor(x)}
function clamp(x,a,b){return Math.max(a,Math.min(b,x))}
function smoothstep(a,b,x){x=clamp((x-a)/(b-a),0,1);return x*x*(3-2*x)}
function mix(a,b,t){return a+(b-a)*t}
function mix3(a,b,t){return[mix(a[0],b[0],t),mix(a[1],b[1],t),mix(a[2],b[2],t)]}
function rgb8(r,g,b){return[r/255,g/255,b/255]}
function hsv(h,s,v){const x=fract(h)*6,i=Math.floor(x),f=x-i,p=v*(1-s),q=v*(1-s*f),t=v*(1-s*(1-f));return i===0?[v,t,p]:i===1?[q,v,p]:i===2?[p,v,t]:i===3?[p,q,v]:i===4?[t,p,v]:[v,p,q]}
function ramp5(t,c0,c1,c2,c3,c4){return t<.25?mix3(c0,c1,smoothstep(0,.25,t)):t<.5?mix3(c1,c2,smoothstep(.25,.5,t)):t<.75?mix3(c2,c3,smoothstep(.5,.75,t)):mix3(c3,c4,smoothstep(.75,1,t))}
function currentPalette(t0){const t=t0<=.5?2*t0:2-2*t0;if(t<.11)return mix3(rgb8(4,10,27),rgb8(12,53,79),smoothstep(0,.11,t));if(t<.25)return mix3(rgb8(12,53,79),rgb8(31,156,184),smoothstep(.11,.25,t));if(t<.38)return mix3(rgb8(31,156,184),rgb8(91,226,234),smoothstep(.25,.38,t));if(t<.5)return mix3(rgb8(91,226,234),rgb8(66,53,151),smoothstep(.38,.5,t));if(t<.62)return mix3(rgb8(66,53,151),rgb8(139,49,170),smoothstep(.5,.62,t));if(t<.73)return mix3(rgb8(139,49,170),rgb8(232,72,145),smoothstep(.62,.73,t));if(t<.84)return mix3(rgb8(232,72,145),rgb8(255,137,64),smoothstep(.73,.84,t));if(t<.93)return mix3(rgb8(255,137,64),rgb8(255,211,99),smoothstep(.84,.93,t));return mix3(rgb8(255,211,99),rgb8(255,250,223),smoothstep(.93,1,t))}
function paletteColor(id,phase){if(id===0)return currentPalette(phase);if(id===1)return hsv(phase,.92,1);if(id===2){const g=(18+237*(.5-.5*Math.cos(2*Math.PI*phase)))/255;return[g,g,g]}const t=1-Math.abs(2*fract(phase)-1),m={19:[[20,14,26],[72,47,83],[161,92,130],[224,130,164],[249,193,205]],3:[[0,0,2],[40,8,84],[141,36,101],[228,88,48],[252,255,166]],5:[[1,4,14],[7,36,92],[0,132,178],[116,232,240],[242,254,255]],7:[[3,6,20],[63,26,132],[10,146,174],[48,224,140],[229,255,184]],10:[[5,4,2],[44,24,4],[132,83,10],[228,171,46],[255,245,198]],13:[[0,0,14],[21,16,152],[0,188,255],[220,0,255],[255,255,255]],15:[[2,8,26],[18,85,210],[192,244,255],[255,134,36],[255,242,196]]}[id];if(!m)return currentPalette(phase);return ramp5(t,...m.map(c=>rgb8(...c)))}
function interior(id){return id===19?rgb8(14,10,19):[0,0,0]}
function escapedColor(n,sm,style){const phase=fract(style.shift+sm*style.cycle),c=paletteColor(style.palette,phase),edge=clamp(Math.log(1+n)/Math.log(1+Math.max(8,n+32)),0,1),mv=.3+.7*Math.pow(edge,.4),floor=style.palette===2?[2/255,2/255,2/255]:interior(style.palette);return mix3(floor,c,mv)}
function analytic(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2,lhs=q*(q+x),rhs=.25*y2;if(lhs<rhs)return true;const x2=cr+1;return x2*x2+y2<.0625}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='render')return;try{const {w,h,y0,y1,maxIter,re,im,span,style}=d,rgba=new Uint8ClampedArray((y1-y0)*w*4),scale=span/w;let operationLimit=0,heuristicInterior=0,work=0,escaped=0;for(let y=y0;y<y1;y++)for(let x=0;x<w;x++){const cr=re+(x+.5-w/2)*scale,ci=im+(h/2-y-.5)*scale,o=((y-y0)*w+x)*4;if(analytic(cr,ci)){const c=interior(style.palette);rgba[o]=c[0]*255;rgba[o+1]=c[1]*255;rgba[o+2]=c[2]*255;rgba[o+3]=255;continue}let zr=0,zi=0,n=0,cycleR=0,cycleI=0,power=1,lam=0,cls=0,sm=0;for(;n<maxIter;n++){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;const mag=zr*zr+zi*zi;work++;if(mag>4){n++;sm=n+1-Math.log2(Math.max(1e-20,.5*Math.log2(Math.max(4.0000005,mag))));cls=1;escaped++;break}if(n+1>=64){const tol=16*Number.EPSILON*(1+Math.max(Math.abs(zr),Math.abs(zi)));if(Math.max(Math.abs(zr-cycleR),Math.abs(zi-cycleI))<=tol){n++;cls=2;heuristicInterior++;break}}lam++;if(lam>=power){cycleR=zr;cycleI=zi;power=Math.min(65536,power*2);lam=0}}if(cls===1){const c=escapedColor(n,sm,style);rgba[o]=c[0]*255;rgba[o+1]=c[1]*255;rgba[o+2]=c[2]*255;rgba[o+3]=255}else if(cls===2){const c=interior(style.palette);rgba[o]=c[0]*255;rgba[o+1]=c[1]*255;rgba[o+2]=c[2]*255;rgba[o+3]=255}else{operationLimit++;rgba[o]=rgba[o+1]=rgba[o+2]=0;rgba[o+3]=0}}postMessage({type:'done',id:d.id,y0,y1,operationLimit,heuristicInterior,work,escaped,rgba:rgba.buffer},[rgba.buffer])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class CpuFallbackRenderer{
constructor(){this.workers=[];this.serial=0;this.pending=new Map();this.failed=false;this.ctx=null}
ensure(){if(this.workers.length)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.ctx=canvas.getContext('2d');if(!this.ctx)throw new Error('2D canvas unavailable');const count=Math.max(1,Math.min(4,(Number(navigator.hardwareConcurrency)||4)-1));for(let i=0;i<count;i++){const url=URL.createObjectURL(new Blob([cpuFallbackWorkerSource()],{type:'text/javascript'})),w=new Worker(url);w.__url=url;w.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);d.type==='error'?p.reject(new Error(d.error)):p.resolve(d)};w.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'CPU fallback worker error'));this.pending.clear()};this.workers.push(w)}return true}catch(e){this.failed=true;return false}}
request(worker,payload){const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});worker.postMessage({...payload,id})})}
async render(snap,iter,token){if(!this.ensure())throw new Error('CPU fallback workerを作成できません');const w=canvas.width,h=canvas.height,re=fixedNum(snap.re,snap.bits),im=fixedNum(snap.im,snap.bits),span=fixedNum(snap.span,snap.bits),step=span/Math.max(1,w);if(!Number.isFinite(re)||!Number.isFinite(im)||!Number.isFinite(span)||span<=0||step===0||(re!==0&&re+step===re)||(im!==0&&im+step===im))throw new Error('CPU fallback の倍精度範囲を超えています');const style=colorStyleSnapshot(),jobs=[],rows=Math.ceil(h/this.workers.length);for(let i=0;i<this.workers.length;i++){const y0=i*rows,y1=Math.min(h,y0+rows);if(y0<y1)jobs.push(this.request(this.workers[i],{type:'render',w,h,y0,y1,maxIter:iter,re,im,span,style}))}const parts=await Promise.all(jobs);if(token!==state.token)throw new Error('cancelled');const rgba=new Uint8ClampedArray(w*h*4);let operationLimit=0,heuristicInterior=0,work=0,escaped=0;for(const part of parts){rgba.set(new Uint8ClampedArray(part.rgba),part.y0*w*4);operationLimit+=part.operationLimit||0;heuristicInterior+=part.heuristicInterior||0;work+=part.work||0;escaped+=part.escaped||0}this.ctx.putImageData(new ImageData(rgba,w,h),0,0);return{operationLimit,heuristicInterior,work,escaped,workers:this.workers.length}}
cancelPending(reason='cancelled'){for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();this.destroyWorkers()}
destroyWorkers(){for(const w of this.workers){try{w.terminate()}catch{}try{URL.revokeObjectURL(w.__url)}catch{}}this.workers.length=0}
destroy(){this.cancelPending('destroyed');this.ctx=null}
}
let cpuFallback=null;
async function renderCpuFallbackFrame(snap,iter,token){cpuFallback ||= new CpuFallbackRenderer();const result=await cpuFallback.render(snap,iter,token);if(token!==state.token)return false;state.unresolved=result.operationLimit;state.unknownReasons={errorBound:0,escapeUncertain:0,referenceEnd:0,rebaseGap:0,range:0,operationLimit:result.operationLimit};state.heuristicInterior=result.heuristicInterior;const membershipCertified=result.operationLimit===0&&result.heuristicInterior===0;const frontier={stats:{total:result.operationLimit,heuristicInterior:result.heuristicInterior,reasons:state.unknownReasons},iter,minimumIter:iter,rounds:0,processed:result.work,escaped:result.escaped,converged:true,finiteBudgetComplete:true,policy:'cpu-worker-finite-budget',membershipCertified,applied:true};state.frameView=snap;state.fieldView={...snap,iter,frontier,w:canvas.width,h:canvas.height,fastExtended:false,backend:'cpu-worker-fallback',referenceKey:'',referenceReused:false,complete:true,membershipCertified};return true}
// ── 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=true;state.gpuError='WebGPU非対応 · CPU fallbackを使用';updateStats();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=true;state.gpuError='WebGPU adapterがありません · CPU fallbackを使用';updateStats();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuError='';resize();if(!state.rendering&&!state.frameView)markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e)+' · CPU fallbackを使用';updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
let renderClockTimer=0;
function renderDuration(ms){return ms>=1000?(ms/1000).toFixed(2)+' s':Math.round(ms)+' ms'}
function renderTimeLabel(now=performance.now()){
const c=state.renderClock;
if(!c)return '準備完了';
const elapsed=c.status==='running'?now-c.startedAt:c.elapsed;
return (c.status==='running'?'描画中… ':c.status==='cancelled'?'中断 ':c.status==='error'?'停止 ':'')+renderDuration(Math.max(0,elapsed));
}
function startRenderClock(token,startedAt){
if(renderClockTimer)clearInterval(renderClockTimer);
state.renderClock={token,startedAt,status:'running',elapsed:0};
renderClockTimer=setInterval(()=>{$('render').textContent=renderTimeLabel()},250);
}
function finishRenderClock(token,status='complete'){
const c=state.renderClock;if(!c||c.token!==token||c.status!=='running')return;
c.elapsed=performance.now()-c.startedAt;c.status=status;
if(status==='complete'&&!state.colorAuto&&!state.recolorPending)renderer?.releaseColorSource()
clearInterval(renderClockTimer);renderClockTimer=0;
$('render').textContent=renderTimeLabel();
}
function cancelRender(){finishRenderClock(state.token,'cancelled');if(exportJob.active)exportJob.cancelled=true;state.token++;state.rendering=false;state.recolorPending=false;state.deferNumericPublish=false;refs.cancelPending('render cancelled');referenceScorer.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled');for(const service of precisionFallbackPool)service.cancel('render cancelled');cpuFallback?.cancelPending('render cancelled')}
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PENDING';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
async function presentProvisional(r,snap,iter,token,force=false){
if(token!==state.token)return false;if(state.deferNumericPublish)return true;
if(!force&&performance.now()-(r.lastProvisionalAt||0)<125)return true;
if(!await r.recolor(token,iter)||token!==state.token)return false;
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,complete:false};
r.captureColorSource(snap);
r.lastProvisionalAt=performance.now();state.drawState='REFINING';
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();return true;
}
async function renderFinalFrame(r,snap,iter,token){
let decision=chooseBackend(snap,canvas.width),fastExtended=decision.fastExtended;
const historySeed=fastExtended?r.numericHistorySeed(snap,iter,false,true):null,proof=knownInteriorViewportMayOverlap(snap,canvas.width,canvas.height)?certifiedInteriorTiles(snap,canvas.width,canvas.height,32,historySeed):{mask:null,pixels:0};let certified=0,preserveInterior=false;
if(proof.pixels||historySeed){certified=await r.certifyInterior(snap,token,true,proof,historySeed);if(token!==state.token)return null;preserveInterior=certified>0||!!historySeed}
if(certified===canvas.width*canvas.height){decision={backend:'exact-interior',fastExtended:false,reason:'exact-full-interior'};const stats={total:0,heuristicInterior:0,reasons:{errorBound:0,escapeUncertain:0,referenceEnd:0,rebaseGap:0,range:0,operationLimit:0}};state.unresolved=0;state.unknownReasons=stats.reasons;state.heuristicInterior=0;const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;const frontier={stats,iter,minimumIter:iter,rounds:0,processed:0,escaped:0,converged:true,finiteBudgetComplete:true,policy:'exact-interior-proof',membershipCertified:true,applied:true};state.frameView=snap;state.fieldView={...snap,iter,frontier,w:canvas.width,h:canvas.height,fastExtended:false,backend:'exact-interior',referenceKey:'',referenceReused:false,complete:true,membershipCertified:true};r.captureColorSource(snap,state.colorAuto||state.recolorPending);r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});return{decision,referenceReused:false,referencePixel:null,certified}}
let fastCtx=null,referencePixel=null,referenceReused=false;if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true}else{fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height)}}
const deferColor=!!(r.historyReady&&canUseStableReprojection());state.deferNumericPublish=deferColor;const ok=await r.computeFrame(snap,iter,token,referencePixel,fastExtended,fastCtx,preserveInterior,!!historySeed,deferColor);if(!ok||token!==state.token){state.deferNumericPublish=false;return null}if(!deferColor&&!await presentProvisional(r,snap,iter,token,true)){state.deferNumericPublish=false;return null}
let stats=await r.readUnresolvedStats();if(token!==state.token)return null;if(fastExtended){const initialFailed=numericalFailureCount(stats);if(initialFailed>0){const ordered=await recoverNumericalCostOrdered(r,snap,iter,token,stats);if(!ordered||token!==state.token){state.deferNumericPublish=false;return null}stats=ordered.stats}state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.heuristicInterior=stats.heuristicInterior||0;if(initialFailed>0&&!deferColor){const painted=await r.recolor(token,iter);if(!painted||token!==state.token){state.deferNumericPublish=false;return null}}}else{state.unresolved=stats.total;state.unknownReasons=stats.reasons}
const frontier=await refinePixelFrontier(r,snap,iter,token,{total:state.unresolved,reasons:{...(state.unknownReasons||{})}},certified);if(!frontier||token!==state.token){state.deferNumericPublish=false;return null}const effectiveIter=frontier.iter;if(!frontier.converged)throw new Error('全画素反復が安全上限 '+PIXEL_FRONTIER_MAX_ITER+' までに収束しなかったためframeを公開しません');if(frontier.applied&&!deferColor){const painted=await r.recolor(token,effectiveIter);if(!painted||token!==state.token){state.deferNumericPublish=false;return null}}if(deferColor){const painted=await r.recolor(token,effectiveIter);if(!painted||token!==state.token){state.deferNumericPublish=false;return null}}
const remaining=numericalFailureCount({reasons:state.unknownReasons||{}}),unprocessed=Math.max(0,state.unresolved-remaining-(state.unknownReasons?.operationLimit||0));if(unprocessed)throw new Error('未計算画素が '+unprocessed+' px 残っています');if(remaining>0)throw new Error('画素精度補修後も数値未確定が '+remaining+' px 残ったためframeを公開しません');state.frameView=snap;state.fieldView={...snap,iter:effectiveIter,frontier,w:canvas.width,h:canvas.height,fastExtended,backend:frontier.applied?'pixel-frontier-'+decision.backend:decision.backend,referenceKey:fastCtx?.key||'',referenceReused,complete:true,membershipCertified:frontier.membershipCertified};r.captureColorSource(snap,state.colorAuto||state.recolorPending);r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});state.deferNumericPublish=false;return{decision,referenceReused,referencePixel,certified}
}
function numericFrameComplete(){return state.fieldView?.complete===true&&numericalFailureCount({reasons:state.unknownReasons||{}})===0}
async function renderFrame(){
const token=++state.token,snap=snapshot(),adaptiveIter=maxIter(),holdStable=canUseStableReprojection(),t0=performance.now();
if(renderer&&state.fieldView?.complete)renderer.captureColorSource(state.frameView,true);
startRenderClock(token,t0);state.rendering=true;state.dirty=false;
try{
const probe=await selectAdaptiveInitialIter(snap,adaptiveIter,canvas.width,canvas.height,token);if(!probe||token!==state.token)return;
const iter=probe.iter;
state.drawState=holdStable?'REPROJECTED':'COVERING';state.unresolved=0;state.unknownReasons=null;state.heuristicInterior=0;updateStats();
const r=renderer||await initRenderer();if(token!==state.token)return;
if(!r){try{const ok=await renderCpuFallbackFrame(snap,iter,token);if(!ok||token!==state.token)return;state.rendering=false;state.drawState='COVERED';finishRenderClock(token);updateStats();return}catch(error){if(token!==state.token)return;state.gpuError=(state.gpuError?state.gpuError+' · ':'')+'CPU fallback失敗: '+String(error&&error.message||error);finishRenderClock(token,'error');state.rendering=false;state.drawState='ERROR';updateStats();return}}
state.temporalFill=!!r.historyReady;if(holdStable){r.presentFrame(r.presentTransform());state.drawState='REPROJECTED';updateStats()}
const result=await renderFinalFrame(r,snap,iter,token);if(!result||token!==state.token)return;
state.rendering=false;state.drawState='COVERED';if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false}else state.temporalFill=!!r.historyReady;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});
state.rendering=true;await r.device.queue.onSubmittedWorkDone();if(token!==state.token)return;state.rendering=false;finishRenderClock(token);
updateStats();if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor);
}catch(e){state.deferNumericPublish=false;if(token!==state.token){finishRenderClock(token,'error');return}state.drawState='ERROR';state.rendering=false;state.gpuError=String(e&&e.message||e);finishRenderClock(token,'error');updateStats();console.error(e)}
}
let recolorRaf=0;
function requestRecolor(){
state.colorRevision++;state.recolorPending=true;
queueRecolor();
}
function queueRecolor(){
if(recolorRaf)return;
recolorRaf=requestAnimationFrame(()=>{recolorRaf=0;flushRecolor()});
}
function flushRecolor(){
if(state.pointerActive||state.wheelActive)return;
if(state.dirty){schedule();return}
if(state.rendering){if(renderer?.colorSource&&!renderer.recolorPublished())queueRecolor();return}
// Keep the old completed frame valid until the replacement colour pass has
// actually finished. Invalidating history at slider-down caused apparent
// non-response/flicker when GPU work was already in flight.
if(!state.rendering&&!state.recoloring)queueMicrotask(recolor);
}
async function recolor(){
if(!renderer?.frame||!state.fieldView){state.recolorPending=true;return false}
if(state.dirty||state.pointerActive||state.wheelActive||state.rendering||state.recoloring){state.recolorPending=true;return false}
state.recoloring=true;let painted=false;
try{
while(state.recolorPending&&!state.dirty&&!state.pointerActive&&!state.wheelActive&&!state.rendering&&renderer?.frame&&state.fieldView){
state.recolorPending=false;const revision=state.colorRevision,token=state.token,ok=await renderer.recolor(token);
if(!ok||token!==state.token){state.recolorPending=true;continue}
renderer.colorSourceVisible=false;
if(!state.colorAuto)renderer.releaseColorSource();
renderer.presentFrame(renderer.presentTransform());painted=true;
// Only promote a colour frame to stable history if it still represents
// the newest slider value. Rapid dragging therefore collapses to the
// latest value instead of replaying stale colour states.
if(revision===state.colorRevision&&numericFrameComplete()){renderer.commitHistory(state.frameView,false);state.temporalFill=false}
updateStats();
// Yield ownership after one pass: a stream of hue changes must not keep
// recoloring=true forever and prevent a pending pan from computing.
break;
}
return painted
}catch(e){console.error(e);return false}
finally{state.recoloring=false;if(state.dirty)schedule();else if(state.recolorPending&&renderer?.frame&&!state.pointerActive&&!state.wheelActive&&!state.rendering)queueRecolor()}
}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering&&!state.recoloring)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
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()}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;saveHash();markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{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;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;saveHash();markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
function saveHash(){const h='#v='+VERSION+'&b='+state.bits+'&re='+state.re+'&im='+state.im+'&sp='+state.span+'&pal='+state.palette+'&cy='+state.cycle+'&sh='+state.shift+'&it='+state.baseIter+'&ad='+(state.adaptive?1:0)+'&q='+state.quality;lastWrittenHash=h;try{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')){const pal=Math.max(0,Math.min(PALETTE_MAX,Number(p.get('pal'))|0));state.palette=VALID_PALETTE_IDS.has(pal)?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';applyQuality(p.has('q')?p.get('q'):p.has('cb')&&Number(p.get('cb'))>0?(Number(p.get('cb'))<=4096?'fast':Number(p.get('cb'))<=16384?'standard':'high'):state.quality);ensurePrecision();return true}catch{return false}}
function syncControls(){$('quality').value=state.quality;$('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);saveHash();markDirty()};$('zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);saveHash();markDirty()};$('reset').onclick=()=>{reset();syncControls()};
async function copyShareUrl(){
saveHash();const url=location.href;
try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(url);toast('共有URLをコピーしました');return}}
catch{}
try{const ta=document.createElement('textarea');ta.value=url;ta.setAttribute('readonly','');ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();const ok=document.execCommand&&document.execCommand('copy');ta.remove();if(ok){toast('共有URLをコピーしました');return}}catch{}
try{if(navigator.share){await navigator.share({title:'MANDELBROT DEEP ZOOM',url});toast('共有しました');return}}catch(e){if(e?.name==='AbortError')return}
try{prompt('共有URLをコピーしてください',url)}catch{}toast('共有URLを表示しました')
}
$('share').onclick=copyShareUrl;
let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0;
function syncColorAutoButton(){const b=$('colorAuto');b.textContent=state.colorAuto?'色相変化停止':'色相変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')}
function stopColorAuto(repaint=true){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton();if(repaint)requestRecolor()}
function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;/* Hue animation advances shift directly. No cycle or phase compensation is applied. */state.shift=fract01(state.shift+.09*dt);$('shift').value=String(state.shift);$('shiftO').textContent=state.shift.toFixed(2);if(!state.pointerActive&&!state.wheelActive&&now-colorAutoPaint>=(state.rendering?100:50)){colorAutoPaint=now;requestRecolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)}
$('colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf){colorAutoRaf=requestAnimationFrame(colorAutoStep);}else if(!state.colorAuto)stopColorAuto()};
$('palette').addEventListener('change',e=>{const pal=Math.max(0,Math.min(PALETTE_MAX,Number(e.target.value)|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;requestRecolor();saveHash()});
function applyCycleControl(e){state.cycle=sliderToCycle(e.target.value);$('cycleO').textContent=state.cycle.toFixed(4);requestRecolor()}
function applyShiftControl(e){if(state.colorAuto)stopColorAuto();state.shift=Math.max(0,Math.min(1,Number(e.target.value)||0));$('shiftO').textContent=state.shift.toFixed(2);requestRecolor()}
$('cycle').addEventListener('input',applyCycleControl);$('cycle').addEventListener('change',()=>saveHash());
$('shift').addEventListener('input',applyShiftControl);$('shift').addEventListener('change',()=>saveHash());
$('quality').onchange=e=>{cancelRender();applyQuality(e.target.value);resize();saveHash();markDirty(false);try{localStorage.setItem('mandelbrot.quality',state.quality)}catch{}};
addEventListener('resize',()=>{const w=canvas.width,h=canvas.height;resize();if(w!==canvas.width||h!==canvas.height)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();saveHash();markDirty()}});
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);const before=numericViewKey();if(loadHash()){syncControls();if(before===numericViewKey())requestRecolor();else{resize();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)}
let CRC_TABLE;
function crc32Parts(parts){const t=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})();let c=0xffffffff;for(const part of parts)for(const b of part)c=t[(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 yieldExportUi(lastYield,force=false){if(!force&&performance.now()-lastYield<24)return lastYield;if(globalThis.scheduler?.yield)await scheduler.yield();else await new Promise(resolve=>setTimeout(resolve,0));return performance.now()}
function buildExportMetadata({snap,iter,w,h,ss=1,palette,fastExtended=false,strict=false,unresolvedSamples=0,exportPipeline,exportTileSize=null,exportRingDepth=null,completedField=false}){
const field=completedField?state.fieldView:null,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:completedField?(field?.backend||'completed-field'):(fastExtended?'reduced-reference + fast-perturbation':'f32-direct'),finiteBudgetComplete:completedField?!!field?.frontier?.finiteBudgetComplete:true,membershipCertified:completedField?!!field?.membershipCertified:false,precisionPolicy:completedField?'completed-field':(strict?'strict-gpu':'balanced-gpu'),pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*(ss===2?4:1),unresolvedSamples:completedField?0:unresolvedSamples,iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter,policy:field?.frontier?.policy||'finite-budget'},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette,exportPipeline,shaderVersion:G.version};
if(completedField){meta.operationLimit=state.unknownReasons?.operationLimit||0;meta.heuristicInterior=state.heuristicInterior||0;}
if(exportTileSize!==null)meta.exportTileSize=exportTileSize;
if(exportRingDepth!==null)meta.exportRingDepth=exportRingDepth;
return meta;
}
async function runExport(){
if(exportJob.active)return;
const requestedToken=state.token;
while(state.rendering||state.dirty){
$('exportStatus').textContent='現在の描画が完了するのを待っています…';
await new Promise(resolve=>setTimeout(resolve,50));
if(state.token!==requestedToken){$('exportStatus').textContent='表示位置が変わったため出力を中止しました。';return;}
}
if(state.fieldView?.complete===false){$('exportStatus').textContent='未完了の描画です。計算完了後に出力してください。';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=state.fieldView?.iter??maxIter(),fastExtended=fastNeedsExtended(snap,w),strict=$('exportPrecision').value==='strict';
if(w===canvas.width&&h===canvas.height&&ss===1){
exportJob.active=true;exportJob.cancelled=false;$('exportStart').disabled=true;
const png=new StreamingPng(w,h),generation=state.token;
try{
const paletteParts=r.frontColorKey.split(':'),palette={id:Number(paletteParts[0]),cycle:Number(paletteParts[1]),shift:Number(paletteParts[2]),edgeAA:false},rgba=await r.readCompleteRgba(generation),rows=new Uint8Array((w*4+1)*h);
for(let y=0;y<h;y++)rows.set(rgba.subarray(y*w*4,(y+1)*w*4),y*(w*4+1)+1);
await png.rows(rows);const blob=await png.finish();
if(exportJob.cancelled||generation!==state.token)throw new Error('cancelled');
const base='mandelbrot-'+Date.now(),meta=buildExportMetadata({snap,iter,w,h,palette,completedField:true,exportPipeline:'completed-numeric-field'});
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');$('exportStatus').textContent='計算済みの画像と座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e.message||e)}
finally{exportJob.active=false;$('exportStart').disabled=false}
return;
}
let fastCtx=null;if(fastExtended){$('exportStatus').textContent='高速拡張参照を準備中…';fastCtx=await fastRefs.request(snap,iter,w,h)}
const tile=r.exportTileSize(),ringDepth=iter>4096?1:r.exportRingDepth(tile),totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h);
exportJob.active=true;exportJob.cancelled=false;$('exportProgress').hidden=false;$('exportProgress').value=0;$('exportStart').disabled=true;
let done=0,unresolvedSamples=0,lastYield=performance.now();
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;){
if(exportJob.cancelled)throw new Error('cancelled');lastYield=await yieldExportUi(lastYield);if(exportJob.cancelled)throw new Error('cancelled');
const batch=[];for(let slot=0;slot<ringDepth&&x<w;slot++,x+=tile){const tileX=x,tw=Math.min(tile,w-tileX),args={snap,iter,fastContext:fastCtx,fullW:w,fullH:h,tileX,tileY:y,w:tw,h:th,strict,workspaceSlot:slot,workspaceSize:tile};batch.push({tileX,tw,promise:ss===1?r.renderTileRGBA({...args,sampleX:.5,sampleY:.5,edgeAA:false}):r.renderTileRGBA2x(args)})}
const settled=await Promise.allSettled(batch.map(item=>item.promise)),failed=settled.find(item=>item.status==='rejected');if(failed)throw failed.reason;const results=settled.map(item=>item.value);
for(let i=0;i<batch.length;i++){const {tileX,tw}=batch[i],result=results[i],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+tileX*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);lastYield=await yieldExportUi(lastYield);
}
if(exportJob.cancelled)throw new Error('cancelled');$('exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,palette={id:state.palette,cycle:state.cycle,shift:state.shift,edgeAA:ss===2},meta=buildExportMetadata({snap,iter,w,h,ss,palette,fastExtended,strict,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + '+ringDepth+'-slot staging ring + streaming-png':'gpu-tile + '+ringDepth+'-slot staging ring + streaming-png',exportTileSize:tile,exportRingDepth:ringDepth});
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');
$('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{r.exportWorkspaceDestroy();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()+(state.fieldView?.complete===false?'-preview':'')+'.png')},'image/png');
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8));$('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?'WebGPU 高速':cpuFallback?.workers?.length?'CPU Worker fallback':(state.gpuInitFailed?'WebGPUなし · CPU待機':state.gpuError?'WebGPU 復旧中':'起動中');$('render').textContent=renderTimeLabel();let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'数値補修中':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved){const nf=numericalFailureCount({reasons:state.unknownReasons||{}}),op=state.unknownReasons?.operationLimit||0;status+=' · 未確定 '+state.unresolved+(nf||op?' (数値 '+nf+' / 上限 '+op+')':'')}if(state.heuristicInterior)status+=' · 周期候補 '+state.heuristicInterior;if(state.fieldView?.membershipCertified===false)status+=' · 有限反復完了';if(state.temporalFill)status+=' · 前フレーム保持';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('badge').textContent=status}
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto(false);cancelRender();refs.destroy();referenceScorer.destroy();fastRefs.destroy();for(const service of precisionFallbackPool)service.destroy();cpuFallback?.destroy();if(renderer)renderer.destroy()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';applyQuality(localStorage.getItem('mandelbrot.quality')||localStorage.getItem('mandelbrot.processMode'));localStorage.removeItem('mandelbrot.processMode')}catch{}applyUi();resize();if(!loadHash())reset();syncControls();updateStats();initRenderer().then(()=>{resize();if(!state.rendering&&!state.frameView)markDirty(false)});schedule();
})();
</script>
</body>
</html>