mandelbrot/index.html
2026-08-23 23:27:38 +09:00

1334 lines
155 KiB
HTML
Raw 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.2.8">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#050813">
<title>Mandelbrot Deep Zoom v24.2.8 Fixed96 Sparse Queue Experiment</title>
<style>
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));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,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}#palette{color:#111;background:#f4f5f8}#palette option{color:#111;background:#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}.exact-grid{display:grid;grid-template-columns:54px 1fr;gap:6px;margin-top:8px}.exact-grid input{min-width:0;width:100%;min-height:44px;border:1px solid var(--line);border-radius:8px;background:#070c19;color:var(--text);padding:6px;font:11px ui-monospace,monospace}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.diagnostics{margin-top:8px;color:var(--muted);font:10.5px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.exact-grid input{user-select:text;-webkit-user-select:text}
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}.benchmark-output{width:100%;min-height:180px;margin-top:10px;border:1px solid var(--line);border-radius:9px;background:#070c19;color:var(--text);padding:8px;font:10.5px/1.45 ui-monospace,monospace;resize:vertical}.benchmark-note{font-size:11px;line-height:1.5;color:var(--muted);margin-top:8px}
#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord">…</span></div><div class="row"><span class="muted">倍率</span> <span id="zoom">…</span> <span class="muted">表示幅</span> <span id="span">…</span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render">…</span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大">+</button><button id="zout" aria-label="中心を縮小">−</button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0.001" max="0.05" step="0.0005" value="0.008"><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>
<details><summary>詳細設定・正確な座標</summary>
<div class="line"><label for="iters">基準反復</label><input id="iters" type="range" min="100" max="2500" step="25" value="350"><output id="itersO" for="iters">350</output></div>
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> GPU境界平滑化</label></div>
<div class="exact-grid"><label for="coordReInput">実部</label><input id="coordReInput"><label for="coordImInput">虚部</label><input id="coordImInput"><label for="coordSpanInput">表示幅</label><input id="coordSpanInput"></div>
<div class="mini-actions"><button id="coordApply">座標を適用</button><button id="coordCopy">正確値をコピー</button><button id="undoView">戻す</button><button id="redoView">進む</button></div>
</details>
<details><summary>診断情報</summary><div class="diagnostics"><div id="diagEngine">engine: …</div><div id="diagNumeric">numeric: …</div><div id="diagMemory">memory: …</div></div><div class="mini-actions"><button id="backendBench" type="button">Backend比較</button><button id="sparseBench" type="button">Fixed96 Direct実験</button></div></details>
</div>
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
<div class="hint">ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。</div>
</div>
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI−</button>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="exportDialog" aria-labelledby="exportTitle">
<h2 id="exportTitle">高解像度 PNG 出力</h2>
<div class="export-grid">
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
<label for="exportWidth">幅(px)</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×(高速)</option><option value="2">2×2 AA</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="strict">保守的 (Strict)</option></select>
</div>
<progress id="exportProgress" max="1" value="0" hidden></progress>
<div id="exportStatus" role="status" aria-live="polite"></div>
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
</dialog>
<dialog id="benchmarkDialog" aria-labelledby="benchmarkTitle">
<h2 id="benchmarkTitle">標準 / DS / 深部 比較</h2>
<div class="export-grid">
<label for="benchmarkTrack">測定地点</label><select id="benchmarkTrack"><option value="seahorse">Seahorse boundary</option><option value="cusp">Period-2 cusp</option></select>
<label for="benchmarkSamples">反復測定</label><select id="benchmarkSamples"><option value="2">2回(速い)</option><option value="3" selected>3回</option><option value="5">5回(安定)</option></select>
</div>
<div class="benchmark-note">z0,2,4,6,8,10,12,14,16,20,30,40を測定します。速度は現在のview上の代表3タイルを測って全画面相当に換算し、Deep coldにはBigInt reference構築を加算します。1測定が15秒を超えた場合は停止してエラー表示します。</div>
<progress id="benchmarkProgress" max="12" value="0" hidden></progress>
<div id="benchmarkStatus" role="status" aria-live="polite"></div>
<textarea id="benchmarkOutput" class="benchmark-output" readonly placeholder="結果はここに表示されます"></textarea>
<div class="export-actions"><button id="benchmarkCancel" type="button">閉じる</button><button id="benchmarkSave" type="button" disabled>JSON保存</button><button id="benchmarkStart" class="primary" type="button">測定開始</button></div>
</dialog>
<dialog id="sparseDialog" aria-labelledby="sparseTitle">
<h2 id="sparseTitle">DS + Sparse Fixed96 Direct 実験</h2>
<div class="export-grid">
<label for="sparseTrack">測定地点</label><select id="sparseTrack"><option value="seahorse">Seahorse boundary</option><option value="cusp">Period-2 cusp</option></select>
<label for="sparseSamples">反復測定</label><select id="sparseSamples"><option value="2">2回(速い)</option><option value="3" selected>3回</option><option value="5">5回(安定)</option></select>
</div>
<div class="benchmark-note">z6 / z8 / z10 / z12で、DS Directに感度ガード |dz/dc| を追加し、危険pixelだけreference不要の96-bit符号付きQ8.88整数Directで再計算します。risk=1e11 / 1e12 / 1e13 / 1e14を全候補表示し、失敗時も correction / remain / Δclass / time を隠しません。production routerは変更しません。</div>
<progress id="sparseProgress" max="4" value="0" hidden></progress>
<div id="sparseStatus" role="status" aria-live="polite"></div>
<textarea id="sparseOutput" class="benchmark-output" readonly placeholder="結果はここに表示されます"></textarea>
<div class="export-actions"><button id="sparseCancel" type="button">閉じる</button><button id="sparseSave" type="button" disabled>JSON保存</button><button id="sparseStart" class="primary" type="button">Fixed96実験開始</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 pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Benchmark candidate: whole-frame double-single direct iteration.
// This intentionally has no CPU reference-build cost. It is not selected by
// the production router until the crossover benchmark justifies it.
const DIRECT_DS_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
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_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
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_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_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);
let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var n=0u;
loop{
if(n>=p.maxIter){break;}
z=cds_add(cds_mul(z,z),c); n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
// can be re-evaluated by the existing deep double-single correction pass.
// This shader is benchmark-only and is not selected by the production router.
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
struct HybridStats{
remaining:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<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>;
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
@group(0) @binding(4) var<storage,read_write> riskQueue:array<u32>;
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_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_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn mark_sensitive(out:u32,n:u32){
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;
let slot=atomicAdd(&hybridStats.remaining,1u);
let capacity=p.tileW*p.tileH;
if(slot<capacity){
riskQueue[slot]=out;
atomicAdd(&hybridStats.enqueued,1u);
}else{
atomicAdd(&hybridStats.overflow,1u);
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var deriv=vec2<f32>(0.0);var n=0u;
loop{
if(n>=p.maxIter){break;}
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
z=cds_add(cds_mul(z,z),c);n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
}
`;
// Converts the GPU-written risk queue length into indirect dispatch arguments.
// The correction pass therefore launches one lane per enqueued risky pixel,
// instead of one lane per screen pixel.
const DIRECT_FIXED96_QUEUE_ARGS_WGSL=COMMON+String.raw`
struct HybridStats{
remaining:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 };
@group(0) @binding(0) var<storage,read_write> hybridStats:HybridStats;
@group(0) @binding(1) var<storage,read_write> indirectArgs:IndirectArgs;
@compute @workgroup_size(1)
fn main(){
let n=atomicLoad(&hybridStats.enqueued);
atomicStore(&hybridStats.dispatchCount,n);
indirectArgs.x=(n+63u)/64u;
indirectArgs.y=1u;
indirectArgs.z=1u;
indirectArgs._pad=0u;
}
`;
// Diagnostic sparse fixed-point direct correction. It does not consume a
// deep reference. The guarded DS pass marks uncertain pixels with reason 7;
// this shader re-evaluates only those pixels using signed Q8.88 arithmetic
// implemented entirely with u32 limbs. Integer arithmetic avoids the
// reassociation/fusion sensitivity of float-expansion correction kernels.
const DIRECT_FIXED96_CORRECT_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, maxIter:u32, _flags:u32,
baseReLo:u32, baseReMid:u32, baseReHi:u32,
baseImLo:u32, baseImMid:u32, baseImHi:u32,
stepLo:u32, stepMid:u32, stepHi:u32,
_pad0:u32, _pad1:u32, _pad2:u32,
};
struct FX96{ lo:u32, mid:u32, hi:u32 };
struct FixedStats{
remaining:atomic<u32>, invalidIndex:atomic<u32>, processed:atomic<u32>, staleEntry:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
struct HybridStats{
remaining:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<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>;
@group(0) @binding(3) var<storage,read_write> fixedStats:FixedStats;
@group(0) @binding(4) var<storage,read_write> hybridStats:HybridStats;
@group(0) @binding(5) var<storage,read> riskQueue:array<u32>;
fn fx_add(a:FX96,b:FX96)->FX96{
let lo=a.lo+b.lo; let c0=select(0u,1u,lo<a.lo);
let m0=a.mid+b.mid; let c1=select(0u,1u,m0<a.mid);
let mid=m0+c0; let c2=select(0u,1u,mid<m0);
return FX96(lo,mid,a.hi+b.hi+c1+c2);
}
fn fx_neg(a:FX96)->FX96{
let lo=(~a.lo)+1u; let c0=select(0u,1u,lo==0u);
let mid=(~a.mid)+c0; let c1=select(0u,1u,c0!=0u && mid==0u);
return FX96(lo,mid,(~a.hi)+c1);
}
fn fx_sub(a:FX96,b:FX96)->FX96{return fx_add(a,fx_neg(b));}
fn fx_negative(a:FX96)->bool{return (a.hi&0x80000000u)!=0u;}
fn fx_abs(a:FX96)->FX96{if(fx_negative(a)){return fx_neg(a);}return a;}
fn mul32(a:u32,b:u32)->vec2<u32>{
let a0=a&0xffffu; let a1=a>>16u; let b0=b&0xffffu; let b1=b>>16u;
let p0=a0*b0; let p1=a0*b1; let p2=a1*b0; let p3=a1*b1;
let middle=(p0>>16u)+(p1&0xffffu)+(p2&0xffffu);
let lo=(p0&0xffffu)|(middle<<16u);
let hi=p3+(p1>>16u)+(p2>>16u)+(middle>>16u);
return vec2<u32>(lo,hi);
}
fn add_at(a:ptr<function,array<u32,6>>,idx0:u32,value0:u32){
var idx=idx0; var value=value0;
loop{
if(value==0u || idx>=6u){break;}
let old=(*a)[idx]; let sum=old+value; (*a)[idx]=sum;
value=select(0u,1u,sum<old); idx+=1u;
}
}
fn add_product(a:ptr<function,array<u32,6>>,idx:u32,x:u32,y:u32){
let q=mul32(x,y); add_at(a,idx,q.x); add_at(a,idx+1u,q.y);
}
fn fx_mul(a0:FX96,b0:FX96)->FX96{
let neg=fx_negative(a0)!=fx_negative(b0); let a=fx_abs(a0); let b=fx_abs(b0);
var w=array<u32,6>(0u,0u,0u,0u,0u,0u);
add_product(&w,0u,a.lo,b.lo); add_product(&w,1u,a.lo,b.mid); add_product(&w,2u,a.lo,b.hi);
add_product(&w,1u,a.mid,b.lo); add_product(&w,2u,a.mid,b.mid);add_product(&w,3u,a.mid,b.hi);
add_product(&w,2u,a.hi,b.lo); add_product(&w,3u,a.hi,b.mid); add_product(&w,4u,a.hi,b.hi);
// Q8.88 * Q8.88 -> Q16.176. Shift right by 88 with one-bit rounding.
var r=FX96((w[2]>>24u)|(w[3]<<8u),(w[3]>>24u)|(w[4]<<8u),(w[4]>>24u)|(w[5]<<8u));
if(((w[2]>>23u)&1u)!=0u){r=fx_add(r,FX96(1u,0u,0u));}
if(neg){return fx_neg(r);}return r;
}
fn fx_mul_small(a0:FX96,k:u32)->FX96{
let neg=fx_negative(a0); let a=fx_abs(a0);
let q0=mul32(a.lo,k); let q1=mul32(a.mid,k); let q2=mul32(a.hi,k);
let mid=q1.x+q0.y; let cm=select(0u,1u,mid<q1.x);
let carry=q1.y+cm; let hi=q2.x+carry;
let r=FX96(q0.x,mid,hi); if(neg){return fx_neg(r);}return r;
}
fn fx_cmp_unsigned(a:FX96,b:FX96)->i32{
if(a.hi<b.hi){return -1;} if(a.hi>b.hi){return 1;}
if(a.mid<b.mid){return -1;} if(a.mid>b.mid){return 1;}
if(a.lo<b.lo){return -1;} if(a.lo>b.lo){return 1;} return 0;
}
fn fx_to_f32_positive(a:FX96)->f32{
return f32(a.hi)*5.960464477539063e-8 + f32(a.mid)*1.3877787807814457e-17 + f32(a.lo)*3.2311742677852644e-27;
}
fn fx_to_f32(a:FX96)->f32{
if(fx_negative(a)){return -fx_to_f32_positive(fx_neg(a));}
return fx_to_f32_positive(a);
}
fn write_escape(out:u32,n:u32,zr:FX96,zi:FX96){
// Once either component exceeds 2 in magnitude, escape is mathematically
// certain. Use f32 only for the smooth-color estimate; membership remains
// decided by the integer fixed-point state. This avoids squaring a large
// escaped value and therefore avoids Q8.88 wraparound.
let zrf=fx_to_f32(zr); let zif=fx_to_f32(zi);
let mag=max(4.0000005,zrf*zrf+zif*zif);
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,mag);
atomicAdd(&fixedStats.corrected,1u);
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let qi=gid.x;
let queued=atomicLoad(&hybridStats.dispatchCount);
if(qi>=queued){return;}
atomicAdd(&fixedStats.processed,1u);
let out=riskQueue[qi];
let capacity=p.tileW*p.tileH;
if(out>=capacity){
atomicAdd(&fixedStats.invalidIndex,1u);
atomicAdd(&fixedStats.remaining,1u);
return;
}
let old=fieldMeta[out];
let cls=(old>>28u)&3u; let reason=(old&REASON_MASK)>>REASON_SHIFT;
if(cls!=FIELD_UNKNOWN || reason!=7u){
atomicAdd(&fixedStats.staleEntry,1u);
atomicAdd(&fixedStats.remaining,1u);
return;
}
let px=out%p.tileW; let py=out/p.tileW;
let baseRe=FX96(p.baseReLo,p.baseReMid,p.baseReHi);
let baseIm=FX96(p.baseImLo,p.baseImMid,p.baseImHi);
let step=FX96(p.stepLo,p.stepMid,p.stepHi);
let cr=fx_add(baseRe,fx_mul_small(step,px));
let ci=fx_sub(baseIm,fx_mul_small(step,py));
var zr=FX96(0u,0u,0u); var zi=FX96(0u,0u,0u); var n=0u;
let TWO=FX96(0u,0u,0x02000000u);
let FOUR=FX96(0u,0u,0x04000000u);
loop{
if(n>=p.maxIter){break;}
// The previous state is inside the bailout disk, so these products are
// bounded and cannot overflow Q8.88.
let zr2=fx_mul(zr,zr); let zi2=fx_mul(zi,zi); let zri=fx_mul(zr,zi);
let nextR=fx_add(fx_sub(zr2,zi2),cr);
let nextI=fx_add(fx_add(zri,zri),ci);
zr=nextR; zi=nextI; n+=1u;
// Do not mistake the Q8.88 integer byte for sign-extension. Values such
// as +1.2 are valid. The old range guard rejected them and left virtually
// every risky escaping pixel UNKNOWN.
if(fx_cmp_unsigned(fx_abs(zr),TWO)>0 || fx_cmp_unsigned(fx_abs(zi),TWO)>0){
write_escape(out,n,zr,zi); return;
}
let rr=fx_mul(zr,zr); let ii=fx_mul(zi,zi); let mag=fx_add(rr,ii);
if(fx_cmp_unsigned(mag,FOUR)>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,fx_to_f32_positive(mag)));
atomicAdd(&fixedStats.corrected,1u); return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
atomicAdd(&fixedStats.corrected,1u);
}
`;
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
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 operations=0u;
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
return;
}
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=1.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=refs[m];
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u; operations+=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;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);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);
}
`;
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
// perturbation. It is a visual-quality pass, not a membership certificate.
const DEEP_CORRECT_WGSL=COMMON+String.raw`
const CORRECTION_MARK:u32=128u;
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
fn count_remaining(out:u32){
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
}
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
}
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b;
let e=b-(q-a);
return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b;
let bb=q-a;
let e=(a-(q-bb))+(b-bb);
return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a;
let ah=ca-(ca-a);
let al=a-ah;
let cb=4097.0*b;
let bh=cb-(cb-b);
let bl=b-bh;
var e=ah*bh-q;
e=e+ah*bl;
e=e+al*bh;
e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h);
return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l;
e=e+a.l*b.h;
e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b);
return ds_quick(q.h,q.l+a.l*b);
}
fn ds_pow2(a:DS,e:i32)->DS{
if(e < -126){return DS(0.0,0.0);}
if(e > 126){return DS(8.507059e37,0.0);}
return DS(ldexp(a.h,e),ldexp(a.l,e));
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
loop{
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
if(m>p.refLen){count_remaining(out);return;}
let rp=refs[m];
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
let delta=cds_pow2(w,scaleExp);
let z=cds_add(r,delta);
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
return;
}
let dmag=cds_mag2(delta);
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
continue;
}
if(m>=p.refLen){count_remaining(out);return;}
let linear=cds_scale(cds_mul(r,w),2.0);
let sq=cds_pow2(cds_mul(w,w),scaleExp);
w=cds_add(cds_add(linear,sq),d);
m+=1u; n+=1u; operations+=1u;
let mm=max(cds_maxabs(w),cds_maxabs(d));
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
const COLOR_WGSL=String.raw`
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
}
fn 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; var c=base_color(i);
if(p.edgeAA!=0u){
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
let x=i32(gid.x); let y=i32(gid.y);
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
`;
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.2.8-fixed96-sparse-queue',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DIRECT_FIXED96_QUEUE_ARGS_WGSL,DIRECT_FIXED96_CORRECT_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_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 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 DIRECT_REQUIRED_RATIO=8,DIRECT_DEFAULT_RATIO=128,DEEP_SPEED_MARGIN=.85,DEEP_PROBE_UNKNOWN_MAX=.005,NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,FIXED96_INDIRECT_BYTES=16,FIXED96_WORKGROUP_SIZE=64,CORRECTION_MARK=128;
const BACKEND_PROBE_SPEED_W=224,BACKEND_PROBE_SPEED_H=160,BACKEND_PROBE_QUALITY_W=72,BACKEND_PROBE_QUALITY_H=54;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,backendProbes:0};
const backendProfiles=new Map(),backendViewProbes=new Map(),backendProbePending=new Map();
// ── 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}
const FIXED96_FRAC_BITS=88,FIXED96_WIDTH_BITS=96n,FIXED96_MOD=1n<<FIXED96_WIDTH_BITS,FIXED96_MASK32=0xffffffffn,FIXED96_MIN=-(1n<<95n),FIXED96_MAX=(1n<<95n)-1n;
function q88FromRational(numer,denom,bits){let n=BigInt(numer),d=BigInt(denom),sh=FIXED96_FRAC_BITS-bits;if(sh>=0)n<<=BigInt(sh);else d<<=BigInt(-sh);return roundDivSigned(n,d)}
function q88Words(v){let x=BigInt(v);if(x<FIXED96_MIN||x>FIXED96_MAX)throw new Error('Fixed96 Q8.88 coordinate overflow');if(x<0)x+=FIXED96_MOD;return[Number(x&FIXED96_MASK32),Number((x>>32n)&FIXED96_MASK32),Number((x>>64n)&FIXED96_MASK32)]}
function fixed96TileData(snap,fullW,fullH,tileX,tileY,sampleX=.5,sampleY=.5){const sx4=Math.round(sampleX*4),sy4=Math.round(sampleY*4);if(Math.abs(sampleX*4-sx4)>1e-6||Math.abs(sampleY*4-sy4)>1e-6)throw new Error('fixed96 experiment supports quarter-pixel sample offsets only');const den=4n*BigInt(Math.max(1,fullW)),ox4=4n*BigInt(tileX)+BigInt(sx4)-2n*BigInt(fullW),oy4=2n*BigInt(fullH)-4n*BigInt(tileY)-BigInt(sy4),reNum=snap.re*den+snap.span*ox4,imNum=snap.im*den+snap.span*oy4,baseRe=q88FromRational(reNum,den,snap.bits),baseIm=q88FromRational(imNum,den,snap.bits),step=q88FromRational(snap.span,BigInt(Math.max(1,fullW)),snap.bits);if(step===0n&&snap.span!==0n)throw new Error('Fixed96 Q8.88 pixel step underflow');return{baseRe:q88Words(baseRe),baseIm:q88Words(baseIm),step:q88Words(step)}}
function fixed96QueueIntegrity(guard,fixed,args,capacity){const selected=guard[0]||0,overflow=guard[1]||0,enqueued=guard[2]||0,dispatchCount=guard[3]||0,remaining=fixed[0]||0,invalidIndex=fixed[1]||0,processed=fixed[2]||0,staleEntry=fixed[3]||0,corrected=fixed[7]||0,workgroups=args[0]||0,expectedWorkgroups=Math.ceil(enqueued/FIXED96_WORKGROUP_SIZE),issues=[];if(selected>capacity)issues.push('selected>capacity');if(overflow!==0)issues.push('queue-overflow');if(enqueued!==selected)issues.push('enqueued!=selected');if(dispatchCount!==enqueued)issues.push('dispatch-count!=enqueued');if(workgroups!==expectedWorkgroups||(args[1]||0)!==1||(args[2]||0)!==1)issues.push('indirect-args');if(processed!==enqueued)issues.push('processed!=enqueued');if(invalidIndex!==0)issues.push('invalid-index');if(staleEntry!==0)issues.push('stale-entry');if(corrected+remaining!==processed)issues.push('corrected+remaining!=processed');return{ok:issues.length===0,issues,selected,overflow,enqueued,dispatchCount,workgroups,expectedWorkgroups,processed,remaining,invalidIndex,staleEntry,corrected,capacity}}
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.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
function directMetrics(snap=snapshot(),width=Math.max(1,canvas.width)){const spanLog=log2FixedAt(snap.span,snap.bits),stepLog=spanLog-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp))),eligible=Number.isFinite(ratio)&&ratio>=DIRECT_REQUIRED_RATIO&&spanLog>-145;return{spanLog,stepLog,ratio,eligible,overlap:eligible&&ratio<DIRECT_DEFAULT_RATIO}}
function zoomExpFor(snap){return Math.max(0,Math.log10(3.4)-log2FixedAt(snap.span,snap.bits)/Math.log2(10))}
function backendProfileKey(snap,iter){const z=Math.floor(zoomExpFor(snap)*2)/2,ib=iter<600?0:iter<1500?1:iter<5000?2:3;return z.toFixed(1)+':'+ib}
function backendViewKey(snap,iter,width,height){return[snap.bits,snap.re,snap.im,snap.span,iter,width,height].join(':')}
function chooseBackend(snap=snapshot(),width=Math.max(1,canvas.width),iter=maxIter(),height=Math.max(1,canvas.height)){const m=directMetrics(snap,width),key=backendProfileKey(snap,iter),viewKey=backendViewKey(snap,iter,width,height),speed=backendProfiles.get(key),viewProbe=backendViewProbes.get(viewKey),pixels=Math.max(1,width*Math.max(1,height)),profile=speed?{...speed,predictedDirectMs:speed.directMsPerPixel*pixels,predictedDeepMs:speed.referenceBuildMs+speed.deepMsPerPixel*pixels}:null;let backend='deep',reason='direct-coordinate-limit';if(m.eligible){if(m.ratio>=DIRECT_DEFAULT_RATIO){backend='direct';reason='direct-wide-margin'}else if(!viewProbe){backend='probe';reason='overlap-view-probe-required'}else if(!viewProbe.qualityPass){backend='deep';reason='direct-quality-rejected'}else if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*DEEP_SPEED_MARGIN){backend='deep';reason='deep-view-probe-faster'}else{backend='direct';reason='direct-view-probe-preferred'}}return{backend,deep:backend==='deep',probe:backend==='probe',reason,key,viewKey,metrics:m,profile,viewProbe:viewProbe||null}}
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width),height=Math.max(1,canvas.height)){const d=chooseBackend(snap,width,maxIter(),height);return d.deep||d.probe}
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}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
// ── iteration / quality policy ───────────────────────────────────────────
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
'use strict';
const MAX_REF=150001,MAX_LEVELS=20;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),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,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class ReferenceService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter,fresh=false){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.2.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
}
const refs=new ReferenceService();
// ── 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.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,sdm,shm,qm,fm,xm,zm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('direct-ds-benchmark',G.DIRECT_DS_WGSL),this.module('direct-ds-guarded-experiment',G.DIRECT_DS_GUARDED_WGSL),this.module('direct-fixed96-queue-args-experiment',G.DIRECT_FIXED96_QUEUE_ARGS_WGSL),this.module('direct-fixed96-correction-experiment',G.DIRECT_FIXED96_CORRECT_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
this.dsDirect=this.device.createComputePipeline({layout:'auto',compute:{module:sdm,entryPoint:'main'}});
this.dsGuarded=this.device.createComputePipeline({layout:'auto',compute:{module:shm,entryPoint:'main'}});
this.fixed96QueueArgs=this.device.createComputePipeline({layout:'auto',compute:{module:qm,entryPoint:'main'}});
this.fixed96Correct=this.device.createComputePipeline({layout:'auto',compute:{module:fm,entryPoint:'main'}});
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}});
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
webgpuCanvasClaimed=true;this.configure();
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
ensureFrame(w,h){
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
}
ensureExportWorkspace(){
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
}
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5){const cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),crh=Math.fround(cr),cih=Math.fround(ci),sph=Math.fround(sp),invW=1/Math.max(1,fullW),invh=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,crh,true);d.setFloat32(36,Math.fround(cr-crh),true);d.setFloat32(40,cih,true);d.setFloat32(44,Math.fround(ci-cih),true);d.setFloat32(48,sph,true);d.setFloat32(52,Math.fround(sp-sph),true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,invh,true);d.setFloat32(68,Math.fround(invW-invh),true)})}
dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sx=.5,sy=.5){const a=this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx,sy),d=new DataView(a);d.setFloat32(72,Math.fround(riskLimit),true);return a}
fixed96Params(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5){const q=fixed96TileData(snap,fullW,fullH,tileX,tileY,sx,sy);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,iter,0,...q.baseRe,...q.baseIm,...q.step,0,0,0].forEach((v,i)=>d.setUint32(i*4,v>>>0,true))})}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDsDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsDirect.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsDirect);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDsGuardedNumeric(encoder,{pbuf,meta,smooth,stats,queue,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsGuarded.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:stats}},{binding:4,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsGuarded);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeFixed96QueueArgs(encoder,{guardStats,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.fixed96QueueArgs.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:guardStats}},{binding:1,resource:{buffer:indirect}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fixed96QueueArgs);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end()}
encodeFixed96CorrectionNumeric(encoder,{pbuf,meta,smooth,stats,guardStats,queue,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.fixed96Correct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:stats}},{binding:4,resource:{buffer:guardStats}},{binding:5,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fixed96Correct);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()}
encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
}
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
async readUnresolved(){return(await this.readUnresolvedStats()).total}
async correctUnknownFrame(snap,iter,token){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,f.w*.5,f.h*.5,1,f.w,0));const e=d.createCommandEncoder({label:'sparse-deep-correction'}),bg=d.createBindGroup({layout:this.correct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),p=e.beginComputePass();p.setPipeline(this.correct);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;runtime.correctionPasses++;return true}
async benchmarkDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap));const e=d.createCommandEncoder({label:'ds-direct-benchmark'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;[meta,fieldSmooth,pbuf].forEach(destroy);return{gpuMs}}
async renderTileMetaDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,n*4,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const e=d.createCommandEncoder({label:'ds-direct-meta'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});e.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,fieldSmooth,pbuf,read].forEach(destroy);return out}
async benchmarkFixed96Direct({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13}){
await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,
meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
guardStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),correctStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
riskQueue=buf(d,n*4,B.STORAGE|B.COPY_DST,'fixed96-risk-queue'),indirect=buf(d,FIXED96_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'fixed96-indirect'),
guardPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),correctPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
read=buf(d,2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES,B.COPY_DST|B.MAP_READ);
d.queue.writeBuffer(guardStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(correctStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(indirect,0,new Uint32Array(FIXED96_INDIRECT_BYTES/4));
d.queue.writeBuffer(guardPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit));d.queue.writeBuffer(correctPbuf,0,this.fixed96Params(w,h,fullW,fullH,tileX,tileY,iter,snap));
const e=d.createCommandEncoder({label:'ds-fixed96-queued-benchmark'});this.encodeDsGuardedNumeric(e,{pbuf:guardPbuf,meta,smooth:fieldSmooth,stats:guardStats,queue:riskQueue,w,h});this.encodeFixed96QueueArgs(e,{guardStats,indirect});this.encodeFixed96CorrectionNumeric(e,{pbuf:correctPbuf,meta,smooth:fieldSmooth,stats:correctStats,guardStats,queue:riskQueue,indirect});
e.copyBufferToBuffer(guardStats,0,read,0,UNRESOLVED_BYTES);e.copyBufferToBuffer(correctStats,0,read,UNRESOLVED_BYTES,UNRESOLVED_BYTES);e.copyBufferToBuffer(indirect,0,read,2*UNRESOLVED_BYTES,FIXED96_INDIRECT_BYTES);
const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;
await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),a=new Uint32Array(raw.slice(0,UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(UNRESOLVED_BYTES,2*UNRESOLVED_BYTES)),args=new Uint32Array(raw.slice(2*UNRESOLVED_BYTES,2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES));read.unmap();
const integrity=fixed96QueueIntegrity(a,b,args,n);[meta,fieldSmooth,guardStats,correctStats,riskQueue,indirect,guardPbuf,correctPbuf,read].forEach(destroy);if(!integrity.ok)throw new Error('Fixed96 queue integrity failure: '+integrity.issues.join(', '));return{gpuMs,uncertain:integrity.selected,remaining:integrity.remaining,corrected:integrity.corrected,queue:integrity};
}
async renderTileMetaFixed96Direct({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13,sampleX=.5,sampleY=.5}){
await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,
meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
guardStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),correctStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
riskQueue=buf(d,n*4,B.STORAGE|B.COPY_DST,'fixed96-risk-queue-meta'),indirect=buf(d,FIXED96_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'fixed96-indirect-meta'),
guardPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),correctPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
read=buf(d,n*4+2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES,B.COPY_DST|B.MAP_READ);
d.queue.writeBuffer(guardStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(correctStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(indirect,0,new Uint32Array(FIXED96_INDIRECT_BYTES/4));
d.queue.writeBuffer(guardPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sampleX,sampleY));d.queue.writeBuffer(correctPbuf,0,this.fixed96Params(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));
const e=d.createCommandEncoder({label:'ds-fixed96-queued-meta'});this.encodeDsGuardedNumeric(e,{pbuf:guardPbuf,meta,smooth:fieldSmooth,stats:guardStats,queue:riskQueue,w,h});this.encodeFixed96QueueArgs(e,{guardStats,indirect});this.encodeFixed96CorrectionNumeric(e,{pbuf:correctPbuf,meta,smooth:fieldSmooth,stats:correctStats,guardStats,queue:riskQueue,indirect});
e.copyBufferToBuffer(meta,0,read,0,n*4);e.copyBufferToBuffer(guardStats,0,read,n*4,UNRESOLVED_BYTES);e.copyBufferToBuffer(correctStats,0,read,n*4+UNRESOLVED_BYTES,UNRESOLVED_BYTES);e.copyBufferToBuffer(indirect,0,read,n*4+2*UNRESOLVED_BYTES,FIXED96_INDIRECT_BYTES);
d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),a=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(n*4+UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES)),args=new Uint32Array(raw.slice(n*4+2*UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES));read.unmap();
const integrity=fixed96QueueIntegrity(a,b,args,n);[meta,fieldSmooth,guardStats,correctStats,riskQueue,indirect,guardPbuf,correctPbuf,read].forEach(destroy);if(!integrity.ok)throw new Error('Fixed96 queue integrity failure: '+integrity.issues.join(', '));out.uncertain=integrity.selected;out.remaining=integrity.remaining;out.corrected=integrity.corrected;out.queue=integrity;return out;
}
async benchmarkNumeric({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,forceStrict=false,correctUnknown=false}){await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|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),read=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const e=d.createCommandEncoder({label:'backend-benchmark'});if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,forceStrict?1:0));this.encodeDeepNumeric(e,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){e.clearBuffer(unresolved);this.encodeCorrectionNumeric(e,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap));this.encodeDirectNumeric(e,{pbuf,meta,smooth,w,h})}e.copyBufferToBuffer(unresolved,0,read,0,UNRESOLVED_BYTES);const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;await read.mapAsync(GPUMapMode.READ);const a=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return{gpuMs,unknown:a[0]||0,corrected:a[7]||0}}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
await this.ready;if(deep)this.setDeepContext(deepContext);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){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
}
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(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),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,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);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){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(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()}
}
function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true}
function backendQualityTiles(fullW,fullH){const w=Math.min(BACKEND_PROBE_QUALITY_W,fullW),h=Math.min(BACKEND_PROBE_QUALITY_H,fullH),x1=Math.max(0,fullW-w),y1=Math.max(0,fullH-h),xc=Math.max(0,Math.floor((fullW-w)/2)),yc=Math.max(0,Math.floor((fullH-h)/2));return[[0,0],[x1,0],[0,y1],[x1,y1],[xc,yc]].map(([tileX,tileY])=>({tileX,tileY,w,h}))}
async function performBackendProbe(snap,iter,decision,token){if(!renderer||!decision.metrics.overlap||token!==state.token)return null;const fullW=canvas.width,fullH=canvas.height,sw=Math.min(BACKEND_PROBE_SPEED_W,fullW),sh=Math.min(BACKEND_PROBE_SPEED_H,fullH),sx=Math.max(0,Math.floor((fullW-sw)/2)),sy=Math.max(0,Math.floor((fullH-sh)/2)),speedPixels=sw*sh,fullPixels=Math.max(1,fullW*fullH),direct=await renderer.benchmarkNumeric({snap,iter,deep:false,deepContext:null,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh});if(token!==state.token)return null;const rb=performance.now(),ctx=await refs.request(snap,iter),buildWall=performance.now()-rb;if(token!==state.token||ctx.checkpointMismatch)return null;const deepProbe=await renderer.benchmarkNumeric({snap,iter,deep:true,deepContext:ctx,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh,correctUnknown:true});if(token!==state.token)return null;let classDisagreement=0,deepUnknown=0,knownCompared=0,qualityPixels=0,corrected=0;for(const q of backendQualityTiles(fullW,fullH)){const directMeta=await renderer.renderTileMeta({snap,iter,deep:false,deepContext:null,fullW,fullH,...q});if(token!==state.token)return null;const deepMeta=await renderer.renderTileMeta({snap,iter,deep:true,deepContext:ctx,fullW,fullH,...q,correctUnknown:true});if(token!==state.token)return null;qualityPixels+=q.w*q.h;corrected+=deepMeta.corrected||0;for(let i=0;i<deepMeta.length;i++){const dc=(deepMeta[i]>>>28)&3;if(dc===FIELD_UNKNOWN){deepUnknown++;continue}const rc=(directMeta[i]>>>28)&3;knownCompared++;if((dc===FIELD_ESCAPED)!==(rc===FIELD_ESCAPED))classDisagreement++}}const buildMs=ctx.buildMs||buildWall,directMsPerPixel=direct.gpuMs/Math.max(1,speedPixels),deepMsPerPixel=deepProbe.gpuMs/Math.max(1,speedPixels),sample={directMsPerPixel,deepMsPerPixel,referenceBuildMs:buildMs},prev=backendProfiles.get(decision.key),a=prev?.samples?0.3:1,profile={samples:(prev?.samples||0)+1,directMsPerPixel:(prev?.directMsPerPixel||sample.directMsPerPixel)*(1-a)+sample.directMsPerPixel*a,deepMsPerPixel:(prev?.deepMsPerPixel||sample.deepMsPerPixel)*(1-a)+sample.deepMsPerPixel*a,referenceBuildMs:(prev?.referenceBuildMs||sample.referenceBuildMs)*(1-a)+sample.referenceBuildMs*a};backendProfiles.set(decision.key,profile);const deepUnknownRate=deepUnknown/Math.max(1,qualityPixels),qualityPass=classDisagreement===0&&deepUnknownRate<=DEEP_PROBE_UNKNOWN_MAX,viewProbe={qualityPass,deepUnknownRate,classDisagreement,knownCompared,qualityPixels,corrected,predictedDirectMs:directMsPerPixel*fullPixels,predictedDeepMs:buildMs+deepMsPerPixel*fullPixels,ratio:decision.metrics.ratio,zoom:zoomExpFor(snap),iter,speedProbe:{w:sw,h:sh,tileX:sx,tileY:sy},qualityTiles:backendQualityTiles(fullW,fullH)};backendViewProbes.set(decision.viewKey,viewProbe);runtime.backendProbes++;return viewProbe}
function ensureBackendProbe(snap,iter,decision,token){if(!decision.metrics.overlap)return Promise.resolve(null);const hit=backendViewProbes.get(decision.viewKey);if(hit)return Promise.resolve(hit);const pending=backendProbePending.get(decision.viewKey);if(pending)return pending;const task=performBackendProbe(snap,iter,decision,token).catch(e=>{if(token===state.token)console.warn('backend probe failed',e);return null}).finally(()=>backendProbePending.delete(decision.viewKey));backendProbePending.set(decision.viewKey,task);return task}
// ── backend crossover benchmark (diagnostic only) ───────────────────────
const CROSSOVER_TRACKS={
seahorse:{id:'seahorse',label:'Seahorse boundary',re:'-0.7453983606667815',im:'0.1125046349959942'},
cusp:{id:'cusp',label:'Period-2 cusp',re:'-0.75',im:'0'}
};
const CROSSOVER_DEPTHS=[0,2,4,6,8,10,12,14,16,20,30,40];
const benchmarkJob={active:false,cancelled:false,last:null};
function fixedAtDecimal(text,bits){let s=String(text).trim(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const parts=s.toLowerCase().split('e'),mant=parts[0],exp=parts[1]?parseInt(parts[1],10):0,ab=mant.split('.'),ii=ab[0]||'0',ff=ab[1]||'';let digits=(ii+ff).replace(/^0+(?=\d)/,'')||'0',places=ff.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),scale=1n<<BigInt(bits),v=(BigInt(digits)*scale+den/2n)/den;return neg?-v:v}
function crossoverSnap(track,z){const span='3.4e-'+z,bits=Math.max(320,decimalRequiredBits(track.re)+64,decimalRequiredBits(track.im)+64,decimalRequiredBits(span)+96);return{bits,re:fixedAtDecimal(track.re,bits),im:fixedAtDecimal(track.im,bits),span:fixedAtDecimal(span,bits)}}
function iterForZoom(z,base=350){return Math.max(base,base+Math.floor(70*Math.sqrt(Math.max(0,z))+15*Math.max(0,z)))}
function median(values){const a=[...values].sort((x,y)=>x-y),n=a.length;return n?a.length%2?a[(n-1)>>1]:(a[n/2-1]+a[n/2])/2:NaN}
function compareMeta(reference,candidate){let referenceUnknown=0,candidateUnknown=0,classMismatch=0,escapedCompared=0,iterMismatch=0,maxIterDelta=0,sumIterDelta=0;const n=Math.min(reference.length,candidate.length);for(let i=0;i<n;i++){const rm=reference[i]>>>0,cm=candidate[i]>>>0,rc=(rm>>>28)&3,cc=(cm>>>28)&3;if(rc===FIELD_UNKNOWN){referenceUnknown++;continue}if(cc===FIELD_UNKNOWN){candidateUnknown++;continue}const re=rc===FIELD_ESCAPED,ce=cc===FIELD_ESCAPED;if(re!==ce){classMismatch++;continue}if(re){escapedCompared++;const d=Math.abs((rm&ITER_MASK)-(cm&ITER_MASK));if(d){iterMismatch++;sumIterDelta+=d;if(d>maxIterDelta)maxIterDelta=d}}}return{pixels:n,referenceUnknown,candidateUnknown,classMismatch,classMismatchRate:classMismatch/Math.max(1,n-referenceUnknown),escapedCompared,escapeIterationMismatch:iterMismatch,escapeIterationMismatchRate:iterMismatch/Math.max(1,escapedCompared),meanEscapeIterationDelta:iterMismatch?sumIterDelta/iterMismatch:0,maxEscapeIterationDelta:maxIterDelta}}
function crossoverQualityTiles(fullW,fullH,w,h){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[0,0],[Math.max(0,fullW-w),0],[0,Math.max(0,fullH-h)],[Math.max(0,fullW-w),Math.max(0,fullH-h)],[Math.max(0,Math.floor((fullW-w)/2)),Math.max(0,Math.floor((fullH-h)/2))]];return pts.map(([tileX,tileY])=>({tileX,tileY,w,h}))}
function aggregateQuality(list){const out={pixels:0,referenceUnknown:0,candidateUnknown:0,classMismatch:0,escapedCompared:0,escapeIterationMismatch:0,sumEscapeIterationDelta:0,maxEscapeIterationDelta:0};for(const q of list){out.pixels+=q.pixels;out.referenceUnknown+=q.referenceUnknown;out.candidateUnknown+=q.candidateUnknown;out.classMismatch+=q.classMismatch;out.escapedCompared+=q.escapedCompared;out.escapeIterationMismatch+=q.escapeIterationMismatch;out.sumEscapeIterationDelta+=q.meanEscapeIterationDelta*q.escapeIterationMismatch;out.maxEscapeIterationDelta=Math.max(out.maxEscapeIterationDelta,q.maxEscapeIterationDelta)}out.classMismatchRate=out.classMismatch/Math.max(1,out.pixels-out.referenceUnknown);out.escapeIterationMismatchRate=out.escapeIterationMismatch/Math.max(1,out.escapedCompared);out.meanEscapeIterationDelta=out.escapeIterationMismatch?out.sumEscapeIterationDelta/out.escapeIterationMismatch:0;delete out.sumEscapeIterationDelta;return out}
const CROSSOVER_SPEED_W=256,CROSSOVER_SPEED_H=144,CROSSOVER_GPU_TIMEOUT_MS=15000,CROSSOVER_REF_TIMEOUT_MS=15000;
function crossoverSpeedTiles(fullW,fullH,w=CROSSOVER_SPEED_W,h=CROSSOVER_SPEED_H){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[.5,.5],[.28,.36],[.72,.64]],seen=new Set(),out=[];for(const [fx,fy] of pts){const tileX=Math.max(0,Math.min(fullW-w,Math.round(fullW*fx-w/2))),tileY=Math.max(0,Math.min(fullH-h,Math.round(fullH*fy-h/2))),k=tileX+':'+tileY;if(!seen.has(k)){seen.add(k);out.push({tileX,tileY,w,h})}}return out}
function benchmarkTimeout(promise,ms,label,onTimeout){let timer;const timeout=new Promise((_,reject)=>{timer=setTimeout(()=>{try{onTimeout?.()}catch{}reject(new Error(label+' timed out after '+Math.round(ms/1000)+'s'))},ms)});return Promise.race([promise,timeout]).finally(()=>clearTimeout(timer))}
async function benchmarkTileBatch(fn,tiles){let gpuMs=0,pixels=0;for(const q of tiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h}return{gpuMs,pixels}}
async function timedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[];for(let i=0;i<samples;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'sample',index:i+1,total:samples});values.push((await fn()).gpuMs)}return{medianMs:median(values),samplesMs:values}}
function extrapolateTiming(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels),fullMedianMs=msPerPixel*fullPixels;return{...timing,speedPixels,msPerPixel,fullMedianMs}}
async function runCrossoverBenchmark({track='seahorse',depths=CROSSOVER_DEPTHS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
if(benchmarkJob.active)throw new Error('benchmark already running');benchmarkJob.active=true;benchmarkJob.cancelled=false;
try{
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown benchmark track');
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
for(let di=0;di<depths.length;di++){
if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z),metrics=directMetrics(snap,fullW);
onProgress?.({index:di,total:depths.length,z,phase:'reference'});
const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'z'+z+' reference',()=>refs.cancelPending('benchmark reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
const timed=(name,fn)=>timedMedian(()=>gpuCall(benchmarkTileBatch(fn,speedTiles),'z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
const tileCommon=q=>({snap,iter,fullW,fullH,...q});
onProgress?.({index:di,total:depths.length,z,phase:'f32'});
const f32=extrapolateTiming(await timed('f32',q=>r.benchmarkNumeric({...tileCommon(q),deep:false,deepContext:null})),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'ds'});
const ds=extrapolateTiming(await timed('ds',q=>r.benchmarkDsDirect(tileCommon(q))),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'deep'});
const deep=extrapolateTiming(await timed('deep',q=>r.benchmarkNumeric({...tileCommon(q),deep:true,deepContext:ctx,correctUnknown:true})),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'quality'});
const f32Parts=[],dsParts=[],qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH);let deepUnknownCount=0,qualityPixels=0,deepCorrected=0;
for(const q of qTiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const qCommon={snap,iter,fullW,fullH,...q},deepMeta=await gpuCall(r.renderTileMeta({...qCommon,deep:true,deepContext:ctx,correctUnknown:true}),'z'+z+' quality deep'),f32Meta=await gpuCall(r.renderTileMeta({...qCommon,deep:false,deepContext:null}),'z'+z+' quality f32'),dsMeta=await gpuCall(r.renderTileMetaDsDirect(qCommon),'z'+z+' quality ds');qualityPixels+=q.w*q.h;deepUnknownCount+=deepMeta.unresolved||0;deepCorrected+=deepMeta.corrected||0;f32Parts.push(compareMeta(deepMeta,f32Meta));dsParts.push(compareMeta(deepMeta,dsMeta))}
const f32Quality=aggregateQuality(f32Parts),dsQuality=aggregateQuality(dsParts),deepUnknown=deepUnknownCount/Math.max(1,qualityPixels),referenceBuildMs=ctx.buildMs||referenceWallMs;
const row={track:tr.id,z,span:'3.4e-'+z,iter,resolution:[fullW,fullH],speedTiles,speedPixels,qualityTile:[qualityW,qualityH],qualityTiles:qTiles,directRatio:metrics.ratio,
f32:{...f32,quality:f32Quality},ds:{...ds,quality:dsQuality},deep:{...deep,referenceBuildMs,referenceWallMs,coldMedianMs:referenceBuildMs+deep.fullMedianMs,warmMedianMs:deep.fullMedianMs,unknownRate:deepUnknown,corrected:deepCorrected}};
const qualityLimit=.01;
row.assessment={f32VisuallySafe:f32Quality.classMismatchRate<=qualityLimit&&metrics.ratio>=1,dsVisuallySafe:dsQuality.classMismatchRate<=qualityLimit,deepVisuallySafe:deepUnknown<=.001,
fastestWarm:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.warmMedianMs]].sort((a,b)=>a[1]-b[1])[0][0],
fastestCold:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.coldMedianMs]].sort((a,b)=>a[1]-b[1])[0][0]};
rows.push(row);onProgress?.({index:di+1,total:depths.length,z,phase:'done',row});
}
const result={format:'mandelbrot-webgpu-crossover-v2',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Speed timing uses three bounded representative tiles and extrapolates ms/pixel to the current full resolution, preventing long full-frame Deep benchmark dispatches. Deep cold adds verified BigInt reference construction. Quality remains measured on five spatial tiles. Any single benchmark GPU batch or reference build exceeding 15 s aborts with an explicit error instead of waiting indefinitely.'};
benchmarkJob.last=result;return result;
}finally{benchmarkJob.active=false}
}
// ── DS + sparse integer fixed-point direct experiment (diagnostic only) ──
const FIXED96_DEPTHS=[6,8,10,12],FIXED96_RISK_LIMITS=[1e11,1e12,1e13,1e14];
const fixed96Job={active:false,cancelled:false,last:null};
async function fixed96TileBatch(fn,tiles){let gpuMs=0,pixels=0,uncertain=0,remaining=0,corrected=0,enqueued=0,processed=0,workgroups=0;for(const q of tiles){if(fixed96Job.cancelled)throw new Error('fixed96 experiment cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h;uncertain+=r.uncertain||0;remaining+=r.remaining||0;corrected+=r.corrected||0;if(r.queue){enqueued+=r.queue.enqueued||0;processed+=r.queue.processed||0;workgroups+=r.queue.workgroups||0}}return{gpuMs,pixels,uncertain,remaining,corrected,enqueued,processed,workgroups}}
async function fixed96TimedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(fixed96Job.cancelled)throw new Error('fixed96 experiment cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[],stats=[];for(let i=0;i<samples;i++){if(fixed96Job.cancelled)throw new Error('fixed96 experiment cancelled');onStep?.({kind:'sample',index:i+1,total:samples});const r=await fn();values.push(r.gpuMs);stats.push(r)}return{medianMs:median(values),samplesMs:values,sampleStats:stats}}
function fixed96Extrapolate(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels);return{...timing,speedPixels,msPerPixel,fullMedianMs:msPerPixel*fullPixels}}
async function runFixed96Experiment({track='seahorse',depths=FIXED96_DEPTHS,riskLimits=FIXED96_RISK_LIMITS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
if(fixed96Job.active||benchmarkJob.active)throw new Error('another benchmark is running');fixed96Job.active=true;fixed96Job.cancelled=false;
try{
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown experiment track');
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
for(let di=0;di<depths.length;di++){
if(fixed96Job.cancelled)throw new Error('fixed96 experiment cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z),common=q=>({snap,iter,fullW,fullH,...q});
const timed=(name,fn)=>fixed96TimedMedian(()=>gpuCall(fixed96TileBatch(fn,speedTiles),'fixed96 z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
onProgress?.({index:di,total:depths.length,z,phase:'DS baseline'});const ds=fixed96Extrapolate(await timed('DS baseline',async q=>r.benchmarkDsDirect(common(q))),speedPixels,fullPixels);
const variants=[];
for(const riskLimit of riskLimits){onProgress?.({index:di,total:depths.length,z,phase:'Fixed96 '+riskLimit.toExponential(0)});const timing=fixed96Extrapolate(await timed('Fixed96 '+riskLimit.toExponential(0),q=>r.benchmarkFixed96Direct({...common(q),riskLimit})),speedPixels,fullPixels),speedQueueRate=median(timing.sampleStats.map(x=>(x.enqueued||0)/Math.max(1,x.pixels||speedPixels))),speedProcessedRate=median(timing.sampleStats.map(x=>(x.processed||0)/Math.max(1,x.pixels||speedPixels)));variants.push({riskLimit,...timing,speedQueueRate,speedProcessedRate});}
onProgress?.({index:di,total:depths.length,z,phase:'Deep reference'});const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'fixed96 z'+z+' reference',()=>refs.cancelPending('fixed96 reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
const deep=fixed96Extrapolate(await timed('Full Deep',q=>r.benchmarkNumeric({...common(q),deep:true,deepContext:ctx,correctUnknown:true})),speedPixels,fullPixels),referenceBuildMs=ctx.buildMs||referenceWallMs,deepCold=referenceBuildMs+deep.fullMedianMs;
const qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH),deepMetas=[];let deepUnknown=0;onProgress?.({index:di,total:depths.length,z,phase:'quality reference'});
for(const q of qTiles){if(fixed96Job.cancelled)throw new Error('fixed96 experiment cancelled');const m=await gpuCall(r.renderTileMeta({...common(q),deep:true,deepContext:ctx,correctUnknown:true}),'fixed96 z'+z+' quality deep');deepUnknown+=m.unresolved||0;deepMetas.push(m)}
for(const v of variants){const parts=[];let uncertain=0,remaining=0,corrected=0,enqueued=0,processed=0,workgroups=0,qualityPixels=0;for(let qi=0;qi<qTiles.length;qi++){const q=qTiles[qi],fm=await gpuCall(r.renderTileMetaFixed96Direct({...common(q),riskLimit:v.riskLimit}),'fixed96 z'+z+' quality '+v.riskLimit);qualityPixels+=q.w*q.h;uncertain+=fm.uncertain||0;remaining+=fm.remaining||0;corrected+=fm.corrected||0;enqueued+=fm.queue?.enqueued||0;processed+=fm.queue?.processed||0;workgroups+=fm.queue?.workgroups||0;parts.push(compareMeta(deepMetas[qi],fm))}const quality=aggregateQuality(parts),uncertainRate=uncertain/Math.max(1,qualityPixels),remainingRate=remaining/Math.max(1,qualityPixels),dispatchRate=processed/Math.max(1,qualityPixels),samplingDelta=Math.abs((v.speedQueueRate||0)-uncertainRate),timingRepresentative=samplingDelta<=Math.max(.03,uncertainRate*.5),safe=quality.classMismatch===0&&remaining===0,failureReason=quality.classMismatch!==0?'class-mismatch':remaining!==0?'remaining-unknown':!timingRepresentative?'sampling-mismatch':null;Object.assign(v,{quality,qualityPixels,uncertain,uncertainRate,remaining,remainingRate,corrected,enqueued,processed,workgroups,dispatchRate,queueIntegrity:true,samplingDelta,timingRepresentative,safe,failureReason,speedupVsDeepCold:deepCold/Math.max(1e-9,v.fullMedianMs)})}
const viable=variants.filter(v=>v.safe&&v.timingRepresentative).sort((a,b)=>a.fullMedianMs-b.fullMedianMs),best=viable[0]||null,recommendation=best&&best.fullMedianMs<deepCold*.9&&best.uncertainRate<.75?'fixed96-candidate':'full-deep';
rows.push({track:tr.id,z,iter,resolution:[fullW,fullH],ds,deep:{...deep,referenceBuildMs,coldMedianMs:deepCold,unknownRate:deepUnknown/Math.max(1,qTiles.reduce((n,q)=>n+q.w*q.h,0))},variants,bestRiskLimit:best?.riskLimit??null,bestFixed96Ms:best?.fullMedianMs??null,recommendation});onProgress?.({index:di+1,total:depths.length,z,phase:'done'});
}
const result={format:'mandelbrot-ds-sparse-fixed96-queue-v2',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],riskLimits:[...riskLimits],fixedPoint:{bits:96,fractionBits:88,format:'signed Q8.88 u32 limbs'},warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Diagnostic only. Guarded DS Direct compacts sensitivity-risk pixels into a GPU queue. Fixed96 consumes only that queue through dispatchWorkgroupsIndirect; queue integrity is validated for every timing and quality sample. Every risk candidate is reported even when it fails quality gates. Full Deep+correction is the quality baseline. Production routing is unchanged.'};fixed96Job.last=result;return result;
}finally{fixed96Job.active=false}
}
// ── GPU startup / rendering orchestration ────────────────────────────────
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
async function renderFrame(){
const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height);
state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats();
try{
const r=renderer||await initRenderer();if(token!==state.token)return;
if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}
if(decision.probe){state.lastEngine='WebGPU · direct/deep view probe';state.drawState='COVERING';updateStats();await ensureBackendProbe(snap,iter,decision,token);if(token!==state.token)return;decision=chooseBackend(snap,canvas.width,iter,canvas.height);if(decision.probe)decision={...decision,backend:'deep',deep:true,probe:false,reason:'overlap-probe-failed-safe-deep'};state.backendDecision=decision;updateStats()}
const deep=decision.deep;let ctx=null;
if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded perturbation'}else state.lastEngine='WebGPU · f32 direct';
const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;
const firstPaint=performance.now()-t0;
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,backend:decision.backend};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=firstPaint;state.rendering=deep;state.unresolved=0;
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();
if(!deep){state.rendering=false;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();return}
let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats();
const n=canvas.width*canvas.height;if(shouldApplySparseCorrection(state.unresolved,n)){state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats();const corrected=await r.correctUnknownFrame(snap,iter,token);if(!corrected||token!==state.token)return;state.correctionPasses=1;stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;const painted=await r.recolor(token);if(!painted||token!==state.token)return;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats()}
if(token!==state.token)return;state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · guarded perturbation'+(state.correctionPasses?' + DS correction':'');const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();
}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
}
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback(深部非対応)';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,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';ensurePrecision();return true}catch{return false}}
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
$('#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';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
const exportJob={active:false,cancelled:false};
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
class StreamingPng{
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
async rows(filteredRows){await this.writer.write(filteredRows)}
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
}
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
async function runExport(){
if(exportJob.active)return;
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),strict=$('#exportPrecision').value==='strict';
let ctx=null;
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
let done=0,unresolvedSamples=0;
try{
for(let y=0;y<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
function benchmarkSummary(result){const lines=['z | f32 est ms | DS est ms | deep warm est | deep cold est | f32 Δclass | DS Δclass | direct ratio | fastest cold','--|------------|-----------|---------------|---------------|------------|-----------|--------------|-------------'];for(const r of result.rows)lines.push([String(r.z).padStart(2),r.f32.fullMedianMs.toFixed(1),r.ds.fullMedianMs.toFixed(1),r.deep.warmMedianMs.toFixed(1),r.deep.coldMedianMs.toFixed(1),(100*r.f32.quality.classMismatchRate).toFixed(2)+'%',(100*r.ds.quality.classMismatchRate).toFixed(2)+'%',Number.isFinite(r.directRatio)?r.directRatio.toExponential(2):'n/a',r.assessment.fastestCold].join(' | '));return lines.join('\n')}
$('#backendBench').onclick=()=>{const d=$('#benchmarkDialog');$('#benchmarkStatus').textContent='';$('#benchmarkProgress').hidden=true;$('#benchmarkOutput').value=benchmarkJob.last?benchmarkSummary(benchmarkJob.last):'';$('#benchmarkSave').disabled=!benchmarkJob.last;d.showModal?d.showModal():d.setAttribute('open','')};
$('#benchmarkStart').onclick=async()=>{if(benchmarkJob.active)return;cancelRender();state.dirty=false;const start=$('#benchmarkStart'),progress=$('#benchmarkProgress'),status=$('#benchmarkStatus');start.disabled=true;$('#benchmarkSave').disabled=true;progress.hidden=false;progress.max=CROSSOVER_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#benchmarkOutput').value='';try{const result=await runCrossoverBenchmark({track:$('#benchmarkTrack').value,samples:Math.max(1,Number($('#benchmarkSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});progress.value=progress.max;status.textContent='測定完了 · '+result.rows.length+' depths';$('#benchmarkOutput').value=benchmarkSummary(result);$('#benchmarkSave').disabled=false}catch(e){status.textContent=String(e&&e.message||e)}finally{start.disabled=false;markDirty(false)}};
$('#benchmarkCancel').onclick=()=>{if(benchmarkJob.active){benchmarkJob.cancelled=true;refs.cancelPending('benchmark cancelled');$('#benchmarkStatus').textContent='中止しています…'}else $('#benchmarkDialog').close()};
$('#benchmarkSave').onclick=()=>{if(!benchmarkJob.last)return;downloadBlob(new Blob([JSON.stringify(benchmarkJob.last,null,2)],{type:'application/json'}),'mandelbrot-crossover-'+Date.now()+'.json')};
function fixed96Summary(result){const lines=['z | risk | fixed96 est | deep cold | speedup | correction | speed q | dispatch | remain | Δclass | queue | status','--|------|-------------|-----------|---------|------------|---------|----------|--------|--------|-------|-------'];for(const r of result.rows){for(const v of r.variants){const status=v.failureReason||(v.safe&&v.timingRepresentative?(v.fullMedianMs<r.deep.coldMedianMs*.9&&v.uncertainRate<.75?'candidate':'safe-but-slower'):'rejected'),queue=v.queueIntegrity===false?'ERROR':Math.abs((v.dispatchRate||0)-(v.uncertainRate||0))>1e-9?'MISMATCH':'ok';lines.push([String(r.z).padStart(2),v.riskLimit.toExponential(0),v.fullMedianMs.toFixed(1),r.deep.coldMedianMs.toFixed(1),(r.deep.coldMedianMs/Math.max(1e-9,v.fullMedianMs)).toFixed(2)+'×',(100*v.uncertainRate).toFixed(1)+'%',(100*(v.speedQueueRate||0)).toFixed(1)+'%',(100*(v.dispatchRate||0)).toFixed(1)+'%',(100*v.remainingRate).toFixed(2)+'%',(100*v.quality.classMismatchRate).toFixed(2)+'%',queue,status].join(' | '))}}return lines.join('\n')}
$('#sparseBench').onclick=()=>{const d=$('#sparseDialog');$('#sparseStatus').textContent='';$('#sparseProgress').hidden=true;$('#sparseOutput').value=fixed96Job.last?fixed96Summary(fixed96Job.last):'';$('#sparseSave').disabled=!fixed96Job.last;d.showModal?d.showModal():d.setAttribute('open','')};
$('#sparseStart').onclick=async()=>{if(fixed96Job.active)return;cancelRender();state.dirty=false;const start=$('#sparseStart'),progress=$('#sparseProgress'),status=$('#sparseStatus');start.disabled=true;$('#sparseSave').disabled=true;progress.hidden=false;progress.max=FIXED96_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#sparseOutput').value='';try{const result=await runFixed96Experiment({track:$('#sparseTrack').value,samples:Math.max(1,Number($('#sparseSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});progress.value=progress.max;status.textContent='実験完了 · '+result.rows.length+' depths';$('#sparseOutput').value=fixed96Summary(result);$('#sparseSave').disabled=false}catch(e){status.textContent=String(e&&e.message||e)}finally{start.disabled=false;markDirty(false)}};
$('#sparseCancel').onclick=()=>{if(fixed96Job.active){fixed96Job.cancelled=true;refs.cancelPending('fixed96 experiment cancelled');$('#sparseStatus').textContent='中止しています…'}else $('#sparseDialog').close()};
$('#sparseSave').onclick=()=>{if(!fixed96Job.last)return;downloadBlob(new Blob([JSON.stringify(fixed96Job.last,null,2)],{type:'application/json'}),'mandelbrot-fixed96-'+Date.now()+'.json')};
// ── diagnostics ──────────────────────────────────────────────────────────
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend(snapshot(),Math.max(1,canvas.width),maxIter(),Math.max(1,canvas.height));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.probe?'WebGPU 判定中':decision.deep?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'深部補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.correctedPixels)status+=' · 回収 '+state.correctedPixels;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx,ratio=decision.metrics&&Number.isFinite(decision.metrics.ratio)?decision.metrics.ratio:0,prof=decision.profile,rs=state.unknownReasons,reasonText=rs?Object.entries(rs).filter(([,v])=>v).map(([k,v])=>k+':'+v).join(' '):'';$('#diagEngine').textContent='engine: '+state.lastEngine+' | route '+decision.backend+' ('+decision.reason+') | ratio '+(ratio?ratio.toExponential(2):'n/a')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'')+(reasonText?' | UNKNOWN '+reasonText:'')+(decision.viewProbe?' | view probe direct '+decision.viewProbe.predictedDirectMs.toFixed(0)+'ms / deep '+decision.viewProbe.predictedDeepMs.toFixed(0)+'ms / u '+(100*decision.viewProbe.deepUnknownRate).toFixed(2)+'% / Δclass '+decision.viewProbe.classDisagreement:'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height+' | speed profiles '+backendProfiles.size+' / view probes '+backendViewProbes.size}
globalThis.__MANDEL_TEST__={
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
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,h):!!forceDeep;let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
async runCrossoverBenchmark(options={}){return runCrossoverBenchmark(options)},
async runFixed96Experiment(options={}){return runFixed96Experiment(options)},
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,h);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,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}}
};
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:(state.backendDecision||chooseBackend()).deep,backendDecision:state.backendDecision,backendProfiles:Array.from(backendProfiles.entries()),backendViewProbes:Array.from(backendViewProbes.entries()),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,unknownReasons:state.unknownReasons,correctionPasses:state.correctionPasses,correctedPixels:state.correctedPixels,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.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'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
})();
</script>
</body>
</html>