mandelbrot/index.html

1216 lines
137 KiB
HTML
Raw Normal View History

2026-08-22 20:55:27 +09:00
<!doctype html>
<html lang="ja">
<head>
2026-08-24 20:30:26 +09:00
<meta name="mandelbrot-bundle" content="single-file-v24.2.26">
2026-08-22 20:55:27 +09:00
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#050813">
2026-08-24 20:30:26 +09:00
<title>Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement</title>
2026-08-22 20:55:27 +09:00
<style>
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
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-22 20:55:27 +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}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
2026-08-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}
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord"></span></div><div class="row"><span class="muted">倍率</span> <span id="zoom"></span> <span class="muted">表示幅</span> <span id="span"></span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render"></span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大"></button><button id="zout" aria-label="中心を縮小"></button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
2026-08-24 20:30:26 +09:00
<div class="line"><label for="renderMode">描画方式</label><select id="renderMode"><option value="fast" selected>高速</option><option value="accurate">正確</option></select><output></output></div>
<div class="line"><label for="processMode">負荷設定</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
2026-08-22 20:55:27 +09:00
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
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-24 20:30:26 +09:00
<div class="mini-actions"><button id="colorAuto" type="button" aria-pressed="false">色を自動変化</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>
2026-08-24 20:30:26 +09:00
<div class="hint">ドラッグ中は直前フレームを再投影して再利用。ホイール / ピンチでズーム、HキーでUI表示を切り替え。</div>
2026-08-22 20:55:27 +09:00
</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;
fn pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
2026-08-24 20:30:26 +09:00
// Fast-mode precision extension. This keeps the interactive path independent
// of the verified Deep backend: a reduced-precision reference orbit is built
// quickly on the CPU worker and this lightweight perturbation kernel preserves
// sub-f32 pixel offsets once absolute Direct coordinates begin to collapse.
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{
if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;}
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-08-23 23:27:38 +09:00
2026-08-24 20:30:26 +09:00
// Rebase the perturbation when it grows beyond the reference value. This
// keeps the fast path usable across pans without any verified correction.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp < -126){mark_fast_unresolved(out,n,REASON_REBASE_GAP);return;}
w=z;
d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp);
scaleExp=0; m=0u;
continue;
}
if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=rp;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u;
let mm=max(maxabs(w),maxabs(d));
if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;}
if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;}
if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;}
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;
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){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
return;
}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=1.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
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);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
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-24 20:30:26 +09:00
// Idle refinement is compiled as a separate guarded-Deep module so the
// production Fast/Deep main shaders remain byte-for-byte identical to v24.2.18.
const DEEP_LIKELY_REFINE_WGSL=DEEP_PERTURB_WGSL.replace(
'@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;',
'@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var<storage,read> refineQueue:array<u32>;\n@group(1) @binding(1) var<uniform> refineBatch:RefineBatch;'
)+String.raw`
@compute @workgroup_size(64)
fn refine_likely(@builtin(global_invocation_id) gid:vec3<u32>){
let slot=refineBatch.base+gid.x;
if(slot>=refineBatch.end){return;}
let out=refineQueue[slot];
if(((fieldMeta[out]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}
let x=out%p.fullW; let y=out/p.fullW;
render_pixel(out,f32(x)+p.sampleX,f32(y)+p.sampleY,p.strict!=0u);
}
`;
// 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));
}
@compute @workgroup_size(64)
fn main(
@builtin(local_invocation_id) lid3:vec3<u32>,
@builtin(workgroup_id) wid:vec3<u32>
){
let lane=lid3.x;
if(lane<8u){atomicStore(&localCounts[lane],0u);}
workgroupBarrier();
let lx=wid.x*64u+lane;
let ly=wid.y;
if(lx<p.tileW && ly<p.tileH){
let out=p.outputBase+ly*p.outputStride+lx;
let packed=fieldMeta[out];
if(((packed>>28u)&3u)==FIELD_UNKNOWN){
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));
}
@compute @workgroup_size(64)
fn main(
@builtin(local_invocation_id) lid3:vec3<u32>,
@builtin(workgroup_id) wid:vec3<u32>
){
let lane=lid3.x;
if(lane<8u){
atomicStore(&localCounts[lane],0u);
atomicStore(&localRanks[lane],0u);
groupBase[lane]=0u;
}
workgroupBarrier();
let lx=wid.x*64u+lane;
let ly=wid.y;
var out=0u;
var hit=0u;
var bucket=0u;
if(lx<p.tileW && ly<p.tileH){
out=p.outputBase+ly*p.outputStride+lx;
let packed=fieldMeta[out];
if(((packed>>28u)&3u)==FIELD_UNKNOWN){
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;
fn count_remaining(out:u32){
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
}
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
}
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b;
let e=b-(q-a);
return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b;
let bb=q-a;
let e=(a-(q-bb))+(b-bb);
return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a;
let ah=ca-(ca-a);
let al=a-ah;
let cb=4097.0*b;
let bh=cb-(cb-b);
let bl=b-bh;
var e=ah*bh-q;
e=e+ah*bl;
e=e+al*bh;
e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h);
return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l;
e=e+a.l*b.h;
e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b);
return ds_quick(q.h,q.l+a.l*b);
}
fn ds_pow2(a:DS,e:i32)->DS{
if(e < -126){return DS(0.0,0.0);}
if(e > 126){return DS(8.507059e37,0.0);}
return DS(ldexp(a.h,e),ldexp(a.l,e));
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
var scaleExp=p.spanExp;
2026-08-24 20:30:26 +09:00
var n=0u; var m=0u;
2026-08-23 23:27:38 +09:00
loop{
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
let rp=refs[m];
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
let delta=cds_pow2(w,scaleExp);
let z=cds_add(r,delta);
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
return;
}
let dmag=cds_mag2(delta);
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
continue;
}
if(m>=p.refLen){count_remaining(out);return;}
let linear=cds_scale(cds_mul(r,w),2.0);
let sq=cds_pow2(cds_mul(w,w),scaleExp);
w=cds_add(cds_add(linear,sq),d);
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));
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
2026-08-24 20:30:26 +09:00
if(scaleExp>126){count_remaining(out);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;
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
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;
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}`,
`@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let qi=gid.x;
let queued=atomicLoad(&sparseQueueStats.dispatchCount);
if(qi>=queued){return;}
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;}
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;}
let gx=f32(p.tileX+lx)+p.sampleX;
let gy=f32(p.tileY+ly)+p.sampleY;
correct_pixel(out,gx,gy);
}`);
if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)')){
throw new Error('failed to derive queued deep correction shader');
}
const LIKELY_QUEUE_WGSL=COMMON+String.raw`
struct QueueParams{width:u32,height:u32,mode:u32,sparseStride:u32};
struct QueueStats{count:atomic<u32>,overflow:atomic<u32>,_p0:atomic<u32>,_p1:atomic<u32>};
@group(0) @binding(0) var<uniform> p:QueueParams;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> queue:array<u32>;
@group(0) @binding(3) var<storage,read_write> stats:QueueStats;
fn is_frontier(x:u32,y:u32,r:i32)->bool{let xi=i32(x);let yi=i32(y);var oy=-1;loop{if(oy>1){break;}var ox=-1;loop{if(ox>1){break;}if(ox!=0||oy!=0){let xx=xi+ox*r;let yy=yi+oy*r;if(xx>=0&&yy>=0&&xx<i32(p.width)&&yy<i32(p.height)){let cls=(fieldMeta[u32(yy)*p.width+u32(xx)]>>28u)&3u;if(cls==FIELD_ESCAPED||cls==FIELD_UNKNOWN){return true;}}}ox+=1;}oy+=1;}return false;}
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){let i=gid.x;let total=p.width*p.height;if(i>=total){return;}if(((fieldMeta[i]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}let x=i%p.width;let y=i/p.width;var selected=false;if(p.mode==0u){selected=true;}else{let radius:i32=select(2,select(3,4,p.mode>=3u),p.mode>=2u);selected=is_frontier(x,y,radius);if(!selected&&p.sparseStride>0u){selected=(x%p.sparseStride==0u)&&(y%p.sparseStride==0u);}}if(!selected){return;}let slot=atomicAdd(&stats.count,1u);if(slot<total){queue[slot]=i;}else{atomicAdd(&stats.overflow,1u);}}
`;
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);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
2026-08-24 20:30:26 +09:00
fn palette_color(phase:f32)->vec3<f32>{
if(p.palette==1u){return hsv(phase,0.92,1.0);}if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3<f32>(g);}return current_palette(phase);
}
fn escaped_color(m:u32,sm:f32)->vec3<f32>{
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.34+0.66*pow(edge,0.38);let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv);
}
fn provisional_unknown(m:u32)->vec3<f32>{
let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3<f32>(6,10,22)/255.0,vec3<f32>(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52);
}
2026-08-23 23:27:38 +09:00
fn base_color(i:u32)->vec3<f32>{
2026-08-24 20:30:26 +09:00
let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3<f32>(0.0);}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-24 20:30:26 +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;var c=base_color(i);let x=i32(gid.x);let y=i32(gid.y);
if(cls==FIELD_UNKNOWN){
var fill=vec3<f32>(0.0);var fillCount=0.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_UNKNOWN){fill+=linearize(base_color(j));fillCount+=1.0;}}}
if(fillCount>0.0){c=delinearize(fill/fillCount);}
}
2026-08-23 23:27:38 +09:00
if(p.edgeAA!=0u){
2026-08-24 20:30:26 +09:00
var boundary=false;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];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
2026-08-23 23:27:38 +09:00
if(boundary){c=delinearize(sum/cnt);}
}
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;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
2026-08-24 20:30:26 +09:00
// v24.2.14 production screen path: defer UNKNOWN statistics out of the long
// perturbation kernel. The existing bucket histogram pass already scans the
// same field before sparse correction, so it can aggregate remaining/reason
// counters per workgroup without per-UNKNOWN global atomics in the primary.
// The counted primary is retained for export and verification paths.
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}');
const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL
.replace('@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;', '@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;\n@group(0) @binding(3) var<storage,read_write> unresolvedStats:array<atomic<u32>>;')
.replace('var<workgroup> localCounts:array<atomic<u32>,8>;', 'var<workgroup> localCounts:array<atomic<u32>,8>;\nvar<workgroup> localReasons:array<atomic<u32>,8>;')
.replace('if(lane<8u){atomicStore(&localCounts[lane],0u);}', 'if(lane<8u){atomicStore(&localCounts[lane],0u);atomicStore(&localReasons[lane],0u);}')
.replace('atomicAdd(&localCounts[deep_bucket(packed)],1u);', `atomicAdd(&localCounts[deep_bucket(packed)],1u);
atomicAdd(&localReasons[0],1u);
let reason=(packed&REASON_MASK)>>REASON_SHIFT;
if(reason>=1u && reason<=6u){atomicAdd(&localReasons[reason],1u);}`)
.replace('if(c>0u){atomicAdd(&bucketState[lane],c);}', `if(c>0u){atomicAdd(&bucketState[lane],c);}
let r=atomicLoad(&localReasons[lane]);
if(r>0u){atomicAdd(&unresolvedStats[lane],r);}`);
if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('v24.2.14 post-stats shader derivation failed');
2026-08-23 23:27:38 +09:00
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
2026-08-24 20:30:26 +09:00
version:'24.2.26-baseline-idle-refinement',DIRECT_F32_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_LIKELY_REFINE_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,LIKELY_QUEUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_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-24 20:30:26 +09:00
const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000;
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-24 20:30:26 +09:00
const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96,REFINE_QUEUE_PARAM_BYTES=16,REFINE_STATS_BYTES=16,REFINE_BATCH_BYTES=16;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorCycleDir:1,colorShiftDir:1,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,refinementRunning:false,refinementStage:0,refinementQueue:0,refinedIter:0};
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,idleRefineTimer=0,lastWrittenHash='',navigationHash='';
2026-08-23 23:27:38 +09:00
const viewHistory=[];let viewHistoryIndex=-1;
2026-08-24 20:30:26 +09:00
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,idleRefineBatches:0,idleRefineQueued:0};
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)}
function chooseBackend(snap=snapshot(),w=canvas.width){const deep=state.renderMode==='accurate',fastExtended=!deep&&fastNeedsExtended(snap,w);return{backend:deep?'deep':fastExtended?'fast-extended':'direct',deep,fastExtended,probe:false,reason:deep?'manual-accurate':fastExtended?'manual-fast-extended':'manual-fast'}}
function deepNeeded(){return state.renderMode==='accurate'}
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,renderMode:state.renderMode}}
2026-08-23 23:27:38 +09:00
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
2026-08-24 20:30:26 +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,renderMode:v.renderMode||state.renderMode});ensurePrecision();syncControls();saveHash(false);markDirty()}
2026-08-23 23:27:38 +09:00
// ── iteration / quality policy ───────────────────────────────────────────
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
2026-08-24 20:30:26 +09:00
function effectiveColorCycle(iter=maxIter()){const base=Math.max(1,state.baseIter),effective=Math.max(base,Number(iter)||base);return state.cycle*base/effective}
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-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';
const MAX_REF=150001,MAX_LEVELS=20;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
2026-08-24 20:30:26 +09:00
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n;for(let n=0;n<iter;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)return n+1}return 0}
function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]}
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-23 23:27:38 +09:00
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
2026-08-24 20:30:26 +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,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),limit=Math.min(MAX_REF-1,d.iter),bits=sourceBits+64,centerRef=orbit(bits,sourceRe<<64n,sourceIm<<64n,limit),chosen=chooseReference(sourceBits,sourceRe,sourceIm,sourceSpan,w,h,limit,centerRef.escape),re=chosen.re<<64n,im=chosen.im<<64n,ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,limit),v=verify(sourceBits,chosen.re,chosen.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,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{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16}
2026-08-24 20:30:26 +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([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,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>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter,width=1,height=1,fresh=false){const key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,'guarded-perturb-v24.2.26'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})}
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-08-24 20:30:26 +09:00
function fastReferenceBits(snap){const need=Math.ceil(Math.max(0,-log2FixedAt(snap.span,snap.bits)))+48;return Math.max(96,Math.min(snap.bits,need))}
function fastReferenceWorkerSource(){return String.raw`
'use strict';
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){if(b<=0)return v;const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<<BigInt(d):roundShift(v,-d)}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n;for(let n=0;n<iter;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)return n+1}return 0}
function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]}
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.score){best={...c,escape,score}}if(escape===0)break}return best}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),bits=d.targetBits,centerRe=requant(sourceRe,d.sourceBits,bits),centerIm=requant(sourceIm,d.sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,d.iter),chosen=chooseReference(d.sourceBits,sourceRe,sourceIm,sourceSpan,w,h,d.iter,centerRef.escape),re=requant(chosen.re,d.sourceBits,bits),im=requant(chosen.im,d.sourceBits,bits),ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,d.iter),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class FastReferenceService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=12}
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>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'fast reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.2.26'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im: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})})}
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-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-08-24 20:30:26 +09:00
this.frame=null;this.deepCtx=null;this.fastCtx=null;this.refineDeepCtx=null;this.exportWs=null;this.refinePipelinePromise=null;this.likelyQueue=null;this.deepLikelyRefine=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
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();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
2026-08-24 20:30:26 +09:00
const [dm,fm,xm,xhm,xpm,xsm,zm,zqm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),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('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_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-24 20:30:26 +09:00
this.fast=this.device.createComputePipeline({layout:'auto',compute:{module:fm,entryPoint:'main'}});
2026-08-23 23:27:38 +09:00
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
2026-08-24 20:30:26 +09:00
const [xpsm,xhsm]=await Promise.all([this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL),this.module('deep-bucket-histogram-stats-production',G.DEEP_BUCKET_HIST_STATS_WGSL)]);
this.deepPostStats=this.device.createComputePipeline({layout:'auto',compute:{module:xpsm,entryPoint:'main'}});
this.deepBucketHist=this.device.createComputePipeline({layout:'auto',compute:{module:xhm,entryPoint:'main'}});
this.deepBucketHistStats=this.device.createComputePipeline({layout:'auto',compute:{module:xhsm,entryPoint:'main'}});
this.deepBucketPrefix=this.device.createComputePipeline({layout:'auto',compute:{module:xpm,entryPoint:'main'}});
this.deepBucketScatter=this.device.createComputePipeline({layout:'auto',compute:{module:xsm,entryPoint:'main'}});
2026-08-23 23:27:38 +09:00
this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}});
2026-08-24 20:30:26 +09:00
this.correctQueued=this.device.createComputePipeline({layout:'auto',compute:{module:zqm,entryPoint:'main'}});
2026-08-23 23:27:38 +09:00
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
webgpuCanvasClaimed=true;this.configure();
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
2026-08-24 20:30:26 +09:00
async ensureRefinePipelines(){if(this.likelyQueue&&this.deepLikelyRefine)return;if(this.refinePipelinePromise)return this.refinePipelinePromise;this.refinePipelinePromise=(async()=>{const [lqm,drm]=await Promise.all([this.module('likely-refine-queue',G.LIKELY_QUEUE_WGSL),this.module('deep-likely-refine',G.DEEP_LIKELY_REFINE_WGSL)]);this.likelyQueue=this.device.createComputePipeline({layout:'auto',compute:{module:lqm,entryPoint:'main'}});this.deepLikelyRefine=this.device.createComputePipeline({layout:'auto',compute:{module:drm,entryPoint:'refine_likely'}})})().finally(()=>{this.refinePipelinePromise=null});return this.refinePipelinePromise}
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','refineQueue','refineStats','refineQueueParams','refineBatchParams','refineRead','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
2026-08-23 23:27:38 +09:00
ensureFrame(w,h){
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
2026-08-24 20:30:26 +09:00
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,refineQueue:null,refineStats:null,refineQueueParams:null,refineBatchParams:null,refineRead:null,numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
2026-08-23 23:27:38 +09:00
}
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}
ensureRefineWorkspace(f=this.frame){if(!f)return null;if(f.refineQueue&&f.refineStats&&f.refineQueueParams&&f.refineBatchParams&&f.refineRead)return f;const d=this.device,B=GPUBufferUsage;f.refineQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-queue');f.refineStats=buf(d,REFINE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-stats');f.refineQueueParams=buf(d,REFINE_QUEUE_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-queue-params');f.refineBatchParams=buf(d,REFINE_BATCH_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-batch-params');f.refineRead=buf(d,REFINE_STATS_BYTES,B.COPY_DST|B.MAP_READ,'likely-refine-readback');return f}
2026-08-23 23:27:38 +09:00
ensureExportWorkspace(){
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
}
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
2026-08-24 20:30:26 +09:00
setRefineDeepContext(ctx){if(this.refineDeepCtx&&this.refineDeepCtx.key===ctx.key)return;this.destroyRefineDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'idle-refinement-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.refineDeepCtx={...ctx,refsB}}
destroyRefineDeepContext(){if(this.refineDeepCtx)destroy(this.refineDeepCtx.refsB);this.refineDeepCtx=null}
setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}}
destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null}
2026-08-23 23:27:38 +09:00
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
2026-08-24 20:30:26 +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){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,0,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()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,effectiveColorCycle(iter),true);d.setFloat32(20,state.shift,true)})}
2026-08-23 23:27:38 +09:00
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
2026-08-24 20:30:26 +09:00
encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,pipeline=this.deepPostStats,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
2026-08-23 23:27:38 +09:00
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
2026-08-24 20:30:26 +09:00
encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.fast.getBindGroupLayout(0),entries:[{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()}
encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHist.getBindGroupLayout(0),entries:[{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 d=this.device,bg=d.createBindGroup({layout:this.deepBucketHistStats.getBindGroupLayout(0),entries:[{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 d=this.device,bg=d.createBindGroup({layout:this.deepBucketPrefix.getBindGroupLayout(0),entries:[{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 d=this.device,bg=d.createBindGroup({layout:this.deepBucketScatter.getBindGroupLayout(0),entries:[{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 d=this.device,bg=d.createBindGroup({layout:this.correctQueued.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{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-24 20:30:26 +09:00
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null){
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep){this.setDeepContext(deepContext);this.ensureDeepBucketState(f)}else if(fastExtended){this.setFastContext(fastContext)}d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep)d.queue.writeBuffer(f.deepBucketState,0,new Uint32Array(DEEP_BUCKET_STATE_BYTES/4));
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;const params=deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,refX,refY):fastExtended?this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap);d.queue.writeBuffer(f.numericParams,0,params);
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
if(deep){this.encodeDeepPostStatsNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeDeepBucketHistogramStats(encoder,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,unresolved:f.unresolved,w:f.w,h:f.h});}
else if(fastExtended){this.encodeFastNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});}
else{this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});}
if(!deep){const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();}
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;if(!deep)[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
2026-08-23 23:27:38 +09:00
}
2026-08-24 20:30:26 +09:00
async buildLikelyRefineQueue(token,mode=1,sparseStride=4){await this.ready;await this.ensureRefinePipelines();if(token!==state.token)return{count:0,overflow:0,cancelled:true};const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;d.queue.writeBuffer(f.refineStats,0,new Uint32Array(REFINE_STATS_BYTES/4));d.queue.writeBuffer(f.refineQueueParams,0,new Uint32Array([f.w,f.h,mode>>>0,sparseStride>>>0]));const e=d.createCommandEncoder({label:'likely-refine-queue-build'}),bg=d.createBindGroup({layout:this.likelyQueue.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.refineQueueParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.refineQueue}},{binding:3,resource:{buffer:f.refineStats}}]}),pass=e.beginComputePass();pass.setPipeline(this.likelyQueue);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.n/256));pass.end();e.copyBufferToBuffer(f.refineStats,0,f.refineRead,0,REFINE_STATS_BYTES);d.queue.submit([e.finish()]);await f.refineRead.mapAsync(GPUMapMode.READ);const a=new Uint32Array(f.refineRead.getMappedRange().slice(0));f.refineRead.unmap();if(token!==state.token)return{count:0,overflow:0,cancelled:true};return{count:Math.min(f.n,a[0]||0),overflow:a[1]||0,cancelled:false}}
async refineLikelyBatch({snap,iter,deepContext,token,base,end,forceStrict=false}){await this.ready;await this.ensureRefinePipelines();if(token!==state.token||base>=end)return false;const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setRefineDeepContext(deepContext);const ref=referencePixelForSource(deepContext?.source,snap,f.w,f.h);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,ref.x,ref.y,0,f.w,0));d.queue.writeBuffer(f.refineBatchParams,0,new Uint32Array([base>>>0,end>>>0,0,0]));const pipeline=this.deepLikelyRefine,bg0=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.refineDeepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),bg1=d.createBindGroup({layout:pipeline.getBindGroupLayout(1),entries:[{binding:0,resource:{buffer:f.refineQueue}},{binding:1,resource:{buffer:f.refineBatchParams}}]}),e=d.createCommandEncoder({label:'likely-idle-refine'}),pass=e.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg0);pass.setBindGroup(1,bg1);pass.dispatchWorkgroups(Math.ceil((end-base)/64));pass.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.idleRefineBatches++;return true}
async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
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)}}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
async readUnresolved(){return(await this.readUnresolvedStats()).total}
2026-08-24 20:30:26 +09:00
async correctUnknownFrame(snap,iter,token,referencePixel=null){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');this.ensureDeepQueueWorkspace(f);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.deepQueueStats,0,new Uint32Array(SPARSE_QUEUE_STATS_BYTES/4));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array(SPARSE_INDIRECT_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:'deep-bucketed-correction'});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.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});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}){
await this.ready;const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
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-08-24 20:30:26 +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}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){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-24 20:30:26 +09:00
async renderTileRGBA2x({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h,iter);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
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-24 20:30:26 +09:00
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyRefineDeepContext();this.destroyDeepContext();this.destroyFastContext()}
2026-08-23 23:27:38 +09:00
}
function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true}
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}}
function reusableDeepReference(snap,iter,w,h){const ctx=renderer&&renderer.deepCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}}
function reusableFastReference(snap,iter,w,h){const ctx=renderer&&renderer.fastCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}}
2026-08-23 23:27:38 +09:00
// ── GPU startup / rendering orchestration ────────────────────────────────
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
2026-08-24 20:30:26 +09:00
function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span}
function cancelIdleRefinement(){if(idleRefineTimer){clearTimeout(idleRefineTimer);idleRefineTimer=0}state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0}
function idleRefinementTargets(baseIter){if(baseIter>=8000)return[];const z=zoomExp(),mode=state.processMode,mul=mode==='power'?.75:mode==='fine'?1.25:mode==='validate'?1.4:1,cap=Math.min(8000,Math.max(baseIter*2,Math.round((1200+300*z)*mul))),raw=mode==='power'?[Math.min(cap,baseIter*2)]:[Math.min(cap,baseIter*2),Math.min(cap,baseIter*4),cap],out=[];for(const v of raw){const n=Math.max(baseIter+1,Math.floor(v));if(n>baseIter&&(!out.length||n!==out[out.length-1]))out.push(n)}return out}
function idleRefinementBatchBudget(){const m=state.processMode;if(m==='power')return 900000;if(m==='fine')return 2800000;if(m==='validate')return 1800000;return 1800000}
function idleRefinementQueuePolicy(stage){if(stage<=0)return{mode:1,stride:4};if(stage===1)return{mode:2,stride:8};return{mode:3,stride:16}}
function idleRefinementSliceMs(){return state.processMode==='power'?100:state.processMode==='fine'?220:160}
function idleRefinementPauseMs(){return state.processMode==='power'?160:80}
function canIdleRefine(token,snap,baseIter){return token===state.token&&renderer&&!state.rendering&&!state.pointerActive&&!state.wheelActive&&sameSnapshot(snap,snapshot())&&state.fieldView&&state.fieldView.iter===baseIter&&state.fieldView.w===canvas.width&&state.fieldView.h===canvas.height}
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function paintIdleRefinement(token,snap,baseIter){while(state.recoloring&&canIdleRefine(token,snap,baseIter))await sleep(12);if(!canIdleRefine(token,snap,baseIter))return false;await recolor();while((state.recoloring||state.recolorPending)&&canIdleRefine(token,snap,baseIter))await sleep(8);return canIdleRefine(token,snap,baseIter)}
function scheduleIdleRefinement(snap,baseIter,decision,token){cancelIdleRefinement();if(!renderer||(!decision.deep&&!decision.fastExtended))return;const targets=idleRefinementTargets(baseIter);if(!targets.length)return;idleRefineTimer=setTimeout(()=>{idleRefineTimer=0;runIdleRefinement(snap,baseIter,decision,token,targets)},320)}
async function runIdleRefinement(snap,baseIter,decision,token,targets){if(!canIdleRefine(token,snap,baseIter))return;state.refinementRunning=true;state.refinedIter=baseIter;updateStats();try{for(let stage=0;stage<targets.length;stage++){if(!canIdleRefine(token,snap,baseIter))return;const targetIter=targets[stage];state.refinementStage=stage+1;state.refinedIter=targetIter;const deepCtx=await refs.request(snap,targetIter,canvas.width,canvas.height);if(!canIdleRefine(token,snap,baseIter))return;if(deepCtx.checkpointMismatch)throw new Error('idle refinement reference guard checkpoint mismatch');const policy=idleRefinementQueuePolicy(stage),q=await renderer.buildLikelyRefineQueue(token,policy.mode,policy.stride);if(!canIdleRefine(token,snap,baseIter)||q.cancelled)return;state.refinementQueue=q.count;runtime.idleRefineQueued+=q.count;updateStats();if(q.count<=0)continue;const budget=idleRefinementBatchBudget(),batchPixels=Math.max(64,Math.floor(Math.max(64,budget/Math.max(1,targetIter))/64)*64);let base=0,lastPaint=performance.now();while(base<q.count){if(!canIdleRefine(token,snap,baseIter))return;const end=Math.min(q.count,base+batchPixels),ok=await renderer.refineLikelyBatch({snap,iter:targetIter,deepContext:deepCtx,token,base,end,forceStrict:state.processMode==='validate'});if(!ok||!canIdleRefine(token,snap,baseIter))return;base=end;state.refinementQueue=q.count-base;const now=performance.now();if(now-lastPaint>=idleRefinementSliceMs()){const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;updateStats();lastPaint=performance.now();await sleep(idleRefinementPauseMs())}}const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;state.fieldView.refinedIter=targetIter;updateStats();await sleep(idleRefinementPauseMs())}}catch(e){if(token===state.token){console.warn('idle refinement stopped:',e);state.gpuError=String(e&&e.message||e)}}finally{if(token===state.token){state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0;updateStats()}}}
function cancelRender(){cancelIdleRefinement();state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled');fastRefs.cancelPending('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)}
async function renderFrame(){
const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height);
state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats();
try{
const r=renderer||await initRenderer();if(token!==state.token)return;
if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}
2026-08-24 20:30:26 +09:00
const deep=decision.deep,fastExtended=decision.fastExtended;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false;
if(deep){const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height);if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 正確 reference再利用'}else{state.lastEngine='WebGPU · 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · 正確 perturbation'}}
else if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 高速拡張 reference再利用'}else{state.lastEngine='WebGPU · 高速拡張準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height);state.lastEngine='WebGPU · 高速拡張'}}
else state.lastEngine='WebGPU · 高速';
const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate',referencePixel,fastExtended,fastCtx);if(!ok)return;
if(!deep){
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false;
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});
if(fastExtended){const stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons}
const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token);return;
}
// Accurate mode keeps the previous committed frame visible while the new
// primary field is checked/corrected. The incomplete primary texture lives
// only in frame.back and is never presented to the viewer.
2026-08-23 23:27:38 +09:00
let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats();
2026-08-24 20:30:26 +09:00
const n=canvas.width*canvas.height;
if(shouldApplySparseCorrection(state.unresolved,n)){
state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats();
const corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return;state.correctionPasses=1;
stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;
}
const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return;
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · '+(referenceReused?'正確 reference再利用':'正確 reference自動選択')+(state.correctionPasses?' + DS correction':'');
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token);
2026-08-23 23:27:38 +09:00
}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
}
2026-08-24 20:30:26 +09:00
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · 正確モード非対応';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*effectiveColorCycle(iter)+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback正確モード非対応';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
2026-08-23 23:27:38 +09:00
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
2026-08-24 20:30:26 +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');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'))state.palette=Math.max(0,Math.min(2,Number(p.get('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';if(p.has('rm'))state.renderMode=p.get('rm')==='accurate'?'accurate':'fast';ensurePrecision();return true}catch{return false}}
function syncHistoryButtons(){}
function syncControls(){$('#renderMode').value=state.renderMode;$('#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()};
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
2026-08-24 20:30:26 +09:00
let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0;
function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色アニメ停止':'色を自動変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')}
function stopColorAuto(){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton()}
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;const cmin=.001,cmax=.05,smin=0,smax=1;state.cycle+=state.colorCycleDir*.0006*dt;state.shift+=state.colorShiftDir*.012*dt;if(state.cycle>=cmax){state.cycle=cmax;state.colorCycleDir=-1}else if(state.cycle<=cmin){state.cycle=cmin;state.colorCycleDir=1}if(state.shift>=smax){state.shift=smax;state.colorShiftDir=-1}else if(state.shift<=smin){state.shift=smin;state.colorShiftDir=1}$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;recolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)}
$('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf)colorAutoRaf=requestAnimationFrame(colorAutoStep);else if(!state.colorAuto)stopColorAuto()};
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
$('#renderMode').onchange=e=>{state.renderMode=e.target.value==='accurate'?'accurate':'fast';try{localStorage.setItem('mandelbrot.renderMode',state.renderMode)}catch{}markDirty()};
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
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)}}
async function runExport(){
if(exportJob.active)return;
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
2026-08-24 20:30:26 +09:00
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),fastExtended=!deep&&fastNeedsExtended(snap,w),strict=$('#exportPrecision').value==='strict';
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-08-23 23:27:38 +09:00
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
let done=0,unresolvedSamples=0;
try{
for(let y=0;y<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
2026-08-24 20:30:26 +09:00
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
2026-08-23 23:27:38 +09:00
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
2026-08-24 20:30:26 +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 + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
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}
}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
2026-08-24 20:30:26 +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?(decision.deep?'WebGPU 正確':decision.fastExtended?'WebGPU 高速':'WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'正確描画の補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.refinementRunning)status+=' · 補足精密化 '+state.refinementStage+' ('+state.refinedIter+' iter, '+state.refinementQueue+')';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
globalThis.__MANDEL_TEST__={
2026-08-24 20:30:26 +09:00
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard',renderMode='fast'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.renderMode=renderMode==='accurate'?'accurate':'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');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-24 20:30:26 +09:00
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter(),renderMode:state.renderMode,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused}),
async panPixels(dx,dy){pan(dx,dy);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test pan timeout');return this.state()},
async 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():!!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(),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
};
// ── boot / teardown ──────────────────────────────────────────────────────
2026-08-24 20:30:26 +09:00
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();refs.destroy();fastRefs.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'}state.renderMode=localStorage.getItem('mandelbrot.renderMode')==='accurate'?'accurate':'fast'}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
2026-08-23 23:27:38 +09:00
})();
</script>
2026-08-22 20:55:27 +09:00
</body>
</html>