2026-08-22 20:55:27 +09:00
<!doctype html>
< html lang = "ja" >
< head >
2026-08-23 23:27:38 +09:00
< meta name = "mandelbrot-bundle" content = "single-file-v24.2.8" >
2026-08-22 20:55:27 +09:00
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width,initial-scale=1,viewport-fit=cover" >
< meta name = "theme-color" content = "#050813" >
2026-08-23 23:27:38 +09:00
< title > Mandelbrot Deep Zoom v24.2.8 Fixed96 Sparse Queue Experiment< / title >
2026-08-22 20:55:27 +09:00
< 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)}
2026-08-23 23:27:38 +09:00
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}
2026-08-22 20:55:27 +09:00
#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" >
2026-08-23 17:12:29 +09:00
< 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 >
2026-08-22 20:55:27 +09:00
< 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 >
2026-08-23 17:12:29 +09:00
< div class = "checks" > < label > < input id = "adaptive" type = "checkbox" checked > 反復回数を自動調整< / label > < label > < input id = "hq" type = "checkbox" > GPU境界平滑化< / label > < / div >
2026-08-22 20:55:27 +09:00
< 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 >
2026-08-23 23:27:38 +09:00
< 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 >
2026-08-22 20:55:27 +09:00
< / 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 >
2026-08-23 17:12:29 +09:00
< label for = "exportPrecision" > 精度方針< / label > < select id = "exportPrecision" > < option value = "balanced" > Balanced< / option > < option value = "strict" > 保守的 (Strict)< / option > < / select >
2026-08-22 20:55:27 +09:00
< / 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 >
2026-08-23 23:27:38 +09:00
< 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 >
2026-08-22 20:55:27 +09:00
< / body >
< / html >