food_chain/docs/reported-view-bugfix/index.before.html
2026-09-29 23:45:12 +09:00

1970 lines
219 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>