mandelbrot/index.html

2130 lines
241 KiB
HTML
Raw Normal View History

2026-08-22 20:55:27 +09:00
<!doctype html>
<html lang="ja">
<head>
2026-09-06 23:28:03 +09:00
<meta name="mandelbrot-bundle" content="single-file-v24.4.0">
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">
2026-08-25 14:03:13 +09:00
<meta name="theme-color" content="#232a38">
2026-09-06 23:28:03 +09:00
<title>Mandelbrot Deep Zoom v24.4.0</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}
2026-08-25 14:03:13 +09:00
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#000;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#000;image-rendering:auto;touch-action:none}
2026-08-22 20:55:27 +09:00
.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%)}
2026-08-25 14:03:13 +09:00
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand-return{display:block;margin-top:3px;color:var(--muted);font-size:10px;font-weight:650;letter-spacing:0;text-decoration:none}.brand-return:hover{text-decoration:underline;color:var(--text)}.brand-return:focus-visible{outline:2px solid #fff;outline-offset:2px;border-radius:3px}
2026-08-22 20:55:27 +09:00
.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)}
2026-08-24 20:30:26 +09:00
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));max-height:calc(100dvh - 20px);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.18);background:#000;color:#fff;min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}select option{background:#000;color:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
2026-08-22 20:55:27 +09:00
.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}
2026-08-24 20:30:26 +09:00
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
2026-08-25 14:03:13 +09:00
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
2026-08-24 20:30:26 +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}#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)}
2026-08-22 20:55:27 +09:00
.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}
2026-08-25 14:03:13 +09:00
@media(max-width:700px){.stats{display:none}.brand-return{font-size:9.5px}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.compact-status{display:block}button,select{min-height:44px}}
2026-08-22 20:55:27 +09:00
@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>
2026-08-25 14:03:13 +09:00
<div class="top"><div class="brand"><span>MANDELBROT DEEP ZOOM</span><a class="brand-return" href="https://2012r2.nishi.boats/~333/">333の部屋に戻る</a></div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord"></span></div><div class="row"><span class="muted">倍率</span> <span id="zoom"></span> <span class="muted">表示幅</span> <span id="span"></span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render"></span></div></div></div>
2026-08-22 20:55:27 +09:00
<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-24 20:30:26 +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-09-06 23:28:03 +09:00
<div class="line"><label for="continuationBudget">反復上限</label><select id="continuationBudget"><option value="0">自動</option><option value="4096">4,096 回</option><option value="16384">16,384 回</option><option value="32768">32,768 回</option><option value="65536">65,536 回</option><option value="150000">150,000 回</option></select><output></output></div>
2026-08-25 14:03:13 +09:00
<div class="line"><label for="palette">彩色</label><select id="palette" aria-label="彩色プリセット"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option><option value="3">インフェルノ</option><option value="5">氷晶</option><option value="7">オーロラ</option><option value="9">深海</option><option value="10">黄金</option><option value="11">ネオン</option><option value="13">電光</option><option value="15">蒼橙</option><option value="17">紅碧</option><option value="18">桃翠</option></select><output></output></div>
2026-08-24 20:30:26 +09:00
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0" max="1000" step="1" value="532" aria-label="色周期(対数スケール)"><output id="cycleO" for="cycle">0.0080</output></div>
2026-08-22 20:55:27 +09:00
<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>
2026-08-25 14:03:13 +09:00
<div class="mini-actions"><button id="colorAuto" type="button" aria-pressed="false" title="色周期は固定し、色相位置だけを動かします">色相変化</button></div>
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>
<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
<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;
2026-08-25 14:03:13 +09:00
fn scale_scalar(v:f32,e:i32)->f32{
// ldexp rounds the combined v*2^e result. Do not reject solely because e
// is below the normal f32 exponent: a large mantissa can still produce a
// perfectly normal representable result (critical around 10^38.461 zoom).
return ldexp(v,e);
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
return ldexp(v,vec2<i32>(e));
2026-08-23 23:27:38 +09:00
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
2026-08-25 14:03:13 +09:00
let propagated=abs(scale_scalar(errScaled,scaleExp));
2026-08-23 23:27:38 +09:00
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
2026-09-06 23:28:03 +09:00
sampleY:f32, outputStride:u32, outputBase:u32, _p2:f32,
2026-08-23 23:27:38 +09:00
};
@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;}
2026-09-06 23:28:03 +09:00
let out=p.outputBase+gid.y*p.outputStride+gid.x;
2026-08-23 23:27:38 +09:00
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;}
}
2026-08-25 14:03:13 +09:00
fieldMeta[out]=pack_unknown(p.maxIter,REASON_OPERATION_LIMIT); fieldSmooth[out]=0.0;
2026-08-23 23:27:38 +09:00
}
`;
2026-09-06 23:28:03 +09:00
// Continuation queues are compacted after each bounded chunk. State is
// indexed by pixel so queue order cannot change the arithmetic sequence.
2026-08-25 21:33:45 +09:00
const ACTIVE_PREPARE_WGSL=String.raw`
@group(0) @binding(0) var<storage,read_write> activeCount:array<atomic<u32>>;
@group(0) @binding(1) var<storage,read_write> indirect:array<u32>;
@compute @workgroup_size(1) fn main(){let n=atomicLoad(&activeCount[0]);indirect[0]=(n+63u)/64u;indirect[1]=1u;indirect[2]=1u;}
`;
2026-08-24 20:30:26 +09:00
const FAST_PERTURB_WGSL=COMMON+String.raw`
2026-08-23 23:27:38 +09:00
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
2026-08-24 20:30:26 +09:00
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,
2026-08-23 23:27:38 +09:00
};
2026-08-24 20:30:26 +09:00
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
2026-08-23 23:27:38 +09:00
@group(0) @binding(0) var<uniform> p:Params;
2026-08-24 20:30:26 +09:00
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:array<atomic<u32>>;
fn mark_fast_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u);
if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);}
2026-08-23 23:27:38 +09:00
}
2026-08-24 20:30:26 +09:00
fn render_fast(out:u32,gx:f32,gy:f32){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u;
loop{
let rp=refs[min(m,p.refLen)];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
2026-09-06 23:28:03 +09:00
if(n>=p.maxIter){mark_fast_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);return;}
2026-08-25 14:03:13 +09:00
// Reference-near-zero episodes are not an automatic failure. Continue the
// valid perturbation recurrence; explicit range/reference guards remain.
2026-08-24 20:30:26 +09:00
if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=rp;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
2026-08-25 14:03:13 +09:00
let sq=scaled_to_f32(cmul(w,w),scaleExp);
2026-08-24 20:30:26 +09:00
w=linear+sq+d; m+=1u; n+=1u;
let mm=max(maxabs(w),maxabs(d));
if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;}
if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;}
if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;}
2026-08-23 23:27:38 +09:00
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
2026-08-24 20:30:26 +09:00
let out=p.outputBase+gid.y*p.outputStride+gid.x;
2026-08-25 14:03:13 +09:00
if(p.unknownOnly!=0u){
let prior=fieldMeta[out];let cls=(prior>>28u)&3u;
if(cls!=FIELD_UNKNOWN){return;}
let reason=(prior>>20u)&0xffu;
// mode 2 repairs only untouched coverage holes (zero-filled metadata).
if(p.unknownOnly==2u && reason!=0u){return;}
}
2026-08-24 20:30:26 +09:00
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
render_fast(out,gx,gy);
2026-08-23 23:27:38 +09:00
}
`;
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;
2026-08-24 20:30:26 +09:00
var n=0u; var m=0u;
2026-08-23 23:27:38 +09:00
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
2026-09-06 23:28:03 +09:00
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let rpEnd=refs[m];
2026-08-23 23:27:38 +09:00
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
2026-09-06 23:28:03 +09:00
let magEnd=dot(zEnd,zEnd);
if(magEnd>4.0){
if(length(zEnd)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,magEnd);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
if(length(zEnd)+errAbs>2.0){mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;}
2026-08-23 23:27:38 +09:00
let limit=select(1.0e-3,1.0e-4,strictMode);
2026-08-25 14:03:13 +09:00
if(errAbs<=limit){mark_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
2026-08-23 23:27:38 +09:00
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;
}
2026-08-25 14:03:13 +09:00
// Keep the perturbation recurrence through reference-near-zero episodes.
// Escape acceptance is still guarded by the propagated absolute error.
2026-08-23 23:27:38 +09:00
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
2026-08-24 20:30:26 +09:00
let r=rp;
2026-08-23 23:27:38 +09:00
let refAbs=maxabs(r.hi)+maxabs(r.lo);
2026-08-25 14:03:13 +09:00
let wAbs=maxabs(w); let dAbs=maxabs(d);
let actualDelta=scaled_to_f32(w,scaleExp);
let deltaAbs=maxabs(actualDelta);
let sq=scaled_to_f32(cmul(w,w),scaleExp);
let sqAbs=maxabs(sq);
let gain=2.0*refAbs+2.0*deltaAbs;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30);
2026-08-23 23:27:38 +09:00
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
2026-08-25 14:03:13 +09:00
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp. Scale the product
// directly so e<-126 does not discard a representable combined value.
2026-08-24 20:30:26 +09:00
w=linear+sq+d; m+=1u; n+=1u;
2026-08-23 23:27:38 +09:00
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
2026-08-25 21:33:45 +09:00
const DEEP_ACTIVE_RESUME_INIT_WGSL=COMMON+String.raw`
struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32};
struct DeepState{d:vec2<f32>,w:vec2<f32>,scaleExp:i32,n:u32,m:u32,errScaled:f32};
@group(0) @binding(0) var<uniform> p:Params;@group(0) @binding(1) var<storage,read_write> states:array<DeepState>;@group(0) @binding(2) var<storage,read> queue:array<u32>;
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){let qi=gid.x;if(qi>=p.unknownOnly){return;}let out=queue[qi];if(out>=p.fullW*p.fullH){return;}let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW);let d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);states[out]=DeepState(d,vec2<f32>(0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));}
`;
const DEEP_ACTIVE_CONTINUE_WGSL=COMMON+String.raw`
struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32};
struct RefPoint{hi:vec2<f32>,lo:vec2<f32>};struct DeepState{d:vec2<f32>,w:vec2<f32>,scaleExp:i32,n:u32,m:u32,errScaled:f32};
@group(0) @binding(0) var<uniform> p:Params;@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;@group(0) @binding(2) var<storage,read_write> states:array<DeepState>;@group(0) @binding(3) var<storage,read> queueIn:array<u32>;@group(0) @binding(4) var<storage,read_write> countIn:array<atomic<u32>>;@group(0) @binding(5) var<storage,read_write> queueOut:array<u32>;@group(0) @binding(6) var<storage,read_write> countOut:array<atomic<u32>>;@group(0) @binding(7) var<storage,read_write> fieldMeta:array<u32>;@group(0) @binding(8) var<storage,read_write> fieldSmooth:array<f32>;
fn fail(out:u32,n:u32,reason:u32){fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;}
2026-09-06 23:28:03 +09:00
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){if(gid.x>=atomicLoad(&countIn[0])){return;}let local=queueIn[gid.x];let out=p.outputBase+(local/p.tileW)*p.outputStride+(local%p.tileW);if(((fieldMeta[out]>>28u)&3u)==FIELD_INTERIOR_PROVEN){return;}var s=states[local];let stop=min(p.targetIter,s.n+max(1u,p.chunkSize));loop{if(s.n>=stop){break;}if(s.m>p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}let rp=refs[s.m];let delta=scaled_to_f32(s.w,s.scaleExp);let z=rp.hi+(rp.lo+delta);let mag=dot(z,z);if(mag>4.0){let errAbs=safe_abs_error(s.errScaled,s.scaleExp,z,delta);if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(s.n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(s.n,mag);return;}fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;}if(s.m>=p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}let refAbs=maxabs(rp.hi)+maxabs(rp.lo);let wAbs=maxabs(s.w);let dAbs=maxabs(s.d);let deltaAbs=maxabs(delta);let sq=scaled_to_f32(cmul(s.w,s.w),s.scaleExp);let sqAbs=maxabs(sq);let gain=2.0*refAbs+2.0*deltaAbs;let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30);s.errScaled=gain*s.errScaled+roundErr;let linear=2.0*(cmul(rp.hi,s.w)+cmul(rp.lo,s.w));s.w=linear+sq+s.d;s.m++;s.n++;if(maxabs(s.w)>=1.0e30||maxabs(s.d)>=1.0e30){fail(out,s.n,REASON_RANGE);return;}let mm=max(maxabs(s.w),maxabs(s.d));if(mm>65536.0){s.w*=0.0000152587890625;s.d*=0.0000152587890625;s.errScaled*=0.0000152587890625;s.scaleExp+=16;}else if(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp){s.w*=65536.0;s.d*=65536.0;s.errScaled*=65536.0;s.scaleExp-=16;}if(s.scaleExp>126||s.errScaled!=s.errScaled||s.errScaled>1.0e35){fail(out,s.n,REASON_RANGE);return;}}
// Evaluate z_target before adopting the finite-budget result. A valid
// nonescaping state stays in the queue for the next target.
if(s.n>=p.targetIter){
if(s.m>p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}
let rpEnd=refs[s.m];let deltaEnd=scaled_to_f32(s.w,s.scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);let mag=dot(zEnd,zEnd);
let errAbs=safe_abs_error(s.errScaled,s.scaleExp,zEnd,deltaEnd);
if(mag!=mag||errAbs!=errAbs){fail(out,s.n,REASON_RANGE);return;}
if(mag>4.0){
if(length(zEnd)-errAbs>2.0){fieldMeta[out]=pack_meta(s.n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(s.n,mag);return;}
fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;
}
if(length(zEnd)+errAbs>2.0){fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;}
let limit=select(1.0e-3,1.0e-4,p.strict!=0u);
if(errAbs>limit){fail(out,s.n,REASON_ERROR_BOUND);return;}
fail(out,s.n,REASON_OPERATION_LIMIT);
}
states[local]=s;let q=atomicAdd(&countOut[0],1u);queueOut[q]=local;}
2026-08-25 21:33:45 +09:00
`;
2026-09-06 23:28:03 +09:00
// Sparse candidates use compact slots. Dense frames keep the 32-byte state
// and direct pixel indices, avoiding an extra mapping on the largest frames.
const DEEP_COMPACT_INIT_WGSL=DEEP_ACTIVE_RESUME_INIT_WGSL
.replace('errScaled:f32};','errScaled:f32,pixel:u32,_pad:u32};')
.replace('var<storage,read> queue:','var<storage,read_write> queue:')
.replace('states[out]=DeepState(', 'states[qi]=DeepState(')
.replace('F32_U*maxabs(d));}', 'F32_U*maxabs(d),out,0u);queue[qi]=qi;}');
const DEEP_COMPACT_CONTINUE_WGSL=DEEP_ACTIVE_CONTINUE_WGSL
.replace('errScaled:f32};','errScaled:f32,pixel:u32,_pad:u32};')
.replace('let out=p.outputBase+(local/p.tileW)*p.outputStride+(local%p.tileW);','let out=states[local].pixel;');
2026-08-25 21:33:45 +09:00
2026-08-24 20:30:26 +09:00
// Production Deep sparse path: keep the long-running perturbation kernel
// untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets.
// Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS
// correction kernel sees less workgroup-level iteration divergence.
//
// Bucket state layout (24 u32 atomics):
// [0..7] histogram counts
// [8..15] scatter cursors (initialized from prefix offsets)
// [16..23] immutable prefix offsets for queue accounting
const DEEP_BUCKET_HIST_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
var<workgroup> localCounts:array<atomic<u32>,8>;
fn deep_bucket(packed:u32)->u32{
let n=packed&ITER_MASK;
return min(7u,(n*8u)/max(1u,p.maxIter));
}
2026-08-25 14:03:13 +09:00
fn selected_unknown(packed:u32)->bool{
if(((packed>>28u)&3u)!=FIELD_UNKNOWN){return false;}
if(p.unknownOnly==2u){let reason=(packed&REASON_MASK)>>REASON_SHIFT;return reason>=1u && reason<=5u;}
return true;
}
2026-08-24 20:30:26 +09:00
@compute @workgroup_size(64)
fn main(
@builtin(local_invocation_id) lid3:vec3<u32>,
@builtin(workgroup_id) wid:vec3<u32>
){
let lane=lid3.x;
if(lane<8u){atomicStore(&localCounts[lane],0u);}
workgroupBarrier();
let lx=wid.x*64u+lane;
let ly=wid.y;
if(lx<p.tileW && ly<p.tileH){
let out=p.outputBase+ly*p.outputStride+lx;
let packed=fieldMeta[out];
2026-08-25 14:03:13 +09:00
if(selected_unknown(packed)){
2026-08-24 20:30:26 +09:00
atomicAdd(&localCounts[deep_bucket(packed)],1u);
}
}
workgroupBarrier();
if(lane<8u){
let c=atomicLoad(&localCounts[lane]);
if(c>0u){atomicAdd(&bucketState[lane],c);}
}
}
`;
// Convert the 8-bin histogram into contiguous queue ranges, initialize each
// scatter cursor, and produce the correction indirect-dispatch arguments.
const DEEP_BUCKET_PREFIX_WGSL=String.raw`
struct SparseQueueStats{
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
};
struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 };
@group(0) @binding(0) var<storage,read_write> bucketState:array<atomic<u32>>;
@group(0) @binding(1) var<storage,read_write> sparseQueueStats:SparseQueueStats;
@group(0) @binding(2) var<storage,read_write> indirectArgs:IndirectArgs;
@compute @workgroup_size(1)
fn main(){
var total=0u;
var b=0u;
loop{
if(b>=8u){break;}
let c=atomicLoad(&bucketState[b]);
atomicStore(&bucketState[8u+b],total);
atomicStore(&bucketState[16u+b],total);
total+=c;
b+=1u;
}
atomicStore(&sparseQueueStats.selected,total);
atomicStore(&sparseQueueStats.overflow,0u);
atomicStore(&sparseQueueStats.enqueued,total);
atomicStore(&sparseQueueStats.dispatchCount,total);
indirectArgs.x=(total+63u)/64u;
indirectArgs.y=1u;
indirectArgs.z=1u;
indirectArgs._pad=0u;
}
`;
// Scatter UNKNOWN indices into the precomputed bucket ranges. Within each
// 64-lane workgroup, workgroup-memory atomics allocate local ranks; each
// non-empty bucket reserves one global subrange, so global atomics scale with
// non-empty (workgroup,bucket) pairs rather than with UNKNOWN pixels.
const DEEP_BUCKET_SCATTER_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct SparseQueueStats{
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
@group(0) @binding(3) var<storage,read_write> sparseQueueStats:SparseQueueStats;
@group(0) @binding(4) var<storage,read_write> unknownQueue:array<u32>;
var<workgroup> localCounts:array<atomic<u32>,8>;
var<workgroup> localRanks:array<atomic<u32>,8>;
var<workgroup> groupBase:array<u32,8>;
fn deep_bucket(packed:u32)->u32{
let n=packed&ITER_MASK;
return min(7u,(n*8u)/max(1u,p.maxIter));
}
2026-08-25 14:03:13 +09:00
fn selected_unknown(packed:u32)->bool{
if(((packed>>28u)&3u)!=FIELD_UNKNOWN){return false;}
if(p.unknownOnly==2u){let reason=(packed&REASON_MASK)>>REASON_SHIFT;return reason>=1u && reason<=5u;}
return true;
}
2026-08-24 20:30:26 +09:00
@compute @workgroup_size(64)
fn main(
@builtin(local_invocation_id) lid3:vec3<u32>,
@builtin(workgroup_id) wid:vec3<u32>
){
let lane=lid3.x;
if(lane<8u){
atomicStore(&localCounts[lane],0u);
atomicStore(&localRanks[lane],0u);
groupBase[lane]=0u;
}
workgroupBarrier();
let lx=wid.x*64u+lane;
let ly=wid.y;
var out=0u;
var hit=0u;
var bucket=0u;
if(lx<p.tileW && ly<p.tileH){
out=p.outputBase+ly*p.outputStride+lx;
let packed=fieldMeta[out];
2026-08-25 14:03:13 +09:00
if(selected_unknown(packed)){
2026-08-24 20:30:26 +09:00
hit=1u;
bucket=deep_bucket(packed);
atomicAdd(&localCounts[bucket],1u);
}
}
workgroupBarrier();
if(lane<8u){
let c=atomicLoad(&localCounts[lane]);
if(c>0u){groupBase[lane]=atomicAdd(&bucketState[8u+lane],c);}
}
workgroupBarrier();
if(hit!=0u){
let rank=atomicAdd(&localRanks[bucket],1u);
let qi=groupBase[bucket]+rank;
let capacity=p.tileW*p.tileH;
if(qi<capacity){unknownQueue[qi]=out;}
else{atomicAdd(&sparseQueueStats.overflow,1u);}
}
}
`;
2026-08-23 23:27:38 +09:00
// 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;
2026-08-25 14:03:13 +09:00
fn count_remaining(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
2026-08-23 23:27:38 +09:00
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
}
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
}
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b;
let e=b-(q-a);
return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b;
let bb=q-a;
let e=(a-(q-bb))+(b-bb);
return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a;
let ah=ca-(ca-a);
let al=a-ah;
let cb=4097.0*b;
let bh=cb-(cb-b);
let bl=b-bh;
var e=ah*bh-q;
e=e+ah*bl;
e=e+al*bh;
e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h);
return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l;
e=e+a.l*b.h;
e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b);
return ds_quick(q.h,q.l+a.l*b);
}
fn ds_pow2(a:DS,e:i32)->DS{
return DS(ldexp(a.h,e),ldexp(a.l,e));
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
var scaleExp=p.spanExp;
2026-08-24 20:30:26 +09:00
var n=0u; var m=0u;
2026-08-23 23:27:38 +09:00
loop{
let rp=refs[m];
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
let delta=cds_pow2(w,scaleExp);
let z=cds_add(r,delta);
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
return;
}
2026-09-06 23:28:03 +09:00
if(n>=p.maxIter){count_remaining(out,p.maxIter,REASON_OPERATION_LIMIT);return;}
2026-08-25 14:03:13 +09:00
// DS correction follows the same continuous perturbation recurrence.
if(m>=p.refLen){count_remaining(out,n,REASON_REFERENCE_END);return;}
2026-08-23 23:27:38 +09:00
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);
2026-08-24 20:30:26 +09:00
m+=1u; n+=1u;
2026-08-23 23:27:38 +09:00
let mm=max(cds_maxabs(w),cds_maxabs(d));
2026-08-25 14:03:13 +09:00
if(mm>=1.0e30 || mm!=mm){count_remaining(out,n,REASON_RANGE);return;}
2026-08-23 23:27:38 +09:00
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;}
2026-08-25 14:03:13 +09:00
if(scaleExp>126){count_remaining(out,n,REASON_RANGE);return;}
2026-08-23 23:27:38 +09:00
}
}
@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;
2026-08-25 14:03:13 +09:00
let prior=fieldMeta[out];
if(((prior>>28u)&3u)!=FIELD_UNKNOWN){return;}
if(p.unknownOnly==2u){
let reason=(prior&REASON_MASK)>>REASON_SHIFT;
if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;}
}
2026-08-23 23:27:38 +09:00
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
2026-08-24 20:30:26 +09:00
// Production sparse correction variant. It shares the complete DS arithmetic
// with DEEP_CORRECT_WGSL, but consumes only indices emitted by the queued deep
// perturbation pass and launches via dispatchWorkgroupsIndirect.
const DEEP_CORRECT_QUEUE_WGSL=DEEP_CORRECT_WGSL
.replace(
`struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};`,
`struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct SparseQueueStats{
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
};`)
.replace(
`@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;`,
`@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
@group(0) @binding(5) var<storage,read_write> sparseQueueStats:SparseQueueStats;
@group(0) @binding(6) var<storage,read> unknownQueue:array<u32>;`)
.replace(
`@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
2026-08-25 14:03:13 +09:00
let prior=fieldMeta[out];
if(((prior>>28u)&3u)!=FIELD_UNKNOWN){return;}
if(p.unknownOnly==2u){
let reason=(prior&REASON_MASK)>>REASON_SHIFT;
if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;}
}
2026-08-24 20:30:26 +09:00
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}`,
`@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
2026-08-25 21:33:45 +09:00
let localQi=gid.x;
2026-08-24 20:30:26 +09:00
let queued=atomicLoad(&sparseQueueStats.dispatchCount);
2026-08-25 21:33:45 +09:00
if(localQi>=queued){return;}
let qi=bitcast<u32>(p._numeric2)+localQi;
2026-08-24 20:30:26 +09:00
let out=unknownQueue[qi];
atomicAdd(&sparseQueueStats.processed,1u);
if(out<p.outputBase){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;}
let local=out-p.outputBase;
let ly=local/p.outputStride;
let lx=local-ly*p.outputStride;
if(lx>=p.tileW||ly>=p.tileH){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;}
2026-08-25 14:03:13 +09:00
let prior=fieldMeta[out];
if(((prior>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;}
if(p.unknownOnly==2u){
let reason=(prior&REASON_MASK)>>REASON_SHIFT;
if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;}
}
2026-08-24 20:30:26 +09:00
let gx=f32(p.tileX+lx)+p.sampleX;
let gy=f32(p.tileY+ly)+p.sampleY;
correct_pixel(out,gx,gy);
}`);
if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)')){
throw new Error('failed to derive queued deep correction shader');
}
2026-09-06 23:28:03 +09:00
const INTERIOR_MASK_WGSL=String.raw`
struct Mask{width:u32,height:u32,tile:u32,columns:u32,flags:array<u32>};
@group(0) @binding(0) var<storage,read> mask:Mask;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=mask.width||gid.y>=mask.height){return;}
if(mask.flags[(gid.y/mask.tile)*mask.columns+gid.x/mask.tile]==0u){return;}
let i=gid.y*mask.width+gid.x;fieldMeta[i]=3u<<28u;fieldSmooth[i]=0.0;
}
`;
const PRECISION_SCATTER_WGSL=String.raw`
struct Result{index:u32,packed:u32,smoothed:f32,accepted:u32};
@group(0) @binding(0) var<storage,read> results:array<Result>;
@group(0) @binding(1) var<storage,read_write> packed:array<u32>;
@group(0) @binding(2) var<storage,read_write> smoothed:array<f32>;
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=arrayLength(&results)){return;}let r=results[gid.x];
if(r.accepted==0u||r.index>=arrayLength(&packed)){return;}
let prior=packed[r.index];let reason=(prior>>20u)&255u;
if(((prior>>28u)&3u)!=0u||reason<1u||reason>5u){return;}
packed[r.index]=r.packed;smoothed[r.index]=r.smoothed;
}
`;
2026-08-23 23:27:38 +09:00
const COLOR_WGSL=String.raw`
2026-08-24 20:30:26 +09:00
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
2026-08-23 23:27:38 +09:00
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);
}
2026-08-25 14:03:13 +09:00
fn rgb8(r:f32,g:f32,b:f32)->vec3<f32>{return vec3<f32>(r,g,b)/255.0;}
fn tri_phase(t0:f32)->f32{return 1.0-abs(2.0*fract(t0)-1.0);}
fn ramp5(t:f32,c0:vec3<f32>,c1:vec3<f32>,c2:vec3<f32>,c3:vec3<f32>,c4:vec3<f32>)->vec3<f32>{
if(t<0.25){return mix(c0,c1,smoothstep(0.0,0.25,t));}
if(t<0.50){return mix(c1,c2,smoothstep(0.25,0.50,t));}
if(t<0.75){return mix(c2,c3,smoothstep(0.50,0.75,t));}
return mix(c3,c4,smoothstep(0.75,1.0,t));
}
2026-08-23 23:27:38 +09:00
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
2026-08-25 14:03:13 +09:00
if(t<0.11){return mix(rgb8(4.0,10.0,27.0),rgb8(12.0,53.0,79.0),smoothstep(0.0,0.11,t));}
if(t<0.25){return mix(rgb8(12.0,53.0,79.0),rgb8(31.0,156.0,184.0),smoothstep(0.11,0.25,t));}
if(t<0.38){return mix(rgb8(31.0,156.0,184.0),rgb8(91.0,226.0,234.0),smoothstep(0.25,0.38,t));}
if(t<0.50){return mix(rgb8(91.0,226.0,234.0),rgb8(66.0,53.0,151.0),smoothstep(0.38,0.50,t));}
if(t<0.62){return mix(rgb8(66.0,53.0,151.0),rgb8(139.0,49.0,170.0),smoothstep(0.50,0.62,t));}
if(t<0.73){return mix(rgb8(139.0,49.0,170.0),rgb8(232.0,72.0,145.0),smoothstep(0.62,0.73,t));}
if(t<0.84){return mix(rgb8(232.0,72.0,145.0),rgb8(255.0,137.0,64.0),smoothstep(0.73,0.84,t));}
if(t<0.93){return mix(rgb8(255.0,137.0,64.0),rgb8(255.0,211.0,99.0),smoothstep(0.84,0.93,t));}
return mix(rgb8(255.0,211.0,99.0),rgb8(255.0,250.0,223.0),smoothstep(0.93,1.0,t));
2026-08-23 23:27:38 +09:00
}
2026-08-24 20:30:26 +09:00
fn palette_color(phase:f32)->vec3<f32>{
2026-08-25 14:03:13 +09:00
if(p.palette==0u){return current_palette(phase);}
if(p.palette==1u){return hsv(phase,0.92,1.0);}
if(p.palette==2u){let g=(18.0+237.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3<f32>(g);}
let t=tri_phase(phase);
if(p.palette==3u){return ramp5(t,rgb8(0.0,0.0,2.0),rgb8(40.0,8.0,84.0),rgb8(141.0,36.0,101.0),rgb8(228.0,88.0,48.0),rgb8(252.0,255.0,166.0));}
if(p.palette==5u){return ramp5(t,rgb8(1.0,4.0,14.0),rgb8(7.0,36.0,92.0),rgb8(0.0,132.0,178.0),rgb8(116.0,232.0,240.0),rgb8(242.0,254.0,255.0));}
if(p.palette==7u){return ramp5(t,rgb8(3.0,6.0,20.0),rgb8(63.0,26.0,132.0),rgb8(10.0,146.0,174.0),rgb8(48.0,224.0,140.0),rgb8(229.0,255.0,184.0));}
if(p.palette==9u){return ramp5(t,rgb8(0.0,5.0,20.0),rgb8(4.0,42.0,82.0),rgb8(0.0,113.0,136.0),rgb8(34.0,176.0,162.0),rgb8(221.0,246.0,202.0));}
if(p.palette==10u){return ramp5(t,rgb8(5.0,4.0,2.0),rgb8(44.0,24.0,4.0),rgb8(132.0,83.0,10.0),rgb8(228.0,171.0,46.0),rgb8(255.0,245.0,198.0));}
if(p.palette==11u){return ramp5(t,rgb8(3.0,0.0,16.0),rgb8(250.0,0.0,132.0),rgb8(0.0,228.0,255.0),rgb8(126.0,255.0,0.0),rgb8(255.0,247.0,255.0));}
if(p.palette==13u){return ramp5(t,rgb8(0.0,0.0,14.0),rgb8(21.0,16.0,152.0),rgb8(0.0,188.0,255.0),rgb8(220.0,0.0,255.0),rgb8(255.0,255.0,255.0));}
if(p.palette==15u){return ramp5(t,rgb8(2.0,8.0,26.0),rgb8(18.0,85.0,210.0),rgb8(192.0,244.0,255.0),rgb8(255.0,134.0,36.0),rgb8(255.0,242.0,196.0));}
if(p.palette==17u){return ramp5(t,rgb8(20.0,1.0,14.0),rgb8(204.0,26.0,72.0),rgb8(255.0,226.0,182.0),rgb8(0.0,202.0,226.0),rgb8(226.0,249.0,255.0));}
if(p.palette==18u){return ramp5(t,rgb8(22.0,8.0,24.0),rgb8(171.0,36.0,122.0),rgb8(255.0,211.0,149.0),rgb8(45.0,204.0,176.0),rgb8(232.0,255.0,243.0));}
return current_palette(phase);
2026-08-24 20:30:26 +09:00
}
fn escaped_color(m:u32,sm:f32)->vec3<f32>{
2026-08-25 14:03:13 +09:00
// Direct phase mapping: no iteration-band/anchor compensation.
let phase=fract(p.shift+sm*p.cycle);
let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.30+0.70*pow(edge,0.40);let floorc=select(vec3<f32>(0.0,0.0,0.0),vec3<f32>(2.0/255.0),p.palette==2u);return mix(floorc,c,mixv);
2026-08-24 20:30:26 +09:00
}
2026-08-23 23:27:38 +09:00
fn base_color(i:u32)->vec3<f32>{
2026-08-25 14:03:13 +09:00
let m=fieldMeta[i];return escaped_color(m,fieldSmooth[i]);
2026-08-23 23:27:38 +09:00
}
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>){
2026-08-25 14:03:13 +09:00
if(gid.x>=p.width||gid.y>=p.height){return;}
let i=gid.y*p.width+gid.x;let m=fieldMeta[i];let cls=(m>>28u)&3u;
2026-08-24 20:30:26 +09:00
if(cls==FIELD_UNKNOWN){
2026-08-25 14:03:13 +09:00
let reason=(m>>20u)&0xffu;
// Iteration-limit pixels are not a numeric hole: display them as opaque
// black so temporal history cannot paint stale exterior colour through.
if(reason==6u){textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(0.0,0.0,0.0,1.0));}
else{textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(0.0));}
return;
2026-08-24 20:30:26 +09:00
}
2026-08-25 14:03:13 +09:00
if(cls!=FIELD_ESCAPED){
// Certified / likely interior is real image content, not a temporal hole.
// Keep it opaque so a reprojected history frame can never leak exterior
// colour into an already-known Mandelbrot interior pixel.
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(0.0,0.0,0.0,1.0));
return;
}
var c=escaped_color(m,fieldSmooth[i]);
2026-08-23 23:27:38 +09:00
if(p.edgeAA!=0u){
2026-08-25 14:03:13 +09:00
let x=i32(gid.x);let y=i32(gid.y);var sum=linearize(c);var cnt=1.0;
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){
if(ox==0&&oy==0){continue;}
let xx=x+ox;let yy=y+oy;
if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}
let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];
if(((mj>>28u)&3u)==FIELD_ESCAPED){sum+=linearize(escaped_color(mj,fieldSmooth[j]));cnt+=1.0;}
}}
c=delinearize(sum/cnt);
2026-08-23 23:27:38 +09:00
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
2026-08-24 20:30:26 +09:00
`
2026-08-23 23:27:38 +09:00
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;}
2026-08-25 14:03:13 +09:00
let q=vec2<i32>(gid.xy);let va=textureLoad(a,q,0);let vb=textureLoad(b,q,0);let vc=textureLoad(c,q,0);let vd=textureLoad(d,q,0);
let alpha=(va.a+vb.a+vc.a+vd.a)*0.25;
let premulLin=(lin3(va.rgb)*va.a+lin3(vb.rgb)*vb.a+lin3(vc.rgb)*vc.a+lin3(vd.rgb)*vd.a)*0.25;
if(alpha<=0.00001){textureStore(outTex,q,vec4<f32>(0.0));return;}
let straightLin=premulLin/alpha;
textureStore(outTex,q,vec4<f32>(srgb3(straightLin)*alpha,alpha));
2026-08-23 23:27:38 +09:00
}
`;
const PRESENT_WGSL=String.raw`
2026-08-25 14:03:13 +09:00
struct Params{
frontScaleX:f32,frontScaleY:f32,frontOffsetX:f32,frontOffsetY:f32,
histScaleX:f32,histScaleY:f32,histOffsetX:f32,histOffsetY:f32,
historyEnabled:f32,_p0:f32,_p1:f32,_p2:f32,
};
2026-08-23 23:27:38 +09:00
@group(0) @binding(0) var samp:sampler;
2026-08-25 14:03:13 +09:00
@group(0) @binding(1) var frontTex:texture_2d<f32>;
@group(0) @binding(2) var historyTex:texture_2d<f32>;
@group(0) @binding(3) var<uniform> p:Params;
2026-08-23 23:27:38 +09:00
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;
}
2026-08-25 14:03:13 +09:00
fn mapped_uv(uv:vec2<f32>,scale:vec2<f32>,offset:vec2<f32>)->vec2<f32>{
return vec2<f32>(0.5)+(uv-vec2<f32>(0.5))*scale+offset;
}
fn inside(uv:vec2<f32>)->bool{return all(uv>=vec2<f32>(0.0))&&all(uv<=vec2<f32>(1.0));}
2026-08-23 23:27:38 +09:00
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
2026-08-25 14:03:13 +09:00
let fuv=mapped_uv(in.uv,vec2<f32>(p.frontScaleX,p.frontScaleY),vec2<f32>(p.frontOffsetX,p.frontOffsetY));
var front=vec4<f32>(0.0);
if(inside(fuv)){front=textureSampleLevel(frontTex,samp,fuv,0.0);}
if(p.historyEnabled<0.5||front.a>=0.999){return front;}
let huv=mapped_uv(in.uv,vec2<f32>(p.histScaleX,p.histScaleY),vec2<f32>(p.histOffsetX,p.histOffsetY));
var hist=vec4<f32>(0.0);
if(inside(huv)){hist=textureSampleLevel(historyTex,samp,huv,0.0);}
return front+hist*(1.0-front.a);
2026-08-23 23:27:38 +09:00
}
`;
2026-08-24 20:30:26 +09:00
const DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u);\n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}');
2026-09-06 23:28:03 +09:00
if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL)throw new Error('post-stats shader derivation failed');
2026-08-24 20:30:26 +09:00
2026-08-25 21:33:45 +09:00
const FAST_PERTURB_POSTSTATS_WGSL=FAST_PERTURB_WGSL.replace('fn mark_fast_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u);\n if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);}\n}','fn mark_fast_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;\n}');
2026-09-06 23:28:03 +09:00
if(FAST_PERTURB_POSTSTATS_WGSL===FAST_PERTURB_WGSL)throw new Error('screen post-stats shader derivation failed');
2026-08-25 21:33:45 +09:00
2026-09-06 23:28:03 +09:00
// One O(pixels) reduction is shared by Direct, Accurate DS, FAST and Deep.
// Workgroup-local atomics replace up to two contended global atomics for
2026-08-25 21:33:45 +09:00
// every UNKNOWN pixel, and reason zero remains visible in the total count.
const UNKNOWN_STATS_WGSL=COMMON+String.raw`
struct Params{pixelCount:u32,_p0:u32,_p1:u32,_p2:u32};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> unresolvedStats:array<atomic<u32>>;
var<workgroup> localReasons:array<atomic<u32>,8>;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) gid:vec3<u32>,@builtin(local_invocation_id) lid:vec3<u32>){
if(lid.x<8u){atomicStore(&localReasons[lid.x],0u);}
workgroupBarrier();
if(gid.x<p.pixelCount){
let packed=fieldMeta[gid.x];
if(((packed>>28u)&3u)==FIELD_UNKNOWN){
atomicAdd(&localReasons[0],1u);
let reason=(packed&REASON_MASK)>>REASON_SHIFT;
if(reason>=1u && reason<=6u){atomicAdd(&localReasons[reason],1u);}
}
}
workgroupBarrier();
if(lid.x<8u){let count=atomicLoad(&localReasons[lid.x]);if(count>0u){atomicAdd(&unresolvedStats[lid.x],count);}}
}
`;
const SYMMETRY_COPY_WGSL=String.raw`
struct Params{width:u32,height:u32,_p0:u32,_p1:u32};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let half=p.height/2u;if(gid.x>=p.width||gid.y>=half){return;}
let src=gid.y*p.width+gid.x;let dst=(p.height-1u-gid.y)*p.width+gid.x;
fieldMeta[dst]=fieldMeta[src];fieldSmooth[dst]=fieldSmooth[src];
}
`;
const FAILURE_TILE_MAP_WGSL=String.raw`
struct Params{width:u32,height:u32,tileSize:u32,cols:u32,rows:u32,_p0:u32,_p1:u32,_p2:u32};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> failureMap:array<u32>;
var<workgroup> counts:array<atomic<u32>,6>;
@compute @workgroup_size(64)
fn main(@builtin(local_invocation_id) lid:vec3<u32>,@builtin(workgroup_id) wid:vec3<u32>){
if(lid.x<5u){atomicStore(&counts[lid.x],0u);}if(lid.x==5u){atomicStore(&counts[5],0xffffffffu);}workgroupBarrier();
let x0=wid.x*p.tileSize;let y0=wid.y*p.tileSize;let area=p.tileSize*p.tileSize;var i=lid.x;loop{if(i>=area){break;}let x=x0+i%p.tileSize;let y=y0+i/p.tileSize;if(x<p.width&&y<p.height){let index=y*p.width+x;let packed=fieldMeta[index];if(((packed>>28u)&3u)==0u){let reason=(packed>>20u)&255u;if(reason>=1u&&reason<=5u){atomicAdd(&counts[reason-1u],1u);atomicMin(&counts[5],index);}}}i+=64u;}workgroupBarrier();if(lid.x<6u){failureMap[(wid.y*p.cols+wid.x)*6u+lid.x]=atomicLoad(&counts[lid.x]);}
}
`;
2026-08-23 23:27:38 +09:00
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
2026-09-06 23:28:03 +09:00
version:'24.4.0-bounded-continuation',DEEP_COMPACT_INIT_WGSL,DEEP_COMPACT_CONTINUE_WGSL,INTERIOR_MASK_WGSL,PRECISION_SCATTER_WGSL,DIRECT_F32_WGSL,ACTIVE_PREPARE_WGSL,FAST_PERTURB_WGSL,FAST_PERTURB_POSTSTATS_WGSL,DEEP_PERTURB_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,DEEP_ACTIVE_RESUME_INIT_WGSL,DEEP_ACTIVE_CONTINUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,UNKNOWN_STATS_WGSL,SYMMETRY_COPY_WGSL,FAILURE_TILE_MAP_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
2026-08-23 23:27:38 +09:00
});
})();
</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;
2026-08-25 14:03:13 +09:00
const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000,PALETTE_MAX=18,VALID_PALETTE_IDS=new Set([0,1,2,3,5,7,9,10,11,13,15,17,18]);
2026-08-23 23:27:38 +09:00
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'];
2026-08-25 14:03:13 +09:00
const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96;
2026-09-06 23:22:55 +09:00
const PIXEL_FRONTIER_PRODUCTION=true,PIXEL_FRONTIER_WORK=4000000,PIXEL_FRONTIER_MAX_ITER=150000;
2026-09-06 23:28:03 +09:00
const PIXEL_FRONTIER_ENABLED=PIXEL_FRONTIER_PRODUCTION;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,continuationBudget:0,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,renderClock:null,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,multiRefPasses:0,multiRefRecovered:0,frontierRounds:0,frontierPixels:0,frontierEscaped:0,frontierIter:0,frontierMinimumIter:0,frontierConverged:false,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,qualityStage:'idle',qualityIter:0,qualityFinalIter:0,previewMs:0,refineMs:0,refinePending:false,temporalFill:false,numericRecoveryPending:false,numericRecoveryRunning:false,gpuStage:'',gpuTileRows:0};
2026-08-25 14:03:13 +09:00
let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,refineTimer=0,lastWrittenHash='',navigationHash='';
2026-08-23 23:27:38 +09:00
const viewHistory=[];let viewHistoryIndex=-1;
2026-09-06 23:28:03 +09:00
const TEST_MODE=new URLSearchParams(location.search).has('test');
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,multiRefBuilds:0,numericSubmits:0,correctionSubmits:0,gpuSyncWaits:0,stageCounts:{},stageMs:{},generationMetrics:[]};
2026-08-25 21:33:45 +09:00
function generationMetric(token=state.token){for(let i=runtime.generationMetrics.length-1;i>=0;i--)if(runtime.generationMetrics[i].token===token)return runtime.generationMetrics[i];return null}
2026-08-23 23:27:38 +09:00
// ── exact fixed-point view state ─────────────────────────────────────────
function one(bits=state.bits){return 1n<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
function f32ToFixed(x,bits){
x=Math.fround(x);if(!Number.isFinite(x))throw new Error('non-finite f32 split component');if(x===0)return 0n;
const ab=new ArrayBuffer(4),dv=new DataView(ab);dv.setFloat32(0,x,false);const u=dv.getUint32(0,false),neg=(u>>>31)!==0,eb=(u>>>23)&255,frac=u&0x7fffff,m=BigInt(eb===0?frac:(0x800000|frac)),e=(eb===0?-149:eb-127-23),sh=bits+e;
let q=sh>=0?m<<BigInt(sh):roundDivSigned(m,1n<<BigInt(-sh));return neg?-q:q;
}
function splitFixedF32(v,bits,count=3){const out=[];let r=v;for(let i=0;i<count;i++){const x=Math.fround(fixedNum(r,bits));out.push(x);if(x===0)continue;r-=f32ToFixed(x,bits)}return out}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
2026-08-24 20:30:26 +09:00
function f32Ulp(x){x=Math.abs(Math.fround(x));if(!Number.isFinite(x))return Infinity;if(x===0)return Math.pow(2,-149);if(x<Math.pow(2,-126))return Math.pow(2,-149);return Math.pow(2,Math.floor(Math.log2(x))-23)}
function directPixelRatio(snap,w){const cr=Math.abs(fixedNum(snap.re,snap.bits)),ci=Math.abs(fixedNum(snap.im,snap.bits)),sp=Math.abs(fixedNum(snap.span,snap.bits)),step=sp/Math.max(1,w),scale=Math.max(cr,ci,sp*.75,Math.pow(2,-126)),ulp=f32Ulp(scale);return step/ulp}
function fastNeedsExtended(snap,w){return !(directPixelRatio(snap,w)>=4)}
2026-09-06 23:28:03 +09:00
function chooseBackend(snap=snapshot(),w=canvas.width){const fastExtended=fastNeedsExtended(snap,w);return{backend:fastExtended?'fast-extended':'direct',deep:false,fastExtended,probe:false,reason:fastExtended?'auto-fast-extended':'auto-direct'}}
2026-08-25 21:33:45 +09:00
function pixelPrecisionBits(snap=snapshot(),w=canvas.width,guard=32){const pixelLog2=log2FixedAt(snap.span,snap.bits)-Math.log2(Math.max(1,w)),need=Math.ceil(Math.max(0,-pixelLog2))+Math.max(16,guard|0);return Math.max(96,Math.min(snap.bits+64,need))}
2026-09-06 23:28:03 +09:00
function deepNeeded(){return false}
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,continuationBudget:state.continuationBudget,renderMode:state.renderMode}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive,v.continuationBudget||0].join(':')}
2026-08-23 23:27:38 +09:00
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()}
2026-09-06 23:28:03 +09:00
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,continuationBudget:v.continuationBudget||0,renderMode:'fast'});ensurePrecision();syncControls();saveHash(false);markDirty()}
2026-08-23 23:27:38 +09:00
// ── iteration / quality policy ───────────────────────────────────────────
2026-08-25 21:33:45 +09:00
const ADAPTIVE_ITER_QUANTUM=64;
function adaptiveIterationBonus(screenBits,q,quantum=ADAPTIVE_ITER_QUANTUM){const raw=48*Math.max(0,Number(q)||0)*Math.max(0,Number(screenBits)||0);return raw>0?Math.ceil(raw/quantum)*quantum:0}
2026-09-06 23:28:03 +09:00
function maxIter(){if(!state.adaptive)return Math.min(state.baseIter,state.continuationBudget||state.baseIter);const z=zoomExp(),w=Math.max(1,canvas.width||innerWidth||1),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),q=state.processMode==='power'?.72:state.processMode==='fine'?1.25:state.processMode==='validate'?1.5:1,bonus=adaptiveIterationBonus(screenBits,q);return Math.min(state.continuationBudget||12000,12000,Math.max(state.baseIter,state.baseIter+bonus))}
2026-08-25 14:03:13 +09:00
function previewIter(finalIter=maxIter()){
const z=Math.max(0,zoomExp()),m=state.processMode==='power'?.72:state.processMode==='fine'?1.28:state.processMode==='validate'?1.55:1;
// The old fixed 1100-iteration ceiling starved deep previews: around 10^12,
// finalIter is ~2700 while many visible boundary pixels need >1100 steps.
// Make the first pass track final quality as zoom deepens, with a mode-aware
// ceiling so responsiveness is still bounded on extreme views.
const legacy=Math.round(m*(420+24*z));
const fractionBase=Math.min(.84,.48+.03*z);
const fraction=state.processMode==='power'?Math.min(.68,fractionBase*.82):state.processMode==='fine'?Math.min(.90,fractionBase+.04):state.processMode==='validate'?Math.min(.94,fractionBase+.08):fractionBase;
const deepFloor=Math.round(finalIter*fraction);
const cap=state.processMode==='power'?1800:state.processMode==='fine'?4800:state.processMode==='validate'?6000:3600;
return Math.min(finalIter,Math.max(state.baseIter,Math.min(cap,Math.max(legacy,deepFloor))));
}
function canUseStableReprojection(){
if(!renderer?.historyReady||!state.frameView)return false;
// Covers normal wheel/pinch, one 2x toolbar zoom, and ordinary pans while
// avoiding grotesque reuse after a reset or a distant shared-URL jump.
return reprojectionSafe(renderer.presentTransform(),2.25,.45);
}
function qualityStages(finalIter=maxIter()){
// Once a completed nearby frame exists, temporal reprojection is the
// preview. Do not replace it with a lower-iteration numerical frame: that
// was the source of the stressful black patches during small zoom/pan.
2026-08-25 21:33:45 +09:00
// Deep FAST cannot reuse a preview built from a shorter reference key. A
// separate preview therefore repeats the expensive perturbation frame and
// reference build without reducing final latency. Compute final once.
2026-09-06 23:28:03 +09:00
if(canUseStableReprojection()||fastNeedsExtended(snapshot(),canvas.width))return[finalIter];
2026-08-25 14:03:13 +09:00
const p=previewIter(finalIter);
return finalIter>p?[p,finalIter]:[p];
}
function refinementDelay(stageIndex){
// Full-quality latency matters more than leaving a long idle gap after the
// first preview. Keep a short interaction settling window only.
if(stageIndex<=1)return state.processMode==='validate'?300:state.processMode==='fine'?240:state.processMode==='power'?420:160;
return state.processMode==='validate'?240:state.processMode==='fine'?200:360;
}
function sameViewSnapshot(a,b){return !!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span}
2026-08-25 21:33:45 +09:00
function canUseRealAxisSymmetry(snap){return !!snap&&snap.im===0n}
2026-08-25 14:03:13 +09:00
function reprojectionSafe(t,maxZoom=1.30,maxShift=.18){const s=Math.abs(Number(t?.scaleX));if(!Number.isFinite(s)||s<=0)return false;const z=Math.max(s,1/s),shift=Math.hypot(Number(t?.offsetX)||0,Number(t?.offsetY)||0);return z<=maxZoom&&shift<=maxShift}
2026-08-24 20:30:26 +09:00
function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))}
function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)}
2026-08-25 14:03:13 +09:00
function fract01(x){return x-Math.floor(x)}
function colorStyleSnapshot(){return{palette:state.palette,cycle:state.cycle,shift:state.shift,hq:state.hq}}
function colorStyleKey(style=colorStyleSnapshot()){return[style.palette,style.cycle,style.shift,style.hq?1:0].join(':')}
2026-08-23 23:27:38 +09:00
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';
2026-09-06 23:28:03 +09:00
const MAX_REF=150001;
2026-08-23 23:27:38 +09:00
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}
2026-08-25 21:33:45 +09:00
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<<BigInt(d):roundShift(v,-d)}
2026-09-06 23:28:03 +09:00
// Base and verifier have independent exact fixed-point terminal states.
const orbitCache=new Map();
function orbit(bits,re,im,iter){
const key=[bits,re,im].join(':'),old=orbitCache.get(key);
let r=old||{rr:new Float64Array(iter+1),ri:new Float64Array(iter+1),zr:0n,zi:0n,zr2:0n,zi2:0n,n:0,escape:0,terminalEscape:0,verifiedTo:-1};
const previousLen=r.n;
if(r.rr.length<=iter){const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);rr.set(r.rr);ri.set(r.ri);r.rr=rr;r.ri=ri;}
const bail=16n*(1n<<BigInt(bits));
while(r.n<iter&&!r.terminalEscape){
r.zi=roundShift(2n*r.zr*r.zi,bits)+im;r.zr=r.zr2-r.zi2+re;
r.zr2=roundShift(r.zr*r.zr,bits);r.zi2=roundShift(r.zi*r.zi,bits);r.n++;
r.rr[r.n]=fixedNum(r.zr,bits);r.ri[r.n]=fixedNum(r.zi,bits);
if(r.zr2+r.zi2>bail)r.terminalEscape=r.n;
}
r.escape=r.terminalEscape<=iter?r.terminalEscape:0;r.refLen=Math.min(r.n,iter);r.previousLen=Math.min(previousLen,r.refLen);
orbitCache.delete(key);orbitCache.set(key,r);
while(orbitCache.size>4)orbitCache.delete(orbitCache.keys().next().value);
return r;
}
2026-08-25 21:33:45 +09:00
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<iter;n++){const zri=roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2>BAIL)return n+1}return 0}
function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const out=[{re,im}],den=8n,bh=BigInt(h),bw=BigInt(w);for(let gy=-3;gy<=3;gy++)for(let gx=-3;gx<=3;gx++){if(!gx&&!gy)continue;out.push({re:re+span*BigInt(gx)/den,im:im+span*bh*BigInt(gy)/(den*bw)})}return out}
2026-08-24 20:30:26 +09:00
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.score){best={...c,escape,score}}if(escape===0)break}return best}
2026-08-25 21:33:45 +09:00
function samePacked(a,b){const ah=Math.fround(a),bh=Math.fround(b);return Object.is(ah,bh)&&Object.is(Math.fround(a-ah),Math.fround(b-bh))}
2026-09-06 23:28:03 +09:00
function verify(baseBits,re,im,ref,refLen){
const high=orbit(baseBits+32,re<<32n,im<<32n,refLen);
let mismatch=high.refLen<refLen,checked=0;
// Checkpoints are independent of the requested orbit length; every new
// endpoint is also compared, including an escaping endpoint.
for(let n=Math.max(0,ref.verifiedTo+1);n<=refLen&&!mismatch;n++){
if(n!==refLen&&n%256!==0&&(n&(n-1))!==0)continue;
checked++;if(!samePacked(high.rr[n],ref.rr[n])||!samePacked(high.ri[n],ref.ri[n]))mismatch=true;
}
if(!mismatch)ref.verifiedTo=Math.max(ref.verifiedTo,refLen);
return{mismatch,checked};
}
2026-08-23 23:27:38 +09:00
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}
2026-09-06 23:28:03 +09:00
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceBits=d.bits,sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,fixed=d.fixedRe!=null&&d.fixedIm!=null,baseRe=fixed?BigInt(d.fixedRe):sourceRe,baseIm=fixed?BigInt(d.fixedIm):sourceIm,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),limit=Math.min(MAX_REF-1,d.iter),bits=d.targetBits,centerRe=requant(baseRe,sourceBits,bits),centerIm=requant(baseIm,sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,limit),chosen=fixed?{re:baseRe,im:baseIm,escape:centerRef.escape,score:centerRef.escape||limit}:chooseReference(sourceBits,sourceRe,sourceIm,sourceSpan,w,h,limit,centerRef.escape),re=requant(chosen.re,sourceBits,bits),im=requant(chosen.im,sourceBits,bits),ref=(chosen.re===baseRe&&chosen.im===baseIm)?centerRef:orbit(bits,re,im,limit),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,referenceId:[bits,chosen.re,chosen.im,sourceBits,'guarded-v1'].join(':'),extendedFrom:ref.previousLen,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
2026-08-23 23:27:38 +09:00
`}
class ReferenceService{
2026-09-06 23:28:03 +09:00
constructor(maxCacheBytes=8*1024*1024){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=8;this.maxCacheBytes=maxCacheBytes}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;if(d.type!=='error'&&d.checkpointMismatch&&p.attempt<3){p.attempt++;p.message.targetBits+=32;this.worker.postMessage(p.message);return}this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,precisionBits:d.precisionBits,referenceId:d.referenceId,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>1&&(this.cache.size>this.maxCache||Array.from(this.cache.values()).reduce((n,c)=>n+c.refs.byteLength,0)>this.maxCacheBytes))this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
2026-08-25 21:33:45 +09:00
request(snap,iter,width=1,height=1,fresh=false){const targetBits=pixelPrecisionBits(snap,width,40),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'pixel-guarded-perturb-v24.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail}),message={type:'build',id,key,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter};this.pending.set(id,{resolve,reject,key,promise,message,attempt:0,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage(message);return promise}
2026-09-06 23:28:03 +09:00
requestFixed(snap,iter,width,height,fixedSource){if(!fixedSource)return this.request(snap,iter,width,height);const targetBits=Math.max(pixelPrecisionBits(snap,width,40),fixedSource.precisionBits||0),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,fixedSource.re,fixedSource.im,'pixel-fixed-reference-v24.3'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded fixed reference request');if(!this.ensure())return Promise.reject(new Error('Reference Worker unavailable'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail}),message={type:'build',id,key,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter,fixedRe:fixedSource.re.toString(),fixedIm:fixedSource.im.toString()};this.pending.set(id,{resolve,reject,key,promise,message,attempt:0,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage(message);return promise}
2026-08-23 23:27:38 +09:00
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();
2026-09-06 23:28:03 +09:00
const tileReferencePool=Array.from({length:2},()=>new ReferenceService(2*1024*1024));
2026-08-23 23:27:38 +09:00
2026-08-25 21:33:45 +09:00
function fastReferenceBits(snap,w=canvas.width){return pixelPrecisionBits(snap,w,32)}
2026-08-24 20:30:26 +09:00
function fastReferenceWorkerSource(){return String.raw`
'use strict';
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){if(b<=0)return v;const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<<BigInt(d):roundShift(v,-d)}
2026-08-25 21:33:45 +09:00
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zri=roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<iter;n++){const zri=roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2>BAIL)return n+1}return 0}
function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const out=[{re,im}],den=8n,bh=BigInt(h),bw=BigInt(w);for(let gy=-3;gy<=3;gy++)for(let gx=-3;gx<=3;gx++){if(!gx&&!gy)continue;out.push({re:re+span*BigInt(gx)/den,im:im+span*bh*BigInt(gy)/(den*bw)})}return out}
2026-08-24 20:30:26 +09:00
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.score){best={...c,escape,score}}if(escape===0)break}return best}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),bits=d.targetBits,centerRe=requant(sourceRe,d.sourceBits,bits),centerIm=requant(sourceIm,d.sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,d.iter),chosen=chooseReference(d.sourceBits,sourceRe,sourceIm,sourceSpan,w,h,d.iter,centerRef.escape),re=requant(chosen.re,d.sourceBits,bits),im=requant(chosen.im,d.sourceBits,bits),ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,d.iter),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class FastReferenceService{
2026-09-06 23:28:03 +09:00
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=8;this.maxCacheBytes=8*1024*1024}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([fastReferenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};this.cache.set(d.key,d);while(this.cache.size>1&&(this.cache.size>this.maxCache||Array.from(this.cache.values()).reduce((n,c)=>n+c.refs.byteLength,0)>this.maxCacheBytes))this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'fast reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
2026-08-25 21:33:45 +09:00
request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap,width),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.3'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail});this.pending.set(id,{resolve,reject,key,promise,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter});return promise}
2026-08-24 20:30:26 +09:00
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');this.cache.clear()}
}
const fastRefs=new FastReferenceService();
2026-08-25 21:33:45 +09:00
function precisionFallbackWorkerSource(){return String.raw`
'use strict';
2026-09-06 23:28:03 +09:00
let orbitCount=0;
2026-08-25 21:33:45 +09:00
const ITER_MASK=0x000fffff,FIELD_ESCAPED=1,REASON_SHIFT=20;
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<<BigInt(d):roundShift(v,-d)}
function smooth(n,mag,bits){const m=Math.max(4.0000005,fixedNum(mag,bits)),u=Math.log2(m);return Math.fround(n+1-Math.log2(Math.max(1e-20,.5*u)))}
2026-09-06 23:28:03 +09:00
function pixel(s,index,bits){orbitCount++;const B=BigInt(bits),ONE=1n<<B,w=BigInt(s.width),x=BigInt(index%s.width),y=BigInt(Math.floor(index/s.width)),den=2n*w,re=requant(BigInt(s.re),s.bits,bits),im=requant(BigInt(s.im),s.bits,bits),span=requant(BigInt(s.span),s.bits,bits),cr=re+roundDiv(span*(2n*x+1n-w),den),ci=im+roundDiv(span*(BigInt(s.height)-2n*y-1n),den),bail=4n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<s.iter;n++){zi=roundShift(2n*zr*zi,bits)+ci;zr=zr2-zi2+cr;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);const mag=zr2+zi2;if(mag>bail)return{escape:n+1,smooth:smooth(n+1,mag,bits)}}return{escape:0,smooth:0}}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='solve')return;orbitCount=0;try{const indices=new Uint32Array(d.indices),meta=new Uint32Array(indices.length),smoothOut=new Float32Array(indices.length),accepted=new Uint8Array(indices.length),s={bits:d.bits,re:d.re,im:d.im,span:d.span,width:d.width,height:d.height,iter:d.iter},baseBits=d.targetBits;for(let i=0;i<indices.length;i++){let a=pixel(s,indices[i],baseBits);for(let attempt=0;attempt<4&&!accepted[i];attempt++){const bits=baseBits+attempt*32,b=pixel(s,indices[i],bits+32);if(a.escape&&a.escape===b.escape&&Object.is(a.smooth,b.smooth)){meta[i]=(a.escape&ITER_MASK)|(FIELD_ESCAPED<<28);smoothOut[i]=a.smooth;accepted[i]=1}else if(!a.escape&&!b.escape){meta[i]=d.terminalClass>=0?((d.iter&ITER_MASK)|((d.terminalClass&3)<<28)):((d.iter&ITER_MASK)|(6<<REASON_SHIFT));accepted[i]=1}a=b}}postMessage({type:'solved',id:d.id,orbitCount,meta:meta.buffer,smooth:smoothOut.buffer,accepted:accepted.buffer},[meta.buffer,smoothOut.buffer,accepted.buffer])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
2026-08-25 21:33:45 +09:00
`}
class PrecisionFallbackService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.failed=false}
2026-09-06 23:28:03 +09:00
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([precisionFallbackWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else p.resolve({indices:p.indices,orbitCount:d.orbitCount||0,meta:new Uint32Array(d.meta),smooth:new Float32Array(d.smooth),accepted:new Uint8Array(d.accepted)})};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'precision fallback worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
2026-08-25 21:33:45 +09:00
request(snap,iter,width,height,indices,terminalClass=-1){if(this.pending.size)this.cancel('superseded precision fallback');if(!this.ensure())return Promise.reject(new Error('precision fallback Workerを作成できません'));const id=++this.serial,copy=Uint32Array.from(indices),targetBits=pixelPrecisionBits(snap,width,40);return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,indices:Uint32Array.from(indices)});this.worker.postMessage({type:'solve',id,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter,terminalClass,indices:copy.buffer},[copy.buffer])})}
cancel(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancel('destroyed')}
}
const precisionFallback=new PrecisionFallbackService();
2026-09-06 23:28:03 +09:00
const precisionFallbackPool=Array.from({length:Math.max(1,Math.min(2,(Number(navigator.hardwareConcurrency)||4)-1))},()=>new PrecisionFallbackService());
2026-08-25 14:03:13 +09:00
2026-08-23 23:27:38 +09:00
// ── 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();
2026-09-06 23:28:03 +09:00
this.frame=null;this.deepCtx=null;this.fastCtx=null;this.exportWs=[];this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.activeGpuStage='';this.lastCompletedGpuStage='';this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false;this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});this.reprojectSampler=device.createSampler({magFilter:'nearest',minFilter:'nearest'});
2026-08-23 23:27:38 +09:00
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();
2026-08-25 21:33:45 +09:00
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;const where=this.activeGpuStage||this.lastCompletedGpuStage||'unknown-stage';state.gpuStage=where;state.gpuError='device lost @ '+where+': '+this.lossReason;if(renderer===this){renderer=null;state.gpuInitFailed=true;markDirty(false);setTimeout(()=>{state.gpuInitFailed=false;initRenderer().then(()=>markDirty(false))},1200)}});
2026-08-25 14:03:13 +09:00
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'premultiplied'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>'line '+x.lineNum+':'+x.linePos+' '+x.message).join('\n'))}return m}
2026-08-23 23:27:38 +09:00
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
2026-08-25 21:33:45 +09:00
const [dm,um,sm,cm,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('unknown-stats',G.UNKNOWN_STATS_WGSL),this.module('real-axis-symmetry-copy',G.SYMMETRY_COPY_WGSL),this.module('color',G.COLOR_WGSL),this.module('present',G.PRESENT_WGSL)]);
2026-08-23 23:27:38 +09:00
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
2026-08-25 21:33:45 +09:00
this.unknownStats=this.device.createComputePipeline({layout:'auto',compute:{module:um,entryPoint:'main'}});
this.symmetryCopy=this.device.createComputePipeline({layout:'auto',compute:{module:sm,entryPoint:'main'}});
2026-08-23 23:27:38 +09:00
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
2026-08-25 14:03:13 +09:00
this.configure();
2026-08-23 23:27:38 +09:00
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
2026-08-25 21:33:45 +09:00
makeCompute(module){return this.device.createComputePipelineAsync?this.device.createComputePipelineAsync({layout:'auto',compute:{module,entryPoint:'main'}}):this.device.createComputePipeline({layout:'auto',compute:{module,entryPoint:'main'}})}
async ensureFastPipelines(){if(this.fast&&this.fastPostStats)return;if(this.fastPipelinePromise)return this.fastPipelinePromise;this.fastPipelinePromise=(async()=>{const [base,screen]=await Promise.all([this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('fast-perturb-post-stats',G.FAST_PERTURB_POSTSTATS_WGSL)]);[this.fast,this.fastPostStats]=await Promise.all([this.makeCompute(base),this.makeCompute(screen)])})();try{return await this.fastPipelinePromise}finally{this.fastPipelinePromise=null}}
async ensureDeepPipelines(){if(this.deep&&this.deepPostStats)return;if(this.deepPipelinePromise)return this.deepPipelinePromise;this.deepPipelinePromise=(async()=>{const [base,screen]=await Promise.all([this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL)]);[this.deep,this.deepPostStats]=await Promise.all([this.makeCompute(base),this.makeCompute(screen)])})();try{return await this.deepPipelinePromise}finally{this.deepPipelinePromise=null}}
async ensureCorrectionPipelines(){await this.ensureDeepPipelines();if(this.correct&&this.correctQueued&&this.deepBucketHist&&this.deepBucketPrefix&&this.deepBucketScatter&&this.failureTileMap)return;if(this.correctionPipelinePromise)return this.correctionPipelinePromise;this.correctionPipelinePromise=(async()=>{const modules=await Promise.all([this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('failure-tile-map',G.FAILURE_TILE_MAP_WGSL)]);[this.deepBucketHist,this.deepBucketPrefix,this.deepBucketScatter,this.correct,this.correctQueued,this.failureTileMap]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.correctionPipelinePromise}finally{this.correctionPipelinePromise=null}}
async ensureAaPipeline(){if(this.aaResolve)return;if(this.aaPipelinePromise)return this.aaPipelinePromise;this.aaPipelinePromise=(async()=>{this.aaResolve=await this.makeCompute(await this.module('aa-resolve',G.AA_RESOLVE_WGSL))})();try{return await this.aaPipelinePromise}finally{this.aaPipelinePromise=null}}
2026-09-06 23:28:03 +09:00
async ensureDeepActivePipelines(){if(this.activePrepare&&this.deepActiveResumeInit&&this.deepActiveContinue)return;if(this.deepActivePipelinePromise)return this.deepActivePipelinePromise;this.deepActivePipelinePromise=(async()=>{const modules=await Promise.all([this.module('active-prepare-production',G.ACTIVE_PREPARE_WGSL),this.module('deep-active-resume-init-production',G.DEEP_ACTIVE_RESUME_INIT_WGSL),this.module('deep-active-continue-production',G.DEEP_ACTIVE_CONTINUE_WGSL)]);[this.activePrepare,this.deepActiveResumeInit,this.deepActiveContinue]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.deepActivePipelinePromise}finally{this.deepActivePipelinePromise=null}}
async ensureCompactActivePipelines(){
if(this.deepCompactInit&&this.deepCompactContinue)return;
this.deepCompactInit=await this.makeCompute(await this.module('deep-compact-init',G.DEEP_COMPACT_INIT_WGSL));
this.deepCompactContinue=await this.makeCompute(await this.module('deep-compact-continue',G.DEEP_COMPACT_CONTINUE_WGSL));
}
2026-08-25 21:33:45 +09:00
clearBindGroupCache(){if(this.bindGroupCache)this.bindGroupCache.clear()}
bindResourceId(resource){const object=resource?.buffer||resource;if(!object||typeof object!=='object')return String(object);if(!this.bindResourceIds){this.bindResourceIds=new WeakMap();this.nextBindResourceId=1}let id=this.bindResourceIds.get(object);if(!id){id=this.nextBindResourceId++;this.bindResourceIds.set(object,id)}return id}
cachedTextureView(texture){if(!this.textureViews)this.textureViews=new WeakMap();let view=this.textureViews.get(texture);if(!view){view=texture.createView();this.textureViews.set(texture,view)}return view}
colorTarget(f=this.frame){if(!f)throw new Error('color target requested without a frame');if(f.back===f.history){const old=f.history;f.back=f.spare;f.spare=old}return f.back}
cachedBindGroup(pipeline,entries){if(!this.bindGroupCache)this.bindGroupCache=new Map();const key=this.bindResourceId(pipeline)+'|'+entries.map(e=>e.binding+':'+this.bindResourceId(e.resource)+':'+(e.resource?.offset||0)+':'+(e.resource?.size||0)).join('|');let bg=this.bindGroupCache.get(key);if(!bg){bg=this.device.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries});this.bindGroupCache.set(key,bg)}return bg}
2026-09-06 23:28:03 +09:00
frameDestroy(){this.releaseColorSource();if(!this.frame)return;this.clearBindGroupCache();for(const ring of Object.values(this.frame.readbackRings||{}))for(const b of ring.buffers)destroy(b);for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead','failureMap','failureParams','numericParams','statsParams','symmetryParams','colorParams','presentParams'])destroy(this.frame[k]);for(const t of new Set([this.frame.front,this.frame.back,this.frame.history,this.frame.spare]))destroy(t);this.frame=null;this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false}
2026-08-23 23:27:38 +09:00
ensureFrame(w,h){
2026-08-25 21:33:45 +09:00
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,colorUsage=T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,front=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'front-color'}),back=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'back-color'}),history=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'stable-history'});
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,deepActiveCapacity:0,deepActiveState:null,deepActiveQueueA:null,deepActiveQueueB:null,deepActiveCountA:null,deepActiveCountB:null,deepActiveIndirect:null,deepActiveRead:null,failureMap:null,failureMapBytes:0,readbackRings:{},failureParams:buf(d,32,B.UNIFORM|B.COPY_DST,'failure-map-params'),numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),statsParams:buf(d,16,B.UNIFORM|B.COPY_DST,'unknown-stats-params'),symmetryParams:buf(d,16,B.UNIFORM|B.COPY_DST,'symmetry-copy-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,48,B.UNIFORM|B.COPY_DST,'present-params'),front,back,history,spare:history};return this.frame;
2026-08-23 23:27:38 +09:00
}
2026-09-06 23:28:03 +09:00
frameReadback(kind,size){if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+size;const f=this.frame;if(!f)throw new Error('frame readback requested without a frame');let ring=f.readbackRings[kind];if(!ring){const B=GPUBufferUsage;ring=f.readbackRings[kind]={cursor:0,buffers:Array.from({length:3},(_,i)=>buf(this.device,size,B.COPY_DST|B.MAP_READ,kind+'-readback-'+i))}}for(let tries=0;tries<ring.buffers.length;tries++){const i=ring.cursor++%ring.buffers.length,b=ring.buffers[i];if(!b.mapState||b.mapState==='unmapped')return b}throw new Error(kind+' readback ring is saturated')}
2026-08-24 20:30:26 +09:00
ensureDeepBucketState(f=this.frame){if(!f)return null;if(!f.deepBucketState){const d=this.device,B=GPUBufferUsage;f.deepBucketState=buf(d,DEEP_BUCKET_STATE_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-bucket-state')}return f}
ensureDeepQueueWorkspace(f=this.frame){if(!f)return null;this.ensureDeepBucketState(f);if(f.deepQueue&&f.deepQueueStats&&f.deepIndirect)return f;const d=this.device,B=GPUBufferUsage;f.deepQueueStats=buf(d,SPARSE_QUEUE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue-stats');f.deepQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue');f.deepIndirect=buf(d,SPARSE_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'deep-correction-indirect');return f}
2026-09-06 23:28:03 +09:00
ensureDeepActiveWorkspace(capacity,f=this.frame,stride=32){if(!f)return null;if(f.deepActiveCapacity>=capacity&&capacity>=f.deepActiveCapacity/4&&f.deepActiveState&&f.deepActiveStride===stride)return f;this.clearBindGroupCache();for(const k of ['deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead'])destroy(f[k]);const d=this.device,B=GPUBufferUsage;f.deepActiveCapacity=capacity;f.deepActiveStride=stride;f.deepActiveState=buf(d,capacity*stride,B.STORAGE|B.COPY_DST,'deep-active-state');f.deepActiveQueueA=buf(d,capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-queue-a');f.deepActiveQueueB=buf(d,capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-queue-b');f.deepActiveCountA=buf(d,4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-count-a');f.deepActiveCountB=buf(d,4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-count-b');f.deepActiveIndirect=buf(d,12,B.STORAGE|B.INDIRECT|B.COPY_DST,'deep-active-indirect');f.deepActiveRead=buf(d,4,B.COPY_DST|B.MAP_READ,'deep-active-count-readback');return f}
2026-08-25 21:33:45 +09:00
ensureFailureMap(bytes,f=this.frame){if(!f)return null;if(f.failureMap&&f.failureMapBytes>=bytes)return f;destroy(f.failureMap);const d=this.device,B=GPUBufferUsage;f.failureMap=buf(d,bytes,B.STORAGE|B.COPY_SRC|B.COPY_DST,'numerical-failure-tile-map');f.failureMapBytes=bytes;return f}
exportTileSize(){const l=this.device.limits||{},memory=Number(navigator.deviceMemory||8),storage=Number(l.maxStorageBufferBindingSize||0),maxBuffer=Number(l.maxBufferSize||0);return memory<=4||(storage&&storage<2*1024*1024)||(maxBuffer&&maxBuffer<8*1024*1024)?256:512}
exportRingDepth(tile=this.exportTileSize()){const memory=Number(navigator.deviceMemory||8),limit=Number(this.device.limits?.maxBufferSize||0);return memory<=4||(limit&&limit<32*1024*1024)||tile<512?2:3}
destroyExportWorkspace(ws){if(!ws)return;this.clearBindGroupCache();for(const k of ['meta','smooth','cbuf','tex','read'])destroy(ws[k]);for(const b of ws.unresolveds)destroy(b);for(const b of ws.pbufs)destroy(b);for(const t of ws.samples)destroy(t)}
ensureExportWorkspace(slot=0,S=this.exportTileSize()){
const old=this.exportWs[slot];if(old&&old.size===S)return old;if(old)this.destroyExportWorkspace(old);const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,n=S*S,bpr=S*4,pixelBytes=bpr*S,label='export-'+slot+'-';
const ws={slot,size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,label+'unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,label+'numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,label+'color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:label+'sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:label+'resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,label+'readback')};this.exportWs[slot]=ws;return ws;
2026-08-23 23:27:38 +09:00
}
2026-08-25 21:33:45 +09:00
exportWorkspaceDestroy(){for(const ws of this.exportWs)this.destroyExportWorkspace(ws);this.exportWs=[]}
2026-09-06 23:28:03 +09:00
setDeepContext(ctx){
if(this.deepCtx?.key===ctx.key)return;
const d=this.device,B=GPUBufferUsage,id=ctx.referenceId||ctx.key;
if(!this.deepContexts)this.deepContexts=new Map();
let old=this.deepContexts.get(id),refsB=old?.refsB;
const bytes=ctx.refs.byteLength;
if(!refsB||refsB.size<bytes){
const capacity=Math.min((PIXEL_FRONTIER_MAX_ITER+1)*16,Math.max(bytes,old?old.refsB.size*2:bytes));
refsB=buf(d,capacity,B.STORAGE|B.COPY_DST,'reference-orbit');
d.queue.writeBuffer(refsB,0,ctx.refs);
if(old){const retired=old.refsB;d.queue.onSubmittedWorkDone().then(()=>destroy(retired),()=>destroy(retired));this.clearBindGroupCache();}
}else if(bytes>old.refs.byteLength){
const offset=old.refs.byteLength;d.queue.writeBuffer(refsB,offset,ctx.refs,offset,bytes-offset);
}
// A shorter cached result must not forget the uploaded prefix length.
const entry=old&&old.refs.byteLength>bytes?{...ctx,refsB,refs:old.refs}:{...ctx,refsB};
this.deepContexts.delete(id);this.deepContexts.set(id,entry);this.deepCtx=entry;
while(this.deepContexts.size>3){const key=this.deepContexts.keys().next().value,retired=this.deepContexts.get(key);this.deepContexts.delete(key);d.queue.onSubmittedWorkDone().then(()=>destroy(retired.refsB),()=>destroy(retired.refsB));this.clearBindGroupCache();}
}
destroyDeepContext(){
for(const ctx of this.deepContexts?.values()||[])destroy(ctx.refsB);
this.deepContexts?.clear();this.deepCtx=null;this.clearBindGroupCache();
}
2026-08-24 20:30:26 +09:00
setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}}
2026-09-06 23:28:03 +09:00
destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null;this.clearBindGroupCache()}
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,outputStride=w,outputBase=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true);d.setUint32(52,outputStride,true);d.setUint32(56,outputBase,true)})}
2026-08-25 21:33:45 +09:00
directActiveParams(w,h,iter,snap,targetIter=iter,chunkSize=128){const data=this.directParams(w,h,w,h,0,0,iter,snap);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(64,targetIter,true);d.setUint32(68,chunkSize,true)})}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0,queueOffset=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true);d.setUint32(92,queueOffset,true)})}
deepActiveParams(w,h,iter,snap,refPixel,targetIter=iter,chunkSize=128,strict=0,selectionCount=0){const data=this.deepParams(w,h,w,h,0,0,iter,snap,.5,.5,strict,refPixel.x,refPixel.y,selectionCount,w,0);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(84,targetIter,true);d.setUint32(88,chunkSize,true)})}
deepActiveTileParams(w,h,fullW,fullH,tileX,tileY,iter,snap,refPixel,targetIter=iter,chunkSize=128,strict=0,outputStride=fullW,outputBase=0){const data=this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,strict,refPixel.x,refPixel.y,0,outputStride,outputBase);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(84,targetIter,true);d.setUint32(88,chunkSize,true)})}
certifiedInteriorParams(count,ctx,snap,width,refPixel){const se=spanMantExp(snap),hi=Math.fround(se.mant);return writeU32F32(48,d=>{d.setUint32(0,count,true);d.setUint32(4,ctx.refLen,true);d.setInt32(8,se.exp,true);d.setFloat32(16,hi,true);d.setFloat32(20,Math.fround(se.mant-hi),true);d.setFloat32(24,width,true);d.setFloat32(28,Math.fround(refPixel.x),true);d.setFloat32(32,Math.fround(refPixel.y),true)})}
2026-08-25 14:03:13 +09:00
fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0,unknownOnly=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
colorParamsData(w,h,_iter=state.fieldView?.iter??maxIter(),style=colorStyleSnapshot()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,style.palette,true);d.setUint32(12,style.hq?1:0,true);d.setFloat32(16,style.cycle,true);d.setFloat32(20,style.shift,true)})}
2026-08-25 21:33:45 +09:00
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const pipeline=this.deepPostStats,bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const bg=this.cachedBindGroup(this.direct,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const bg=this.cachedBindGroup(this.fast,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass();pass.setPipeline(this.fast);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeFastPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const bg=this.cachedBindGroup(this.fastPostStats,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]),pass=encoder.beginComputePass();pass.setPipeline(this.fastPostStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeUnknownStats(encoder,{meta,unresolved,n,params=this.frame?.statsParams}){const d=this.device;if(!params)throw new Error('unknown stats params are missing');d.queue.writeBuffer(params,0,new Uint32Array([n,0,0,0]));encoder.clearBuffer(unresolved,0,28);const bg=this.cachedBindGroup(this.unknownStats,[{binding:0,resource:{buffer:params}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass({label:'unknown-post-stats'});pass.setPipeline(this.unknownStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(n/256));pass.end()}
encodeSymmetryCopy(encoder,{meta,smooth,w,h,params=this.frame?.symmetryParams}){if(h<2)return;const d=this.device;if(!params)throw new Error('symmetry params are missing');d.queue.writeBuffer(params,0,new Uint32Array([w,h,0,0]));const bg=this.cachedBindGroup(this.symmetryCopy,[{binding:0,resource:{buffer:params}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]),pass=encoder.beginComputePass({label:'real-axis-symmetry-copy'});pass.setPipeline(this.symmetryCopy);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(Math.floor(h/2)/8));pass.end()}
encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const bg=this.cachedBindGroup(this.deepBucketHist,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHist);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const bg=this.cachedBindGroup(this.deepBucketHistStats,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
encodeDeepBucketPrefix(encoder,{bucketState,queueStats,indirect}){const bg=this.cachedBindGroup(this.deepBucketPrefix,[{binding:0,resource:{buffer:bucketState}},{binding:1,resource:{buffer:queueStats}},{binding:2,resource:{buffer:indirect}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketPrefix);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end()}
encodeDeepBucketScatter(encoder,{pbuf,meta,bucketState,queueStats,queue,w,h}){const bg=this.cachedBindGroup(this.deepBucketScatter,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
encodeQueuedDeepCorrection(encoder,{pbuf,meta,smooth,unresolved,queueStats,queue,indirect}){const bg=this.cachedBindGroup(this.correctQueued,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{binding:5,resource:{buffer:queueStats}},{binding:6,resource:{buffer:queue}}]),pass=encoder.beginComputePass();pass.setPipeline(this.correctQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()}
2026-08-23 23:27:38 +09:00
encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
2026-08-25 21:33:45 +09:00
numericInFlightBudgetMs(){return state.pointerActive||state.wheelActive||performance.now()-state.lastInteraction<180?24:40}
numericBurstLimit(predictedStripMs){return Math.max(1,Math.min(8,Math.floor(this.numericInFlightBudgetMs()/Math.max(1,Number(predictedStripMs)||20))))}
2026-09-06 23:28:03 +09:00
numericTileShape(w,iter,deep){
const maxPixels=Math.floor((deep?4000000:48000000)/Math.max(1,iter));
if(maxPixels<1)throw new Error('iteration budget requires continuation');
const width=Math.min(w,maxPixels);
return {width,rows:Math.max(1,Math.min(128,Math.floor(maxPixels/width))),maxPixels};
}
2026-08-25 14:03:13 +09:00
initialNumericRows(w,iter,deep){
// Structural workload cap, not a zoom threshold. The first submission is
// intentionally conservative; subsequent strips self-tune from measured
// queue completion time. This keeps one GPU submission bounded even when
// maxIter reaches 12000.
2026-08-25 21:33:45 +09:00
const workBudget=deep?4000000:48000000;
2026-08-25 14:03:13 +09:00
return Math.max(1,Math.min(128,Math.floor(workBudget/Math.max(1,w*iter))));
}
adaptNumericRows(rows,elapsedMs,remaining,deep){
if(remaining<=0)return rows;
// Never grow beyond the initial work-bound. Timing feedback may only
// reduce a strip if this adapter is slower than expected, so the
// worst-case pixel-iteration cap remains invariant across the frame.
const targetMs=deep?18:22,ms=Math.max(.25,Number(elapsedMs)||targetMs);
if(ms<=targetMs*1.25)return Math.max(1,Math.min(rows,remaining));
const factor=Math.max(.5,Math.min(1,Math.sqrt(targetMs/ms)));
return Math.max(1,Math.min(rows,remaining,Math.round(rows*factor)));
}
async submitStage(encoder,stage,token,counter='numeric'){
this.activeGpuStage=stage;state.gpuStage=stage;
const t0=performance.now();this.device.queue.submit([encoder.finish()]);
await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++;
const ms=performance.now()-t0;this.lastCompletedGpuStage=stage;this.activeGpuStage='';
2026-09-06 23:28:03 +09:00
if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++;if(TEST_MODE){const stageKey=String(stage).replace(/\d+/g,'#');runtime.stageCounts[stageKey]=(runtime.stageCounts[stageKey]||0)+1;runtime.stageMs[stageKey]=(runtime.stageMs[stageKey]||0)+ms;}
2026-08-25 14:03:13 +09:00
return token===undefined||token===state.token?ms:null;
}
submitStageDeferred(encoder,stage,counter='numeric'){this.activeGpuStage=stage;state.gpuStage=stage;this.device.queue.submit([encoder.finish()]);if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++;return stage}
async waitStageBurst(stage,token,t0,count=1){await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++;const ms=(performance.now()-t0)/Math.max(1,count);this.lastCompletedGpuStage=stage;this.activeGpuStage='';return token===undefined||token===state.token?ms:null}
2026-09-06 23:28:03 +09:00
encodeDeepActiveResumeInit(e,{pbuf,stateB,queue,count,pipeline=this.deepActiveResumeInit}){const d=this.device,bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:stateB}},{binding:2,resource:{buffer:queue}}]),pass=e.beginComputePass({label:'deep-active-resume-init-production'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(count/64));pass.end()}
async readDeepActiveCount(buffer){if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+4;await buffer.mapAsync(GPUMapMode.READ,0,4);const n=new Uint32Array(buffer.getMappedRange(0,4))[0]||0;buffer.unmap();return n}
async certifyInterior(snap,token){
if(token!==state.token)return 0;
const f=this.frame,{mask,pixels}=certifiedInteriorTiles(snap,f.w,f.h);if(!pixels)return 0;
if(!this.interiorMask)this.interiorMask=await this.makeCompute(await this.module('exact-interior-mask',G.INTERIOR_MASK_WGSL));
if(token!==state.token)return 0;
const d=this.device,input=buf(d,mask.byteLength,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST,'interior-mask');
try{
d.queue.writeBuffer(input,0,mask);const e=d.createCommandEncoder();
const bg=d.createBindGroup({layout:this.interiorMask.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:input}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]});
const p=e.beginComputePass();p.setPipeline(this.interiorMask);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
return await this.submitStage(e,'exact interior tiles',token,'correction')===null?0:pixels;
}finally{destroy(input)}
}
async readActiveIndices(session,slots=false){
if(!session.active)return new Uint32Array();
const d=this.device,bytes=session.active*4;if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+bytes;const r=buf(d,bytes,GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ,'active-indices');
try{const e=d.createCommandEncoder();e.copyBufferToBuffer(session.queues[session.input],0,r,0,bytes);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return session.compact&&!slots?Uint32Array.from(out,i=>session.mapping[i]):out;}finally{destroy(r)}
}
2026-08-25 21:33:45 +09:00
async beginOperationLimitContinuation(snap,token,ctx,refPixel,indices,strict=false){
2026-09-06 23:28:03 +09:00
await this.ready;await this.ensureDeepActivePipelines();if(!ctx)throw new Error('active continuation reference context is missing');const f=this.ensureFrame(canvas.width,canvas.height),limit=Math.min(Number(this.device.limits?.maxStorageBufferBindingSize||this.adapterLimits.maxStorageBufferBindingSize||Infinity),Number(this.device.limits?.maxBufferSize||this.adapterLimits.maxBufferSize||Infinity)),compact=indices.length*52<f.n*32,stride=compact?40:32,capacity=compact?indices.length:f.n,stateBytes=capacity*stride;if(stateBytes>limit)throw new Error('active continuation state exceeds adapter buffer limit: '+stateBytes+' > '+limit);if(indices.length>f.n)throw new Error('active continuation candidate overflow');this.setDeepContext(ctx);if(compact)await this.ensureCompactActivePipelines();if(token!==state.token)return null;this.ensureDeepActiveWorkspace(capacity,f,stride);const d=this.device,queues=[f.deepActiveQueueA,f.deepActiveQueueB],counts=[f.deepActiveCountA,f.deepActiveCountB],count=indices.length;d.queue.writeBuffer(queues[0],0,indices);d.queue.writeBuffer(counts[0],0,new Uint32Array([count]));d.queue.writeBuffer(counts[1],0,new Uint32Array([0]));d.queue.writeBuffer(f.numericParams,0,this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER+1,snap,refPixel,0,1,strict?1:0,count));const e=d.createCommandEncoder({label:'all-pixel-active-init'});this.encodeDeepActiveResumeInit(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queue:queues[0],count,pipeline:compact?this.deepCompactInit:this.deepActiveResumeInit});const ms=await this.submitStage(e,'all-pixel active init '+count,token,'correction');if(ms===null)return null;if(TEST_MODE)runtime.activeInitialized=(runtime.activeInitialized||0)+count;return{f,queues,counts,compact,mapping:compact?indices:null,pipeline:compact?this.deepCompactContinue:this.deepActiveContinue,input:0,active:count,initial:count,progress:0,maxDispatchWork:0,dispatches:0,pixelIterations:0,strict:!!strict};
}
encodeDeepActiveChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input,passes,refsB=this.deepCtx.refsB,pipeline=this.deepActiveContinue}){const d=this.device;for(let k=0;k<passes;k++){const output=1-input;e.clearBuffer(counts[output]);let bg=d.createBindGroup({layout:this.activePrepare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:counts[input]}},{binding:1,resource:{buffer:indirect}}]}),pass=e.beginComputePass({label:'deep-active-indirect-prepare'});pass.setPipeline(this.activePrepare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end();bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:refsB}},{binding:2,resource:{buffer:stateB}},{binding:3,resource:{buffer:queues[input]}},{binding:4,resource:{buffer:counts[input]}},{binding:5,resource:{buffer:queues[output]}},{binding:6,resource:{buffer:counts[output]}},{binding:7,resource:{buffer:meta}},{binding:8,resource:{buffer:smooth}}]});pass=e.beginComputePass({label:'deep-active-chunk'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end();input=output}return input}
async advancePriorityTarget(session,snap,target,token,ctx,refPixel,previousMeta){
// Small queues are faster as a single batch. For large expensive rounds,
// show the existing boundary first, then give every remaining pixel its
// full target. Queue ordering never changes the finite iteration budget.
if(session.active<4096||(target-session.progress)*session.active<8000000)
return this.continueOperationLimitActive(session,snap,target,token,ctx,refPixel);
const edge=new Set(operationLimitFrontier(previousMeta,session.f.w,session.f.h));
if(!edge.size)return this.continueOperationLimitActive(session,snap,target,token,ctx,refPixel);
const slots=await this.readActiveIndices(session,true);if(token!==state.token)return null;
const groups=[[],[]];for(const slot of slots){const pixel=session.compact?session.mapping[slot]:slot;groups[edge.has(pixel)?0:1].push(slot);}
if(!groups[0].length||!groups[1].length)return this.continueOperationLimitActive(session,snap,target,token,ctx,refPixel);
const start=session.progress,survivors=[],d=this.device;
for(const group of groups){
if(token!==state.token)return null;
d.queue.writeBuffer(session.queues[0],0,Uint32Array.from(group));d.queue.writeBuffer(session.counts[0],0,Uint32Array.of(group.length));
session.input=0;session.active=group.length;session.progress=start;
if(!await this.continueOperationLimitActive(session,snap,target,token,ctx,refPixel))return null;
survivors.push(await this.readActiveIndices(session,true));if(token!==state.token)return null;
if(!await presentProvisional(this,snap,target,token))return null;
}
const active=survivors.reduce((n,g)=>n+g.length,0),merged=new Uint32Array(active);let offset=0;
for(const group of survivors){merged.set(group,offset);offset+=group.length;}
if(active)d.queue.writeBuffer(session.queues[0],0,merged);d.queue.writeBuffer(session.counts[0],0,Uint32Array.of(active));
session.input=0;session.active=active;session.progress=target;return session;
2026-08-25 21:33:45 +09:00
}
async continueOperationLimitActive(session,snap,target,token,ctx,refPixel){
2026-09-06 23:28:03 +09:00
if(!session||!ctx)throw new Error('active continuation session/context is missing');
if(token!==state.token)return null;
this.setDeepContext(ctx);const {f,queues,counts}=session,d=this.device,refsB=this.deepCtx.refsB;
while(session.active>0&&session.progress<target){
if(token!==state.token)return null;
const before=session.active,remaining=target-session.progress;
const chunk=Math.min(256,remaining,Math.floor(PIXEL_FRONTIER_WORK/before));
if(chunk<1)throw new Error('active workspace exceeds per-dispatch work capacity');
const passes=Math.min(session.batchPasses||1,Math.ceil(remaining/chunk),4);
const params=this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER,snap,refPixel,target,chunk,session.strict?1:0);new DataView(params).setUint32(28,ctx.refLen,true);d.queue.writeBuffer(f.numericParams,0,params);
const e=d.createCommandEncoder({label:'active-batch'}),started=performance.now();
session.input=this.encodeDeepActiveChunks(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queues,counts,indirect:f.deepActiveIndirect,meta:f.meta,smooth:f.smooth,input:session.input,passes,refsB,pipeline:session.pipeline});
e.copyBufferToBuffer(counts[session.input],0,f.deepActiveRead,0,4);
this.submitStageDeferred(e,'active batch @ '+target,'correction');
// Mapping this copy provides the completion fence; no second queue wait.
session.active=await this.readDeepActiveCount(f.deepActiveRead);runtime.gpuSyncWaits++;
if(token!==state.token)return null;
if(session.active>before)throw new Error('active queue grew within a batch');
const elapsed=performance.now()-started,work=before*chunk;
session.progress=Math.min(target,session.progress+chunk*passes);
session.dispatches+=passes;session.pixelIterations+=work*passes;if(TEST_MODE){runtime.activeBatches=(runtime.activeBatches||0)+1;runtime.activeWorkBound=(runtime.activeWorkBound||0)+work*passes;}
session.maxDispatchWork=Math.max(session.maxDispatchWork,work);
runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,work);
session.batchPasses=elapsed<10?Math.min(4,passes+1):elapsed>32?1:passes;
if(TEST_MODE&&this.activeBatchLimit)session.batchPasses=Math.min(session.batchPasses,this.activeBatchLimit);
this.lastCompletedGpuStage='active batch';this.activeGpuStage='';
2026-08-25 21:33:45 +09:00
}
2026-09-06 23:28:03 +09:00
if(token!==state.token)return null;
const e=d.createCommandEncoder({label:'active-post-stats'});
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
this.submitStageDeferred(e,'active post-stats','correction');
return session;
2026-08-25 21:33:45 +09:00
}
2026-09-06 23:28:03 +09:00
async computePerturbFrameTiled(snap,iter,deep,token,forceStrict,referencePixel,fastExtended,colorStyle,colorKey){
2026-08-25 14:03:13 +09:00
const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;
d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-09-06 23:28:03 +09:00
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h,previous=state.fieldView,currentReferenceKey=(deep?this.deepCtx:this.fastCtx)?.key||'',reusePreview=!!previous&&sameViewSnapshot(previous,snap)&&previous.w===f.w&&previous.h===f.h&&previous.iter<iter&&previous.referenceKey===currentReferenceKey&&previous.deep===deep&&previous.fastExtended===fastExtended;
const tileWidth=this.numericTileShape(f.w,iter,deep).width;let rows=this.initialNumericRows(tileWidth,iter,deep),lastUi=0,burstCount=0,burstStart=performance.now(),lastStage='',predictedStripMs=deep?18:22,burstLimit=this.numericBurstLimit(predictedStripMs);
2026-08-25 21:33:45 +09:00
for(let y=0;y<computeH;){
2026-08-25 14:03:13 +09:00
if(token!==state.token)return false;
2026-09-06 23:28:03 +09:00
const th=Math.min(rows,computeH-y);
for(let x=0;x<f.w;x+=tileWidth){
if(token!==state.token)return false;
const tw=Math.min(tileWidth,f.w-x),base=y*f.w+x;
const params=(deep?this.deepParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,forceStrict?1:0,refX,refY,reusePreview?1:0,f.w,base):this.fastParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,refX,refY,f.w,base,reusePreview?1:0));
2026-08-25 14:03:13 +09:00
d.queue.writeBuffer(f.numericParams,0,params);
2026-09-06 23:28:03 +09:00
const e=d.createCommandEncoder({label:(deep?'deep':'fast')+'-numeric-strip-'+y});
2026-08-25 14:03:13 +09:00
// A fresh numeric frame starts from an explicit zero/UNKNOWN sentinel.
// If any strip or pixel is accidentally left untouched, the coverage
// repair below can detect and recompute it instead of publishing stale
// metadata or a rectangular transparent/black hole.
2026-09-06 23:28:03 +09:00
if(x===0&&y===0&&!reusePreview){e.clearBuffer(f.meta);e.clearBuffer(f.smooth);}
if(deep)this.encodeDeepPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});
else this.encodeFastPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});
lastStage=(deep?'deep':'fast')+' primary rows '+y+'..'+(y+th-1);
2026-08-25 14:03:13 +09:00
this.submitStageDeferred(e,lastStage,'numeric');
2026-09-06 23:28:03 +09:00
burstCount++;state.gpuTileRows=th;
if(burstCount>=burstLimit||x+tw>=f.w){
const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;
2026-08-25 21:33:45 +09:00
rows=this.adaptNumericRows(rows,ms,computeH-y,deep);predictedStripMs=.65*predictedStripMs+.35*ms;burstLimit=this.numericBurstLimit(predictedStripMs);burstCount=0;burstStart=performance.now();
2026-09-06 23:28:03 +09:00
const now=performance.now();if(now-lastUi>100){lastUi=now;state.lastEngine='WebGPU · '+(deep?'高精度補修':'高速拡張')+' 計算 '+Math.round(100*y/computeH)+'%';updateStats()}
2026-08-25 14:03:13 +09:00
}
2026-09-06 23:28:03 +09:00
}
y+=th;
2026-08-25 14:03:13 +09:00
}
2026-08-25 21:33:45 +09:00
if(deep){
const e=d.createCommandEncoder({label:'deep-unknown-post-stats'});if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
const ms=await this.submitStage(e,'deep UNKNOWN post-stats',token,'numeric');if(ms===null)return false;
}else{
2026-08-25 14:03:13 +09:00
// Coverage contract: zero metadata means a pixel was never written by
// the tiled primary path. Re-run only those pixels through the original
// FAST recurrence before colouring. This pass is O(pixels) for the
// trivial guard but performs iterations only for actual coverage holes.
d.queue.writeBuffer(f.numericParams,0,this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY,f.w,0,2));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));
const e=d.createCommandEncoder({label:'fast-coverage-repair-and-color'});
2026-08-25 21:33:45 +09:00
if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});
2026-08-25 14:03:13 +09:00
this.encodeFastNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});
2026-08-25 21:33:45 +09:00
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),cp=e.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
2026-09-06 23:28:03 +09:00
const ms=await this.submitStage(e,'fast coverage + color',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;
2026-08-25 14:03:13 +09:00
}
runtime.gpuFrames++;return true;
2026-08-23 23:27:38 +09:00
}
2026-09-06 23:28:03 +09:00
async computeFrame(snap,iter,token,referencePixel=null,fastExtended=false,fastContext=null){
await this.ready;if(fastExtended)await this.ensureFastPipelines();if(token!==state.token)return false;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);
if(fastExtended){this.setFastContext(fastContext);return this.computePerturbFrameTiled(snap,iter,false,token,false,referencePixel,true,colorStyle,colorKey)}
// Direct arithmetic is unchanged; large frames use bounded tiles.
2026-08-25 14:03:13 +09:00
d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-09-06 23:28:03 +09:00
const mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h,tiled=f.w*computeH*iter>48000000;
if(tiled){const shape=this.numericTileShape(f.w,iter,false);for(let y=0;y<computeH;y+=shape.rows)for(let x=0;x<f.w;x+=shape.width){
if(token!==state.token)return false;const tw=Math.min(shape.width,f.w-x),th=Math.min(shape.rows,computeH-y);
d.queue.writeBuffer(f.numericParams,0,this.directParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,0,f.w,y*f.w+x));
const e=d.createCommandEncoder();this.encodeDirectNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});
if(await this.submitStage(e,'direct bounded tile',token,'numeric')===null)return false;
}}else d.queue.writeBuffer(f.numericParams,0,this.directParams(f.w,computeH,f.w,f.h,0,0,iter,snap));
2026-08-25 14:03:13 +09:00
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const encoder=d.createCommandEncoder({label:'mandelbrot-direct-frame'});
2026-09-06 23:28:03 +09:00
if(!tiled)this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:computeH});
2026-08-25 21:33:45 +09:00
if(mirror)this.encodeSymmetryCopy(encoder,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});
this.encodeUnknownStats(encoder,{meta:f.meta,unresolved:f.unresolved,n:f.n});
const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
2026-08-25 14:03:13 +09:00
const ms=await this.submitStage(encoder,'direct primary',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuFrames++;return true;
}
2026-09-06 23:28:03 +09:00
releaseColorSource(){
const source=this.colorSource;this.colorSource=null;this.colorSourceVisible=false;
if(source){this.clearBindGroupCache();for(const k of ['meta','smooth','params','texture'])destroy(source[k])}
}
captureColorSource(view=state.frameView){
this.colorSourceVisible=false;
if(!state.colorAuto){this.releaseColorSource();return}
const f=this.frame;if(!f||!view)return;
const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
if(!this.colorSource){this.colorSource={
meta:buf(d,f.n*4,B.STORAGE|B.COPY_DST,'color-snapshot-meta'),
smooth:buf(d,f.n*4,B.STORAGE|B.COPY_DST,'color-snapshot-smooth'),
params:buf(d,32,B.UNIFORM|B.COPY_DST,'color-snapshot-params'),
texture:d.createTexture({size:[f.w,f.h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'animated-published-color'})
}}
const source=this.colorSource;source.view={...view};
const e=d.createCommandEncoder({label:'published-color-snapshot'});
e.copyBufferToBuffer(f.meta,0,source.meta,0,f.n*4);e.copyBufferToBuffer(f.smooth,0,source.smooth,0,f.n*4);d.queue.submit([e.finish()]);
}
recolorPublished(){
// Only published numerical data is sampled during in-flight refinement.
// This pass uses its own uniforms/texture and never swaps numeric front/back.
const source=this.colorSource,f=this.frame;if(!source||!f)return false;
const d=this.device;d.queue.writeBuffer(source.params,0,this.colorParamsData(f.w,f.h));
const bg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:source.params}},{binding:1,resource:{buffer:source.meta}},{binding:2,resource:{buffer:source.smooth}},{binding:3,resource:this.cachedTextureView(source.texture)}]);
const e=d.createCommandEncoder({label:'animate-published-color'}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);
this.colorSourceVisible=true;runtime.gpuRecolors++;this.presentFrame();return true;
}
async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(token!==state.token||!this.frame)return false;const f=this.frame,d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder(),bg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuRecolors++;return true}
2026-08-23 23:27:38 +09:00
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)}}
2026-09-06 23:28:03 +09:00
commitHistory(view=state.frameView){if(!this.frame||!view||state.fieldView?.complete===false)return false;const f=this.frame;if(f.history!==f.front){f.history=f.front;this.clearBindGroupCache()}this.historyView=view;this.historyColorKey=this.frontColorKey;this.historyReady=true;const metric=generationMetric();if(metric&&!metric.historyCommitMs)metric.historyCommitMs=performance.now()-metric.startedAt;return true}
2026-08-25 14:03:13 +09:00
invalidateHistory(){this.historyView=null;this.historyColorKey='';this.historyReady=false}
2026-09-06 23:28:03 +09:00
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const colored=this.colorSourceVisible&&this.colorSource;if(colored)transform=this.presentTransform(colored.view);const f=this.frame,d=this.device,pb=f.presentParams,interactive=state.pointerActive||state.wheelActive,finite=Number.isFinite(transform?.scaleX)&&Number.isFinite(transform?.scaleY)&&Number.isFinite(transform?.offsetX)&&Number.isFinite(transform?.offsetY)&&transform.scaleX>0&&transform.scaleY>0,ft=finite?transform:{scaleX:1,scaleY:1,offsetX:0,offsetY:0},historyCandidate=!!(!colored&&state.temporalFill&&this.historyReady&&this.historyView&&this.historyColorKey===colorStyleKey()),rawHistory=historyCandidate?this.presentTransform(this.historyView):{scaleX:1,scaleY:1,offsetX:0,offsetY:0},useHistory=!!(historyCandidate&&reprojectionSafe(rawHistory,2.25,.45)),ht=useHistory?rawHistory:{scaleX:1,scaleY:1,offsetX:0,offsetY:0};d.queue.writeBuffer(pb,0,new Float32Array([ft.scaleX,ft.scaleY,ft.offsetX,ft.offsetY,ht.scaleX,ht.scaleY,ht.offsetX,ht.offsetY,useHistory?1:0,0,0,0]));const bg=this.cachedBindGroup(this.present,[{binding:0,resource:interactive?this.reprojectSampler:this.sampler},{binding:1,resource:this.cachedTextureView(colored?colored.texture:f.front)},{binding:2,resource:this.cachedTextureView(f.history)},{binding:3,resource:{buffer:pb}}]),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:0,g:0,b:0,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
2026-08-23 23:27:38 +09:00
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}
2026-09-06 23:28:03 +09:00
async readNumericalIndices(token){
await this.ensureCorrectionPipelines();if(token!==state.token)return null;
const f=this.ensureDeepQueueWorkspace(this.frame),d=this.device;
const params=writeU32F32(NUMERIC_PARAM_BYTES,p=>{[f.w,f.h,f.w,f.h,0,0,PIXEL_FRONTIER_MAX_ITER,0,0,2,f.w,0].forEach((v,i)=>p.setUint32(i*4,v,true))});
d.queue.writeBuffer(f.numericParams,0,params);
let e=d.createCommandEncoder({label:'numerical-failure-index-build'});e.clearBuffer(f.deepBucketState);e.clearBuffer(f.deepQueueStats);e.clearBuffer(f.deepIndirect);
this.encodeDeepBucketHistogram(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,w:f.w,h:f.h});
this.encodeDeepBucketPrefix(e,{bucketState:f.deepBucketState,queueStats:f.deepQueueStats,indirect:f.deepIndirect});
this.encodeDeepBucketScatter(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,queueStats:f.deepQueueStats,queue:f.deepQueue,w:f.w,h:f.h});
this.submitStageDeferred(e,'numerical failure indices','correction');
const q=await this.readDeepQueueStats();if(token!==state.token)return null;
if(q.selected!==q.enqueued||q.overflow||q.invalidIndex||q.selected>f.n)throw new Error('numerical failure queue overflow or missing pixel');
if(!q.selected)return new Uint32Array();
const bytes=q.selected*4;if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+bytes;const read=buf(d,bytes,GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ,'numerical-failure-indices');
try{e=d.createCommandEncoder();e.copyBufferToBuffer(f.deepQueue,0,read,0,bytes);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();return token===state.token?out:null;}finally{destroy(read)}
}
async readMetaAll(){if(!this.frame)return new Uint32Array();const f=this.frame,d=this.device,B=GPUBufferUsage,r=buf(d,f.n*4,B.COPY_DST|B.MAP_READ,'field-meta-readback'),e=d.createCommandEncoder({label:'field-meta-readback'});e.copyBufferToBuffer(f.meta,0,r,0,f.n*4);if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+f.n*4;d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
2026-08-25 21:33:45 +09:00
async readFieldAll(){if(!this.frame)return{meta:new Uint32Array(),smoothBits:new Uint32Array()};const f=this.frame,d=this.device,B=GPUBufferUsage,bytes=f.n*4,r=buf(d,bytes*2,B.COPY_DST|B.MAP_READ,'field-meta-smooth-readback'),e=d.createCommandEncoder({label:'field-meta-smooth-readback'});e.copyBufferToBuffer(f.meta,0,r,0,bytes);e.copyBufferToBuffer(f.smooth,0,r,bytes,bytes);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const raw=r.getMappedRange(),meta=new Uint32Array(raw.slice(0,bytes)),smoothBits=new Uint32Array(raw.slice(bytes,bytes*2));r.unmap();destroy(r);return{meta,smoothBits}}
async readNumericalFailureTiles(size){await this.ready;await this.ensureCorrectionPipelines();if(!this.frame)return[];const f=this.frame,d=this.device,B=GPUBufferUsage,cols=Math.ceil(f.w/size),rows=Math.ceil(f.h/size),stride=6,bytes=cols*rows*stride*4;this.ensureFailureMap(bytes,f);d.queue.writeBuffer(f.failureParams,0,new Uint32Array([f.w,f.h,size,cols,rows,0,0,0]));const read=buf(d,bytes,B.COPY_DST|B.MAP_READ,'failure-map-readback'),e=d.createCommandEncoder({label:'failure-tile-map'}),bg=d.createBindGroup({layout:this.failureTileMap.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.failureParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.failureMap}}]}),pass=e.beginComputePass({label:'failure-tile-map'});pass.setPipeline(this.failureTileMap);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(cols,rows);pass.end();e.copyBufferToBuffer(f.failureMap,0,read,0,bytes);d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();await read.mapAsync(GPUMapMode.READ);const raw=new Uint32Array(read.getMappedRange().slice(0));read.unmap();destroy(read);runtime.failureMapReadBytes=(runtime.failureMapReadBytes||0)+bytes;const tiles=[];for(let ty=0;ty<rows;ty++)for(let tx=0;tx<cols;tx++){const o=(ty*cols+tx)*stride,reasons={errorBound:raw[o]||0,escapeUncertain:raw[o+1]||0,referenceEnd:raw[o+2]||0,rebaseGap:raw[o+3]||0,range:raw[o+4]||0},numeric=reasons.errorBound+reasons.escapeUncertain+reasons.referenceEnd+reasons.rebaseGap+reasons.range;if(!numeric)continue;const x=tx*size,y=ty*size,w=Math.min(size,f.w-x),h=Math.min(size,f.h-y),representative=raw[o+5],cx=representative<f.n?representative%f.w+.5:x+w*.5,cy=representative<f.n?Math.floor(representative/f.w)+.5:y+h*.5;tiles.push({x,y,w,h,numeric,sumX:cx*numeric,sumY:cy*numeric,minX:x,minY:y,maxX:x+w-1,maxY:y+h-1,representative,reasons})}return tiles.sort((a,b)=>b.numeric-a.numeric)}
2026-09-06 23:28:03 +09:00
async applyPrecisionFallback(result,token){
if(token!==state.token||!this.frame||!result)return null;
if(!this.precisionScatter)this.precisionScatter=await this.makeCompute(await this.module('precision-scatter',G.PRECISION_SCATTER_WGSL));
if(token!==state.token)return null;
const f=this.frame,d=this.device,{indices,meta,smooth,accepted}=result;
const data=new ArrayBuffer(indices.length*16),u=new Uint32Array(data),v=new Float32Array(data);let applied=0;
for(let i=0;i<indices.length;i++){u[i*4]=indices[i];u[i*4+1]=meta[i];v[i*4+2]=smooth[i];u[i*4+3]=accepted[i];applied+=accepted[i]?1:0;}
if(!indices.length)return {applied:0};
const input=buf(d,data.byteLength,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST,'precision-results');
try{
if(token!==state.token)return null;
d.queue.writeBuffer(input,0,data);
const e=d.createCommandEncoder({label:'precision-results-scatter'});
const bg=d.createBindGroup({layout:this.precisionScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:input}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]});
const p=e.beginComputePass();p.setPipeline(this.precisionScatter);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(indices.length/64));p.end();
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
const ms=await this.submitStage(e,'precision scatter',token,'correction');
return ms===null?null:{applied,ms};
}finally{destroy(input)}
}
2026-08-25 14:03:13 +09:00
async recoverUnknownTile(snap,iter,token,deepContext,tile,referencePixel=null){
2026-08-25 21:33:45 +09:00
await this.ready;await this.ensureCorrectionPipelines();const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!deepContext)throw new Error('tile deep reference context is missing');this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-08-25 14:03:13 +09:00
const refX=referencePixel?.x??(tile.x+tile.w*.5),refY=referencePixel?.y??(tile.y+tile.h*.5);let rows=this.initialNumericRows(tile.w,iter,true);
2026-08-25 21:33:45 +09:00
for(let sy=0;sy<tile.h;){if(token!==state.token)return null;const th=Math.min(rows,tile.h-sy),gy=tile.y+sy,base=gy*f.w+tile.x;d.queue.writeBuffer(f.numericParams,0,this.deepParams(tile.w,th,f.w,f.h,tile.x,gy,iter,snap,.5,.5,1,refX,refY,2,f.w,base));const e=d.createCommandEncoder({label:'tile-multireference-recovery-'+gy});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:tile.w,h:th});const ms=await this.submitStage(e,'multi-ref correction rows '+gy+'..'+(gy+th-1),token,'correction');if(ms===null)return null;sy+=th;rows=this.adaptNumericRows(rows,ms,tile.h-sy,true)}const statsEncoder=d.createCommandEncoder({label:'tile-recovery-post-stats'});this.encodeUnknownStats(statsEncoder,{meta:f.meta,unresolved:f.unresolved,n:f.n});const statsMs=await this.submitStage(statsEncoder,'tile recovery post-stats',token,'correction');if(statsMs===null)return null;runtime.correctionPasses++;runtime.multiRefBuilds++;return this.readUnresolvedStats()
2026-08-25 14:03:13 +09:00
}
2026-08-25 21:33:45 +09:00
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,r=this.frameReadback('unknown-stats',UNRESOLVED_BYTES),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
2026-08-23 23:27:38 +09:00
async readUnresolved(){return(await this.readUnresolvedStats()).total}
2026-08-25 21:33:45 +09:00
async readDeepQueueStats(){if(!this.frame?.deepQueueStats)return{selected:0,overflow:0,enqueued:0,dispatchCount:0,invalidIndex:0,staleEntry:0,processed:0};const d=this.device,r=this.frameReadback('deep-queue-stats',SPARSE_QUEUE_STATS_BYTES),e=d.createCommandEncoder({label:'deep-queue-stats-readback'});e.copyBufferToBuffer(this.frame.deepQueueStats,0,r,0,SPARSE_QUEUE_STATS_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return{selected:a[0]||0,overflow:a[1]||0,enqueued:a[2]||0,dispatchCount:a[3]||0,invalidIndex:a[4]||0,staleEntry:a[5]||0,processed:a[6]||0}}
2026-09-06 23:28:03 +09:00
async correctUnknownFrame(snap,iter,token,referencePixel=null,numericCount=0,tile=null){
2026-08-25 21:33:45 +09:00
await this.ready;await this.ensureCorrectionPipelines();const f=this.ensureDeepQueueWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-09-06 23:28:03 +09:00
const area=tile||{x:0,y:0,w:f.w,h:f.h};
2026-08-25 21:33:45 +09:00
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;
2026-09-06 23:28:03 +09:00
d.queue.writeBuffer(f.numericParams,0,this.deepParams(area.w,area.h,f.w,f.h,area.x,area.y,iter,snap,.5,.5,0,refX,refY,2,f.w,area.y*f.w+area.x));
2026-08-25 21:33:45 +09:00
let e=d.createCommandEncoder({label:'deep-sparse-queue-build'});e.clearBuffer(f.deepBucketState);e.clearBuffer(f.deepQueueStats);e.clearBuffer(f.deepIndirect);
2026-09-06 23:28:03 +09:00
this.encodeDeepBucketHistogram(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,w:area.w,h:area.h});
2026-08-25 21:33:45 +09:00
this.encodeDeepBucketPrefix(e,{bucketState:f.deepBucketState,queueStats:f.deepQueueStats,indirect:f.deepIndirect});
2026-09-06 23:28:03 +09:00
this.encodeDeepBucketScatter(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,queueStats:f.deepQueueStats,queue:f.deepQueue,w:area.w,h:area.h});
2026-08-25 21:33:45 +09:00
let totalMs=await this.submitStage(e,'queued DS build',token,'correction');if(totalMs===null)return false;
const built=await this.readDeepQueueStats();if(token!==state.token)return false;
const buildValid=built.selected===built.enqueued&&built.overflow===0&&built.invalidIndex===0;if(!buildValid){console.warn('Sparse correction build invariant failed; using bounded full scan',built);return this.correctUnknownFrameScan(snap,iter,token,referencePixel)}
const maxBatch=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,iter)));let offset=0;
2026-09-06 23:28:03 +09:00
while(offset<built.selected){if(token!==state.token)return false;const count=Math.min(maxBatch,built.selected-offset),params=this.deepParams(area.w,area.h,f.w,f.h,area.x,area.y,iter,snap,.5,.5,0,refX,refY,2,f.w,area.y*f.w+area.x,offset);d.queue.writeBuffer(f.numericParams,0,params);d.queue.writeBuffer(f.deepQueueStats,12,new Uint32Array([count]));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array([Math.ceil(count/64),1,1]));e=d.createCommandEncoder({label:'deep-sparse-correction-'+offset});this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});const ms=await this.submitStage(e,'queued DS correction '+(offset+1)+'..'+(offset+count)+'/'+built.selected,token,'correction');if(ms===null)return false;totalMs+=ms;runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,count*Math.max(1,iter));offset+=count}
2026-08-25 21:33:45 +09:00
d.queue.writeBuffer(f.deepQueueStats,12,new Uint32Array([built.selected]));e=d.createCommandEncoder({label:'deep-sparse-correction-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const statsMs=await this.submitStage(e,'queued DS post-stats',token,'correction');if(statsMs===null)return false;totalMs+=statsMs;
const q=await this.readDeepQueueStats();if(token!==state.token)return false;
const valid=q.selected===q.enqueued&&q.enqueued===q.dispatchCount&&q.dispatchCount===q.processed&&q.overflow===0&&q.invalidIndex===0&&q.staleEntry===0;
runtime.correctionPasses++;runtime.lastSparseQueue={...q,valid,ms:totalMs,batches:Math.ceil(built.selected/maxBatch)};
if(valid)return{mode:'queue',queue:q};
console.warn('Sparse correction invariant failed; using bounded full scan',q);
return this.correctUnknownFrameScan(snap,iter,token,referencePixel)
2026-08-25 14:03:13 +09:00
}
2026-08-25 21:33:45 +09:00
async correctUnknownFrameScan(snap,iter,token,referencePixel=null){const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,maxPixels=Math.max(1,Math.floor(4000000/Math.max(1,iter))),tileW=Math.min(f.w,maxPixels),rows=Math.max(1,Math.min(128,Math.floor(maxPixels/tileW)));for(let y=0;y<f.h;y+=rows){const th=Math.min(rows,f.h-y);for(let x=0;x<f.w;x+=tileW){if(token!==state.token)return false;const tw=Math.min(tileW,f.w-x),base=y*f.w+x;d.queue.writeBuffer(f.numericParams,0,this.deepParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,0,refX,refY,2,f.w,base));const e=d.createCommandEncoder({label:'deep-correction-fallback-'+x+'-'+y});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:tw,h:th});const ms=await this.submitStage(e,'DS fallback tile '+x+','+y+' '+tw+'x'+th,token,'correction');if(ms===null)return false}}const e=d.createCommandEncoder({label:'deep-correction-fallback-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms=await this.submitStage(e,'DS fallback post-stats',token,'correction');if(ms===null)return false;runtime.correctionPasses++;return{mode:'scan'}}
async refineOperationLimitIndices(snap,iter,token,deepContext,referencePixel,indices){
await this.ready;await this.ensureCorrectionPipelines();if(!deepContext)throw new Error('frontier reference context is missing');
const f=this.ensureDeepQueueWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setDeepContext(deepContext);
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,chunkSize=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,iter)));let processed=0,maxDispatchWork=0;
for(let start=0;start<indices.length;start+=chunkSize){
if(token!==state.token)return null;const chunk=indices.subarray?indices.subarray(start,Math.min(indices.length,start+chunkSize)):indices.slice(start,start+chunkSize),count=chunk.length;if(!count)continue;
d.queue.writeBuffer(f.deepQueue,0,chunk);d.queue.writeBuffer(f.deepQueueStats,0,new Uint32Array([count,0,count,count,0,0,0,0]));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array([Math.ceil(count/64),1,1,0]));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,0,f.w,0));
const e=d.createCommandEncoder({label:'pixel-frontier-'+iter+'-'+start});e.clearBuffer(f.unresolved);this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});const ms=await this.submitStage(e,'pixel frontier '+(start+1)+'..'+(start+count)+'/'+indices.length+' @ '+iter,token,'correction');if(ms===null)return null;
const q=await this.readDeepQueueStats();if(token!==state.token)return null;const valid=q.selected===count&&q.enqueued===count&&q.dispatchCount===count&&q.processed===count&&q.overflow===0&&q.invalidIndex===0&&q.staleEntry===0;if(!valid)throw new Error('pixel frontier queue invariant failed: '+JSON.stringify(q));processed+=count;maxDispatchWork=Math.max(maxDispatchWork,count*iter);
}
const e=d.createCommandEncoder({label:'pixel-frontier-post-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms=await this.submitStage(e,'pixel frontier post-stats',token,'correction');if(ms===null)return null;runtime.frontierDispatches=(runtime.frontierDispatches||0)+Math.ceil(indices.length/Math.max(1,chunkSize));runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,maxDispatchWork);return{stats:await this.readUnresolvedStats(),processed,maxDispatchWork,chunkSize};
}
2026-09-06 23:28:03 +09:00
async extendUnknownFrame(snap,iter,token,deepContext,referencePixel=null){await this.ready;await this.ensureCorrectionPipelines();if(token!==state.token)return null;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!deepContext)throw new Error('extended deep reference context is missing');this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,1,f.w,0));const e=d.createCommandEncoder({label:'targeted-deep-unknown-recovery'});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
2026-08-23 23:27:38 +09:00
2026-08-24 20:30:26 +09:00
async renderTileMeta({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
2026-08-25 21:33:45 +09:00
await this.ready;const fastExtended=!deep&&!!fastContext;if(deep){await this.ensureDeepPipelines();if(correctUnknown)await this.ensureCorrectionPipelines();this.setDeepContext(deepContext)}else if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-08-24 20:30:26 +09:00
if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
2026-08-23 23:27:38 +09:00
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;
}
2026-09-06 23:28:03 +09:00
async readCompleteRgba(token=state.token){
if(token!==state.token||state.rendering||state.refinePending||state.qualityStage!=='final'||!numericFrameComplete())throw new Error('current frame is incomplete');
const f=this.frame,d=this.device,bpr=Math.ceil(f.w*4/256)*256,read=buf(d,bpr*f.h,GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ,'complete-frame-rgba');
try{const e=d.createCommandEncoder();e.copyTextureToBuffer({texture:f.front},{buffer:read,bytesPerRow:bpr,rowsPerImage:f.h},{width:f.w,height:f.h});d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=new Uint8Array(read.getMappedRange()),rgba=new Uint8Array(f.n*4);for(let y=0;y<f.h;y++)rgba.set(raw.subarray(y*bpr,y*bpr+f.w*4),y*f.w*4);read.unmap();if(token!==state.token)throw new Error('cancelled');return rgba;}finally{destroy(read)}
}
2026-08-25 21:33:45 +09:00
async renderTileRGBA({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true,workspaceSlot=0,workspaceSize=this.exportTileSize()}){
2026-09-06 23:28:03 +09:00
if(w*h*Math.max(1,iter)>(deep?4000000:48000000)){
const shape=this.numericTileShape(w,iter,deep),out=new Uint8ClampedArray(w*h*4),generation=state.token;
let unresolved=0,corrected=0;
for(let y=0;y<h;y+=shape.rows)for(let x=0;x<w;x+=shape.width){
if(generation!==state.token||exportJob.cancelled&&exportJob.active)throw new Error('cancelled');
const tw=Math.min(shape.width,w-x),th=Math.min(shape.rows,h-y);
const part=await this.renderTileRGBA({snap,iter,deep,deepContext,fastContext,fullW,fullH,sampleX,sampleY,edgeAA,forceStrict,correctUnknown,workspaceSlot,workspaceSize,tileX:tileX+x,tileY:tileY+y,w:tw,h:th});
for(let row=0;row<th;row++)out.set(part.rgba.subarray(row*tw*4,(row+1)*tw*4),((y+row)*w+x)*4);
unresolved+=part.unresolved;corrected+=part.corrected;
}
return{rgba:out,unresolved,corrected};
}
2026-08-25 21:33:45 +09:00
await this.ready;const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep){await this.ensureDeepPipelines();if(correctUnknown)await this.ensureCorrectionPipelines();this.setDeepContext(deepContext)}else if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-08-24 20:30:26 +09:00
if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const ca=this.colorParamsData(w,h,iter),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
2026-08-23 23:27:38 +09:00
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};
}
2026-08-25 21:33:45 +09:00
async renderTileRGBA2x({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true,workspaceSlot=0,workspaceSize=this.exportTileSize()}){
2026-09-06 23:28:03 +09:00
if(w*h*Math.max(1,iter)>(deep?4000000:48000000)){
const shape=this.numericTileShape(w,iter,deep),out=new Uint8ClampedArray(w*h*4),generation=state.token;
let unresolved=0,corrected=0;
for(let y=0;y<h;y+=shape.rows)for(let x=0;x<w;x+=shape.width){
if(generation!==state.token||exportJob.cancelled&&exportJob.active)throw new Error('cancelled');
const tw=Math.min(shape.width,w-x),th=Math.min(shape.rows,h-y);
const part=await this.renderTileRGBA2x({snap,iter,deep,deepContext,fastContext,fullW,fullH,forceStrict,correctUnknown,workspaceSlot,workspaceSize,tileX:tileX+x,tileY:tileY+y,w:tw,h:th});
for(let row=0;row<th;row++)out.set(part.rgba.subarray(row*tw*4,(row+1)*tw*4),((y+row)*w+x)*4);
unresolved+=part.unresolved;corrected+=part.corrected;
}
return{rgba:out,unresolved,corrected};
}
2026-08-25 21:33:45 +09:00
await this.ready;await this.ensureAaPipeline();const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep){await this.ensureDeepPipelines();if(correctUnknown)await this.ensureCorrectionPipelines();this.setDeepContext(deepContext)}else if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h,iter);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
2026-08-24 20:30:26 +09:00
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
2026-08-23 23:27:38 +09:00
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};
}
2026-08-25 14:03:13 +09:00
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext();this.destroyFastContext()}
}
function numericalFailureCount(stats){const r=stats&&stats.reasons||{};return (r.errorBound||0)+(r.escapeUncertain||0)+(r.referenceEnd||0)+(r.rebaseGap||0)+(r.range||0)}
// A broad failure is cheaper to normalize once with the full-frame Deep pass;
// sparse/moderate failures go directly to local references.
function tileRecoveryConfig(){const z=zoomExp(),deep=z>=32;if(state.processMode==='power')return{size:deep?224:320,maxRefs:4,minFailed:deep?4:48};if(state.processMode==='fine')return{size:deep?128:192,maxRefs:20,minFailed:deep?1:8};if(state.processMode==='validate')return{size:deep?112:160,maxRefs:32,minFailed:1};return{size:deep?160:224,maxRefs:12,minFailed:deep?1:12}}
2026-08-25 21:33:45 +09:00
function isOperationLimitMeta(m){return ((m>>>28)&3)===FIELD_UNKNOWN&&((m>>>REASON_SHIFT)&255)===6}
function operationLimitIndices(meta){const out=[];for(let i=0;i<meta.length;i++)if(isOperationLimitMeta(meta[i]))out.push(i);return Uint32Array.from(out)}
function numericalFailureIndices(meta){const out=[];for(let i=0;i<meta.length;i++){if(((meta[i]>>>28)&3)!==FIELD_UNKNOWN)continue;const reason=(meta[i]>>>REASON_SHIFT)&255;if(reason>=1&&reason<=5)out.push(i)}return Uint32Array.from(out)}
function operationLimitFrontier(meta,w,h){const out=[];for(let y=0;y<h;y++){const row=y*w;for(let x=0;x<w;x++){const i=row+x;if(!isOperationLimitMeta(meta[i]))continue;let edge=false;for(let oy=-1;oy<=1&&!edge;oy++){const yy=y+oy;if(yy<0||yy>=h)continue;for(let ox=-1;ox<=1;ox++){if(ox===0&&oy===0)continue;const xx=x+ox;if(xx<0||xx>=w)continue;if(((meta[yy*w+xx]>>>28)&3)===FIELD_ESCAPED){edge=true;break}}}if(edge)out.push(i)}}return Uint32Array.from(out)}
function pixelEscapeAdvance(before,after,w,h){let escaped=0,outsideEnvelope=0;for(let i=0;i<after.length;i++){if(((after[i]>>>28)&3)!==FIELD_ESCAPED||((before[i]>>>28)&3)===FIELD_ESCAPED)continue;escaped++;const x=i%w,y=Math.floor(i/w);let adjacent=false;for(let oy=-1;oy<=1&&!adjacent;oy++){const yy=y+oy;if(yy<0||yy>=h)continue;for(let ox=-1;ox<=1;ox++){if(!ox&&!oy)continue;const xx=x+ox;if(xx>=0&&xx<w&&((before[yy*w+xx]>>>28)&3)===FIELD_ESCAPED){adjacent=true;break}}}if(!adjacent)outsideEnvelope++}return{escaped,outsideEnvelope,pixelStable:outsideEnvelope===0}}
2026-09-06 23:28:03 +09:00
// Integer interval tests over exact rational pixel coordinates. No f32
// rounding, periodic-orbit guess, or agreement of two precisions is a proof.
function certifiedInteriorTiles(snap,width,height,tile=32){
const columns=Math.ceil(width/tile),rows=Math.ceil(height/tile),mask=new Uint32Array(4+columns*rows);
mask.set([width,height,tile,columns]);let pixels=0;
const unit=1n<<BigInt(snap.bits),den=2n*BigInt(width)*unit,den2=den*den;
const sq=(a,b)=>{const aa=a*a,bb=b*b;return[a<=0n&&b>=0n?0n:aa<bb?aa:bb,aa>bb?aa:bb]};
const productUpper=(a,b,c,d)=>{const p=[a*c,a*d,b*c,b*d];return p.reduce((v,x)=>x>v?x:v)};
const real=x=>2n*BigInt(width)*snap.re+snap.span*BigInt(2*x+1-width);
const imag=y=>2n*BigInt(width)*snap.im+snap.span*BigInt(height-2*y-1);
for(let y=0;y<height;y+=tile)for(let x=0;x<width;x+=tile){
const w=Math.min(tile,width-x),h=Math.min(tile,height-y),xl=real(x),xh=real(x+w-1),yl=imag(y+h-1),yh=imag(y);
const y2=sq(yl,yh),bulb=sq(xl+den,xh+den);
let inside=16n*(bulb[1]+y2[1])<den2;
if(!inside){
const lo=xl-den/4n,hi=xh-den/4n,x2=sq(lo,hi),ql=x2[0]+y2[0],qh=x2[1]+y2[1];
inside=4n*productUpper(ql,qh,ql+lo*den,qh+hi*den)<y2[0]*den2;
}
if(inside){mask[4+(y/tile)*columns+x/tile]=1;pixels+=w*h;}
}
return{mask,pixels};
}
function pixelFrontierIterationFloor(snap,w,initialIter){if(state.continuationBudget)return Math.max(initialIter,state.continuationBudget);const pixelBits=Math.max(1,Math.log2(Math.max(1,w))-log2FixedAt(snap.span,snap.bits)),scaled=Math.ceil(pixelBits*256/ADAPTIVE_ITER_QUANTUM)*ADAPTIVE_ITER_QUANTUM;return Math.min(PIXEL_FRONTIER_MAX_ITER,Math.max(initialIter,scaled))}
2026-08-25 14:03:13 +09:00
function tileReferenceSnapshot(snap,tile,fullW,fullH){const cx=tile.numeric?tile.sumX/tile.numeric:tile.x+tile.w*.5,cy=tile.numeric?tile.sumY/tile.numeric:tile.y+tile.h*.5,bw=Math.max(1,tile.maxX-tile.minX+1),bh=Math.max(1,tile.maxY-tile.minY+1),refW=Math.max(8,Math.min(tile.w,Math.ceil(bw*1.75))),refH=Math.max(8,Math.min(tile.h,Math.ceil(bh*1.75))),den=2n*BigInt(fullW),nx=BigInt(Math.round(2*cx-fullW)),ny=BigInt(Math.round(fullH-2*cy)),re=snap.re+roundDivSigned(snap.span*nx,den),im=snap.im+roundDivSigned(snap.span*ny,den),localSpan=(()=>{const q=roundDivSigned(snap.span*BigInt(refW),BigInt(fullW));return q>0n?q:1n})();return{snap:{bits:snap.bits,re,im,span:localSpan},width:refW,height:refH}}
2026-08-25 21:33:45 +09:00
async function recoverNumericalUnknownTiles(r,snap,iter,token,initialStats){
const initialFailed=numericalFailureCount(initialStats),cfg=tileRecoveryConfig();
if(initialFailed<cfg.minFailed)return{stats:initialStats,passes:0,recovered:0,attempted:0};
state.lastEngine='WebGPU · 数値失敗領域を局所referenceで解析';updateStats();
2026-09-06 23:28:03 +09:00
const tried=new Set();let passes=0,attempted=0,stats=initialStats,budget=cfg.maxRefs;
2026-08-25 21:33:45 +09:00
const sizes=[cfg.size,Math.max(56,Math.floor(cfg.size*.5))];
for(let round=0;round<sizes.length&&budget>0;round++){
stats=await r.readUnresolvedStats();if(token!==state.token)return null;
const failed=numericalFailureCount(stats);if(failed<cfg.minFailed)break;const roundStartFailed=failed;
const tiles=await r.readNumericalFailureTiles(sizes[round]);if(token!==state.token)return null;if(!tiles.length)break;
const roundCap=round===0?Math.min(budget,Math.max(1,Math.ceil(cfg.maxRefs*.67))):budget;let used=0;
for(let cursor=0;cursor<tiles.length&&used<roundCap&&budget>0;){
if(token!==state.token)return null;const count=Math.min(tileReferencePool.length,tiles.length-cursor,roundCap-used,budget),batch=tiles.slice(cursor,cursor+count);cursor+=count;
state.lastEngine='CPU · 局所reference '+(passes+1)+''+(passes+count)+'/'+cfg.maxRefs+' を並列準備';updateStats();
2026-09-06 23:28:03 +09:00
const prepared=await Promise.all(batch.map(async(tile,i)=>{const local=tileReferenceSnapshot(snap,tile,canvas.width,canvas.height),fixed={re:local.snap.re,im:local.snap.im},identity=[fixed.re,fixed.im,local.snap.bits,pixelPrecisionBits(local.snap,local.width,40),iter].join(':');if(tried.has(identity))return null;tried.add(identity);const ctx=await tileReferencePool[i].requestFixed(local.snap,iter,local.width,local.height,fixed);return{tile,ctx}}));if(token!==state.token)return null;
for(const {tile,ctx} of prepared.filter(Boolean)){
if(token!==state.token)return null;
if(ctx.checkpointMismatch)throw new Error('tile reference guard checkpoint mismatch');
r.setDeepContext(ctx);
const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);
const next=await r.correctUnknownFrame(snap,iter,token,refPixel,0,tile);
if(!next||token!==state.token)return null;
stats=await r.readUnresolvedStats();if(token!==state.token)return null;
attempted+=next.queue?.selected||tile.numeric;passes++;used++;budget--;state.multiRefPasses=passes;
}
2026-08-25 21:33:45 +09:00
}
if(!used||numericalFailureCount(stats)>=roundStartFailed)break;
}
if(passes){stats=await r.readUnresolvedStats();if(token!==state.token)return null}
const recovered=Math.max(0,initialFailed-numericalFailureCount(stats));state.multiRefRecovered=recovered;state.unresolved=stats.total;state.unknownReasons=stats.reasons;return{stats,passes,recovered,attempted};
}
2026-09-06 23:28:03 +09:00
function precisionBatchSize(iter,bits){return Math.max(1,Math.min(64,Math.floor(500000/(Math.max(1,iter)*5*Math.max(1,bits/128)))))}
2026-08-25 21:33:45 +09:00
async function recoverResidualPrecision(r,snap,iter,token,initialStats,terminalClass=-1){
2026-09-06 23:28:03 +09:00
if(numericalFailureCount(initialStats)<=0)return{stats:initialStats,attempted:0,applied:0};
const indices=await r.readNumericalIndices(token);if(!indices||token!==state.token)return null;
const bits=pixelPrecisionBits(snap,canvas.width,40);
const active=new Map(),completed=[];let completedCount=0,lastApply=performance.now();let next=0,attempted=0,appliedCount=0,batchSize=precisionBatchSize(iter,bits);
const launch=worker=>{
if(next>=indices.length||token!==state.token)return;
const end=Math.min(indices.length,next+batchSize),part=indices.slice(next,end),started=performance.now();next=end;
// Resolve failures into values so cancellation never leaves an unhandled job.
active.set(worker,precisionFallbackPool[worker].request(snap,iter,canvas.width,canvas.height,part,terminalClass)
.then(result=>({worker,result,ms:performance.now()-started}),error=>({worker,error})));
};
for(let i=0;i<precisionFallbackPool.length;i++)launch(i);
try{
while(active.size){
const done=await Promise.race(active.values());active.delete(done.worker);
if(token!==state.token)return null;
if(done.error)throw done.error;
if(TEST_MODE)runtime.precisionOrbits=(runtime.precisionOrbits||0)+done.result.orbitCount;completed.push(done.result);completedCount+=done.result.indices.length;attempted+=done.result.indices.length;
if(done.ms>40)batchSize=Math.max(1,Math.floor(batchSize/2));
launch(done.worker);
if(completedCount>=64||performance.now()-lastApply>=32||!active.size){
const joined={indices:new Uint32Array(completedCount),meta:new Uint32Array(completedCount),smooth:new Float32Array(completedCount),accepted:new Uint8Array(completedCount)};let offset=0;
for(const part of completed){for(const key of ['indices','meta','smooth','accepted'])joined[key].set(part[key],offset);offset+=part.indices.length;}
const applied=await r.applyPrecisionFallback(joined,token);if(!applied||token!==state.token)return null;appliedCount+=applied.applied;
completed.length=0;completedCount=0;lastApply=performance.now();
state.lastEngine='CPU · 数値補修 '+attempted+'/'+indices.length+' px';updateStats();
}
}
const stats=await r.readUnresolvedStats();if(token!==state.token)return null;
state.precisionFallbackAttempted=(state.precisionFallbackAttempted||0)+attempted;
state.precisionFallbackApplied=(state.precisionFallbackApplied||0)+appliedCount;
return{stats,attempted,applied:appliedCount};
}finally{
for(const worker of active.keys())precisionFallbackPool[worker].cancel('cancelled precision batch');
}
2026-08-25 21:33:45 +09:00
}
async function refinePixelFrontier(r,snap,initialIter,token,initialStats){
if(!PIXEL_FRONTIER_ENABLED)return{stats:initialStats,iter:initialIter,rounds:0,processed:0,escaped:0,converged:false,applied:false};
2026-09-06 23:28:03 +09:00
const certified=await r.certifyInterior(snap,token);if(token!==state.token)return null;
const explicitBudget=state.continuationBudget||0,maxIter=Math.max(initialIter,explicitBudget||PIXEL_FRONTIER_MAX_ITER),minimumIter=pixelFrontierIterationFloor(snap,canvas.width,initialIter),initialMeta=await r.readMetaAll();if(token!==state.token)return null;state.frontierMinimumIter=minimumIter;let previousMeta=initialMeta,candidates=operationLimitIndices(initialMeta),iter=initialIter,stats=initialStats,rounds=0,processed=0,escaped=0,stableRounds=0,converged=candidates.length===0||iter>=maxIter&&candidates.every(i=>(initialMeta[i]&ITER_MASK)>=maxIter),session=null,fixedSource=null,deferred=new Uint32Array();
2026-08-25 21:33:45 +09:00
while(!converged&&iter<maxIter){
if(token!==state.token)return null;const target=Math.min(maxIter,Math.max(iter+1,iter*2));state.lastEngine='WebGPU · 未脱出全画素 active-list '+candidates.length+' px · '+target+' 反復';state.frontierPixels=candidates.length;updateStats();let ctx=fixedSource?await refs.requestFixed(snap,target,canvas.width,canvas.height,fixedSource):await refs.request(snap,target,canvas.width,canvas.height);if(token!==state.token)return null;if(ctx.checkpointMismatch)throw new Error('all-pixel active reference guard checkpoint mismatch');
// A persistent perturbation state is valid only while its reference orbit
// origin is unchanged and reaches the new target. If it does not, rebuild
// the candidate list from the already-published metadata and restart once
// with a newly selected long reference; no pixel is silently discarded.
2026-09-06 23:28:03 +09:00
if(ctx.refLen<target){ctx=await refs.request(snap,target,canvas.width,canvas.height,true);if(token!==state.token)return null;const meta=await r.readMetaAll();if(token!==state.token)return null;candidates=operationLimitIndices(meta);session=null;fixedSource=null;deferred=new Uint32Array();if(!candidates.length){converged=true;break}}
const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(session&&session.referenceId!==(ctx.referenceId||ctx.key)){session=null;deferred=new Uint32Array();candidates=operationLimitIndices(previousMeta)}if(!session){session=await r.beginOperationLimitContinuation(snap,token,ctx,refPixel,candidates,state.processMode==='validate');if(!session||token!==state.token)return null;fixedSource=ctx.source;session.referenceId=ctx.referenceId||ctx.key}
const workBefore=session.pixelIterations,dispatchBefore=session.dispatches,pass=await r.advancePriorityTarget(session,snap,target,token,ctx,refPixel,previousMeta);if(!pass||token!==state.token)return null;processed+=pass.pixelIterations-workBefore;runtime.frontierDispatches=(runtime.frontierDispatches||0)+(pass.dispatches-dispatchBefore);if(deferred.length){const repaired=await r.refineOperationLimitIndices(snap,target,token,ctx,refPixel,deferred);if(!repaired||token!==state.token)return null;}stats=await r.readUnresolvedStats();if(token!==state.token)return null;let failed=numericalFailureCount(stats);if(failed>0){r.setDeepContext(ctx);const corrected=await r.correctUnknownFrame(snap,target,token,refPixel);if(!corrected||token!==state.token)return null;stats=await r.readUnresolvedStats();if(token!==state.token)return null}if(numericalFailureCount(stats)>512){const beforeLocal=numericalFailureCount(stats),mr=await recoverNumericalUnknownTiles(r,snap,target,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;if(numericalFailureCount(stats)>=beforeLocal)state.multiRefRecovered=0}if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,target,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats}const after=await r.readMetaAll();if(token!==state.token)return null;const advance=pixelEscapeAdvance(previousMeta,after,canvas.width,canvas.height);rounds++;escaped+=advance.escaped;iter=target;stableRounds=advance.pixelStable?stableRounds+1:0;previousMeta=after;state.frontierRounds=rounds;state.frontierEscaped=escaped;state.frontierIter=iter;
if(!await presentProvisional(r,snap,iter,token))return null;
2026-08-25 21:33:45 +09:00
// A quiet pair of doubling rounds is only meaningful after every remaining
// operation-limit pixel has actually reached the budget derived from one
// screen pixel. This prevents an early quiet interval from hiding a later
// visible escape component. Recovery may repopulate an emptied GPU queue,
// so convergence is decided from the post-recovery metadata.
2026-09-06 23:28:03 +09:00
const recoveredCandidates=operationLimitIndices(after);if(!recoveredCandidates.length)converged=true;else if(iter>=maxIter&&recoveredCandidates.every(i=>(after[i]&ITER_MASK)>=maxIter))converged=true;else if(!explicitBudget&&iter>=minimumIter&&stableRounds>=2)converged=true;else{
// Only pixels whose correction supplied metadata without continuation
// state are replayed. Healthy perturbation states never rewind.
const live=await r.readActiveIndices(session);if(token!==state.token)return null;
const liveSet=new Set(live);deferred=Uint32Array.from(recoveredCandidates.filter(i=>!liveSet.has(i)));
candidates=recoveredCandidates;converged=false;
}
2026-08-25 21:33:45 +09:00
}
stats=await r.readUnresolvedStats();if(token!==state.token)return null;
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats}
2026-09-06 23:28:03 +09:00
state.frontierConverged=converged;state.frontierIter=iter;state.unresolved=stats.total;state.unknownReasons=stats.reasons;runtime.frontierFrames=(runtime.frontierFrames||0)+1;runtime.frontierPixels=(runtime.frontierPixels||0)+processed;runtime.frontierEscaped=(runtime.frontierEscaped||0)+escaped;return{stats,iter,minimumIter,rounds,processed,escaped,converged,policy:explicitBudget?'explicit-finite-budget':iter>=maxIter?'automatic-finite-cap':'finite-budget-with-stability-check',membershipCertified:false,applied:escaped>0||certified>0};
2026-08-23 23:27:38 +09:00
}
2026-08-24 20:30:26 +09:00
function referencePixelForSource(src,snap,w,h){if(!src)return{x:w*.5,y:h*.5};const b=Math.max(snap.bits,src.bits),span=align(snap.span,snap.bits,b),dr=align(src.re,src.bits,b)-align(snap.re,snap.bits,b),di=align(src.im,src.bits,b)-align(snap.im,snap.bits,b);return{x:w*.5+fixedRatio(dr,span)*w,y:h*.5-fixedRatio(di,span)*w}}
2026-08-25 21:33:45 +09:00
function referenceOriginMatchesView(src,snap){return !!src&&!!snap&&src.bits===snap.bits&&(src.viewRe??src.re)===snap.re&&(src.viewIm??src.im)===snap.im&&src.span===snap.span}
function reusableReferencePixel(ctx,snap,iter,w,h,requiredBits){const src=ctx&&ctx.source;if(!ctx||!src||!referenceOriginMatchesView(src,snap)||ctx.refLen<iter||(ctx.precisionBits||0)<requiredBits||ctx.checkpointMismatch)return null;const pixel=referencePixelForSource(src,snap,w,h),margin=.125*Math.max(w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x < -margin||pixel.x>w+margin||pixel.y < -margin||pixel.y>h+margin)return null;return{ctx,pixel}}
function reusableFastReference(snap,iter,w,h){return reusableReferencePixel(renderer&&renderer.fastCtx,snap,iter,w,h,fastReferenceBits(snap,w))}
2026-08-23 23:27:38 +09:00
// ── GPU startup / rendering orchestration ────────────────────────────────
2026-09-06 23:28:03 +09:00
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=true;state.gpuUnavailable=true;state.gpuError='WebGPU非対応 · 重いfallbackは無効';updateStats();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=true;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません · fallback無効';updateStats();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();if(!state.rendering&&!state.frameView)markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuUnavailable=false;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e)+' · fallback無効';updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
let renderClockTimer=0;
function renderDuration(ms){return ms>=1000?(ms/1000).toFixed(2)+' s':Math.round(ms)+' ms'}
function renderTimeLabel(now=performance.now()){
const c=state.renderClock;
if(!c)return '準備完了';
const elapsed=c.status==='running'?now-c.startedAt:c.elapsed;
return (c.status==='running'?'描画中… ':c.status==='cancelled'?'中断 ':c.status==='error'?'停止 ':'')+renderDuration(Math.max(0,elapsed));
}
function startRenderClock(token,startedAt){
if(renderClockTimer)clearInterval(renderClockTimer);
state.lastRender=0;state.renderClock={token,startedAt,status:'running',elapsed:0};
renderClockTimer=setInterval(()=>{$('#render').textContent=renderTimeLabel()},250);
}
function finishRenderClock(token,status='complete'){
const c=state.renderClock;if(!c||c.token!==token||c.status!=='running')return;
c.elapsed=performance.now()-c.startedAt;c.status=status;
if(status==='complete')state.lastRender=c.elapsed;
clearInterval(renderClockTimer);renderClockTimer=0;
$('#render').textContent=renderTimeLabel();
}
function cancelRender(){finishRenderClock(state.token,'cancelled');if(exportJob.active)exportJob.cancelled=true;const metric=generationMetric();if(metric&&metric.status==='running'){metric.status='cancelled';metric.cancelMs=performance.now()-metric.startedAt}state.token++;state.rendering=false;state.recolorPending=false;state.refinePending=false;state.numericRecoveryPending=false;state.numericRecoveryRunning=false;if(refineTimer){clearTimeout(refineTimer);refineTimer=0}refs.cancelPending('render cancelled');for(const service of tileReferencePool)service.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled');precisionFallback.cancel('render cancelled');for(const service of precisionFallbackPool)service.cancel('render cancelled')}
2026-08-23 23:27:38 +09:00
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)}
2026-09-06 23:28:03 +09:00
async function presentProvisional(r,snap,iter,token,force=false){
if(token!==state.token)return false;
if(!force&&performance.now()-(r.lastProvisionalAt||0)<125)return true;
if(!await r.recolor(token,iter)||token!==state.token)return false;
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,complete:false,progressive:true};
r.captureColorSource(snap);
r.lastProvisionalAt=performance.now();state.drawState='REFINING';
const metric=generationMetric(token);if(metric&&!metric.firstDisplayMs)metric.firstDisplayMs=performance.now()-metric.startedAt;
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();return true;
}
2026-08-25 14:03:13 +09:00
async function renderQualityStage(r,snap,iter,token,isFinal=false){
2026-09-06 23:28:03 +09:00
let decision=chooseBackend(snap,canvas.width,isFinal),deep=decision.deep,fastExtended=decision.fastExtended;
state.backendDecision=decision;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false;
if(fastExtended){
2026-08-25 14:03:13 +09:00
const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);
if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true}
else{state.lastEngine='WebGPU · '+(isFinal?'最終':'preview')+' 高速拡張 reference準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height)}
}
2026-09-06 23:28:03 +09:00
state.lastEngine='WebGPU · '+(isFinal?'最終品質':'即時preview')+' '+(deep?'高精度補修':fastExtended?'高速拡張':'高速');updateStats();
let ok=await r.computeFrame(snap,iter,token,referencePixel,fastExtended,fastCtx);if(!ok||token!==state.token)return null;
if(isFinal&&!await presentProvisional(r,snap,iter,token,true))return null;
if(isFinal&&fastExtended){
2026-08-25 21:33:45 +09:00
// A FAST frame is not publishable while any numeric UNKNOWN remains.
// Repair locally first, then solve every residual pixel at precision
// derived from span / canvasWidth. This computes the pixels instead of
// disguising them with colour or deferring correctness to a zoom cutoff.
let stats=await r.readUnresolvedStats();if(token!==state.token)return null;const initialFailed=numericalFailureCount(stats);
if(initialFailed>0){const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;}
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats;}
state.unresolved=stats.total;state.unknownReasons=stats.reasons;
if(initialFailed>0){const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;}
}else if(isFinal){
2026-08-25 14:03:13 +09:00
const stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;
}
2026-08-25 21:33:45 +09:00
let effectiveIter=iter,frontierResult=null;
if(isFinal&&PIXEL_FRONTIER_ENABLED){frontierResult=await refinePixelFrontier(r,snap,iter,token,{total:state.unresolved,reasons:{...(state.unknownReasons||{})}});if(!frontierResult||token!==state.token)return null;effectiveIter=frontierResult.iter;if(!frontierResult.converged)throw new Error('全画素反復が安全上限 '+PIXEL_FRONTIER_MAX_ITER+' までに収束しなかったためframeを公開しません');if(frontierResult.applied){const painted=await r.recolor(token,effectiveIter);if(!painted||token!==state.token)return null}}
2026-09-06 23:28:03 +09:00
if(isFinal){const remaining=numericalFailureCount({reasons:state.unknownReasons||{}}),unprocessed=Math.max(0,state.unresolved-remaining-(state.unknownReasons?.operationLimit||0));if(unprocessed)throw new Error('未計算画素が '+unprocessed+' px 残っています');if(remaining>0)throw new Error('画素精度補修後も数値未確定が '+remaining+' px 残ったためframeを公開しません')}
state.frameView=snap;state.fieldView={...snap,iter:effectiveIter,baseIter:iter,frontier:frontierResult,w:canvas.width,h:canvas.height,deep,fastExtended,backend:frontierResult?.applied?'pixel-frontier-'+decision.backend:decision.backend,referenceKey:(deep?ctx:fastExtended?fastCtx:null)?.key||'',referenceReused,complete:isFinal,progressive:true};
r.captureColorSource(snap);
2026-08-25 14:03:13 +09:00
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});
2026-09-06 23:28:03 +09:00
return{decision,referenceReused,referencePixel};
2026-08-25 14:03:13 +09:00
}
2026-09-06 23:28:03 +09:00
function numericFrameComplete(){return state.fieldView?.complete!==false&&numericalFailureCount({reasons:state.unknownReasons||{}})===0&&!state.numericRecoveryPending&&!state.numericRecoveryRunning}
2026-08-25 14:03:13 +09:00
function scheduleQualityRefinement(snap,token,stages,index){
if(index>=stages.length||token!==state.token)return;
state.refinePending=true;if(refineTimer)clearTimeout(refineTimer);
refineTimer=setTimeout(()=>{refineTimer=0;runQualityRefinement(snap,token,stages,index)},refinementDelay(index));
}
2026-08-25 21:33:45 +09:00
function prefetchFinalReference(snap,finalIter,token,decision){if(token!==state.token||!decision||finalIter<=state.qualityIter)return;let work=null;if(decision.deep)work=refs.request(snap,finalIter,canvas.width,canvas.height);else if(decision.fastExtended)work=fastRefs.request(snap,finalIter,canvas.width,canvas.height);if(work){runtime.referencePrefetches=(runtime.referencePrefetches||0)+1;work.catch(e=>{if(!/cancelled|superseded/i.test(String(e&&e.message||e)))console.warn('final reference prefetch skipped:',e)})}}
2026-08-25 14:03:13 +09:00
async function runQualityRefinement(snap,token,stages,index){
if(token!==state.token||state.pointerActive||state.wheelActive||state.dirty||!sameViewSnapshot(snap,snapshot())){state.refinePending=false;return}
2026-09-06 23:28:03 +09:00
if(state.recoloring){refineTimer=setTimeout(()=>{refineTimer=0;runQualityRefinement(snap,token,stages,index)},16);return}
2026-08-25 14:03:13 +09:00
const r=renderer;if(!r){state.refinePending=false;return}
const iter=stages[index],isFinal=index===stages.length-1,t0=performance.now();state.rendering=true;state.refinePending=false;state.drawState='REFINING';state.qualityStage=isFinal?'final':'refining';state.qualityIter=iter;updateStats();
try{
const result=await renderQualityStage(r,snap,iter,token,isFinal);if(!result||token!==state.token)return;
2026-09-06 23:28:03 +09:00
state.refineMs=performance.now()-t0;state.rendering=false;state.drawState=isFinal?'COVERED':'REFINED';state.lastEngine='WebGPU · '+(isFinal?'最終品質':'中間品質')+' '+(result.decision.deep?'高精度補修':result.decision.fastExtended?'高速拡張':'高速');const metric=generationMetric(token);if(metric&&isFinal){metric.finalMs=performance.now()-metric.startedAt;metric.status='final';metric.finalUnknown=state.unresolved;metric.finalReasons={...(state.unknownReasons||{})};metric.finalNumericalFailures=numericalFailureCount({reasons:metric.finalReasons});metric.numericSubmits=runtime.numericSubmits-metric.numericSubmitsStart;metric.correctionSubmits=runtime.correctionSubmits-metric.correctionSubmitsStart;metric.gpuSyncWaits=runtime.gpuSyncWaits-metric.gpuSyncWaitsStart}
if(isFinal){if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false;}else{state.temporalFill=!!r.historyReady;}r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});}
if(isFinal){state.rendering=true;await r.device.queue.onSubmittedWorkDone();if(token!==state.token)return;state.rendering=false;finishRenderClock(token)}
2026-08-25 14:03:13 +09:00
updateStats();
2026-09-06 23:28:03 +09:00
if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor);
2026-08-25 14:03:13 +09:00
if(!isFinal)scheduleQualityRefinement(snap,token,stages,index+1);
2026-09-06 23:28:03 +09:00
}catch(e){if(token!==state.token)return;finishRenderClock(token,'error');state.drawState='ERROR';state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU refinement error';updateStats();console.error(e)}
2026-08-25 14:03:13 +09:00
}
2026-08-23 23:27:38 +09:00
async function renderFrame(){
2026-08-25 14:03:13 +09:00
const token=++state.token,snap=snapshot(),finalIter=maxIter(),holdStable=canUseStableReprojection(),stages=qualityStages(finalIter),iter=stages[0],isSingleFinal=stages.length===1,t0=performance.now();
2026-09-06 23:28:03 +09:00
if(renderer&&state.fieldView?.complete&&state.colorAuto)renderer.captureColorSource(state.frameView);
startRenderClock(token,t0);
const generation=TEST_MODE?{token,startedAt:t0,viewKey:[snap.bits,snap.re,snap.im,snap.span].join(':'),width:canvas.width,height:canvas.height,previewIter:iter,finalIter,numericSubmitsStart:runtime.numericSubmits,correctionSubmitsStart:runtime.correctionSubmits,gpuSyncWaitsStart:runtime.gpuSyncWaits,status:'running'}:null;if(generation){runtime.generationMetrics.push(generation);if(runtime.generationMetrics.length>16)runtime.generationMetrics.shift();}
state.rendering=true;state.dirty=false;state.drawState=isSingleFinal&&holdStable?'REPROJECTED':'COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.multiRefPasses=0;state.multiRefRecovered=0;state.frontierRounds=0;state.frontierPixels=0;state.frontierEscaped=0;state.frontierIter=iter;state.frontierMinimumIter=PIXEL_FRONTIER_ENABLED?pixelFrontierIterationFloor(snap,canvas.width,iter):iter;state.frontierConverged=false;state.qualityStage=isSingleFinal?'final':'preview';state.qualityIter=iter;state.qualityFinalIter=finalIter;state.previewMs=0;state.refineMs=0;state.refinePending=false;runtime.renderStarts++;runtime.stageCounts={};runtime.stageMs={};runtime.frontierMaxDispatchWork=0;updateStats();
2026-08-23 23:27:38 +09:00
try{
const r=renderer||await initRenderer();if(token!==state.token)return;
2026-09-06 23:28:03 +09:00
if(!r){finishRenderClock(token,'error');state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU必須 · fallback無効';updateStats();return}
2026-08-25 14:03:13 +09:00
state.temporalFill=!!r.historyReady;
// Paint the old completed frame at the new view immediately. The numeric
// buffers may now be rewritten strip by strip, but the user keeps seeing
// a coherent image until the complete new colour frame is ready.
if(holdStable){r.presentFrame(r.presentTransform());state.drawState='REPROJECTED';updateStats()}
const result=await renderQualityStage(r,snap,iter,token,isSingleFinal);if(!result||token!==state.token)return;
2026-09-06 23:28:03 +09:00
const elapsed=performance.now()-t0;if(generation){generation.primaryMs=elapsed;generation.backend=result.decision.backend;generation.referenceReused=!!result.referenceReused;generation.primaryUnknown=state.unresolved;generation.primaryReasons={...(state.unknownReasons||{})};generation.primaryNumericalFailures=numericalFailureCount({reasons:generation.primaryReasons});}state.rendering=false;state.drawState=isSingleFinal?'COVERED':'REFINED';
if(isSingleFinal){state.refineMs=elapsed;state.previewMs=0;state.lastEngine='WebGPU · 最終品質 '+(result.decision.deep?'高精度補修':result.decision.fastExtended?'高速拡張':'高速');if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false;}else{state.temporalFill=!!r.historyReady;}r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});state.qualityStage='final';}
else{state.previewMs=elapsed;state.lastEngine='WebGPU · 即時preview '+(result.decision.deep?'高精度補修':result.decision.fastExtended?'高速拡張':'高速');prefetchFinalReference(snap,finalIter,token,result.decision);}
if(isSingleFinal){state.rendering=true;await r.device.queue.onSubmittedWorkDone();if(token!==state.token)return;state.rendering=false;finishRenderClock(token)}
if(isSingleFinal&&generation){generation.finalMs=elapsed;generation.status='final';generation.finalUnknown=state.unresolved;generation.finalReasons={...(state.unknownReasons||{})};generation.numericSubmits=runtime.numericSubmits-generation.numericSubmitsStart;generation.correctionSubmits=runtime.correctionSubmits-generation.correctionSubmitsStart;generation.gpuSyncWaits=runtime.gpuSyncWaits-generation.gpuSyncWaitsStart}updateStats();
2026-08-25 14:03:13 +09:00
if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor);
if(!isSingleFinal)scheduleQualityRefinement(snap,token,stages,1);
2026-09-06 23:28:03 +09:00
}catch(e){if(generation){generation.status=token===state.token?'error':'cancelled';generation.error=String(e&&e.message||e);}if(token!==state.token)return;finishRenderClock(token,'error');state.drawState='ERROR';state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
2026-08-25 14:03:13 +09:00
}
function requestRecolor(){
state.colorRevision++;state.recolorPending=true;
2026-09-06 23:28:03 +09:00
if(state.rendering||state.numericRecoveryRunning){renderer?.recolorPublished();return}
2026-08-25 14:03:13 +09:00
// Keep the old completed frame valid until the replacement colour pass has
// actually finished. Invalidating history at slider-down caused apparent
// non-response/flicker when GPU work was already in flight.
if(!state.rendering&&!state.recoloring&&!state.numericRecoveryRunning)queueMicrotask(recolor);
}
async function recolor(){
2026-09-06 23:28:03 +09:00
if(!renderer?.frame||!state.fieldView){state.recolorPending=true;return false}
2026-08-25 14:03:13 +09:00
if(state.rendering||state.recoloring||state.numericRecoveryRunning){state.recolorPending=true;return false}
state.recoloring=true;let painted=false;
try{
2026-09-06 23:28:03 +09:00
while(state.recolorPending&&!state.rendering&&!state.numericRecoveryRunning&&renderer?.frame&&state.fieldView){
2026-08-25 14:03:13 +09:00
state.recolorPending=false;const revision=state.colorRevision,token=state.token,ok=await renderer.recolor(token);
if(!ok||token!==state.token){state.recolorPending=true;continue}
2026-09-06 23:28:03 +09:00
renderer.colorSourceVisible=false;
if(!state.colorAuto)renderer.releaseColorSource();
renderer.presentFrame(renderer.presentTransform());painted=true;
2026-08-25 14:03:13 +09:00
// Only promote a colour frame to stable history if it still represents
// the newest slider value. Rapid dragging therefore collapses to the
// latest value instead of replaying stale colour states.
if(revision===state.colorRevision&&state.qualityStage==='final'&&!state.refinePending&&numericFrameComplete()){renderer.commitHistory(state.frameView);state.temporalFill=false}
updateStats();
2026-08-24 20:30:26 +09:00
}
2026-08-25 14:03:13 +09:00
return painted
}catch(e){console.error(e);return false}
2026-09-06 23:28:03 +09:00
finally{state.recoloring=false;if(state.dirty)schedule();if(state.recolorPending&&renderer?.frame&&!state.rendering&&!state.numericRecoveryRunning)queueMicrotask(recolor)}
2026-08-23 23:27:38 +09:00
}
2026-09-06 23:28:03 +09:00
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering&&!state.recoloring)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
2026-08-23 23:27:38 +09:00
// ── 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 ───────────────────────────────────────────────────────
2026-09-06 23:28:03 +09:00
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');if(state.continuationBudget)p.set('cb',String(state.continuationBudget));p.set('rm',state.renderMode);const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal')){const pal=Math.max(0,Math.min(PALETTE_MAX,Number(p.get('pal'))|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;}if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(Number(p.get('cb'))||0)));state.renderMode='fast';ensurePrecision();return true}catch{return false}}
2026-08-24 20:30:26 +09:00
function syncHistoryButtons(){}
2026-09-06 23:28:03 +09:00
function syncControls(){$('#continuationBudget').value=String(state.continuationBudget);$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()}
2026-08-23 23:27:38 +09:00
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()};
2026-08-25 14:03:13 +09:00
async function copyShareUrl(){
saveHash(false);const url=location.href;
try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(url);toast('共有URLをコピーしました');return}}
catch{}
try{const ta=document.createElement('textarea');ta.value=url;ta.setAttribute('readonly','');ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();const ok=document.execCommand&&document.execCommand('copy');ta.remove();if(ok){toast('共有URLをコピーしました');return}}catch{}
try{if(navigator.share){await navigator.share({title:'MANDELBROT DEEP ZOOM',url});toast('共有しました');return}}catch(e){if(e?.name==='AbortError')return}
try{prompt('共有URLをコピーしてください',url)}catch{}toast('共有URLを表示しました')
}
$('#share').onclick=copyShareUrl;
2026-08-24 20:30:26 +09:00
let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0;
2026-08-25 14:03:13 +09:00
function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色相変化停止':'色相変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')}
2026-09-06 23:28:03 +09:00
function stopColorAuto(repaint=true){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton();if(repaint)requestRecolor()}
2026-08-25 14:03:13 +09:00
function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;/* Hue animation advances shift directly. No cycle or phase compensation is applied. */state.shift=fract01(state.shift+.09*dt);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;requestRecolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)}
2026-09-06 23:28:03 +09:00
$('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf){if(!state.rendering&&!state.numericRecoveryRunning)renderer?.captureColorSource();colorAutoRaf=requestAnimationFrame(colorAutoStep);}else if(!state.colorAuto)stopColorAuto()};
2026-08-25 14:03:13 +09:00
$('#palette').addEventListener('change',e=>{const pal=Math.max(0,Math.min(PALETTE_MAX,Number(e.target.value)|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;requestRecolor();saveHash(false)});
function applyCycleControl(e){state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);requestRecolor()}
function applyShiftControl(e){if(state.colorAuto)stopColorAuto();state.shift=Math.max(0,Math.min(1,Number(e.target.value)||0));$('#shiftO').textContent=state.shift.toFixed(2);requestRecolor()}
$('#cycle').addEventListener('input',applyCycleControl);$('#cycle').addEventListener('change',()=>saveHash(false));
$('#shift').addEventListener('input',applyShiftControl);$('#shift').addEventListener('change',()=>saveHash(false));
2026-09-06 23:28:03 +09:00
$('#continuationBudget').onchange=e=>{state.continuationBudget=Math.max(0,Math.min(150000,Number(e.target.value)||0));saveHash(false);recordView();markDirty()};
2026-08-24 20:30:26 +09:00
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
2026-08-23 23:27:38 +09:00
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)}}
2026-08-25 21:33:45 +09:00
async function yieldExportUi(lastYield,force=false){if(!force&&performance.now()-lastYield<24)return lastYield;if(globalThis.scheduler?.yield)await scheduler.yield();else await new Promise(resolve=>setTimeout(resolve,0));return performance.now()}
2026-08-23 23:27:38 +09:00
async function runExport(){
if(exportJob.active)return;
2026-09-06 23:28:03 +09:00
const requestedToken=state.token;
while(state.rendering||state.dirty||state.refinePending){
$('#exportStatus').textContent='現在の描画が完了するのを待っています…';
await new Promise(resolve=>setTimeout(resolve,50));
if(state.token!==requestedToken){$('#exportStatus').textContent='表示位置が変わったため出力を中止しました。';return;}
}
if(state.fieldView?.complete===false){$('#exportStatus').textContent='未完了の描画です。計算完了後に出力してください。';return;}
2026-08-23 23:27:38 +09:00
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
2026-09-06 23:28:03 +09:00
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=state.fieldView?.iter??maxIter(),deep=deepNeeded(snap,w,h),fastExtended=!deep&&fastNeedsExtended(snap,w),strict=$('#exportPrecision').value==='strict';
if(w===canvas.width&&h===canvas.height&&ss===1){
exportJob.active=true;exportJob.cancelled=false;$('#exportStart').disabled=true;
const png=new StreamingPng(w,h),generation=state.token;
try{
const paletteParts=r.frontColorKey.split(':'),palette={id:Number(paletteParts[0]),cycle:Number(paletteParts[1]),shift:Number(paletteParts[2]),edgeAA:paletteParts[3]==='1'},rgba=await r.readCompleteRgba(generation),rows=new Uint8Array((w*4+1)*h);
for(let y=0;y<h;y++)rows.set(rgba.subarray(y*w*4,(y+1)*w*4),y*(w*4+1)+1);
await png.rows(rows);const blob=await png.finish();
if(exportJob.cancelled||generation!==state.token)throw new Error('cancelled');
const base='mandelbrot-'+Date.now(),meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,width:w,height:h,supersampling:1,iterationPolicy:{base:state.baseIter,effective:iter,policy:state.fieldView.frontier?.policy||'finite-budget'},membershipCertified:false,numericalFailures:0,operationLimit:state.unknownReasons?.operationLimit||0,view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette,exportPipeline:'completed-numeric-field',shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;$('#exportStatus').textContent='計算済みの画像と座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
return;
}
2026-08-24 20:30:26 +09:00
let ctx=null,fastCtx=null;
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}else if(fastExtended){$('#exportStatus').textContent='高速拡張参照を準備中…';fastCtx=await fastRefs.request(snap,iter,w,h)}
2026-09-06 23:28:03 +09:00
const tile=r.exportTileSize(),ringDepth=deep||iter>4096?1:r.exportRingDepth(tile),totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
2026-08-23 23:27:38 +09:00
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
2026-08-25 21:33:45 +09:00
let done=0,unresolvedSamples=0,lastYield=performance.now();
2026-08-23 23:27:38 +09:00
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);
2026-08-25 21:33:45 +09:00
for(let x=0;x<w;){
if(exportJob.cancelled)throw new Error('cancelled');lastYield=await yieldExportUi(lastYield);if(exportJob.cancelled)throw new Error('cancelled');
const batch=[];for(let slot=0;slot<ringDepth&&x<w;slot++,x+=tile){const tileX=x,tw=Math.min(tile,w-tileX),args={snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX,tileY:y,w:tw,h:th,forceStrict:strict,workspaceSlot:slot,workspaceSize:tile};batch.push({tileX,tw,promise:ss===1?r.renderTileRGBA({...args,sampleX:.5,sampleY:.5,edgeAA:false}):r.renderTileRGBA2x(args)})}
const settled=await Promise.allSettled(batch.map(item=>item.promise)),failed=settled.find(item=>item.status==='rejected');if(failed)throw failed.reason;const results=settled.map(item=>item.value);
for(let i=0;i<batch.length;i++){const {tileX,tw}=batch[i],result=results[i],data=result.rgba;unresolvedSamples+=result.unresolved||0;for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+tileX*4);done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'')}
2026-08-23 23:27:38 +09:00
}
2026-08-25 21:33:45 +09:00
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);lastYield=await yieldExportUi(lastYield);
2026-08-23 23:27:38 +09:00
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
2026-09-06 23:28:03 +09:00
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':fastExtended?'reduced-reference + fast-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + '+ringDepth+'-slot staging ring + streaming-png':'gpu-tile + '+ringDepth+'-slot staging ring + streaming-png',exportTileSize:tile,exportRingDepth:ringDepth,iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch}:null,shaderVersion:G.version};
2026-08-23 23:27:38 +09:00
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}
}
2026-09-06 23:28:03 +09:00
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+(state.fieldView?.complete===false?'-preview':'')+'.png')},'image/png');
2026-08-23 23:27:38 +09:00
2026-09-06 23:28:03 +09:00
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?('WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'WebGPU 復旧中':'起動中');$('#render').textContent=renderTimeLabel();let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'数値補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.fieldView?.complete===false)status+=' · 暫定表示';if(state.unresolved){const nf=numericalFailureCount({reasons:state.unknownReasons||{}}),op=state.unknownReasons?.operationLimit||0;status+=' · 未確定 '+state.unresolved+(nf||op?' (数値 '+nf+' / 上限 '+op+')':'');}if(state.correctionPasses)status+=' · DS補正';if(state.multiRefPasses)status+=' · 局所ref '+state.multiRefPasses+' / 回収 '+state.multiRefRecovered;if(state.qualityStage==='preview')status+=' · preview '+state.qualityIter+'/'+state.qualityFinalIter;if(state.refinePending)status+=' · 最終品質待機';if(state.numericRecoveryPending)status+=' · 数値穴補修待機';if(state.numericRecoveryRunning)status+=' · 数値穴補修中';if(state.qualityStage==='refining')status+=' · 精細化 '+state.qualityIter+'/'+state.qualityFinalIter;if(state.qualityStage==='final')status+=' · 最終 '+(state.fieldView?.iter??state.qualityIter);if(state.fieldView?.frontier?.policy==='automatic-finite-cap')status+=' · 反復上限で完了';if(state.temporalFill)status+=' · 前フレーム保持';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status}
2026-08-23 23:27:38 +09:00
2026-09-06 23:28:03 +09:00
if(TEST_MODE){
async function waitForTestState(predicate,label,timeout=120000){const start=performance.now();for(;;){if(state.drawState==='ERROR')throw new Error(state.gpuError||'render failed');if(predicate())return true;if(performance.now()-start>=timeout)throw new Error(label+' timeout');if(state.dirty&&!state.rendering)schedule();await new Promise(resolve=>setTimeout(resolve,20))}}
2026-08-25 21:33:45 +09:00
async function sha256View(view){const bytes=new Uint8Array(view.buffer,view.byteOffset,view.byteLength),digest=await crypto.subtle.digest('SHA-256',bytes),out=[];for(const b of new Uint8Array(digest))out.push(b.toString(16).padStart(2,'0'));return out.join('')}
2026-08-23 23:27:38 +09:00
globalThis.__MANDEL_TEST__={
2026-09-06 23:28:03 +09:00
efficiency:()=>({precisionOrbits:runtime.precisionOrbits||0,activeBatches:runtime.activeBatches||0,activeWorkBound:runtime.activeWorkBound||0,activeInitialized:runtime.activeInitialized||0,readbackBytes:runtime.readbackBytes||0,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,activeGpuBytes:(renderer?.frame?.deepActiveCapacity||0)*((renderer?.frame?.deepActiveStride||32)+8)}),
kernelAccess:()=>({renderer,token:state.token}),
async pngRoundTrip(){
if(!renderer?.frame||state.fieldView?.complete===false||state.rendering)throw new Error('PNG requires a completed numeric field');
const r=renderer,f=r.frame,rgba=await r.readCompleteRgba();
{
const png=new StreamingPng(f.w,f.h),rows=new Uint8Array((1+f.w*4)*f.h);
for(let y=0;y<f.h;y++)rows.set(rgba.subarray(y*f.w*4,(y+1)*f.w*4),y*(1+f.w*4)+1);
await png.rows(rows);const blob=await png.finish(),bitmap=await createImageBitmap(blob),surface=new OffscreenCanvas(f.w,f.h),ctx=surface.getContext('2d');
ctx.drawImage(bitmap,0,0);bitmap.close();const decoded=ctx.getImageData(0,0,f.w,f.h).data;
let mismatches=0;for(let i=0;i<rgba.length;i++)if(rgba[i]!==decoded[i])mismatches++;
return{width:f.w,height:f.h,bytes:blob.size,mismatches,iter:state.fieldView.iter};
}
},
async independentSamples(indices){
if(!renderer?.frame||state.rendering)throw new Error('sample verification requires a completed frame');
const f=renderer.frame,iter=state.fieldView.iter;
const solved=await precisionFallback.request(snapshot(),iter,f.w,f.h,indices);
const field=await renderer.readFieldAll(),smoothed=new Float32Array(field.smoothBits.buffer);
return Array.from(solved.indices,(index,j)=>({index,packed:field.meta[index],smoothed:smoothed[index],oracle:solved.meta[j],oracleSmooth:solved.smooth[j],accepted:solved.accepted[j]}));
},
2026-08-25 21:33:45 +09:00
async ensureGpuReady(){const r=renderer||await initRenderer();return{ready:!!r,navigatorGpu:!!navigator.gpu,renderer:!!renderer,gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError}},
2026-09-06 23:28:03 +09:00
async setView({re,im,span,bits,baseIter=350,adaptive=false,continuationBudget=0,processMode='standard',renderMode='fast',fixed=false}){const ready=await this.ensureGpuReady();if(!ready.ready)throw new Error('WebGPU initialization failed: '+JSON.stringify(ready));cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fixed?BigInt(re):fromDec(re);state.im=fixed?BigInt(im):fromDec(im);state.span=fixed?BigInt(span):fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(continuationBudget)));state.processMode=processMode;state.renderMode='fast';state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');if(!renderer?.frame||!state.fieldView)throw new Error('test render completed without a GPU frame: '+JSON.stringify({gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError,drawState:state.drawState,engine:state.lastEngine}));return{width:canvas.width,height:canvas.height,state:this.state()}},
2026-08-23 23:27:38 +09:00
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)},
2026-08-25 21:33:45 +09:00
async waitForPrimary({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&!!state.fieldView,'primary',timeout);return this.state()},
2026-09-06 23:28:03 +09:00
async waitForFinal({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&!state.refinePending&&state.qualityStage==='final'&&state.fieldView?.complete===true&&state.renderClock?.status==='complete','final',timeout);return this.state()},
2026-08-25 21:33:45 +09:00
async waitForNumericComplete({timeout=120000}={}){await this.waitForFinal({timeout});await waitForTestState(()=>!state.numericRecoveryPending&&!state.numericRecoveryRunning,'numeric recovery',timeout);const result=this.state();if(result.numericalFailures)throw new Error('numeric recovery quiesced with '+result.numericalFailures+' unresolved failures');return result},
async fieldHashes(){if(!renderer?.frame)throw new Error('WebGPU field unavailable');const field=await renderer.readFieldAll();return{pixels:field.meta.length,metaSha256:await sha256View(field.meta),smoothSha256:await sha256View(field.smoothBits)}},
gpuDiagnostics:()=>renderer?{adapterInfo:{...renderer.adapterInfo},adapterLimits:{...renderer.adapterLimits},compilation:renderer.compilation.map(x=>({...x})),uncapturedErrors:[...renderer.uncapturedErrors],lossReason:renderer.lossReason||'',activeGpuStage:renderer.activeGpuStage,lastCompletedGpuStage:renderer.lastCompletedGpuStage}:null,
2026-09-06 23:28:03 +09:00
stabilityPolicy:()=>({adaptiveIterationQuantum:ADAPTIVE_ITER_QUANTUM,pixelFrontierProduction:PIXEL_FRONTIER_PRODUCTION,pixelFrontierEnabled:PIXEL_FRONTIER_ENABLED,pixelFrontierWorkLimit:PIXEL_FRONTIER_WORK,pixelFrontierMaxIter:PIXEL_FRONTIER_MAX_ITER,pixelFrontierMinimumIter:pixelFrontierIterationFloor(snapshot(),canvas.width,maxIter()),stableHistoryReady:!!renderer?.historyReady,deepCorrectionDispatchWork:PIXEL_FRONTIER_WORK}),
state:()=>{const snap=snapshot(),se=spanMantExp(snap),reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),spanExp:se.exp,directPixelRatio:directPixelRatio(snap,canvas.width),width:canvas.width,height:canvas.height,iter:maxIter(),effectiveIter:state.fieldView?.iter??maxIter(),continuationBudget:state.continuationBudget,renderMode:state.renderMode,palette:state.palette,cycle:state.cycle,shift:state.shift,colorRevision:state.colorRevision,colorAuto:state.colorAuto,rendering:state.rendering,recoloring:state.recoloring,recolorPending:state.recolorPending,lastRender:state.lastRender,renderClock:state.renderClock?{...state.renderClock}:null,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,provisional:state.fieldView?.complete===false,qualityStage:state.qualityStage,qualityIter:state.qualityIter,qualityFinalIter:state.qualityFinalIter,previewMs:state.previewMs,refineMs:state.refineMs,refinePending:state.refinePending,temporalFill:state.temporalFill,numericRecoveryPending:state.numericRecoveryPending,numericRecoveryRunning:state.numericRecoveryRunning,historyReady:!!renderer?.historyReady,multiRefPasses:state.multiRefPasses,multiRefRecovered:state.multiRefRecovered,frontierRounds:state.frontierRounds,frontierPixels:state.frontierPixels,frontierEscaped:state.frontierEscaped,frontierIter:state.frontierIter,frontierMinimumIter:state.frontierMinimumIter,frontierConverged:state.frontierConverged,frontierMaxDispatchWork:runtime.frontierMaxDispatchWork||0,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version,gpuStage:state.gpuStage,gpuTileRows:state.gpuTileRows,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,stageCounts:{...runtime.stageCounts},stageMs:{...runtime.stageMs},generationMetrics:runtime.generationMetrics.slice(-16).map(x=>({...x}))}},
2026-08-24 20:30:26 +09:00
async panPixels(dx,dy){pan(dx,dy);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test pan timeout');return this.state()},
2026-08-25 21:33:45 +09:00
async compareCurrentToFrontierBaseline({strict=true}={}){if(!renderer?.frame)throw new Error('WebGPU frame unavailable');const snap=snapshot(),iter=state.fieldView?.iter??maxIter(),w=canvas.width,h=canvas.height,view=state.fieldView||{},deep=true,exactRequired=!!view.deep,ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch)throw new Error('frontier baseline reference guard checkpoint mismatch');const rows=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,w*iter))),baseline=new Uint32Array(w*h);let maxDispatchWork=0;for(let y=0;y<h;y+=rows){const th=Math.min(rows,h-y),part=await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:y,w,h:th,forceStrict:!!strict,correctUnknown:true});baseline.set(part,y*w);maxDispatchWork=Math.max(maxDispatchWork,w*th*iter)}const current=await renderer.readMetaAll(),disputes=[];let mismatch=0,classMismatch=0,escapeIterationMismatch=0,first=-1,escaped=0,baselineEscaped=0;for(let i=0;i<current.length;i++){const a=current[i]>>>0,b=baseline[i]>>>0,ac=(a>>>28)&3,bc=(b>>>28)&3;if(ac===FIELD_ESCAPED)escaped++;if(bc===FIELD_ESCAPED)baselineEscaped++;const sameClass=(ac===FIELD_ESCAPED)===(bc===FIELD_ESCAPED),sameIter=ac!==FIELD_ESCAPED||(a&ITER_MASK)===(b&ITER_MASK);if(!sameClass)classMismatch++;else if(!sameIter)escapeIterationMismatch++;if(!sameClass||(exactRequired&&!sameIter)){mismatch++;disputes.push(i);if(first<0)first=i}}let oracleChecked=0,oracleInconclusive=0,oracleCurrentMismatch=0,oracleBaselineMismatch=0;if(disputes.length){const solved=await precisionFallback.request(snap,iter,w,h,disputes,-1);for(let j=0;j<solved.indices.length;j++){if(!solved.accepted[j]){oracleInconclusive++;continue}oracleChecked++;const i=solved.indices[j],o=solved.meta[j]>>>0,a=current[i]>>>0,b=baseline[i]>>>0,oc=(o>>>28)&3,ac=(a>>>28)&3,bc=(b>>>28)&3,currentSame=(oc===FIELD_ESCAPED)===(ac===FIELD_ESCAPED)&&(!exactRequired||oc!==FIELD_ESCAPED||(o&ITER_MASK)===(a&ITER_MASK)),baselineSame=(oc===FIELD_ESCAPED)===(bc===FIELD_ESCAPED)&&(!exactRequired||oc!==FIELD_ESCAPED||(o&ITER_MASK)===(b&ITER_MASK));if(!currentSame)oracleCurrentMismatch++;if(!baselineSame)oracleBaselineMismatch++}}const oracleGate=oracleCurrentMismatch===0&&oracleInconclusive===0;return{pixels:current.length,mismatch,classMismatch,escapeIterationMismatch,exactRequired,first,escaped,baselineEscaped,iter,baseline:'deep-gold',oracleChecked,oracleInconclusive,oracleCurrentMismatch,oracleBaselineMismatch,oracleGate,maxDispatchWork,workLimit:PIXEL_FRONTIER_WORK,gatePassed:oracleGate&&maxDispatchWork<=PIXEL_FRONTIER_WORK}},
2026-08-25 14:03:13 +09:00
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w):!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
2026-08-23 23:27:38 +09:00
};
2026-09-06 23:28:03 +09:00
} // TEST_MODE
2026-08-25 21:33:45 +09:00
2026-08-23 23:27:38 +09:00
// ── boot / teardown ──────────────────────────────────────────────────────
2026-09-06 23:28:03 +09:00
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto(false);cancelRender();refs.destroy();fastRefs.destroy();precisionFallback.destroy();for(const service of precisionFallbackPool)service.destroy();if(renderer)renderer.destroy()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}localStorage.removeItem('mandelbrot.renderMode')}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();if(!state.rendering&&!state.frameView)markDirty(false)});schedule();
2026-08-23 23:27:38 +09:00
})();
</script>
2026-08-22 20:55:27 +09:00
</body>
</html>