mandelbrot/index.html
2026-09-06 23:28:03 +09:00

2130 lines
241 KiB
HTML
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.4.0">
<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.4.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,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand-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}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));max-height:calc(100dvh - 20px);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.18);background:#000;color:#fff;min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}select option{background:#000;color:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:700px){.stats{display:none}.brand-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}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand"><span>MANDELBROT DEEP ZOOM</span><a class="brand-return" href="https://2012r2.nishi.boats/~333/">333の部屋に戻る</a></div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord"></span></div><div class="row"><span class="muted">倍率</span> <span id="zoom"></span> <span class="muted">表示幅</span> <span id="span"></span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render"></span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大"></button><button id="zout" aria-label="中心を縮小"></button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
<div class="line"><label for="processMode">負荷設定</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
<div class="line"><label for="continuationBudget">反復上限</label><select id="continuationBudget"><option value="0">自動</option><option value="4096">4,096 回</option><option value="16384">16,384 回</option><option value="32768">32,768 回</option><option value="65536">65,536 回</option><option value="150000">150,000 回</option></select><output></output></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="9">深海</option><option value="10">黄金</option><option value="11">ネオン</option><option value="13">電光</option><option value="15">蒼橙</option><option value="17">紅碧</option><option value="18">桃翠</option></select><output></output></div>
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0" max="1000" step="1" value="532" aria-label="色周期(対数スケール)"><output id="cycleO" for="cycle">0.0080</output></div>
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
<div class="mini-actions"><button id="colorAuto" type="button" aria-pressed="false" title="色周期は固定し、色相位置だけを動かします">色相変化</button></div>
</div>
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
</div>
<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_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x000fffffu;
const REASON_SHIFT:u32=20u;
const REASON_MASK:u32=0x0ff00000u;
const REASON_NONE:u32=0u;
const REASON_ERROR_BOUND:u32=1u;
const REASON_ESCAPE_UNCERTAIN:u32=2u;
const REASON_REFERENCE_END:u32=3u;
const REASON_REBASE_GAP:u32=4u;
const REASON_RANGE:u32=5u;
const REASON_OPERATION_LIMIT:u32=6u;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
fn 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;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_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 FAST_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:array<atomic<u32>>;
fn mark_fast_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u);
if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);}
}
fn render_fast(out:u32,gx:f32,gy:f32){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u;
loop{
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(p.unknownOnly!=0u){
let prior=fieldMeta[out];let cls=(prior>>28u)&3u;
if(cls!=FIELD_UNKNOWN){return;}
let reason=(prior>>20u)&0xffu;
// mode 2 repairs only untouched coverage holes (zero-filled metadata).
if(p.unknownOnly==2u && reason!=0u){return;}
}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
render_fast(out,gx,gy);
}
`;
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct UnresolvedHead{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u;
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let rpEnd=refs[m];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let magEnd=dot(zEnd,zEnd);
if(magEnd>4.0){
if(length(zEnd)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,magEnd);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
if(length(zEnd)+errAbs>2.0){mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;}
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){mark_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
return;
}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
// Keep the perturbation recurrence through reference-near-zero episodes.
// Escape acceptance is still guarded by the propagated absolute error.
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=rp;
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d);
let actualDelta=scaled_to_f32(w,scaleExp);
let deltaAbs=maxabs(actualDelta);
let sq=scaled_to_f32(cmul(w,w),scaleExp);
let sqAbs=maxabs(sq);
let gain=2.0*refAbs+2.0*deltaAbs;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp. Scale the product
// directly so e<-126 does not discard a representable combined value.
w=linear+sq+d; m+=1u; n+=1u;
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
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>;
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){let qi=gid.x;if(qi>=p.unknownOnly){return;}let out=queue[qi];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[out]=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>;
fn fail(out:u32,n:u32,reason:u32){fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;}
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){if(gid.x>=atomicLoad(&countIn[0])){return;}let local=queueIn[gid.x];let out=p.outputBase+(local/p.tileW)*p.outputStride+(local%p.tileW);if(((fieldMeta[out]>>28u)&3u)==FIELD_INTERIOR_PROVEN){return;}var s=states[local];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);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;}}
// Evaluate z_target before adopting the finite-budget result. A valid
// nonescaping state stays in the queue for the next target.
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);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;}
fail(out,s.n,REASON_OPERATION_LIMIT);
}
states[local]=s;let q=atomicAdd(&countOut[0],1u);queueOut[q]=local;}
`;
// Sparse candidates use compact slots. Dense frames keep the 32-byte state
// and direct pixel indices, avoiding an extra mapping on the largest frames.
const DEEP_COMPACT_INIT_WGSL=DEEP_ACTIVE_RESUME_INIT_WGSL
.replace('errScaled:f32};','errScaled:f32,pixel:u32,_pad:u32};')
.replace('var<storage,read> queue:','var<storage,read_write> queue:')
.replace('states[out]=DeepState(', 'states[qi]=DeepState(')
.replace('F32_U*maxabs(d));}', 'F32_U*maxabs(d),out,0u);queue[qi]=qi;}');
const DEEP_COMPACT_CONTINUE_WGSL=DEEP_ACTIVE_CONTINUE_WGSL
.replace('errScaled:f32};','errScaled:f32,pixel:u32,_pad:u32};')
.replace('let out=p.outputBase+(local/p.tileW)*p.outputStride+(local%p.tileW);','let out=states[local].pixel;');
// Production Deep sparse path: keep the long-running perturbation kernel
// untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets.
// Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS
// correction kernel sees less workgroup-level iteration divergence.
//
// Bucket state layout (24 u32 atomics):
// [0..7] histogram counts
// [8..15] scatter cursors (initialized from prefix offsets)
// [16..23] immutable prefix offsets for queue accounting
const DEEP_BUCKET_HIST_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
var<workgroup> localCounts:array<atomic<u32>,8>;
fn deep_bucket(packed:u32)->u32{
let n=packed&ITER_MASK;
return min(7u,(n*8u)/max(1u,p.maxIter));
}
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==3u){return ramp5(t,rgb8(0.0,0.0,2.0),rgb8(40.0,8.0,84.0),rgb8(141.0,36.0,101.0),rgb8(228.0,88.0,48.0),rgb8(252.0,255.0,166.0));}
if(p.palette==5u){return ramp5(t,rgb8(1.0,4.0,14.0),rgb8(7.0,36.0,92.0),rgb8(0.0,132.0,178.0),rgb8(116.0,232.0,240.0),rgb8(242.0,254.0,255.0));}
if(p.palette==7u){return ramp5(t,rgb8(3.0,6.0,20.0),rgb8(63.0,26.0,132.0),rgb8(10.0,146.0,174.0),rgb8(48.0,224.0,140.0),rgb8(229.0,255.0,184.0));}
if(p.palette==9u){return ramp5(t,rgb8(0.0,5.0,20.0),rgb8(4.0,42.0,82.0),rgb8(0.0,113.0,136.0),rgb8(34.0,176.0,162.0),rgb8(221.0,246.0,202.0));}
if(p.palette==10u){return ramp5(t,rgb8(5.0,4.0,2.0),rgb8(44.0,24.0,4.0),rgb8(132.0,83.0,10.0),rgb8(228.0,171.0,46.0),rgb8(255.0,245.0,198.0));}
if(p.palette==11u){return ramp5(t,rgb8(3.0,0.0,16.0),rgb8(250.0,0.0,132.0),rgb8(0.0,228.0,255.0),rgb8(126.0,255.0,0.0),rgb8(255.0,247.0,255.0));}
if(p.palette==13u){return ramp5(t,rgb8(0.0,0.0,14.0),rgb8(21.0,16.0,152.0),rgb8(0.0,188.0,255.0),rgb8(220.0,0.0,255.0),rgb8(255.0,255.0,255.0));}
if(p.palette==15u){return ramp5(t,rgb8(2.0,8.0,26.0),rgb8(18.0,85.0,210.0),rgb8(192.0,244.0,255.0),rgb8(255.0,134.0,36.0),rgb8(255.0,242.0,196.0));}
if(p.palette==17u){return ramp5(t,rgb8(20.0,1.0,14.0),rgb8(204.0,26.0,72.0),rgb8(255.0,226.0,182.0),rgb8(0.0,202.0,226.0),rgb8(226.0,249.0,255.0));}
if(p.palette==18u){return ramp5(t,rgb8(22.0,8.0,24.0),rgb8(171.0,36.0,122.0),rgb8(255.0,211.0,149.0),rgb8(45.0,204.0,176.0),rgb8(232.0,255.0,243.0));}
return current_palette(phase);
}
fn escaped_color(m:u32,sm:f32)->vec3<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(vec3<f32>(0.0,0.0,0.0),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){
let reason=(m>>20u)&0xffu;
// Iteration-limit pixels are not a numeric hole: display them as opaque
// black so temporal history cannot paint stale exterior colour through.
if(reason==6u){textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(0.0,0.0,0.0,1.0));}
else{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>(0.0,0.0,0.0,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 DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u);\n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}');
if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL)throw new Error('post-stats shader derivation failed');
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>,8>;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) gid:vec3<u32>,@builtin(local_invocation_id) lid:vec3<u32>){
if(lid.x<8u){atomicStore(&localReasons[lid.x],0u);}
workgroupBarrier();
if(gid.x<p.pixelCount){
let packed=fieldMeta[gid.x];
if(((packed>>28u)&3u)==FIELD_UNKNOWN){
atomicAdd(&localReasons[0],1u);
let reason=(packed&REASON_MASK)>>REASON_SHIFT;
if(reason>=1u && reason<=6u){atomicAdd(&localReasons[reason],1u);}
}
}
workgroupBarrier();
if(lid.x<8u){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];
}
`;
const FAILURE_TILE_MAP_WGSL=String.raw`
struct Params{width:u32,height:u32,tileSize:u32,cols:u32,rows: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> failureMap:array<u32>;
var<workgroup> counts:array<atomic<u32>,6>;
@compute @workgroup_size(64)
fn main(@builtin(local_invocation_id) lid:vec3<u32>,@builtin(workgroup_id) wid:vec3<u32>){
if(lid.x<5u){atomicStore(&counts[lid.x],0u);}if(lid.x==5u){atomicStore(&counts[5],0xffffffffu);}workgroupBarrier();
let x0=wid.x*p.tileSize;let y0=wid.y*p.tileSize;let area=p.tileSize*p.tileSize;var i=lid.x;loop{if(i>=area){break;}let x=x0+i%p.tileSize;let y=y0+i/p.tileSize;if(x<p.width&&y<p.height){let index=y*p.width+x;let packed=fieldMeta[index];if(((packed>>28u)&3u)==0u){let reason=(packed>>20u)&255u;if(reason>=1u&&reason<=5u){atomicAdd(&counts[reason-1u],1u);atomicMin(&counts[5],index);}}}i+=64u;}workgroupBarrier();if(lid.x<6u){failureMap[(wid.y*p.cols+wid.x)*6u+lid.x]=atomicLoad(&counts[lid.x]);}
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.4.0-bounded-continuation',DEEP_COMPACT_INIT_WGSL,DEEP_COMPACT_CONTINUE_WGSL,INTERIOR_MASK_WGSL,PRECISION_SCATTER_WGSL,DIRECT_F32_WGSL,ACTIVE_PREPARE_WGSL,FAST_PERTURB_WGSL,FAST_PERTURB_POSTSTATS_WGSL,DEEP_PERTURB_WGSL,DEEP_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,FAILURE_TILE_MAP_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 $=s=>document.querySelector(s),canvas=$('#view');
const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000,PALETTE_MAX=18,VALID_PALETTE_IDS=new Set([0,1,2,3,5,7,9,10,11,13,15,17,18]);
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96;
const PIXEL_FRONTIER_PRODUCTION=true,PIXEL_FRONTIER_WORK=4000000,PIXEL_FRONTIER_MAX_ITER=150000;
const PIXEL_FRONTIER_ENABLED=PIXEL_FRONTIER_PRODUCTION;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,continuationBudget:0,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,renderClock:null,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,multiRefPasses:0,multiRefRecovered:0,frontierRounds:0,frontierPixels:0,frontierEscaped:0,frontierIter:0,frontierMinimumIter:0,frontierConverged:false,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,qualityStage:'idle',qualityIter:0,qualityFinalIter:0,previewMs:0,refineMs:0,refinePending:false,temporalFill:false,numericRecoveryPending:false,numericRecoveryRunning:false,gpuStage:'',gpuTileRows:0};
let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,refineTimer=0,lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
const TEST_MODE=new URLSearchParams(location.search).has('test');
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,multiRefBuilds:0,numericSubmits:0,correctionSubmits:0,gpuSyncWaits:0,stageCounts:{},stageMs:{},generationMetrics:[]};
function generationMetric(token=state.token){for(let i=runtime.generationMetrics.length-1;i>=0;i--)if(runtime.generationMetrics[i].token===token)return runtime.generationMetrics[i];return null}
// ── exact fixed-point view state ─────────────────────────────────────────
function one(bits=state.bits){return 1n<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
function f32ToFixed(x,bits){
x=Math.fround(x);if(!Number.isFinite(x))throw new Error('non-finite f32 split component');if(x===0)return 0n;
const ab=new ArrayBuffer(4),dv=new DataView(ab);dv.setFloat32(0,x,false);const u=dv.getUint32(0,false),neg=(u>>>31)!==0,eb=(u>>>23)&255,frac=u&0x7fffff,m=BigInt(eb===0?frac:(0x800000|frac)),e=(eb===0?-149:eb-127-23),sh=bits+e;
let q=sh>=0?m<<BigInt(sh):roundDivSigned(m,1n<<BigInt(-sh));return neg?-q:q;
}
function splitFixedF32(v,bits,count=3){const out=[];let r=v;for(let i=0;i<count;i++){const x=Math.fround(fixedNum(r,bits));out.push(x);if(x===0)continue;r-=f32ToFixed(x,bits)}return out}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
function f32Ulp(x){x=Math.abs(Math.fround(x));if(!Number.isFinite(x))return Infinity;if(x===0)return Math.pow(2,-149);if(x<Math.pow(2,-126))return Math.pow(2,-149);return Math.pow(2,Math.floor(Math.log2(x))-23)}
function directPixelRatio(snap,w){const cr=Math.abs(fixedNum(snap.re,snap.bits)),ci=Math.abs(fixedNum(snap.im,snap.bits)),sp=Math.abs(fixedNum(snap.span,snap.bits)),step=sp/Math.max(1,w),scale=Math.max(cr,ci,sp*.75,Math.pow(2,-126)),ulp=f32Ulp(scale);return step/ulp}
function fastNeedsExtended(snap,w){return !(directPixelRatio(snap,w)>=4)}
function chooseBackend(snap=snapshot(),w=canvas.width){const fastExtended=fastNeedsExtended(snap,w);return{backend:fastExtended?'fast-extended':'direct',deep:false,fastExtended,probe:false,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 deepNeeded(){return false}
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,continuationBudget:state.continuationBudget,renderMode:state.renderMode}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive,v.continuationBudget||0].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,continuationBudget:v.continuationBudget||0,renderMode:'fast'});ensurePrecision();syncControls();saveHash(false);markDirty()}
// ── iteration / quality policy ───────────────────────────────────────────
const ADAPTIVE_ITER_QUANTUM=64;
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),q=state.processMode==='power'?.72:state.processMode==='fine'?1.25:state.processMode==='validate'?1.5:1,bonus=adaptiveIterationBonus(screenBits,q);return Math.min(state.continuationBudget||12000,12000,Math.max(state.baseIter,state.baseIter+bonus))}
function previewIter(finalIter=maxIter()){
const z=Math.max(0,zoomExp()),m=state.processMode==='power'?.72:state.processMode==='fine'?1.28:state.processMode==='validate'?1.55:1;
// The old fixed 1100-iteration ceiling starved deep previews: around 10^12,
// finalIter is ~2700 while many visible boundary pixels need >1100 steps.
// Make the first pass track final quality as zoom deepens, with a mode-aware
// ceiling so responsiveness is still bounded on extreme views.
const legacy=Math.round(m*(420+24*z));
const fractionBase=Math.min(.84,.48+.03*z);
const fraction=state.processMode==='power'?Math.min(.68,fractionBase*.82):state.processMode==='fine'?Math.min(.90,fractionBase+.04):state.processMode==='validate'?Math.min(.94,fractionBase+.08):fractionBase;
const deepFloor=Math.round(finalIter*fraction);
const cap=state.processMode==='power'?1800:state.processMode==='fine'?4800:state.processMode==='validate'?6000:3600;
return Math.min(finalIter,Math.max(state.baseIter,Math.min(cap,Math.max(legacy,deepFloor))));
}
function canUseStableReprojection(){
if(!renderer?.historyReady||!state.frameView)return false;
// Covers normal wheel/pinch, one 2x toolbar zoom, and ordinary pans while
// avoiding grotesque reuse after a reset or a distant shared-URL jump.
return reprojectionSafe(renderer.presentTransform(),2.25,.45);
}
function qualityStages(finalIter=maxIter()){
// Once a completed nearby frame exists, temporal reprojection is the
// preview. Do not replace it with a lower-iteration numerical frame: that
// was the source of the stressful black patches during small zoom/pan.
// Deep FAST cannot reuse a preview built from a shorter reference key. A
// separate preview therefore repeats the expensive perturbation frame and
// reference build without reducing final latency. Compute final once.
if(canUseStableReprojection()||fastNeedsExtended(snapshot(),canvas.width))return[finalIter];
const p=previewIter(finalIter);
return finalIter>p?[p,finalIter]:[p];
}
function refinementDelay(stageIndex){
// Full-quality latency matters more than leaving a long idle gap after the
// first preview. Keep a short interaction settling window only.
if(stageIndex<=1)return state.processMode==='validate'?300:state.processMode==='fine'?240:state.processMode==='power'?420:160;
return state.processMode==='validate'?240:state.processMode==='fine'?200:360;
}
function sameViewSnapshot(a,b){return !!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span}
function 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,hq:state.hq}}
function colorStyleKey(style=colorStyleSnapshot()){return[style.palette,style.cycle,style.shift,style.hq?1:0].join(':')}
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
'use strict';
const MAX_REF=150001;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<<BigInt(d):roundShift(v,-d)}
// 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 bail=16n*(1n<<BigInt(bits));
while(r.n<iter&&!r.terminalEscape){
r.zi=roundShift(2n*r.zr*r.zi,bits)+im;r.zr=r.zr2-r.zi2+re;
r.zr2=roundShift(r.zr*r.zr,bits);r.zi2=roundShift(r.zi*r.zi,bits);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),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<iter;n++){const zri=roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2>BAIL)return n+1}return 0}
function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const out=[{re,im}],den=8n,bh=BigInt(h),bw=BigInt(w);for(let gy=-3;gy<=3;gy++)for(let gx=-3;gx<=3;gx++){if(!gx&&!gy)continue;out.push({re:re+span*BigInt(gx)/den,im:im+span*bh*BigInt(gy)/(den*bw)})}return out}
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.score){best={...c,escape,score}}if(escape===0)break}return best}
function 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 high=orbit(baseBits+32,re<<32n,im<<32n,refLen);
let mismatch=high.refLen<refLen,checked=0;
// Checkpoints are independent of the requested orbit length; every new
// endpoint is also compared, including an escaping endpoint.
for(let n=Math.max(0,ref.verifiedTo+1);n<=refLen&&!mismatch;n++){
if(n!==refLen&&n%256!==0&&(n&(n-1))!==0)continue;
checked++;if(!samePacked(high.rr[n],ref.rr[n])||!samePacked(high.ri[n],ref.ri[n]))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||d.type!=='build')return;const t0=performance.now();try{const sourceBits=d.bits,sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,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),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),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-v1'].join(':'),extendedFrom:ref.previousLen,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class ReferenceService{
constructor(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!=='error'&&d.checkpointMismatch&&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));else{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};runtime.referenceBuilds++;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,fresh=false){const targetBits=pixelPrecisionBits(snap,width,40),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'pixel-guarded-perturb-v24.3'].join(':');if(fresh)this.cache.delete(key);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.3'].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}
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 tileReferencePool=Array.from({length:2},()=>new ReferenceService(2*1024*1024));
function fastReferenceBits(snap,w=canvas.width){return pixelPrecisionBits(snap,w,32)}
function fastReferenceWorkerSource(){return String.raw`
'use strict';
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){if(b<=0)return v;const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<<BigInt(d):roundShift(v,-d)}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,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);const zri=roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);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),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<iter;n++){const zri=roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2>BAIL)return n+1}return 0}
function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const out=[{re,im}],den=8n,bh=BigInt(h),bw=BigInt(w);for(let gy=-3;gy<=3;gy++)for(let gx=-3;gx<=3;gx++){if(!gx&&!gy)continue;out.push({re:re+span*BigInt(gx)/den,im:im+span*bh*BigInt(gy)/(den*bw)})}return out}
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.score){best={...c,escape,score}}if(escape===0)break}return best}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),bits=d.targetBits,centerRe=requant(sourceRe,d.sourceBits,bits),centerIm=requant(sourceIm,d.sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,d.iter),chosen=chooseReference(d.sourceBits,sourceRe,sourceIm,sourceSpan,w,h,d.iter,centerRef.escape),re=requant(chosen.re,d.sourceBits,bits),im=requant(chosen.im,d.sourceBits,bits),ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,d.iter),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class FastReferenceService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=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.3'].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()}
}
const fastRefs=new FastReferenceService();
function precisionFallbackWorkerSource(){return 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 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 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)))}
function pixel(s,index,bits){orbitCount++;const B=BigInt(bits),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=roundShift(2n*zr*zi,bits)+ci;zr=zr2-zi2+cr;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);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')}
}
const precisionFallback=new PrecisionFallbackService();
const precisionFallbackPool=Array.from({length:Math.max(1,Math.min(2,(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}
class WebGpuRenderer{
constructor(adapter,device){
this.adapter=adapter;this.device=device;
const ai=adapter.info||{};
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
this.frame=null;this.deepCtx=null;this.fastCtx=null;this.exportWs=[];this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.activeGpuStage='';this.lastCompletedGpuStage='';this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false;this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});this.reprojectSampler=device.createSampler({magFilter:'nearest',minFilter:'nearest'});
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;const where=this.activeGpuStage||this.lastCompletedGpuStage||'unknown-stage';state.gpuStage=where;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();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>'line '+x.lineNum+':'+x.linePos+' '+x.message).join('\n'))}return m}
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 ensureDeepPipelines(){if(this.deep&&this.deepPostStats)return;if(this.deepPipelinePromise)return this.deepPipelinePromise;this.deepPipelinePromise=(async()=>{const [base,screen]=await Promise.all([this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL)]);[this.deep,this.deepPostStats]=await Promise.all([this.makeCompute(base),this.makeCompute(screen)])})();try{return await this.deepPipelinePromise}finally{this.deepPipelinePromise=null}}
async ensureCorrectionPipelines(){await this.ensureDeepPipelines();if(this.correct&&this.correctQueued&&this.deepBucketHist&&this.deepBucketPrefix&&this.deepBucketScatter&&this.failureTileMap)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.module('failure-tile-map',G.FAILURE_TILE_MAP_WGSL)]);[this.deepBucketHist,this.deepBucketPrefix,this.deepBucketScatter,this.correct,this.correctQueued,this.failureTileMap]=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.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('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.deepActiveResumeInit,this.deepActiveContinue]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.deepActivePipelinePromise}finally{this.deepActivePipelinePromise=null}}
async ensureCompactActivePipelines(){
if(this.deepCompactInit&&this.deepCompactContinue)return;
this.deepCompactInit=await this.makeCompute(await this.module('deep-compact-init',G.DEEP_COMPACT_INIT_WGSL));
this.deepCompactContinue=await this.makeCompute(await this.module('deep-compact-continue',G.DEEP_COMPACT_CONTINUE_WGSL));
}
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 k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead','failureMap','failureParams','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}
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'});
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,deepActiveCapacity:0,deepActiveState:null,deepActiveQueueA:null,deepActiveQueueB:null,deepActiveCountA:null,deepActiveCountB:null,deepActiveIndirect:null,deepActiveRead:null,failureMap:null,failureMapBytes:0,readbackRings:{},failureParams:buf(d,32,B.UNIFORM|B.COPY_DST,'failure-map-params'),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){if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+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;if(f.deepActiveCapacity>=capacity&&capacity>=f.deepActiveCapacity/4&&f.deepActiveState&&f.deepActiveStride===stride)return f;this.clearBindGroupCache();for(const k of ['deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead'])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.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');return f}
ensureFailureMap(bytes,f=this.frame){if(!f)return null;if(f.failureMap&&f.failureMapBytes>=bytes)return f;destroy(f.failureMap);const d=this.device,B=GPUBufferUsage;f.failureMap=buf(d,bytes,B.STORAGE|B.COPY_SRC|B.COPY_DST,'numerical-failure-tile-map');f.failureMapBytes=bytes;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:Array.from({length:4},(_,i)=>d.createTexture({label:label+'sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),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;
}
exportWorkspaceDestroy(){for(const ws of this.exportWs)this.destroyExportWorkspace(ws);this.exportWs=[]}
setDeepContext(ctx){
if(this.deepCtx?.key===ctx.key)return;
const d=this.device,B=GPUBufferUsage,id=ctx.referenceId||ctx.key;
if(!this.deepContexts)this.deepContexts=new Map();
let old=this.deepContexts.get(id),refsB=old?.refsB;
const bytes=ctx.refs.byteLength;
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>old.refs.byteLength){
const offset=old.refs.byteLength;d.queue.writeBuffer(refsB,offset,ctx.refs,offset,bytes-offset);
}
// A shorter cached result must not forget the uploaded prefix length.
const entry=old&&old.refs.byteLength>bytes?{...ctx,refsB,refs:old.refs}:{...ctx,refsB};
this.deepContexts.delete(id);this.deepContexts.set(id,entry);this.deepCtx=entry;
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)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}}
destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null;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)})}
directActiveParams(w,h,iter,snap,targetIter=iter,chunkSize=128){const data=this.directParams(w,h,w,h,0,0,iter,snap);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(64,targetIter,true);d.setUint32(68,chunkSize,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,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)})}
deepActiveTileParams(w,h,fullW,fullH,tileX,tileY,iter,snap,refPixel,targetIter=iter,chunkSize=128,strict=0,outputStride=fullW,outputBase=0){const data=this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,strict,refPixel.x,refPixel.y,0,outputStride,outputBase);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(84,targetIter,true);d.setUint32(88,chunkSize,true)})}
certifiedInteriorParams(count,ctx,snap,width,refPixel){const se=spanMantExp(snap),hi=Math.fround(se.mant);return writeU32F32(48,d=>{d.setUint32(0,count,true);d.setUint32(4,ctx.refLen,true);d.setInt32(8,se.exp,true);d.setFloat32(16,hi,true);d.setFloat32(20,Math.fround(se.mant-hi),true);d.setFloat32(24,width,true);d.setFloat32(28,Math.fround(refPixel.x),true);d.setFloat32(32,Math.fround(refPixel.y),true)})}
fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0,unknownOnly=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
colorParamsData(w,h,_iter=state.fieldView?.iter??maxIter(),style=colorStyleSnapshot()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,style.palette,true);d.setUint32(12,style.hq?1:0,true);d.setFloat32(16,style.cycle,true);d.setFloat32(20,style.shift,true)})}
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const 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()}
encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const pipeline=this.deepPostStats,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}}]),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const 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}}]),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}}]),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,28);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()}
encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const bg=this.cachedBindGroup(this.deepBucketHistStats,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
encodeDeepBucketPrefix(encoder,{bucketState,queueStats,indirect}){const 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}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
numericInFlightBudgetMs(){return state.pointerActive||state.wheelActive||performance.now()-state.lastInteraction<180?24:40}
numericBurstLimit(predictedStripMs){return Math.max(1,Math.min(8,Math.floor(this.numericInFlightBudgetMs()/Math.max(1,Number(predictedStripMs)||20))))}
numericTileShape(w,iter,deep){
const maxPixels=Math.floor((deep?4000000:48000000)/Math.max(1,iter));
if(maxPixels<1)throw new Error('iteration budget requires continuation');
const width=Math.min(w,maxPixels);
return {width,rows:Math.max(1,Math.min(128,Math.floor(maxPixels/width))),maxPixels};
}
initialNumericRows(w,iter,deep){
// Structural workload cap, not a zoom threshold. The first submission is
// intentionally conservative; subsequent strips self-tune from measured
// queue completion time. This keeps one GPU submission bounded even when
// maxIter reaches 12000.
const workBudget=deep?4000000:48000000;
return Math.max(1,Math.min(128,Math.floor(workBudget/Math.max(1,w*iter))));
}
adaptNumericRows(rows,elapsedMs,remaining,deep){
if(remaining<=0)return rows;
// Never grow beyond the initial work-bound. Timing feedback may only
// reduce a strip if this adapter is slower than expected, so the
// worst-case pixel-iteration cap remains invariant across the frame.
const targetMs=deep?18:22,ms=Math.max(.25,Number(elapsedMs)||targetMs);
if(ms<=targetMs*1.25)return Math.max(1,Math.min(rows,remaining));
const factor=Math.max(.5,Math.min(1,Math.sqrt(targetMs/ms)));
return Math.max(1,Math.min(rows,remaining,Math.round(rows*factor)));
}
async submitStage(encoder,stage,token,counter='numeric'){
this.activeGpuStage=stage;state.gpuStage=stage;
const t0=performance.now();this.device.queue.submit([encoder.finish()]);
await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++;
const ms=performance.now()-t0;this.lastCompletedGpuStage=stage;this.activeGpuStage='';
if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++;if(TEST_MODE){const stageKey=String(stage).replace(/\d+/g,'#');runtime.stageCounts[stageKey]=(runtime.stageCounts[stageKey]||0)+1;runtime.stageMs[stageKey]=(runtime.stageMs[stageKey]||0)+ms;}
return token===undefined||token===state.token?ms:null;
}
submitStageDeferred(encoder,stage,counter='numeric'){this.activeGpuStage=stage;state.gpuStage=stage;this.device.queue.submit([encoder.finish()]);if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++;return stage}
async waitStageBurst(stage,token,t0,count=1){await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++;const ms=(performance.now()-t0)/Math.max(1,count);this.lastCompletedGpuStage=stage;this.activeGpuStage='';return token===undefined||token===state.token?ms:null}
encodeDeepActiveResumeInit(e,{pbuf,stateB,queue,count,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}}]),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){if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+4;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){
if(token!==state.token)return 0;
const f=this.frame,{mask,pixels}=certifiedInteriorTiles(snap,f.w,f.h);if(!pixels)return 0;
if(!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=buf(d,mask.byteLength,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST,'interior-mask');
try{
d.queue.writeBuffer(input,0,mask);const e=d.createCommandEncoder();
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 p=e.beginComputePass();p.setPipeline(this.interiorMask);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
return await this.submitStage(e,'exact interior tiles',token,'correction')===null?0:pixels;
}finally{destroy(input)}
}
async readActiveIndices(session,slots=false){
if(!session.active)return new Uint32Array();
const d=this.device,bytes=session.active*4;if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+bytes;const r=buf(d,bytes,GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ,'active-indices');
try{const e=d.createCommandEncoder();e.copyBufferToBuffer(session.queues[session.input],0,r,0,bytes);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return session.compact&&!slots?Uint32Array.from(out,i=>session.mapping[i]):out;}finally{destroy(r)}
}
async beginOperationLimitContinuation(snap,token,ctx,refPixel,indices,strict=false){
await this.ready;await this.ensureDeepActivePipelines();if(!ctx)throw new Error('active continuation reference context is missing');const f=this.ensureFrame(canvas.width,canvas.height),limit=Math.min(Number(this.device.limits?.maxStorageBufferBindingSize||this.adapterLimits.maxStorageBufferBindingSize||Infinity),Number(this.device.limits?.maxBufferSize||this.adapterLimits.maxBufferSize||Infinity)),compact=indices.length*52<f.n*32,stride=compact?40:32,capacity=compact?indices.length:f.n,stateBytes=capacity*stride;if(stateBytes>limit)throw new Error('active continuation state exceeds adapter buffer limit: '+stateBytes+' > '+limit);if(indices.length>f.n)throw new Error('active continuation candidate overflow');this.setDeepContext(ctx);if(compact)await this.ensureCompactActivePipelines();if(token!==state.token)return null;this.ensureDeepActiveWorkspace(capacity,f,stride);const d=this.device,queues=[f.deepActiveQueueA,f.deepActiveQueueB],counts=[f.deepActiveCountA,f.deepActiveCountB],count=indices.length;d.queue.writeBuffer(queues[0],0,indices);d.queue.writeBuffer(counts[0],0,new Uint32Array([count]));d.queue.writeBuffer(counts[1],0,new Uint32Array([0]));d.queue.writeBuffer(f.numericParams,0,this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER+1,snap,refPixel,0,1,strict?1:0,count));const e=d.createCommandEncoder({label:'all-pixel-active-init'});this.encodeDeepActiveResumeInit(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queue:queues[0],count,pipeline:compact?this.deepCompactInit:this.deepActiveResumeInit});const ms=await this.submitStage(e,'all-pixel active init '+count,token,'correction');if(ms===null)return null;if(TEST_MODE)runtime.activeInitialized=(runtime.activeInitialized||0)+count;return{f,queues,counts,compact,mapping:compact?indices:null,pipeline:compact?this.deepCompactContinue:this.deepActiveContinue,input:0,active:count,initial:count,progress:0,maxDispatchWork:0,dispatches:0,pixelIterations:0,strict:!!strict};
}
encodeDeepActiveChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input,passes,refsB=this.deepCtx.refsB,pipeline=this.deepActiveContinue}){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}}]});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 advancePriorityTarget(session,snap,target,token,ctx,refPixel,previousMeta){
// Small queues are faster as a single batch. For large expensive rounds,
// show the existing boundary first, then give every remaining pixel its
// full target. Queue ordering never changes the finite iteration budget.
if(session.active<4096||(target-session.progress)*session.active<8000000)
return this.continueOperationLimitActive(session,snap,target,token,ctx,refPixel);
const edge=new Set(operationLimitFrontier(previousMeta,session.f.w,session.f.h));
if(!edge.size)return this.continueOperationLimitActive(session,snap,target,token,ctx,refPixel);
const slots=await this.readActiveIndices(session,true);if(token!==state.token)return null;
const groups=[[],[]];for(const slot of slots){const pixel=session.compact?session.mapping[slot]:slot;groups[edge.has(pixel)?0:1].push(slot);}
if(!groups[0].length||!groups[1].length)return this.continueOperationLimitActive(session,snap,target,token,ctx,refPixel);
const start=session.progress,survivors=[],d=this.device;
for(const group of groups){
if(token!==state.token)return null;
d.queue.writeBuffer(session.queues[0],0,Uint32Array.from(group));d.queue.writeBuffer(session.counts[0],0,Uint32Array.of(group.length));
session.input=0;session.active=group.length;session.progress=start;
if(!await this.continueOperationLimitActive(session,snap,target,token,ctx,refPixel))return null;
survivors.push(await this.readActiveIndices(session,true));if(token!==state.token)return null;
if(!await presentProvisional(this,snap,target,token))return null;
}
const active=survivors.reduce((n,g)=>n+g.length,0),merged=new Uint32Array(active);let offset=0;
for(const group of survivors){merged.set(group,offset);offset+=group.length;}
if(active)d.queue.writeBuffer(session.queues[0],0,merged);d.queue.writeBuffer(session.counts[0],0,Uint32Array.of(active));
session.input=0;session.active=active;session.progress=target;return session;
}
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(PIXEL_FRONTIER_WORK/before));
if(chunk<1)throw new Error('active workspace exceeds per-dispatch work capacity');
const passes=Math.min(session.batchPasses||1,Math.ceil(remaining/chunk),4);
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);runtime.gpuSyncWaits++;
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;if(TEST_MODE){runtime.activeBatches=(runtime.activeBatches||0)+1;runtime.activeWorkBound=(runtime.activeWorkBound||0)+work*passes;}
session.maxDispatchWork=Math.max(session.maxDispatchWork,work);
runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,work);
session.batchPasses=elapsed<10?Math.min(4,passes+1):elapsed>32?1:passes;
if(TEST_MODE&&this.activeBatchLimit)session.batchPasses=Math.min(session.batchPasses,this.activeBatchLimit);
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,deep,token,forceStrict,referencePixel,fastExtended,colorStyle,colorKey){
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=(deep?this.deepCtx:this.fastCtx)?.key||'',reusePreview=!!previous&&sameViewSnapshot(previous,snap)&&previous.w===f.w&&previous.h===f.h&&previous.iter<iter&&previous.referenceKey===currentReferenceKey&&previous.deep===deep&&previous.fastExtended===fastExtended;
const tileWidth=this.numericTileShape(f.w,iter,deep).width;let rows=this.initialNumericRows(tileWidth,iter,deep),lastUi=0,burstCount=0,burstStart=performance.now(),lastStage='',predictedStripMs=deep?18:22,burstLimit=this.numericBurstLimit(predictedStripMs);
for(let y=0;y<computeH;){
if(token!==state.token)return false;
const th=Math.min(rows,computeH-y);
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;
const params=(deep?this.deepParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,forceStrict?1:0,refX,refY,reusePreview?1:0,f.w,base):this.fastParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,refX,refY,f.w,base,reusePreview?1:0));
d.queue.writeBuffer(f.numericParams,0,params);
const e=d.createCommandEncoder({label:(deep?'deep':'fast')+'-numeric-strip-'+y});
// A fresh numeric frame starts from an explicit zero/UNKNOWN sentinel.
// If any strip or pixel is accidentally left untouched, the coverage
// repair below can detect and recompute it instead of publishing stale
// metadata or a rectangular transparent/black hole.
if(x===0&&y===0&&!reusePreview){e.clearBuffer(f.meta);e.clearBuffer(f.smooth);}
if(deep)this.encodeDeepPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});
else this.encodeFastPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});
lastStage=(deep?'deep':'fast')+' primary rows '+y+'..'+(y+th-1);
this.submitStageDeferred(e,lastStage,'numeric');
burstCount++;state.gpuTileRows=th;
if(burstCount>=burstLimit||x+tw>=f.w){
const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;
rows=this.adaptNumericRows(rows,ms,computeH-y,deep);predictedStripMs=.65*predictedStripMs+.35*ms;burstLimit=this.numericBurstLimit(predictedStripMs);burstCount=0;burstStart=performance.now();
const now=performance.now();if(now-lastUi>100){lastUi=now;state.lastEngine='WebGPU · '+(deep?'高精度補修':'高速拡張')+' 計算 '+Math.round(100*y/computeH)+'%';updateStats()}
}
}
y+=th;
}
if(deep){
const e=d.createCommandEncoder({label:'deep-unknown-post-stats'});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});
const ms=await this.submitStage(e,'deep UNKNOWN post-stats',token,'numeric');if(ms===null)return false;
}else{
// Coverage contract: zero metadata means a pixel was never written by
// the tiled primary path. Re-run only those pixels through the original
// FAST recurrence before colouring. This pass is O(pixels) for the
// trivial guard but performs iterations only for actual coverage holes.
d.queue.writeBuffer(f.numericParams,0,this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY,f.w,0,2));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));
const e=d.createCommandEncoder({label:'fast-coverage-repair-and-color'});
if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});
this.encodeFastNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
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,'fast coverage + color',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;
}
runtime.gpuFrames++;return true;
}
async computeFrame(snap,iter,token,referencePixel=null,fastExtended=false,fastContext=null){
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,false,token,false,referencePixel,true,colorStyle,colorKey)}
// Direct arithmetic is unchanged; large frames use bounded tiles.
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,false);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();this.encodeDirectNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});
if(await this.submitStage(e,'direct bounded tile',token,'numeric')===null)return false;
}}else d.queue.writeBuffer(f.numericParams,0,this.directParams(f.w,computeH,f.w,f.h,0,0,iter,snap));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const encoder=d.createCommandEncoder({label:'mandelbrot-direct-frame'});
if(!tiled)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});
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,'direct primary',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuFrames++;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])}
}
captureColorSource(view=state.frameView){
this.colorSourceVisible=false;
if(!state.colorAuto){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)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;runtime.gpuRecolors++;this.presentFrame();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;runtime.gpuRecolors++;return true}
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
commitHistory(view=state.frameView){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;const metric=generationMetric();if(metric&&!metric.historyCommitMs)metric.historyCommitMs=performance.now()-metric.startedAt;return true}
invalidateHistory(){this.historyView=null;this.historyColorKey='';this.historyReady=false}
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;if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+bytes;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)}
}
async readMetaAll(){if(!this.frame)return new Uint32Array();const f=this.frame,d=this.device,B=GPUBufferUsage,r=buf(d,f.n*4,B.COPY_DST|B.MAP_READ,'field-meta-readback'),e=d.createCommandEncoder({label:'field-meta-readback'});e.copyBufferToBuffer(f.meta,0,r,0,f.n*4);if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+f.n*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 readFieldAll(){if(!this.frame)return{meta:new Uint32Array(),smoothBits:new Uint32Array()};const f=this.frame,d=this.device,B=GPUBufferUsage,bytes=f.n*4,r=buf(d,bytes*2,B.COPY_DST|B.MAP_READ,'field-meta-smooth-readback'),e=d.createCommandEncoder({label:'field-meta-smooth-readback'});e.copyBufferToBuffer(f.meta,0,r,0,bytes);e.copyBufferToBuffer(f.smooth,0,r,bytes,bytes);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const raw=r.getMappedRange(),meta=new Uint32Array(raw.slice(0,bytes)),smoothBits=new Uint32Array(raw.slice(bytes,bytes*2));r.unmap();destroy(r);return{meta,smoothBits}}
async readNumericalFailureTiles(size){await this.ready;await this.ensureCorrectionPipelines();if(!this.frame)return[];const f=this.frame,d=this.device,B=GPUBufferUsage,cols=Math.ceil(f.w/size),rows=Math.ceil(f.h/size),stride=6,bytes=cols*rows*stride*4;this.ensureFailureMap(bytes,f);d.queue.writeBuffer(f.failureParams,0,new Uint32Array([f.w,f.h,size,cols,rows,0,0,0]));const read=buf(d,bytes,B.COPY_DST|B.MAP_READ,'failure-map-readback'),e=d.createCommandEncoder({label:'failure-tile-map'}),bg=d.createBindGroup({layout:this.failureTileMap.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.failureParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.failureMap}}]}),pass=e.beginComputePass({label:'failure-tile-map'});pass.setPipeline(this.failureTileMap);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(cols,rows);pass.end();e.copyBufferToBuffer(f.failureMap,0,read,0,bytes);d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();await read.mapAsync(GPUMapMode.READ);const raw=new Uint32Array(read.getMappedRange().slice(0));read.unmap();destroy(read);runtime.failureMapReadBytes=(runtime.failureMapReadBytes||0)+bytes;const tiles=[];for(let ty=0;ty<rows;ty++)for(let tx=0;tx<cols;tx++){const o=(ty*cols+tx)*stride,reasons={errorBound:raw[o]||0,escapeUncertain:raw[o+1]||0,referenceEnd:raw[o+2]||0,rebaseGap:raw[o+3]||0,range:raw[o+4]||0},numeric=reasons.errorBound+reasons.escapeUncertain+reasons.referenceEnd+reasons.rebaseGap+reasons.range;if(!numeric)continue;const x=tx*size,y=ty*size,w=Math.min(size,f.w-x),h=Math.min(size,f.h-y),representative=raw[o+5],cx=representative<f.n?representative%f.w+.5:x+w*.5,cy=representative<f.n?Math.floor(representative/f.w)+.5:y+h*.5;tiles.push({x,y,w,h,numeric,sumX:cx*numeric,sumY:cy*numeric,minX:x,minY:y,maxX:x+w-1,maxY:y+h-1,representative,reasons})}return tiles.sort((a,b)=>b.numeric-a.numeric)}
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=buf(d,data.byteLength,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST,'precision-results');
try{
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();
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
const ms=await this.submitStage(e,'precision scatter',token,'correction');
return ms===null?null:{applied,ms};
}finally{destroy(input)}
}
async recoverUnknownTile(snap,iter,token,deepContext,tile,referencePixel=null){
await this.ready;await this.ensureCorrectionPipelines();const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!deepContext)throw new Error('tile deep reference context is missing');this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
const refX=referencePixel?.x??(tile.x+tile.w*.5),refY=referencePixel?.y??(tile.y+tile.h*.5);let rows=this.initialNumericRows(tile.w,iter,true);
for(let sy=0;sy<tile.h;){if(token!==state.token)return null;const th=Math.min(rows,tile.h-sy),gy=tile.y+sy,base=gy*f.w+tile.x;d.queue.writeBuffer(f.numericParams,0,this.deepParams(tile.w,th,f.w,f.h,tile.x,gy,iter,snap,.5,.5,1,refX,refY,2,f.w,base));const e=d.createCommandEncoder({label:'tile-multireference-recovery-'+gy});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:tile.w,h:th});const ms=await this.submitStage(e,'multi-ref correction rows '+gy+'..'+(gy+th-1),token,'correction');if(ms===null)return null;sy+=th;rows=this.adaptNumericRows(rows,ms,tile.h-sy,true)}const statsEncoder=d.createCommandEncoder({label:'tile-recovery-post-stats'});this.encodeUnknownStats(statsEncoder,{meta:f.meta,unresolved:f.unresolved,n:f.n});const statsMs=await this.submitStage(statsEncoder,'tile recovery post-stats',token,'correction');if(statsMs===null)return null;runtime.correctionPasses++;runtime.multiRefBuilds++;return this.readUnresolvedStats()
}
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,r=this.frameReadback('unknown-stats',UNRESOLVED_BYTES),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
async readUnresolved(){return(await this.readUnresolvedStats()).total}
async 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;runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,count*Math.max(1,iter));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;
runtime.correctionPasses++;runtime.lastSparseQueue={...q,valid,ms:totalMs,batches:Math.ceil(built.selected/maxBatch)};
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;runtime.correctionPasses++;return{mode:'scan'}}
async refineOperationLimitIndices(snap,iter,token,deepContext,referencePixel,indices){
await this.ready;await this.ensureCorrectionPipelines();if(!deepContext)throw new Error('frontier reference context is missing');
const f=this.ensureDeepQueueWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setDeepContext(deepContext);
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,chunkSize=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,iter)));let processed=0,maxDispatchWork=0;
for(let start=0;start<indices.length;start+=chunkSize){
if(token!==state.token)return null;const chunk=indices.subarray?indices.subarray(start,Math.min(indices.length,start+chunkSize)):indices.slice(start,start+chunkSize),count=chunk.length;if(!count)continue;
d.queue.writeBuffer(f.deepQueue,0,chunk);d.queue.writeBuffer(f.deepQueueStats,0,new Uint32Array([count,0,count,count,0,0,0,0]));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array([Math.ceil(count/64),1,1,0]));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,0,f.w,0));
const e=d.createCommandEncoder({label:'pixel-frontier-'+iter+'-'+start});e.clearBuffer(f.unresolved);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,'pixel frontier '+(start+1)+'..'+(start+count)+'/'+indices.length+' @ '+iter,token,'correction');if(ms===null)return null;
const q=await this.readDeepQueueStats();if(token!==state.token)return null;const valid=q.selected===count&&q.enqueued===count&&q.dispatchCount===count&&q.processed===count&&q.overflow===0&&q.invalidIndex===0&&q.staleEntry===0;if(!valid)throw new Error('pixel frontier queue invariant failed: '+JSON.stringify(q));processed+=count;maxDispatchWork=Math.max(maxDispatchWork,count*iter);
}
const e=d.createCommandEncoder({label:'pixel-frontier-post-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms=await this.submitStage(e,'pixel frontier post-stats',token,'correction');if(ms===null)return null;runtime.frontierDispatches=(runtime.frontierDispatches||0)+Math.ceil(indices.length/Math.max(1,chunkSize));runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,maxDispatchWork);return{stats:await this.readUnresolvedStats(),processed,maxDispatchWork,chunkSize};
}
async extendUnknownFrame(snap,iter,token,deepContext,referencePixel=null){await this.ready;await this.ensureCorrectionPipelines();if(token!==state.token)return null;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!deepContext)throw new Error('extended deep reference context is missing');this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,1,f.w,0));const e=d.createCommandEncoder({label:'targeted-deep-unknown-recovery'});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
async renderTileMeta({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
await this.ready;const fastExtended=!deep&&!!fastContext;if(deep){await this.ensureDeepPipelines();if(correctUnknown)await this.ensureCorrectionPipelines();this.setDeepContext(deepContext)}else if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
}
async readCompleteRgba(token=state.token){
if(token!==state.token||state.rendering||state.refinePending||state.qualityStage!=='final'||!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,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true,workspaceSlot=0,workspaceSize=this.exportTileSize()}){
if(w*h*Math.max(1,iter)>(deep?4000000:48000000)){
const shape=this.numericTileShape(w,iter,deep),out=new Uint8ClampedArray(w*h*4),generation=state.token;
let unresolved=0,corrected=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);
const part=await this.renderTileRGBA({snap,iter,deep,deepContext,fastContext,fullW,fullH,sampleX,sampleY,edgeAA,forceStrict,correctUnknown,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;corrected+=part.corrected;
}
return{rgba:out,unresolved,corrected};
}
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=!deep&&!!fastContext;if(deep){await this.ensureDeepPipelines();if(correctUnknown)await this.ensureCorrectionPipelines();this.setDeepContext(deepContext)}else 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(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const ca=this.colorParamsData(w,h,iter),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes,UNRESOLVED_BYTES/4),unresolvedCount=stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
async renderTileRGBA2x({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true,workspaceSlot=0,workspaceSize=this.exportTileSize()}){
if(w*h*Math.max(1,iter)>(deep?4000000:48000000)){
const shape=this.numericTileShape(w,iter,deep),out=new Uint8ClampedArray(w*h*4),generation=state.token;
let unresolved=0,corrected=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);
const part=await this.renderTileRGBA2x({snap,iter,deep,deepContext,fastContext,fullW,fullH,forceStrict,correctUnknown,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;corrected+=part.corrected;
}
return{rgba:out,unresolved,corrected};
}
await this.ready;await this.ensureAaPipeline();const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep){await this.ensureDeepPipelines();if(correctUnknown)await this.ensureCorrectionPipelines();this.setDeepContext(deepContext)}else 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(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount=0,corrected=0;for(let si=0;si<4;si++){const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4);unresolvedCount+=stats[0]||0;corrected+=stats[7]||0}ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.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.
function tileRecoveryConfig(){const z=zoomExp(),deep=z>=32;if(state.processMode==='power')return{size:deep?224:320,maxRefs:4,minFailed:deep?4:48};if(state.processMode==='fine')return{size:deep?128:192,maxRefs:20,minFailed:deep?1:8};if(state.processMode==='validate')return{size:deep?112:160,maxRefs:32,minFailed:1};return{size:deep?160:224,maxRefs:12,minFailed:deep?1:12}}
function isOperationLimitMeta(m){return ((m>>>28)&3)===FIELD_UNKNOWN&&((m>>>REASON_SHIFT)&255)===6}
function operationLimitIndices(meta){const out=[];for(let i=0;i<meta.length;i++)if(isOperationLimitMeta(meta[i]))out.push(i);return Uint32Array.from(out)}
function numericalFailureIndices(meta){const out=[];for(let i=0;i<meta.length;i++){if(((meta[i]>>>28)&3)!==FIELD_UNKNOWN)continue;const reason=(meta[i]>>>REASON_SHIFT)&255;if(reason>=1&&reason<=5)out.push(i)}return Uint32Array.from(out)}
function operationLimitFrontier(meta,w,h){const out=[];for(let y=0;y<h;y++){const row=y*w;for(let x=0;x<w;x++){const i=row+x;if(!isOperationLimitMeta(meta[i]))continue;let edge=false;for(let oy=-1;oy<=1&&!edge;oy++){const yy=y+oy;if(yy<0||yy>=h)continue;for(let ox=-1;ox<=1;ox++){if(ox===0&&oy===0)continue;const xx=x+ox;if(xx<0||xx>=w)continue;if(((meta[yy*w+xx]>>>28)&3)===FIELD_ESCAPED){edge=true;break}}}if(edge)out.push(i)}}return Uint32Array.from(out)}
function pixelEscapeAdvance(before,after,w,h){let escaped=0,outsideEnvelope=0;for(let i=0;i<after.length;i++){if(((after[i]>>>28)&3)!==FIELD_ESCAPED||((before[i]>>>28)&3)===FIELD_ESCAPED)continue;escaped++;const x=i%w,y=Math.floor(i/w);let adjacent=false;for(let oy=-1;oy<=1&&!adjacent;oy++){const yy=y+oy;if(yy<0||yy>=h)continue;for(let ox=-1;ox<=1;ox++){if(!ox&&!oy)continue;const xx=x+ox;if(xx>=0&&xx<w&&((before[yy*w+xx]>>>28)&3)===FIELD_ESCAPED){adjacent=true;break}}}if(!adjacent)outsideEnvelope++}return{escaped,outsideEnvelope,pixelStable:outsideEnvelope===0}}
// Integer interval tests over exact rational pixel coordinates. No f32
// rounding, periodic-orbit guess, or agreement of two precisions is a proof.
function certifiedInteriorTiles(snap,width,height,tile=32){
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),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};
}
function pixelFrontierIterationFloor(snap,w,initialIter){if(state.continuationBudget)return Math.max(initialIter,state.continuationBudget);const pixelBits=Math.max(1,Math.log2(Math.max(1,w))-log2FixedAt(snap.span,snap.bits)),scaled=Math.ceil(pixelBits*256/ADAPTIVE_ITER_QUANTUM)*ADAPTIVE_ITER_QUANTUM;return Math.min(PIXEL_FRONTIER_MAX_ITER,Math.max(initialIter,scaled))}
function tileReferenceSnapshot(snap,tile,fullW,fullH){const cx=tile.numeric?tile.sumX/tile.numeric:tile.x+tile.w*.5,cy=tile.numeric?tile.sumY/tile.numeric:tile.y+tile.h*.5,bw=Math.max(1,tile.maxX-tile.minX+1),bh=Math.max(1,tile.maxY-tile.minY+1),refW=Math.max(8,Math.min(tile.w,Math.ceil(bw*1.75))),refH=Math.max(8,Math.min(tile.h,Math.ceil(bh*1.75))),den=2n*BigInt(fullW),nx=BigInt(Math.round(2*cx-fullW)),ny=BigInt(Math.round(fullH-2*cy)),re=snap.re+roundDivSigned(snap.span*nx,den),im=snap.im+roundDivSigned(snap.span*ny,den),localSpan=(()=>{const q=roundDivSigned(snap.span*BigInt(refW),BigInt(fullW));return q>0n?q:1n})();return{snap:{bits:snap.bits,re,im,span:localSpan},width:refW,height:refH}}
async function recoverNumericalUnknownTiles(r,snap,iter,token,initialStats){
const initialFailed=numericalFailureCount(initialStats),cfg=tileRecoveryConfig();
if(initialFailed<cfg.minFailed)return{stats:initialStats,passes:0,recovered:0,attempted:0};
state.lastEngine='WebGPU · 数値失敗領域を局所referenceで解析';updateStats();
const tried=new Set();let passes=0,attempted=0,stats=initialStats,budget=cfg.maxRefs;
const sizes=[cfg.size,Math.max(56,Math.floor(cfg.size*.5))];
for(let round=0;round<sizes.length&&budget>0;round++){
stats=await r.readUnresolvedStats();if(token!==state.token)return null;
const failed=numericalFailureCount(stats);if(failed<cfg.minFailed)break;const roundStartFailed=failed;
const tiles=await r.readNumericalFailureTiles(sizes[round]);if(token!==state.token)return null;if(!tiles.length)break;
const roundCap=round===0?Math.min(budget,Math.max(1,Math.ceil(cfg.maxRefs*.67))):budget;let used=0;
for(let cursor=0;cursor<tiles.length&&used<roundCap&&budget>0;){
if(token!==state.token)return null;const count=Math.min(tileReferencePool.length,tiles.length-cursor,roundCap-used,budget),batch=tiles.slice(cursor,cursor+count);cursor+=count;
state.lastEngine='CPU · 局所reference '+(passes+1)+''+(passes+count)+'/'+cfg.maxRefs+' を並列準備';updateStats();
const prepared=await Promise.all(batch.map(async(tile,i)=>{const local=tileReferenceSnapshot(snap,tile,canvas.width,canvas.height),fixed={re:local.snap.re,im:local.snap.im},identity=[fixed.re,fixed.im,local.snap.bits,pixelPrecisionBits(local.snap,local.width,40),iter].join(':');if(tried.has(identity))return null;tried.add(identity);const ctx=await tileReferencePool[i].requestFixed(local.snap,iter,local.width,local.height,fixed);return{tile,ctx}}));if(token!==state.token)return null;
for(const {tile,ctx} of prepared.filter(Boolean)){
if(token!==state.token)return null;
if(ctx.checkpointMismatch)throw new Error('tile reference guard checkpoint mismatch');
r.setDeepContext(ctx);
const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);
const next=await r.correctUnknownFrame(snap,iter,token,refPixel,0,tile);
if(!next||token!==state.token)return null;
stats=await r.readUnresolvedStats();if(token!==state.token)return null;
attempted+=next.queue?.selected||tile.numeric;passes++;used++;budget--;state.multiRefPasses=passes;
}
}
if(!used||numericalFailureCount(stats)>=roundStartFailed)break;
}
if(passes){stats=await r.readUnresolvedStats();if(token!==state.token)return null}
const recovered=Math.max(0,initialFailed-numericalFailureCount(stats));state.multiRefRecovered=recovered;state.unresolved=stats.total;state.unknownReasons=stats.reasons;return{stats,passes,recovered,attempted};
}
function 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);
const active=new Map(),completed=[];let completedCount=0,lastApply=performance.now();let next=0,attempted=0,appliedCount=0,batchSize=precisionBatchSize(iter,bits);
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<precisionFallbackPool.length;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;
if(TEST_MODE)runtime.precisionOrbits=(runtime.precisionOrbits||0)+done.result.orbitCount;completed.push(done.result);completedCount+=done.result.indices.length;attempted+=done.result.indices.length;
if(done.ms>40)batchSize=Math.max(1,Math.floor(batchSize/2));
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();
state.lastEngine='CPU · 数値補修 '+attempted+'/'+indices.length+' px';updateStats();
}
}
const stats=await r.readUnresolvedStats();if(token!==state.token)return null;
state.precisionFallbackAttempted=(state.precisionFallbackAttempted||0)+attempted;
state.precisionFallbackApplied=(state.precisionFallbackApplied||0)+appliedCount;
return{stats,attempted,applied:appliedCount};
}finally{
for(const worker of active.keys())precisionFallbackPool[worker].cancel('cancelled precision batch');
}
}
async function refinePixelFrontier(r,snap,initialIter,token,initialStats){
if(!PIXEL_FRONTIER_ENABLED)return{stats:initialStats,iter:initialIter,rounds:0,processed:0,escaped:0,converged:false,applied:false};
const certified=await r.certifyInterior(snap,token);if(token!==state.token)return null;
const explicitBudget=state.continuationBudget||0,maxIter=Math.max(initialIter,explicitBudget||PIXEL_FRONTIER_MAX_ITER),minimumIter=pixelFrontierIterationFloor(snap,canvas.width,initialIter),initialMeta=await r.readMetaAll();if(token!==state.token)return null;state.frontierMinimumIter=minimumIter;let previousMeta=initialMeta,candidates=operationLimitIndices(initialMeta),iter=initialIter,stats=initialStats,rounds=0,processed=0,escaped=0,stableRounds=0,converged=candidates.length===0||iter>=maxIter&&candidates.every(i=>(initialMeta[i]&ITER_MASK)>=maxIter),session=null,fixedSource=null,deferred=new Uint32Array();
while(!converged&&iter<maxIter){
if(token!==state.token)return null;const target=Math.min(maxIter,Math.max(iter+1,iter*2));state.lastEngine='WebGPU · 未脱出全画素 active-list '+candidates.length+' px · '+target+' 反復';state.frontierPixels=candidates.length;updateStats();let ctx=fixedSource?await refs.requestFixed(snap,target,canvas.width,canvas.height,fixedSource):await refs.request(snap,target,canvas.width,canvas.height);if(token!==state.token)return null;if(ctx.checkpointMismatch)throw new Error('all-pixel active reference guard checkpoint mismatch');
// A persistent perturbation state is valid only while its reference orbit
// origin is unchanged and reaches the new target. If it does not, rebuild
// the candidate list from the already-published metadata and restart once
// with a newly selected long reference; no pixel is silently discarded.
if(ctx.refLen<target){ctx=await refs.request(snap,target,canvas.width,canvas.height,true);if(token!==state.token)return null;const meta=await r.readMetaAll();if(token!==state.token)return null;candidates=operationLimitIndices(meta);session=null;fixedSource=null;deferred=new Uint32Array();if(!candidates.length){converged=true;break}}
const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(session&&session.referenceId!==(ctx.referenceId||ctx.key)){session=null;deferred=new Uint32Array();candidates=operationLimitIndices(previousMeta)}if(!session){session=await r.beginOperationLimitContinuation(snap,token,ctx,refPixel,candidates,state.processMode==='validate');if(!session||token!==state.token)return null;fixedSource=ctx.source;session.referenceId=ctx.referenceId||ctx.key}
const workBefore=session.pixelIterations,dispatchBefore=session.dispatches,pass=await r.advancePriorityTarget(session,snap,target,token,ctx,refPixel,previousMeta);if(!pass||token!==state.token)return null;processed+=pass.pixelIterations-workBefore;runtime.frontierDispatches=(runtime.frontierDispatches||0)+(pass.dispatches-dispatchBefore);if(deferred.length){const repaired=await r.refineOperationLimitIndices(snap,target,token,ctx,refPixel,deferred);if(!repaired||token!==state.token)return null;}stats=await r.readUnresolvedStats();if(token!==state.token)return null;let failed=numericalFailureCount(stats);if(failed>0){r.setDeepContext(ctx);const corrected=await r.correctUnknownFrame(snap,target,token,refPixel);if(!corrected||token!==state.token)return null;stats=await r.readUnresolvedStats();if(token!==state.token)return null}if(numericalFailureCount(stats)>512){const beforeLocal=numericalFailureCount(stats),mr=await recoverNumericalUnknownTiles(r,snap,target,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;if(numericalFailureCount(stats)>=beforeLocal)state.multiRefRecovered=0}if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,target,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats}const after=await r.readMetaAll();if(token!==state.token)return null;const advance=pixelEscapeAdvance(previousMeta,after,canvas.width,canvas.height);rounds++;escaped+=advance.escaped;iter=target;stableRounds=advance.pixelStable?stableRounds+1:0;previousMeta=after;state.frontierRounds=rounds;state.frontierEscaped=escaped;state.frontierIter=iter;
if(!await presentProvisional(r,snap,iter,token))return null;
// A quiet pair of doubling rounds is only meaningful after every remaining
// operation-limit pixel has actually reached the budget derived from one
// screen pixel. This prevents an early quiet interval from hiding a later
// visible escape component. Recovery may repopulate an emptied GPU queue,
// so convergence is decided from the post-recovery metadata.
const recoveredCandidates=operationLimitIndices(after);if(!recoveredCandidates.length)converged=true;else if(iter>=maxIter&&recoveredCandidates.every(i=>(after[i]&ITER_MASK)>=maxIter))converged=true;else if(!explicitBudget&&iter>=minimumIter&&stableRounds>=2)converged=true;else{
// Only pixels whose correction supplied metadata without continuation
// state are replayed. Healthy perturbation states never rewind.
const live=await r.readActiveIndices(session);if(token!==state.token)return null;
const liveSet=new Set(live);deferred=Uint32Array.from(recoveredCandidates.filter(i=>!liveSet.has(i)));
candidates=recoveredCandidates;converged=false;
}
}
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}
state.frontierConverged=converged;state.frontierIter=iter;state.unresolved=stats.total;state.unknownReasons=stats.reasons;runtime.frontierFrames=(runtime.frontierFrames||0)+1;runtime.frontierPixels=(runtime.frontierPixels||0)+processed;runtime.frontierEscaped=(runtime.frontierEscaped||0)+escaped;return{stats,iter,minimumIter,rounds,processed,escaped,converged,policy:explicitBudget?'explicit-finite-budget':iter>=maxIter?'automatic-finite-cap':'finite-budget-with-stability-check',membershipCertified:false,applied:escaped>0||certified>0};
}
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 reusableReferencePixel(ctx,snap,iter,w,h,requiredBits){const src=ctx&&ctx.source;if(!ctx||!src||!referenceOriginMatchesView(src,snap)||ctx.refLen<iter||(ctx.precisionBits||0)<requiredBits||ctx.checkpointMismatch)return null;const pixel=referencePixelForSource(src,snap,w,h),margin=.125*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;return{ctx,pixel}}
function reusableFastReference(snap,iter,w,h){return reusableReferencePixel(renderer&&renderer.fastCtx,snap,iter,w,h,fastReferenceBits(snap,w))}
// ── 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.gpuUnavailable=true;state.gpuError='WebGPU非対応 · 重いfallbackは無効';updateStats();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=true;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません · fallback無効';updateStats();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();if(!state.rendering&&!state.frameView)markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuUnavailable=false;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e)+' · fallback無効';updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
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.lastRender=0;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.lastRender=c.elapsed;
clearInterval(renderClockTimer);renderClockTimer=0;
$('#render').textContent=renderTimeLabel();
}
function cancelRender(){finishRenderClock(state.token,'cancelled');if(exportJob.active)exportJob.cancelled=true;const metric=generationMetric();if(metric&&metric.status==='running'){metric.status='cancelled';metric.cancelMs=performance.now()-metric.startedAt}state.token++;state.rendering=false;state.recolorPending=false;state.refinePending=false;state.numericRecoveryPending=false;state.numericRecoveryRunning=false;if(refineTimer){clearTimeout(refineTimer);refineTimer=0}refs.cancelPending('render cancelled');for(const service of tileReferencePool)service.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled');precisionFallback.cancel('render cancelled');for(const service of precisionFallbackPool)service.cancel('render cancelled')}
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
async function presentProvisional(r,snap,iter,token,force=false){
if(token!==state.token)return false;
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,progressive:true};
r.captureColorSource(snap);
r.lastProvisionalAt=performance.now();state.drawState='REFINING';
const metric=generationMetric(token);if(metric&&!metric.firstDisplayMs)metric.firstDisplayMs=performance.now()-metric.startedAt;
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();return true;
}
async function renderQualityStage(r,snap,iter,token,isFinal=false){
let decision=chooseBackend(snap,canvas.width,isFinal),deep=decision.deep,fastExtended=decision.fastExtended;
state.backendDecision=decision;let ctx=null,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{state.lastEngine='WebGPU · '+(isFinal?'最終':'preview')+' 高速拡張 reference準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height)}
}
state.lastEngine='WebGPU · '+(isFinal?'最終品質':'即時preview')+' '+(deep?'高精度補修':fastExtended?'高速拡張':'高速');updateStats();
let ok=await r.computeFrame(snap,iter,token,referencePixel,fastExtended,fastCtx);if(!ok||token!==state.token)return null;
if(isFinal&&!await presentProvisional(r,snap,iter,token,true))return null;
if(isFinal&&fastExtended){
// A FAST frame is not publishable while any numeric UNKNOWN remains.
// Repair locally first, then solve every residual pixel at precision
// derived from span / canvasWidth. This computes the pixels instead of
// disguising them with colour or deferring correctness to a zoom cutoff.
let stats=await r.readUnresolvedStats();if(token!==state.token)return null;const initialFailed=numericalFailureCount(stats);
if(initialFailed>0){const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;}
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats;}
state.unresolved=stats.total;state.unknownReasons=stats.reasons;
if(initialFailed>0){const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;}
}else if(isFinal){
const stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;
}
let effectiveIter=iter,frontierResult=null;
if(isFinal&&PIXEL_FRONTIER_ENABLED){frontierResult=await refinePixelFrontier(r,snap,iter,token,{total:state.unresolved,reasons:{...(state.unknownReasons||{})}});if(!frontierResult||token!==state.token)return null;effectiveIter=frontierResult.iter;if(!frontierResult.converged)throw new Error('全画素反復が安全上限 '+PIXEL_FRONTIER_MAX_ITER+' までに収束しなかったためframeを公開しません');if(frontierResult.applied){const painted=await r.recolor(token,effectiveIter);if(!painted||token!==state.token)return null}}
if(isFinal){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,baseIter:iter,frontier:frontierResult,w:canvas.width,h:canvas.height,deep,fastExtended,backend:frontierResult?.applied?'pixel-frontier-'+decision.backend:decision.backend,referenceKey:(deep?ctx:fastExtended?fastCtx:null)?.key||'',referenceReused,complete:isFinal,progressive:true};
r.captureColorSource(snap);
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});
return{decision,referenceReused,referencePixel};
}
function numericFrameComplete(){return state.fieldView?.complete!==false&&numericalFailureCount({reasons:state.unknownReasons||{}})===0&&!state.numericRecoveryPending&&!state.numericRecoveryRunning}
function scheduleQualityRefinement(snap,token,stages,index){
if(index>=stages.length||token!==state.token)return;
state.refinePending=true;if(refineTimer)clearTimeout(refineTimer);
refineTimer=setTimeout(()=>{refineTimer=0;runQualityRefinement(snap,token,stages,index)},refinementDelay(index));
}
function prefetchFinalReference(snap,finalIter,token,decision){if(token!==state.token||!decision||finalIter<=state.qualityIter)return;let work=null;if(decision.deep)work=refs.request(snap,finalIter,canvas.width,canvas.height);else if(decision.fastExtended)work=fastRefs.request(snap,finalIter,canvas.width,canvas.height);if(work){runtime.referencePrefetches=(runtime.referencePrefetches||0)+1;work.catch(e=>{if(!/cancelled|superseded/i.test(String(e&&e.message||e)))console.warn('final reference prefetch skipped:',e)})}}
async function runQualityRefinement(snap,token,stages,index){
if(token!==state.token||state.pointerActive||state.wheelActive||state.dirty||!sameViewSnapshot(snap,snapshot())){state.refinePending=false;return}
if(state.recoloring){refineTimer=setTimeout(()=>{refineTimer=0;runQualityRefinement(snap,token,stages,index)},16);return}
const r=renderer;if(!r){state.refinePending=false;return}
const iter=stages[index],isFinal=index===stages.length-1,t0=performance.now();state.rendering=true;state.refinePending=false;state.drawState='REFINING';state.qualityStage=isFinal?'final':'refining';state.qualityIter=iter;updateStats();
try{
const result=await renderQualityStage(r,snap,iter,token,isFinal);if(!result||token!==state.token)return;
state.refineMs=performance.now()-t0;state.rendering=false;state.drawState=isFinal?'COVERED':'REFINED';state.lastEngine='WebGPU · '+(isFinal?'最終品質':'中間品質')+' '+(result.decision.deep?'高精度補修':result.decision.fastExtended?'高速拡張':'高速');const metric=generationMetric(token);if(metric&&isFinal){metric.finalMs=performance.now()-metric.startedAt;metric.status='final';metric.finalUnknown=state.unresolved;metric.finalReasons={...(state.unknownReasons||{})};metric.finalNumericalFailures=numericalFailureCount({reasons:metric.finalReasons});metric.numericSubmits=runtime.numericSubmits-metric.numericSubmitsStart;metric.correctionSubmits=runtime.correctionSubmits-metric.correctionSubmitsStart;metric.gpuSyncWaits=runtime.gpuSyncWaits-metric.gpuSyncWaitsStart}
if(isFinal){if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false;}else{state.temporalFill=!!r.historyReady;}r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});}
if(isFinal){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);
if(!isFinal)scheduleQualityRefinement(snap,token,stages,index+1);
}catch(e){if(token!==state.token)return;finishRenderClock(token,'error');state.drawState='ERROR';state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU refinement error';updateStats();console.error(e)}
}
async function renderFrame(){
const token=++state.token,snap=snapshot(),finalIter=maxIter(),holdStable=canUseStableReprojection(),stages=qualityStages(finalIter),iter=stages[0],isSingleFinal=stages.length===1,t0=performance.now();
if(renderer&&state.fieldView?.complete&&state.colorAuto)renderer.captureColorSource(state.frameView);
startRenderClock(token,t0);
const generation=TEST_MODE?{token,startedAt:t0,viewKey:[snap.bits,snap.re,snap.im,snap.span].join(':'),width:canvas.width,height:canvas.height,previewIter:iter,finalIter,numericSubmitsStart:runtime.numericSubmits,correctionSubmitsStart:runtime.correctionSubmits,gpuSyncWaitsStart:runtime.gpuSyncWaits,status:'running'}:null;if(generation){runtime.generationMetrics.push(generation);if(runtime.generationMetrics.length>16)runtime.generationMetrics.shift();}
state.rendering=true;state.dirty=false;state.drawState=isSingleFinal&&holdStable?'REPROJECTED':'COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.multiRefPasses=0;state.multiRefRecovered=0;state.frontierRounds=0;state.frontierPixels=0;state.frontierEscaped=0;state.frontierIter=iter;state.frontierMinimumIter=PIXEL_FRONTIER_ENABLED?pixelFrontierIterationFloor(snap,canvas.width,iter):iter;state.frontierConverged=false;state.qualityStage=isSingleFinal?'final':'preview';state.qualityIter=iter;state.qualityFinalIter=finalIter;state.previewMs=0;state.refineMs=0;state.refinePending=false;runtime.renderStarts++;runtime.stageCounts={};runtime.stageMs={};runtime.frontierMaxDispatchWork=0;updateStats();
try{
const r=renderer||await initRenderer();if(token!==state.token)return;
if(!r){finishRenderClock(token,'error');state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU必須 · fallback無効';updateStats();return}
state.temporalFill=!!r.historyReady;
// Paint the old completed frame at the new view immediately. The numeric
// buffers may now be rewritten strip by strip, but the user keeps seeing
// a coherent image until the complete new colour frame is ready.
if(holdStable){r.presentFrame(r.presentTransform());state.drawState='REPROJECTED';updateStats()}
const result=await renderQualityStage(r,snap,iter,token,isSingleFinal);if(!result||token!==state.token)return;
const elapsed=performance.now()-t0;if(generation){generation.primaryMs=elapsed;generation.backend=result.decision.backend;generation.referenceReused=!!result.referenceReused;generation.primaryUnknown=state.unresolved;generation.primaryReasons={...(state.unknownReasons||{})};generation.primaryNumericalFailures=numericalFailureCount({reasons:generation.primaryReasons});}state.rendering=false;state.drawState=isSingleFinal?'COVERED':'REFINED';
if(isSingleFinal){state.refineMs=elapsed;state.previewMs=0;state.lastEngine='WebGPU · 最終品質 '+(result.decision.deep?'高精度補修':result.decision.fastExtended?'高速拡張':'高速');if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false;}else{state.temporalFill=!!r.historyReady;}r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});state.qualityStage='final';}
else{state.previewMs=elapsed;state.lastEngine='WebGPU · 即時preview '+(result.decision.deep?'高精度補修':result.decision.fastExtended?'高速拡張':'高速');prefetchFinalReference(snap,finalIter,token,result.decision);}
if(isSingleFinal){state.rendering=true;await r.device.queue.onSubmittedWorkDone();if(token!==state.token)return;state.rendering=false;finishRenderClock(token)}
if(isSingleFinal&&generation){generation.finalMs=elapsed;generation.status='final';generation.finalUnknown=state.unresolved;generation.finalReasons={...(state.unknownReasons||{})};generation.numericSubmits=runtime.numericSubmits-generation.numericSubmitsStart;generation.correctionSubmits=runtime.correctionSubmits-generation.correctionSubmitsStart;generation.gpuSyncWaits=runtime.gpuSyncWaits-generation.gpuSyncWaitsStart}updateStats();
if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor);
if(!isSingleFinal)scheduleQualityRefinement(snap,token,stages,1);
}catch(e){if(generation){generation.status=token===state.token?'error':'cancelled';generation.error=String(e&&e.message||e);}if(token!==state.token)return;finishRenderClock(token,'error');state.drawState='ERROR';state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
}
function requestRecolor(){
state.colorRevision++;state.recolorPending=true;
if(state.rendering||state.numericRecoveryRunning){renderer?.recolorPublished();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&&!state.numericRecoveryRunning)queueMicrotask(recolor);
}
async function recolor(){
if(!renderer?.frame||!state.fieldView){state.recolorPending=true;return false}
if(state.rendering||state.recoloring||state.numericRecoveryRunning){state.recolorPending=true;return false}
state.recoloring=true;let painted=false;
try{
while(state.recolorPending&&!state.rendering&&!state.numericRecoveryRunning&&renderer?.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&&state.qualityStage==='final'&&!state.refinePending&&numericFrameComplete()){renderer.commitHistory(state.frameView);state.temporalFill=false}
updateStats();
}
return painted
}catch(e){console.error(e);return false}
finally{state.recoloring=false;if(state.dirty)schedule();if(state.recolorPending&&renderer?.frame&&!state.rendering&&!state.numericRecoveryRunning)queueMicrotask(recolor)}
}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering&&!state.recoloring)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');if(state.continuationBudget)p.set('cb',String(state.continuationBudget));p.set('rm',state.renderMode);const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal')){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';state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(Number(p.get('cb'))||0)));state.renderMode='fast';ensurePrecision();return true}catch{return false}}
function syncHistoryButtons(){}
function syncControls(){$('#continuationBudget').value=String(state.continuationBudget);$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()}
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI':'UI';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
async function copyShareUrl(){
saveHash(false);const url=location.href;
try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(url);toast('共有URLをコピーしました');return}}
catch{}
try{const ta=document.createElement('textarea');ta.value=url;ta.setAttribute('readonly','');ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();const ok=document.execCommand&&document.execCommand('copy');ta.remove();if(ok){toast('共有URLをコピーしました');return}}catch{}
try{if(navigator.share){await navigator.share({title:'MANDELBROT DEEP ZOOM',url});toast('共有しました');return}}catch(e){if(e?.name==='AbortError')return}
try{prompt('共有URLをコピーしてください',url)}catch{}toast('共有URLを表示しました')
}
$('#share').onclick=copyShareUrl;
let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0;
function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色相変化停止':'色相変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')}
function 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(now-colorAutoPaint>=50){colorAutoPaint=now;requestRecolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)}
$('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf){if(!state.rendering&&!state.numericRecoveryRunning)renderer?.captureColorSource();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(false)});
function applyCycleControl(e){state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);requestRecolor()}
function applyShiftControl(e){if(state.colorAuto)stopColorAuto();state.shift=Math.max(0,Math.min(1,Number(e.target.value)||0));$('#shiftO').textContent=state.shift.toFixed(2);requestRecolor()}
$('#cycle').addEventListener('input',applyCycleControl);$('#cycle').addEventListener('change',()=>saveHash(false));
$('#shift').addEventListener('input',applyShiftControl);$('#shift').addEventListener('change',()=>saveHash(false));
$('#continuationBudget').onchange=e=>{state.continuationBudget=Math.max(0,Math.min(150000,Number(e.target.value)||0));saveHash(false);recordView();markDirty()};
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
const exportJob={active:false,cancelled:false};
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
class StreamingPng{
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
async rows(filteredRows){await this.writer.write(filteredRows)}
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
}
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
async function 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()}
async function runExport(){
if(exportJob.active)return;
const requestedToken=state.token;
while(state.rendering||state.dirty||state.refinePending){
$('#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(),deep=deepNeeded(snap,w,h),fastExtended=!deep&&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:paletteParts[3]==='1'},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={format:'mandelbrot-view-v24',rendererVersion:VERSION,width:w,height:h,supersampling:1,iterationPolicy:{base:state.baseIter,effective:iter,policy:state.fieldView.frontier?.policy||'finite-budget'},membershipCertified:false,numericalFailures:0,operationLimit:state.unknownReasons?.operationLimit||0,view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette,exportPipeline:'completed-numeric-field',shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;$('#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 ctx=null,fastCtx=null;
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}else if(fastExtended){$('#exportStatus').textContent='高速拡張参照を準備中…';fastCtx=await fastRefs.request(snap,iter,w,h)}
const tile=r.exportTileSize(),ringDepth=deep||iter>4096?1:r.exportRingDepth(tile),totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
let done=0,unresolvedSamples=0,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,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX,tileY:y,w:tw,h:th,forceStrict: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,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':fastExtended?'reduced-reference + fast-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + '+ringDepth+'-slot staging ring + streaming-png':'gpu-tile + '+ringDepth+'-slot staging ring + streaming-png',exportTileSize:tile,exportRingDepth:ringDepth,iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'sidecar参照':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+(state.fieldView?.complete===false?'-preview':'')+'.png')},'image/png');
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?('WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'WebGPU 復旧中':'起動中');$('#render').textContent=renderTimeLabel();let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'数値補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.fieldView?.complete===false)status+=' · 暫定表示';if(state.unresolved){const nf=numericalFailureCount({reasons:state.unknownReasons||{}}),op=state.unknownReasons?.operationLimit||0;status+=' · 未確定 '+state.unresolved+(nf||op?' (数値 '+nf+' / 上限 '+op+')':'');}if(state.correctionPasses)status+=' · DS補正';if(state.multiRefPasses)status+=' · 局所ref '+state.multiRefPasses+' / 回収 '+state.multiRefRecovered;if(state.qualityStage==='preview')status+=' · preview '+state.qualityIter+'/'+state.qualityFinalIter;if(state.refinePending)status+=' · 最終品質待機';if(state.numericRecoveryPending)status+=' · 数値穴補修待機';if(state.numericRecoveryRunning)status+=' · 数値穴補修中';if(state.qualityStage==='refining')status+=' · 精細化 '+state.qualityIter+'/'+state.qualityFinalIter;if(state.qualityStage==='final')status+=' · 最終 '+(state.fieldView?.iter??state.qualityIter);if(state.fieldView?.frontier?.policy==='automatic-finite-cap')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;$('#compactStatus').textContent=status}
if(TEST_MODE){
async function waitForTestState(predicate,label,timeout=120000){const start=performance.now();for(;;){if(state.drawState==='ERROR')throw new Error(state.gpuError||'render failed');if(predicate())return true;if(performance.now()-start>=timeout)throw new Error(label+' timeout');if(state.dirty&&!state.rendering)schedule();await new Promise(resolve=>setTimeout(resolve,20))}}
async function sha256View(view){const bytes=new Uint8Array(view.buffer,view.byteOffset,view.byteLength),digest=await crypto.subtle.digest('SHA-256',bytes),out=[];for(const b of new Uint8Array(digest))out.push(b.toString(16).padStart(2,'0'));return out.join('')}
globalThis.__MANDEL_TEST__={
efficiency:()=>({precisionOrbits:runtime.precisionOrbits||0,activeBatches:runtime.activeBatches||0,activeWorkBound:runtime.activeWorkBound||0,activeInitialized:runtime.activeInitialized||0,readbackBytes:runtime.readbackBytes||0,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,activeGpuBytes:(renderer?.frame?.deepActiveCapacity||0)*((renderer?.frame?.deepActiveStride||32)+8)}),
kernelAccess:()=>({renderer,token:state.token}),
async pngRoundTrip(){
if(!renderer?.frame||state.fieldView?.complete===false||state.rendering)throw new Error('PNG requires a completed numeric field');
const r=renderer,f=r.frame,rgba=await r.readCompleteRgba();
{
const png=new StreamingPng(f.w,f.h),rows=new Uint8Array((1+f.w*4)*f.h);
for(let y=0;y<f.h;y++)rows.set(rgba.subarray(y*f.w*4,(y+1)*f.w*4),y*(1+f.w*4)+1);
await png.rows(rows);const blob=await png.finish(),bitmap=await createImageBitmap(blob),surface=new OffscreenCanvas(f.w,f.h),ctx=surface.getContext('2d');
ctx.drawImage(bitmap,0,0);bitmap.close();const decoded=ctx.getImageData(0,0,f.w,f.h).data;
let mismatches=0;for(let i=0;i<rgba.length;i++)if(rgba[i]!==decoded[i])mismatches++;
return{width:f.w,height:f.h,bytes:blob.size,mismatches,iter:state.fieldView.iter};
}
},
async independentSamples(indices){
if(!renderer?.frame||state.rendering)throw new Error('sample verification requires a completed frame');
const f=renderer.frame,iter=state.fieldView.iter;
const solved=await precisionFallback.request(snapshot(),iter,f.w,f.h,indices);
const field=await renderer.readFieldAll(),smoothed=new Float32Array(field.smoothBits.buffer);
return Array.from(solved.indices,(index,j)=>({index,packed:field.meta[index],smoothed:smoothed[index],oracle:solved.meta[j],oracleSmooth:solved.smooth[j],accepted:solved.accepted[j]}));
},
async ensureGpuReady(){const r=renderer||await initRenderer();return{ready:!!r,navigatorGpu:!!navigator.gpu,renderer:!!renderer,gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError}},
async setView({re,im,span,bits,baseIter=350,adaptive=false,continuationBudget=0,processMode='standard',renderMode='fast',fixed=false}){const ready=await this.ensureGpuReady();if(!ready.ready)throw new Error('WebGPU initialization failed: '+JSON.stringify(ready));cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fixed?BigInt(re):fromDec(re);state.im=fixed?BigInt(im):fromDec(im);state.span=fixed?BigInt(span):fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(continuationBudget)));state.processMode=processMode;state.renderMode='fast';state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');if(!renderer?.frame||!state.fieldView)throw new Error('test render completed without a GPU frame: '+JSON.stringify({gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError,drawState:state.drawState,engine:state.lastEngine}));return{width:canvas.width,height:canvas.height,state:this.state()}},
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
async waitForPrimary({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&!!state.fieldView,'primary',timeout);return this.state()},
async waitForFinal({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&!state.refinePending&&state.qualityStage==='final'&&state.fieldView?.complete===true&&state.renderClock?.status==='complete','final',timeout);return this.state()},
async waitForNumericComplete({timeout=120000}={}){await this.waitForFinal({timeout});await waitForTestState(()=>!state.numericRecoveryPending&&!state.numericRecoveryRunning,'numeric recovery',timeout);const result=this.state();if(result.numericalFailures)throw new Error('numeric recovery quiesced with '+result.numericalFailures+' unresolved failures');return result},
async fieldHashes(){if(!renderer?.frame)throw new Error('WebGPU field unavailable');const field=await renderer.readFieldAll();return{pixels:field.meta.length,metaSha256:await sha256View(field.meta),smoothSha256:await sha256View(field.smoothBits)}},
gpuDiagnostics:()=>renderer?{adapterInfo:{...renderer.adapterInfo},adapterLimits:{...renderer.adapterLimits},compilation:renderer.compilation.map(x=>({...x})),uncapturedErrors:[...renderer.uncapturedErrors],lossReason:renderer.lossReason||'',activeGpuStage:renderer.activeGpuStage,lastCompletedGpuStage:renderer.lastCompletedGpuStage}:null,
stabilityPolicy:()=>({adaptiveIterationQuantum:ADAPTIVE_ITER_QUANTUM,pixelFrontierProduction:PIXEL_FRONTIER_PRODUCTION,pixelFrontierEnabled:PIXEL_FRONTIER_ENABLED,pixelFrontierWorkLimit:PIXEL_FRONTIER_WORK,pixelFrontierMaxIter:PIXEL_FRONTIER_MAX_ITER,pixelFrontierMinimumIter:pixelFrontierIterationFloor(snapshot(),canvas.width,maxIter()),stableHistoryReady:!!renderer?.historyReady,deepCorrectionDispatchWork:PIXEL_FRONTIER_WORK}),
state:()=>{const snap=snapshot(),se=spanMantExp(snap),reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),spanExp:se.exp,directPixelRatio:directPixelRatio(snap,canvas.width),width:canvas.width,height:canvas.height,iter:maxIter(),effectiveIter:state.fieldView?.iter??maxIter(),continuationBudget:state.continuationBudget,renderMode:state.renderMode,palette:state.palette,cycle:state.cycle,shift:state.shift,colorRevision:state.colorRevision,colorAuto:state.colorAuto,rendering:state.rendering,recoloring:state.recoloring,recolorPending:state.recolorPending,lastRender:state.lastRender,renderClock:state.renderClock?{...state.renderClock}:null,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,provisional:state.fieldView?.complete===false,qualityStage:state.qualityStage,qualityIter:state.qualityIter,qualityFinalIter:state.qualityFinalIter,previewMs:state.previewMs,refineMs:state.refineMs,refinePending:state.refinePending,temporalFill:state.temporalFill,numericRecoveryPending:state.numericRecoveryPending,numericRecoveryRunning:state.numericRecoveryRunning,historyReady:!!renderer?.historyReady,multiRefPasses:state.multiRefPasses,multiRefRecovered:state.multiRefRecovered,frontierRounds:state.frontierRounds,frontierPixels:state.frontierPixels,frontierEscaped:state.frontierEscaped,frontierIter:state.frontierIter,frontierMinimumIter:state.frontierMinimumIter,frontierConverged:state.frontierConverged,frontierMaxDispatchWork:runtime.frontierMaxDispatchWork||0,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version,gpuStage:state.gpuStage,gpuTileRows:state.gpuTileRows,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,stageCounts:{...runtime.stageCounts},stageMs:{...runtime.stageMs},generationMetrics:runtime.generationMetrics.slice(-16).map(x=>({...x}))}},
async panPixels(dx,dy){pan(dx,dy);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test pan timeout');return this.state()},
async compareCurrentToFrontierBaseline({strict=true}={}){if(!renderer?.frame)throw new Error('WebGPU frame unavailable');const snap=snapshot(),iter=state.fieldView?.iter??maxIter(),w=canvas.width,h=canvas.height,view=state.fieldView||{},deep=true,exactRequired=!!view.deep,ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch)throw new Error('frontier baseline reference guard checkpoint mismatch');const rows=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,w*iter))),baseline=new Uint32Array(w*h);let maxDispatchWork=0;for(let y=0;y<h;y+=rows){const th=Math.min(rows,h-y),part=await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:y,w,h:th,forceStrict:!!strict,correctUnknown:true});baseline.set(part,y*w);maxDispatchWork=Math.max(maxDispatchWork,w*th*iter)}const current=await renderer.readMetaAll(),disputes=[];let mismatch=0,classMismatch=0,escapeIterationMismatch=0,first=-1,escaped=0,baselineEscaped=0;for(let i=0;i<current.length;i++){const a=current[i]>>>0,b=baseline[i]>>>0,ac=(a>>>28)&3,bc=(b>>>28)&3;if(ac===FIELD_ESCAPED)escaped++;if(bc===FIELD_ESCAPED)baselineEscaped++;const sameClass=(ac===FIELD_ESCAPED)===(bc===FIELD_ESCAPED),sameIter=ac!==FIELD_ESCAPED||(a&ITER_MASK)===(b&ITER_MASK);if(!sameClass)classMismatch++;else if(!sameIter)escapeIterationMismatch++;if(!sameClass||(exactRequired&&!sameIter)){mismatch++;disputes.push(i);if(first<0)first=i}}let oracleChecked=0,oracleInconclusive=0,oracleCurrentMismatch=0,oracleBaselineMismatch=0;if(disputes.length){const solved=await precisionFallback.request(snap,iter,w,h,disputes,-1);for(let j=0;j<solved.indices.length;j++){if(!solved.accepted[j]){oracleInconclusive++;continue}oracleChecked++;const i=solved.indices[j],o=solved.meta[j]>>>0,a=current[i]>>>0,b=baseline[i]>>>0,oc=(o>>>28)&3,ac=(a>>>28)&3,bc=(b>>>28)&3,currentSame=(oc===FIELD_ESCAPED)===(ac===FIELD_ESCAPED)&&(!exactRequired||oc!==FIELD_ESCAPED||(o&ITER_MASK)===(a&ITER_MASK)),baselineSame=(oc===FIELD_ESCAPED)===(bc===FIELD_ESCAPED)&&(!exactRequired||oc!==FIELD_ESCAPED||(o&ITER_MASK)===(b&ITER_MASK));if(!currentSame)oracleCurrentMismatch++;if(!baselineSame)oracleBaselineMismatch++}}const oracleGate=oracleCurrentMismatch===0&&oracleInconclusive===0;return{pixels:current.length,mismatch,classMismatch,escapeIterationMismatch,exactRequired,first,escaped,baselineEscaped,iter,baseline:'deep-gold',oracleChecked,oracleInconclusive,oracleCurrentMismatch,oracleBaselineMismatch,oracleGate,maxDispatchWork,workLimit:PIXEL_FRONTIER_WORK,gatePassed:oracleGate&&maxDispatchWork<=PIXEL_FRONTIER_WORK}},
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w):!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
};
} // TEST_MODE
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto(false);cancelRender();refs.destroy();fastRefs.destroy();precisionFallback.destroy();for(const service of precisionFallbackPool)service.destroy();if(renderer)renderer.destroy()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}localStorage.removeItem('mandelbrot.renderMode')}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();if(!state.rendering&&!state.frameView)markDirty(false)});schedule();
})();
</script>
</body>
</html>