2026-08-22 20:55:27 +09:00
<!doctype html>
< html lang = "ja" >
< head >
2026-08-25 21:33:45 +09:00
< meta name = "mandelbrot-bundle" content = "single-file-v24.3.0" >
2026-08-22 20:55:27 +09:00
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width,initial-scale=1,viewport-fit=cover" >
2026-08-25 14:03:13 +09:00
< meta name = "theme-color" content = "#232a38" >
2026-08-25 21:33:45 +09:00
< title > Mandelbrot Deep Zoom v24.3.0 Optimized UNKNOWN Pipeline< / title >
2026-08-22 20:55:27 +09:00
< style >
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
2026-08-25 14:03:13 +09:00
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#000;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#000;image-rendering:auto;touch-action:none}
2026-08-22 20:55:27 +09:00
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
2026-08-25 14:03:13 +09:00
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand-return{display:block;margin-top:3px;color:var(--muted);font-size:10px;font-weight:650;letter-spacing:0;text-decoration:none}.brand-return:hover{text-decoration:underline;color:var(--text)}.brand-return:focus-visible{outline:2px solid #fff;outline-offset:2px;border-radius:3px}
2026-08-22 20:55:27 +09:00
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
2026-08-24 20:30:26 +09:00
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));max-height:calc(100dvh - 20px);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.18);background:#000;color:#fff;min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}select option{background:#000;color:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
2026-08-22 20:55:27 +09:00
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
2026-08-24 20:30:26 +09:00
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
2026-08-25 14:03:13 +09:00
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
2026-08-24 20:30:26 +09:00
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
2026-08-22 20:55:27 +09:00
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
2026-08-25 14:03:13 +09:00
@media(max-width:700px){.stats{display:none}.brand-return{font-size:9.5px}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.compact-status{display:block}button,select{min-height:44px}}
2026-08-22 20:55:27 +09:00
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
< / style >
< / head >
< body >
< canvas id = "view" tabindex = "0" role = "img" aria-label = "マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます" > < / canvas >
2026-08-25 14:03:13 +09:00
< div class = "top" > < div class = "brand" > < span > MANDELBROT DEEP ZOOM< / span > < a class = "brand-return" href = "https://2012r2.nishi.boats/~333/" > 333の部屋に戻る< / a > < / div > < div class = "stats" role = "status" aria-live = "polite" aria-atomic = "true" > < div class = "row" > < span class = "muted" > 中心< / span > < span id = "coord" > …< / span > < / div > < div class = "row" > < span class = "muted" > 倍率< / span > < span id = "zoom" > …< / span > < span class = "muted" > 表示幅< / span > < span id = "span" > …< / span > < / div > < div class = "row" > < span class = "muted" > 計算< / span > < span id = "engine" > 起動中…< / span > < span class = "muted" > 描画< / span > < span id = "render" > …< / span > < / div > < / div > < / div >
2026-08-22 20:55:27 +09:00
< div id = "compactStatus" class = "compact-status" role = "status" aria-live = "polite" > 起動中< / div >
< div id = "controls" class = "panel" >
< div class = "toolbar" > < button id = "zin" aria-label = "中心を拡大" > + < / button > < button id = "zout" aria-label = "中心を縮小" > − < / button > < button id = "reset" > リセット< / button > < button id = "png" > 出力< / button > < / div >
< div class = "group" >
2026-08-24 20:30:26 +09:00
< div class = "line" > < label for = "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-25 14:03:13 +09:00
< div class = "line" > < label for = "palette" > 彩色< / label > < select id = "palette" aria-label = "彩色プリセット" > < option value = "0" > 昼夜< / option > < option value = "1" > 虹色< / option > < option value = "2" > 白黒< / option > < option value = "3" > インフェルノ< / option > < option value = "5" > 氷晶< / option > < option value = "7" > オーロラ< / option > < option value = "9" > 深海< / option > < option value = "10" > 黄金< / option > < option value = "11" > ネオン< / option > < option value = "13" > 電光< / option > < option value = "15" > 蒼橙< / option > < option value = "17" > 紅碧< / option > < option value = "18" > 桃翠< / option > < / select > < output > < / output > < / div >
2026-08-24 20:30:26 +09:00
< div class = "line" > < label for = "cycle" > 色周期< / label > < input id = "cycle" type = "range" min = "0" max = "1000" step = "1" value = "532" aria-label = "色周期(対数スケール)" > < output id = "cycleO" for = "cycle" > 0.0080< / output > < / div >
2026-08-22 20:55:27 +09:00
< div class = "line" > < label for = "shift" > 色相位置< / label > < input id = "shift" type = "range" min = "0" max = "1" step = "0.005" value = "0.18" > < output id = "shiftO" for = "shift" > .18< / output > < / div >
2026-08-25 14:03:13 +09:00
< div class = "mini-actions" > < button id = "colorAuto" type = "button" aria-pressed = "false" title = "色周期は固定し、色相位置だけを動かします" > 色相変化< / button > < / div >
2026-08-22 20:55:27 +09:00
< / div >
< div class = "group bottom" > < span class = "badge" > < span class = "dot" > < / span > < span id = "badge" > 起動中< / span > < / span > < div style = "display:flex;gap:6px" > < button id = "share" > URL共有< / button > < / div > < / div >
< / div >
< button id = "uiToggle" title = "UIを隠す / 表示" aria-controls = "controls" aria-expanded = "true" > UI− < / button >
< div id = "toast" class = "toast" role = "status" aria-live = "polite" > < / div >
< dialog id = "exportDialog" aria-labelledby = "exportTitle" >
< h2 id = "exportTitle" > 高解像度 PNG 出力< / h2 >
< div class = "export-grid" >
< label for = "exportScale" > 出力倍率< / label > < select id = "exportScale" > < option value = "1" > 1× < / option > < option value = "2" > 2× < / option > < option value = "4" > 4× < / option > < option value = "0" > カスタム幅< / option > < / select >
< label for = "exportWidth" > 幅( px) < / label > < input id = "exportWidth" type = "number" min = "64" max = "16384" step = "1" >
< label for = "exportAA" > サブサンプル< / label > < select id = "exportAA" > < option value = "1" > 1× ( 高速) < / option > < option value = "2" > 2× 2 AA< / option > < / select >
2026-08-23 17:12:29 +09:00
< label for = "exportPrecision" > 精度方針< / label > < select id = "exportPrecision" > < option value = "balanced" > Balanced< / option > < option value = "strict" > 保守的 (Strict)< / option > < / select >
2026-08-22 20:55:27 +09:00
< / div >
< progress id = "exportProgress" max = "1" value = "0" hidden > < / progress >
< div id = "exportStatus" role = "status" aria-live = "polite" > < / div >
< div class = "export-actions" > < button id = "exportCancel" type = "button" > 閉じる< / button > < button id = "exportQuick" type = "button" > 表示を即時保存< / button > < button id = "exportStart" class = "primary" type = "button" > PNGを生成< / button > < / div >
< / dialog >
2026-08-23 23:27:38 +09:00
< script data-bundle = "gpu-kernels" >
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x000fffffu;
const REASON_SHIFT:u32=20u;
const REASON_MASK:u32=0x0ff00000u;
const REASON_NONE:u32=0u;
const REASON_ERROR_BOUND:u32=1u;
const REASON_ESCAPE_UNCERTAIN:u32=2u;
const REASON_REFERENCE_END:u32=3u;
const REASON_REBASE_GAP:u32=4u;
const REASON_RANGE:u32=5u;
const REASON_OPERATION_LIMIT:u32=6u;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) < < 28u ) ; }
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) < < REASON_SHIFT ) ; }
fn cmul(a:vec2< f32 > , b:vec2< f32 > )->vec2< f32 > {
return vec2< f32 > (a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2< f32 > )->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
2026-08-25 14:03:13 +09:00
fn scale_scalar(v:f32,e:i32)->f32{
// ldexp rounds the combined v*2^e result. Do not reject solely because e
// is below the normal f32 exponent: a large mantissa can still produce a
// perfectly normal representable result (critical around 10^38.461 zoom).
return ldexp(v,e);
}
fn scaled_to_f32(v:vec2< f32 > , e:i32)->vec2< f32 > {
return ldexp(v,vec2< i32 > (e));
2026-08-23 23:27:38 +09:00
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2< f32 > , delta:vec2< f32 > )->f32{
2026-08-25 14:03:13 +09:00
let propagated=abs(scale_scalar(errScaled,scaleExp));
2026-08-23 23:27:38 +09:00
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
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;}
}
2026-08-25 14:03:13 +09:00
fieldMeta[out]=pack_unknown(p.maxIter,REASON_OPERATION_LIMIT); fieldSmooth[out]=0.0;
2026-08-23 23:27:38 +09:00
}
`;
2026-08-25 21:33:45 +09:00
// Milestone-4 shadow engine. The queue is compacted after each bounded chunk;
// it is deliberately not selected by the production backend until a real-GPU
// zero-tolerance corpus has passed. State is indexed by pixel so queue order
// cannot change any pixel's arithmetic sequence.
const ACTIVE_PREPARE_WGSL=String.raw`
@group(0) @binding(0) var< storage , read_write > activeCount:array< atomic < u32 > >;
@group(0) @binding(1) var< storage , read_write > indirect:array< u32 > ;
@compute @workgroup_size(1) fn main(){let n=atomicLoad(&activeCount[0]); indirect[0]=(n+63u)/64u;indirect[1]=1u;indirect[2]=1u;}
`;
const DIRECT_ACTIVE_INIT_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,targetIter:u32,chunkSize:u32,_a0:u32,_a1:u32};
struct DirectState{zr:f32,zi:f32,n:u32,_pad:u32};
@group(0) @binding(0) var< uniform > p:Params;@group(0) @binding(1) var< storage , read_write > states:array< DirectState > ;@group(0) @binding(2) var< storage , read_write > queueOut:array< u32 > ;@group(0) @binding(3) var< storage , read_write > countOut:array< atomic < u32 > >;@group(0) @binding(4) var< storage , read_write > fieldMeta:array< u32 > ;@group(0) @binding(5) 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;}states[out]=DirectState(0.0,0.0,0u,0u);let q=atomicAdd(&countOut[0],1u); queueOut[q]=out;}
`;
const DIRECT_ACTIVE_CONTINUE_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,targetIter:u32,chunkSize:u32,_a0:u32,_a1:u32};
struct DirectState{zr:f32,zi:f32,n:u32,_pad:u32};
@group(0) @binding(0) var< uniform > p:Params;@group(0) @binding(1) var< storage , read_write > states:array< DirectState > ;@group(0) @binding(2) var< storage , read > queueIn:array< u32 > ;@group(0) @binding(3) var< storage , read_write > countIn:array< atomic < u32 > >;@group(0) @binding(4) var< storage , read_write > queueOut:array< u32 > ;@group(0) @binding(5) var< storage , read_write > countOut:array< atomic < u32 > >;@group(0) @binding(6) var< storage , read_write > fieldMeta:array< u32 > ;@group(0) @binding(7) var< storage , read_write > fieldSmooth:array< f32 > ;
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){if(gid.x>=atomicLoad(&countIn[0])){return; }let out=queueIn[gid.x];var s=states[out];let px=out%p.tileW;let py=out/p.tileW;let gx=f32(p.tileX+px)+p.sampleX;let gy=f32(p.tileY+py)+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;let stop=min(p.targetIter,s.n+max(1u,p.chunkSize));loop{if(s.n>=stop){break;}let zr2=s.zr*s.zr;let zi2=s.zi*s.zi;s.zi=2.0*s.zr*s.zi+ci;s.zr=zr2-zi2+cr;s.n++;let mag=s.zr*s.zr+s.zi*s.zi;if(mag>4.0){fieldMeta[out]=pack_meta(s.n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(s.n,mag);return;}}if(s.n>=p.targetIter&&p.targetIter>=p.maxIter){fieldMeta[out]=pack_unknown(p.maxIter,REASON_OPERATION_LIMIT); fieldSmooth[out]=0.0;return;}states[out]=s;let q=atomicAdd(&countOut[0],1u); queueOut[q]=out;}
`;
const ACTIVE_COMPARE_WGSL=String.raw`
struct Params{count:u32,_p0:u32,_p1:u32,_p2:u32};
@group(0) @binding(0) var< uniform > p:Params;@group(0) @binding(1) var< storage , read > candidateMeta:array< u32 > ;@group(0) @binding(2) var< storage , read > candidateSmooth:array< u32 > ;@group(0) @binding(3) var< storage , read > baselineMeta:array< u32 > ;@group(0) @binding(4) var< storage , read > baselineSmooth:array< u32 > ;@group(0) @binding(5) var< storage , read_write > stats:array< atomic < u32 > >;
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){let i=gid.x;if(i>=p.count){return;}let cm=candidateMeta[i];let bm=baselineMeta[i];if(cm!=bm){atomicAdd(&stats[0],1u); let cc=(cm>>28u)&3u; let bc=(bm>>28u)&3u; if(bc!=0u&&cc==0u){atomicAdd(&stats[2],1u); }}if(candidateSmooth[i]!=baselineSmooth[i]){atomicAdd(&stats[1],1u); }}
`;
2026-08-23 23:27:38 +09:00
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.
2026-08-25 14:03:13 +09:00
// Low/mid-depth accurate screen path. A cheap f32 seed accepts only pixels
// that escape very early; everything delicate is recomputed from iteration 0
// with direct double-single arithmetic. This avoids paying full-screen Deep
// perturbation where absolute pixel coordinates are still widely separated.
const DIRECT_ACCURATE_SEED_WGSL=COMMON+String.raw`
struct Params{
tileW:u32,tileH:u32,fullW:u32,fullH:u32,
tileX:u32,tileY:u32,maxIter:u32,lateThreshold:u32,
outputStride:u32,outputBase:u32,_u0:u32,_u1:u32,
centerReHi:f32,centerReLo:f32,centerImHi:f32,centerImLo:f32,
spanHi:f32,spanLo:f32,invWHi:f32,invWLo:f32,
sampleX:f32,sampleY:f32,_f0:f32,_f1:f32,
};
@group(0) @binding(0) var< uniform > p:Params;
@group(0) @binding(1) var< storage , read_write > fieldMeta:array< u32 > ;
@group(0) @binding(2) var< storage , read_write > fieldSmooth:array< f32 > ;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci;let x=cr-0.25;let q=x*x+y2;
let lhs=q*(q+x);let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs< rhs-margin ) { return true ; }
let x2=cr+1.0;let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb< 0.0625-bulbMargin ;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let centerRe=p.centerReHi+p.centerReLo;
let centerIm=p.centerImHi+p.centerImLo;
let span=p.spanHi+p.spanLo;
let invW=p.invWHi+p.invWLo;
let cr=centerRe+(gx-0.5*f32(p.fullW))*span*invW;
let ci=centerIm+(0.5*f32(p.fullH)-gy)*span*invW;
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){fieldMeta[out]=pack_unknown(p.maxIter,REASON_OPERATION_LIMIT);fieldSmooth[out]=0.0;return;}
2026-08-25 21:33:45 +09:00
if(n>=p.lateThreshold){fieldMeta[out]=pack_unknown(n,REASON_ESCAPE_UNCERTAIN);fieldSmooth[out]=0.0;return;}
2026-08-25 14:03:13 +09:00
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){
if(n< p.lateThreshold ) { fieldMeta [ out ] = pack_meta ( n , FIELD_ESCAPED ) ; fieldSmooth [ out ] = smooth_escape ( n , mag ) ; }
else{fieldMeta[out]=pack_unknown(n,REASON_ESCAPE_UNCERTAIN);fieldSmooth[out]=0.0;}
return;
}
}
}
`;
const DIRECT_DS_CORRECT_WGSL=COMMON+String.raw`
struct Params{
tileW:u32,tileH:u32,fullW:u32,fullH:u32,
tileX:u32,tileY:u32,maxIter:u32,lateThreshold:u32,
outputStride:u32,outputBase:u32,_u0:u32,_u1:u32,
centerReHi:f32,centerReLo:f32,centerImHi:f32,centerImLo:f32,
spanHi:f32,spanLo:f32,invWHi:f32,invWLo:f32,
sampleX:f32,sampleY:f32,_f0:f32,_f1:f32,
};
struct DS{h:f32,l: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 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);let e=q.l+a.h*b.l+a.l*b.h+a.l*b.l;return ds_quick(q.h,e);}
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
fn ds_cmp(a:DS,b:DS)->i32{if(a.h< b.h ) { return -1 ; } if ( a . h > b.h){return 1;}if(a.l< b.l ) { return -1 ; } if ( a . l > b.l){return 1;}return 0;}
fn ds_value(a:DS)->f32{return a.h+a.l;}
@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;
let invW=DS(p.invWHi,p.invWLo);let span=DS(p.spanHi,p.spanLo);let scale=ds_mul(span,invW);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_scale(scale,gx-0.5*f32(p.fullW)));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_scale(scale,0.5*f32(p.fullH)-gy));
2026-08-25 21:33:45 +09:00
var zr=DS(0.0,0.0);var zi=DS(0.0,0.0);
var zr2=DS(0.0,0.0);var zi2=DS(0.0,0.0);var n=0u;
2026-08-25 14:03:13 +09:00
loop{
if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;}
2026-08-25 21:33:45 +09:00
let zri=ds_mul(zr,zi);
2026-08-25 14:03:13 +09:00
zr=ds_add(ds_sub(zr2,zi2),cr);zi=ds_add(ds_scale(zri,2.0),ci);n+=1u;
2026-08-25 21:33:45 +09:00
zr2=ds_mul(zr,zr);zi2=ds_mul(zi,zi);let mag=ds_add(zr2,zi2);
2026-08-25 14:03:13 +09:00
if(ds_cmp(mag,DS(4.0,0.0))>0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;}
let av=max(abs(ds_value(zr)),abs(ds_value(zi)));
if(av!=av||av>1.0e20){fieldMeta[out]=pack_unknown(n,REASON_RANGE);fieldSmooth[out]=0.0;return;}
}
}
`;
2026-08-25 21:33:45 +09:00
const DIRECT_DS_QUEUE_BUILD_WGSL=COMMON+String.raw`
struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,lateThreshold:u32,outputStride:u32,outputBase:u32,queueOffset:u32,queueCount:u32,centerReHi:f32,centerReLo:f32,centerImHi:f32,centerImLo:f32,spanHi:f32,spanLo:f32,invWHi:f32,invWLo:f32,sampleX:f32,sampleY:f32,_f0:f32,_f1: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 > queueStats:array< atomic < u32 > >;@group(0) @binding(3) var< storage , read_write > queue:array< u32 > ;
@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; }atomicAdd(&queueStats[0],1u); let q=atomicAdd(&queueStats[2],1u); let cap=p.fullW*p.fullH;if(q< cap ) { queue [ q ] = out ; } else { atomicAdd ( & queueStats [ 1 ] , 1u ) ; } }
`;
const DIRECT_DS_CORRECT_QUEUE_WGSL=DIRECT_DS_CORRECT_WGSL
.replace(
'@group(0) @binding(2) var< storage , read_write > fieldSmooth:array< f32 > ;',
'@group(0) @binding(2) var< storage , read_write > fieldSmooth:array< f32 > ;\n@group(0) @binding(3) var< storage , read_write > queueStats:array< atomic < u32 > >;\n@group(0) @binding(4) var< storage , read > queue:array< u32 > ;'
)
.replace(
'@compute @workgroup_size(8,8)\nfn main(@builtin(global_invocation_id) gid:vec3< u32 > ){\n if(gid.x>=p.tileW||gid.y>=p.tileH){return;}\n let out=p.outputBase+gid.y*p.outputStride+gid.x;\n if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return; }\n let gx=f32(p.tileX+gid.x)+p.sampleX;let gy=f32(p.tileY+gid.y)+p.sampleY;',
'@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) gid:vec3< u32 > ){\n let qi=gid.x;if(qi>=p._u1){return;}let out=queue[p._u0+qi];let limit=p.outputBase+p.tileH*p.outputStride;if(out< p.outputBase | | out > =limit){atomicAdd(&queueStats[4],1u); return;}if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&queueStats[5],1u); return;}let local=out-p.outputBase;let px=local%p.outputStride;let py=local/p.outputStride;if(px>=p.tileW||py>=p.tileH){atomicAdd(&queueStats[4],1u); return;}atomicAdd(&queueStats[6],1u); \n let gx=f32(p.tileX+px)+p.sampleX;let gy=f32(p.tileY+py)+p.sampleY;'
);
if(DIRECT_DS_CORRECT_QUEUE_WGSL===DIRECT_DS_CORRECT_WGSL||!DIRECT_DS_CORRECT_QUEUE_WGSL.includes('atomicAdd(& queueStats[6],1u)'))throw new Error('queued Direct DS shader derivation failed');
2026-08-24 20:30:26 +09:00
const FAST_PERTURB_WGSL=COMMON+String.raw`
2026-08-23 23:27:38 +09:00
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
2026-08-24 20:30:26 +09:00
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
2026-08-23 23:27:38 +09:00
};
2026-08-24 20:30:26 +09:00
struct RefPoint{ hi:vec2< f32 > , lo:vec2< f32 > };
2026-08-23 23:27:38 +09:00
@group(0) @binding(0) var< uniform > p:Params;
2026-08-24 20:30:26 +09:00
@group(0) @binding(1) var< storage , read > refs:array< RefPoint > ;
@group(0) @binding(2) var< storage , read_write > fieldMeta:array< u32 > ;
@group(0) @binding(3) var< storage , read_write > fieldSmooth:array< f32 > ;
@group(0) @binding(4) var< storage , read_write > unresolved:array< atomic < u32 > >;
fn mark_fast_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u);
if(reason>=1u & & reason< =6u){atomicAdd(&unresolved[reason],1u); }
2026-08-23 23:27:38 +09:00
}
2026-08-24 20:30:26 +09:00
fn render_fast(out:u32,gx:f32,gy:f32){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
var d=vec2< f32 > (p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2< f32 > (0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u;
loop{
2026-08-25 14:03:13 +09:00
if(n>=p.maxIter){mark_fast_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);return;}
2026-08-24 20:30:26 +09:00
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-25 14:03:13 +09:00
// Reference-near-zero episodes are not an automatic failure. Continue the
// valid perturbation recurrence; explicit range/reference guards remain.
2026-08-24 20:30:26 +09:00
if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=rp;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
2026-08-25 14:03:13 +09:00
let sq=scaled_to_f32(cmul(w,w),scaleExp);
2026-08-24 20:30:26 +09:00
w=linear+sq+d; m+=1u; n+=1u;
let mm=max(maxabs(w),maxabs(d));
if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;}
if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;}
else if(mm>0.0 & & mm< 0.0000152587890625 & & scaleExp > p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;}
if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;}
2026-08-23 23:27:38 +09:00
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
2026-08-24 20:30:26 +09:00
let out=p.outputBase+gid.y*p.outputStride+gid.x;
2026-08-25 14:03:13 +09:00
if(p.unknownOnly!=0u){
let prior=fieldMeta[out];let cls=(prior>>28u)&3u;
if(cls!=FIELD_UNKNOWN){return;}
let reason=(prior>>20u)&0xffu;
// mode 2 repairs only untouched coverage holes (zero-filled metadata).
if(p.unknownOnly==2u & & reason!=0u){return;}
}
2026-08-24 20:30:26 +09:00
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
render_fast(out,gx,gy);
2026-08-23 23:27:38 +09:00
}
`;
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2< f32 > , lo:vec2< f32 > };
struct UnresolvedHead{
remaining:atomic< u32 > , errorBound:atomic< u32 > , escapeUncertain:atomic< u32 > , referenceEnd:atomic< u32 > ,
rebaseGap:atomic< u32 > , rangeFailure:atomic< u32 > , operationLimit:atomic< u32 > , corrected:atomic< u32 > ,
};
@group(0) @binding(0) var< uniform > p:Params;
@group(0) @binding(1) var< storage , read > refs:array< RefPoint > ;
@group(0) @binding(2) var< storage , read_write > fieldMeta:array< u32 > ;
@group(0) @binding(3) var< storage , read_write > fieldSmooth:array< f32 > ;
@group(0) @binding(4) var< storage , read_write > unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u); }
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u); }
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u); }
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u); }
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u); }
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u); }
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2< f32 > (p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2< f32 > (0.0);
var scaleExp=p.spanExp;
2026-08-24 20:30:26 +09:00
var n=0u; var m=0u;
2026-08-23 23:27:38 +09:00
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
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);
2026-08-25 14:03:13 +09:00
if(errAbs< =limit){mark_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
2026-08-23 23:27:38 +09:00
return;
}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
2026-08-25 14:03:13 +09:00
// Keep the perturbation recurrence through reference-near-zero episodes.
// Escape acceptance is still guarded by the propagated absolute error.
2026-08-23 23:27:38 +09:00
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
2026-08-24 20:30:26 +09:00
let r=rp;
2026-08-23 23:27:38 +09:00
let refAbs=maxabs(r.hi)+maxabs(r.lo);
2026-08-25 14:03:13 +09:00
let wAbs=maxabs(w); let dAbs=maxabs(d);
let actualDelta=scaled_to_f32(w,scaleExp);
let deltaAbs=maxabs(actualDelta);
let sq=scaled_to_f32(cmul(w,w),scaleExp);
let sqAbs=maxabs(sq);
let gain=2.0*refAbs+2.0*deltaAbs;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30);
2026-08-23 23:27:38 +09:00
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
2026-08-25 14:03:13 +09:00
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp. Scale the product
// directly so e< -126 does not discard a representable combined value .
2026-08-24 20:30:26 +09:00
w=linear+sq+d; m+=1u; n+=1u;
2026-08-23 23:27:38 +09:00
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 & & mm< 0.0000152587890625 & & scaleExp > p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(p.unknownOnly!=0u & & ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return; }
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
2026-08-25 21:33:45 +09:00
const DEEP_ACTIVE_INIT_WGSL=COMMON+String.raw`
struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32};
struct DeepState{d:vec2< f32 > ,w:vec2< f32 > ,scaleExp:i32,n:u32,m:u32,errScaled:f32};
@group(0) @binding(0) var< uniform > p:Params;@group(0) @binding(1) var< storage , read_write > states:array< DeepState > ;@group(0) @binding(2) var< storage , read_write > queueOut:array< u32 > ;@group(0) @binding(3) var< storage , read_write > countOut:array< atomic < u32 > >;
@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 local=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 dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW);let d=vec2< f32 > (p.spanMantHi*dx,p.spanMantHi*dy);states[local]=DeepState(d,vec2< f32 > (0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));let q=atomicAdd(&countOut[0],1u); queueOut[q]=local;}
`;
// Initialize persistent perturbation state only for the screen samples that
// reached the previous operation limit. The uploaded queue contains absolute
// pixel indices, so disconnected late-escape components are never omitted.
const DEEP_ACTIVE_RESUME_INIT_WGSL=COMMON+String.raw`
struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32};
struct DeepState{d:vec2< f32 > ,w:vec2< f32 > ,scaleExp:i32,n:u32,m:u32,errScaled:f32};
@group(0) @binding(0) var< uniform > p:Params;@group(0) @binding(1) var< storage , read_write > states:array< DeepState > ;@group(0) @binding(2) var< storage , read > queue:array< u32 > ;
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){let qi=gid.x;if(qi>=p.unknownOnly){return;}let out=queue[qi];if(out>=p.fullW*p.fullH){return;}let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW);let d=vec2< f32 > (p.spanMantHi*dx,p.spanMantHi*dy);states[out]=DeepState(d,vec2< f32 > (0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));}
`;
const DEEP_ACTIVE_CONTINUE_WGSL=COMMON+String.raw`
struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32};
struct RefPoint{hi:vec2< f32 > ,lo:vec2< f32 > };struct DeepState{d:vec2< f32 > ,w:vec2< f32 > ,scaleExp:i32,n:u32,m:u32,errScaled:f32};
@group(0) @binding(0) var< uniform > p:Params;@group(0) @binding(1) var< storage , read > refs:array< RefPoint > ;@group(0) @binding(2) var< storage , read_write > states:array< DeepState > ;@group(0) @binding(3) var< storage , read > queueIn:array< u32 > ;@group(0) @binding(4) var< storage , read_write > countIn:array< atomic < u32 > >;@group(0) @binding(5) var< storage , read_write > queueOut:array< u32 > ;@group(0) @binding(6) var< storage , read_write > countOut:array< atomic < u32 > >;@group(0) @binding(7) var< storage , read_write > fieldMeta:array< u32 > ;@group(0) @binding(8) var< storage , read_write > fieldSmooth:array< f32 > ;
fn fail(out:u32,n:u32,reason:u32){fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;}
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){if(gid.x>=atomicLoad(&countIn[0])){return; }let local=queueIn[gid.x];let out=p.outputBase+(local/p.tileW)*p.outputStride+(local%p.tileW);var s=states[local];let stop=min(p.targetIter,s.n+max(1u,p.chunkSize));loop{if(s.n>=stop){break;}if(s.m>p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}let rp=refs[s.m];let delta=scaled_to_f32(s.w,s.scaleExp);let z=rp.hi+(rp.lo+delta);let mag=dot(z,z);if(mag>4.0){let errAbs=safe_abs_error(s.errScaled,s.scaleExp,z,delta);if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(s.n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(s.n,mag);return;}fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;}if(s.m>=p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}let refAbs=maxabs(rp.hi)+maxabs(rp.lo);let wAbs=maxabs(s.w);let dAbs=maxabs(s.d);let deltaAbs=maxabs(delta);let sq=scaled_to_f32(cmul(s.w,s.w),s.scaleExp);let sqAbs=maxabs(sq);let gain=2.0*refAbs+2.0*deltaAbs;let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30);s.errScaled=gain*s.errScaled+roundErr;let linear=2.0*(cmul(rp.hi,s.w)+cmul(rp.lo,s.w));s.w=linear+sq+s.d;s.m++;s.n++;if(maxabs(s.w)>=1.0e30||maxabs(s.d)>=1.0e30){fail(out,s.n,REASON_RANGE);return;}let mm=max(maxabs(s.w),maxabs(s.d));if(mm>65536.0){s.w*=0.0000152587890625;s.d*=0.0000152587890625;s.errScaled*=0.0000152587890625;s.scaleExp+=16;}else if(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp){s.w*=65536.0; s.d*=65536.0;s.errScaled*=65536.0;s.scaleExp-=16;}if(s.scaleExp>126||s.errScaled!=s.errScaled||s.errScaled>1.0e35){fail(out,s.n,REASON_RANGE);return;}}
if(s.n>=p.targetIter& & p.targetIter>=p.maxIter){let rpEnd=refs[min(s.m,p.refLen)];let deltaEnd=scaled_to_f32(s.w,s.scaleExp);let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);let errAbs=safe_abs_error(s.errScaled,s.scaleExp,zEnd,deltaEnd);let limit=select(1.0e-3,1.0e-4,p.strict!=0u);if(errAbs< =limit){fail(out,p.maxIter,REASON_OPERATION_LIMIT);}else{fail(out,s.n,REASON_ERROR_BOUND);}return;}states[local]=s;let q=atomicAdd(&countOut[0],1u); queueOut[q]=local;}
`;
// Certified-interior candidate for FAST/Deep shadow validation. c is rebuilt
// as c_ref + dc; the large one-sided margins include coordinate reconstruction
// and f32 expression error. A false result only declines the optimization.
const PERTURB_CERTIFIED_INTERIOR_WGSL=COMMON+String.raw`
struct Params{count:u32,refLen:u32,spanExp:i32,_u0:u32,spanMantHi:f32,spanMantLo:f32,fullW:f32,refPixelX:f32,refPixelY:f32,_f0:f32,_f1:f32,_f2:f32};struct RefPoint{hi:vec2< f32 > ,lo:vec2< f32 > };
@group(0) @binding(0) var< uniform > p:Params;@group(0) @binding(1) var< storage , read > refs:array< RefPoint > ;@group(0) @binding(2) var< storage , read > points:array< vec2 < f32 > >;@group(0) @binding(3) var< storage , read_write > proven:array< u32 > ;
struct I{lo:f32,hi:f32};fn outward(lo:f32,hi:f32)->I{let m=64.0*F32_U*(max(abs(lo),abs(hi))+1.0);return I(lo-m,hi+m);}fn ia(a:I,b:I)->I{return outward(a.lo+b.lo,a.hi+b.hi);}fn im(a:I,b:I)->I{let p0=a.lo*b.lo;let p1=a.lo*b.hi;let p2=a.hi*b.lo;let p3=a.hi*b.hi;return outward(min(min(p0,p1),min(p2,p3)),max(max(p0,p1),max(p2,p3)));}fn isq(a:I)->I{let aa=a.lo*a.lo;let bb=a.hi*a.hi;let lo=select(min(aa,bb),0.0,a.lo< =0.0&&a.hi>=0.0); return outward(lo,max(aa,bb));}fn certified(c:vec2< f32 > ,ce:f32)->bool{let xr=outward(c.x-ce,c.x+ce);let yr=outward(c.y-ce,c.y+ce);let y2=isq(yr);let x=ia(xr,I(-0.25,-0.25));let q=ia(isq(x),y2);let lhs=im(q,ia(q,x));let rhs=im(I(0.25,0.25),y2);if(lhs.hi< rhs.lo ) { return true ; } let bx = ia(xr,I(1.0,1.0));let bulb = ia(isq(bx),y2);return bulb . hi < 0 . 0625 ; }
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){let i=gid.x;if(i>=p.count){return;}if(p.refLen< 1u ) { proven [ i ] = 0u ; return ; } let g = points[i];let dx = (g.x-p.refPixelX)/p.fullW;let dy = (p.refPixelY-g.y)/p.fullW;let radius = max(abs(dx),abs(dy));let dm = (p.spanMantHi+p.spanMantLo)*radius;let dc = vec2<f32 > (p.spanMantHi*dx,p.spanMantHi*dy);let ref=refs[1u];let c=ref.hi+(ref.lo+scaled_to_f32(dc,p.spanExp));let coordErr=abs(p.spanMantLo)*radius+16.0*F32_U*(abs(dm)+maxabs(dc)+1.0e-30);let ce=abs(ldexp(coordErr,p.spanExp))+32.0*F32_U*(maxabs(c)+1.0);proven[i]=select(0u,1u,certified(c,ce));}
`;
2026-08-24 20:30:26 +09:00
// Production Deep sparse path: keep the long-running perturbation kernel
// untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets.
// Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS
// correction kernel sees less workgroup-level iteration divergence.
//
// Bucket state layout (24 u32 atomics):
// [0..7] histogram counts
// [8..15] scatter cursors (initialized from prefix offsets)
// [16..23] immutable prefix offsets for queue accounting
const DEEP_BUCKET_HIST_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
@group(0) @binding(0) var< uniform > p:Params;
@group(0) @binding(1) var< storage , read > fieldMeta:array< u32 > ;
@group(0) @binding(2) var< storage , read_write > bucketState:array< atomic < u32 > >;
var< workgroup > localCounts:array< atomic < u32 > ,8>;
fn deep_bucket(packed:u32)->u32{
let n=packed&ITER_MASK;
return min(7u,(n*8u)/max(1u,p.maxIter));
}
2026-08-25 14:03:13 +09:00
fn selected_unknown(packed:u32)->bool{
if(((packed>>28u)& 3u)!=FIELD_UNKNOWN){return false;}
if(p.unknownOnly==2u){let reason=(packed&REASON_MASK)>>REASON_SHIFT; return reason>=1u & & reason< =5u;}
return true;
}
2026-08-24 20:30:26 +09:00
@compute @workgroup_size(64)
fn main(
@builtin(local_invocation_id) lid3:vec3< u32 > ,
@builtin(workgroup_id) wid:vec3< u32 >
){
let lane=lid3.x;
if(lane< 8u ) { atomicStore ( & localCounts [ lane ] , 0u ) ; }
workgroupBarrier();
let lx=wid.x*64u+lane;
let ly=wid.y;
if(lx< p.tileW & & ly < p . tileH ) {
let out=p.outputBase+ly*p.outputStride+lx;
let packed=fieldMeta[out];
2026-08-25 14:03:13 +09:00
if(selected_unknown(packed)){
2026-08-24 20:30:26 +09:00
atomicAdd(&localCounts[deep_bucket(packed)],1u);
}
}
workgroupBarrier();
if(lane< 8u ) {
let c=atomicLoad(&localCounts[lane]);
if(c>0u){atomicAdd(&bucketState[lane],c); }
}
}
`;
// Convert the 8-bin histogram into contiguous queue ranges, initialize each
// scatter cursor, and produce the correction indirect-dispatch arguments.
const DEEP_BUCKET_PREFIX_WGSL=String.raw`
struct SparseQueueStats{
selected:atomic< u32 > , overflow:atomic< u32 > , enqueued:atomic< u32 > , dispatchCount:atomic< u32 > ,
invalidIndex:atomic< u32 > , staleEntry:atomic< u32 > , processed:atomic< u32 > , _reserved:atomic< u32 > ,
};
struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 };
@group(0) @binding(0) var< storage , read_write > bucketState:array< atomic < u32 > >;
@group(0) @binding(1) var< storage , read_write > sparseQueueStats:SparseQueueStats;
@group(0) @binding(2) var< storage , read_write > indirectArgs:IndirectArgs;
@compute @workgroup_size(1)
fn main(){
var total=0u;
var b=0u;
loop{
if(b>=8u){break;}
let c=atomicLoad(&bucketState[b]);
atomicStore(&bucketState[8u+b],total);
atomicStore(&bucketState[16u+b],total);
total+=c;
b+=1u;
}
atomicStore(&sparseQueueStats.selected,total);
atomicStore(&sparseQueueStats.overflow,0u);
atomicStore(&sparseQueueStats.enqueued,total);
atomicStore(&sparseQueueStats.dispatchCount,total);
indirectArgs.x=(total+63u)/64u;
indirectArgs.y=1u;
indirectArgs.z=1u;
indirectArgs._pad=0u;
}
`;
// Scatter UNKNOWN indices into the precomputed bucket ranges. Within each
// 64-lane workgroup, workgroup-memory atomics allocate local ranks; each
// non-empty bucket reserves one global subrange, so global atomics scale with
// non-empty (workgroup,bucket) pairs rather than with UNKNOWN pixels.
const DEEP_BUCKET_SCATTER_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct SparseQueueStats{
selected:atomic< u32 > , overflow:atomic< u32 > , enqueued:atomic< u32 > , dispatchCount:atomic< u32 > ,
invalidIndex:atomic< u32 > , staleEntry:atomic< u32 > , processed:atomic< u32 > , _reserved:atomic< u32 > ,
};
@group(0) @binding(0) var< uniform > p:Params;
@group(0) @binding(1) var< storage , read > fieldMeta:array< u32 > ;
@group(0) @binding(2) var< storage , read_write > bucketState:array< atomic < u32 > >;
@group(0) @binding(3) var< storage , read_write > sparseQueueStats:SparseQueueStats;
@group(0) @binding(4) var< storage , read_write > unknownQueue:array< u32 > ;
var< workgroup > localCounts:array< atomic < u32 > ,8>;
var< workgroup > localRanks:array< atomic < u32 > ,8>;
var< workgroup > groupBase:array< u32 , 8 > ;
fn deep_bucket(packed:u32)->u32{
let n=packed&ITER_MASK;
return min(7u,(n*8u)/max(1u,p.maxIter));
}
2026-08-25 14:03:13 +09:00
fn selected_unknown(packed:u32)->bool{
if(((packed>>28u)& 3u)!=FIELD_UNKNOWN){return false;}
if(p.unknownOnly==2u){let reason=(packed&REASON_MASK)>>REASON_SHIFT; return reason>=1u & & reason< =5u;}
return true;
}
2026-08-24 20:30:26 +09:00
@compute @workgroup_size(64)
fn main(
@builtin(local_invocation_id) lid3:vec3< u32 > ,
@builtin(workgroup_id) wid:vec3< u32 >
){
let lane=lid3.x;
if(lane< 8u ) {
atomicStore(&localCounts[lane],0u);
atomicStore(&localRanks[lane],0u);
groupBase[lane]=0u;
}
workgroupBarrier();
let lx=wid.x*64u+lane;
let ly=wid.y;
var out=0u;
var hit=0u;
var bucket=0u;
if(lx< p.tileW & & ly < p . tileH ) {
out=p.outputBase+ly*p.outputStride+lx;
let packed=fieldMeta[out];
2026-08-25 14:03:13 +09:00
if(selected_unknown(packed)){
2026-08-24 20:30:26 +09:00
hit=1u;
bucket=deep_bucket(packed);
atomicAdd(&localCounts[bucket],1u);
}
}
workgroupBarrier();
if(lane< 8u ) {
let c=atomicLoad(&localCounts[lane]);
if(c>0u){groupBase[lane]=atomicAdd(&bucketState[8u+lane],c); }
}
workgroupBarrier();
if(hit!=0u){
let rank=atomicAdd(&localRanks[bucket],1u);
let qi=groupBase[bucket]+rank;
let capacity=p.tileW*p.tileH;
if(qi< capacity ) { unknownQueue [ qi ] = out ; }
else{atomicAdd(&sparseQueueStats.overflow,1u); }
}
}
`;
2026-08-23 23:27:38 +09:00
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
// perturbation. It is a visual-quality pass, not a membership certificate.
const DEEP_CORRECT_WGSL=COMMON+String.raw`
const CORRECTION_MARK:u32=128u;
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2< f32 > , lo:vec2< f32 > };
struct CorrectionStats{
remaining:atomic< u32 > , errorBound:atomic< u32 > , escapeUncertain:atomic< u32 > , referenceEnd:atomic< u32 > ,
rebaseGap:atomic< u32 > , rangeFailure:atomic< u32 > , operationLimit:atomic< u32 > , corrected:atomic< u32 > ,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var< uniform > p:Params;
@group(0) @binding(1) var< storage , read > refs:array< RefPoint > ;
@group(0) @binding(2) var< storage , read_write > fieldMeta:array< u32 > ;
@group(0) @binding(3) var< storage , read_write > fieldSmooth:array< f32 > ;
@group(0) @binding(4) var< storage , read_write > stats:CorrectionStats;
2026-08-25 14:03:13 +09:00
fn count_remaining(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
2026-08-23 23:27:38 +09:00
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u); }
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u); }
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u); }
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u); }
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u); }
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u); }
}
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
}
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b;
let e=b-(q-a);
return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b;
let bb=q-a;
let e=(a-(q-bb))+(b-bb);
return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a;
let ah=ca-(ca-a);
let al=a-ah;
let cb=4097.0*b;
let bh=cb-(cb-b);
let bl=b-bh;
var e=ah*bh-q;
e=e+ah*bl;
e=e+al*bh;
e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h);
return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l;
e=e+a.l*b.h;
e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b);
return ds_quick(q.h,q.l+a.l*b);
}
fn ds_pow2(a:DS,e:i32)->DS{
return DS(ldexp(a.h,e),ldexp(a.l,e));
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h< b.h ) { return -1 ; } if ( a . h > b.h){return 1;}
if(a.l< b.l ) { return -1 ; } if ( a . l > b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK< < REASON_SHIFT ) ; }
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
var scaleExp=p.spanExp;
2026-08-24 20:30:26 +09:00
var n=0u; var m=0u;
2026-08-23 23:27:38 +09:00
loop{
2026-08-25 14:03:13 +09:00
if(n>=p.maxIter){count_remaining(out,p.maxIter,REASON_OPERATION_LIMIT);return;}
2026-08-23 23:27:38 +09:00
let rp=refs[m];
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
let delta=cds_pow2(w,scaleExp);
let z=cds_add(r,delta);
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
return;
}
2026-08-25 14:03:13 +09:00
// DS correction follows the same continuous perturbation recurrence.
if(m>=p.refLen){count_remaining(out,n,REASON_REFERENCE_END);return;}
2026-08-23 23:27:38 +09:00
let linear=cds_scale(cds_mul(r,w),2.0);
let sq=cds_pow2(cds_mul(w,w),scaleExp);
w=cds_add(cds_add(linear,sq),d);
2026-08-24 20:30:26 +09:00
m+=1u; n+=1u;
2026-08-23 23:27:38 +09:00
let mm=max(cds_maxabs(w),cds_maxabs(d));
2026-08-25 14:03:13 +09:00
if(mm>=1.0e30 || mm!=mm){count_remaining(out,n,REASON_RANGE);return;}
2026-08-23 23:27:38 +09:00
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
else if(mm>0.0 & & mm< 0.0000152587890625 & & scaleExp > p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
2026-08-25 14:03:13 +09:00
if(scaleExp>126){count_remaining(out,n,REASON_RANGE);return;}
2026-08-23 23:27:38 +09:00
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
2026-08-25 14:03:13 +09:00
let prior=fieldMeta[out];
if(((prior>>28u)&3u)!=FIELD_UNKNOWN){return; }
if(p.unknownOnly==2u){
let reason=(prior&REASON_MASK)>>REASON_SHIFT;
if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;}
}
2026-08-23 23:27:38 +09:00
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
2026-08-24 20:30:26 +09:00
// Production sparse correction variant. It shares the complete DS arithmetic
// with DEEP_CORRECT_WGSL, but consumes only indices emitted by the queued deep
// perturbation pass and launches via dispatchWorkgroupsIndirect.
const DEEP_CORRECT_QUEUE_WGSL=DEEP_CORRECT_WGSL
.replace(
`struct CorrectionStats{
remaining:atomic< u32 > , errorBound:atomic< u32 > , escapeUncertain:atomic< u32 > , referenceEnd:atomic< u32 > ,
rebaseGap:atomic< u32 > , rangeFailure:atomic< u32 > , operationLimit:atomic< u32 > , corrected:atomic< u32 > ,
};`,
`struct CorrectionStats{
remaining:atomic< u32 > , errorBound:atomic< u32 > , escapeUncertain:atomic< u32 > , referenceEnd:atomic< u32 > ,
rebaseGap:atomic< u32 > , rangeFailure:atomic< u32 > , operationLimit:atomic< u32 > , corrected:atomic< u32 > ,
};
struct SparseQueueStats{
selected:atomic< u32 > , overflow:atomic< u32 > , enqueued:atomic< u32 > , dispatchCount:atomic< u32 > ,
invalidIndex:atomic< u32 > , staleEntry:atomic< u32 > , processed:atomic< u32 > , _reserved:atomic< u32 > ,
};`)
.replace(
`@group(0) @binding(4) var< storage , read_write > stats:CorrectionStats;`,
`@group(0) @binding(4) var< storage , read_write > stats:CorrectionStats;
@group(0) @binding(5) var< storage , read_write > sparseQueueStats:SparseQueueStats;
@group(0) @binding(6) var< storage , read > unknownQueue:array< u32 > ;`)
.replace(
`@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
2026-08-25 14:03:13 +09:00
let prior=fieldMeta[out];
if(((prior>>28u)&3u)!=FIELD_UNKNOWN){return; }
if(p.unknownOnly==2u){
let reason=(prior&REASON_MASK)>>REASON_SHIFT;
if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;}
}
2026-08-24 20:30:26 +09:00
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}`,
`@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
2026-08-25 21:33:45 +09:00
let localQi=gid.x;
2026-08-24 20:30:26 +09:00
let queued=atomicLoad(&sparseQueueStats.dispatchCount);
2026-08-25 21:33:45 +09:00
if(localQi>=queued){return;}
let qi=bitcast< u32 > (p._numeric2)+localQi;
2026-08-24 20:30:26 +09:00
let out=unknownQueue[qi];
atomicAdd(&sparseQueueStats.processed,1u);
if(out< p.outputBase ) { atomicAdd ( & sparseQueueStats . invalidIndex , 1u ) ; return ; }
let local=out-p.outputBase;
let ly=local/p.outputStride;
let lx=local-ly*p.outputStride;
if(lx>=p.tileW||ly>=p.tileH){atomicAdd(&sparseQueueStats.invalidIndex,1u); return;}
2026-08-25 14:03:13 +09:00
let prior=fieldMeta[out];
if(((prior>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u); return;}
if(p.unknownOnly==2u){
let reason=(prior&REASON_MASK)>>REASON_SHIFT;
if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;}
}
2026-08-24 20:30:26 +09:00
let gx=f32(p.tileX+lx)+p.sampleX;
let gy=f32(p.tileY+ly)+p.sampleY;
correct_pixel(out,gx,gy);
}`);
if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)')){
throw new Error('failed to derive queued deep correction shader');
}
2026-08-23 23:27:38 +09:00
const COLOR_WGSL=String.raw`
2026-08-24 20:30:26 +09:00
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
2026-08-23 23:27:38 +09:00
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var< uniform > p:Params;
@group(0) @binding(1) var< storage , read > fieldMeta:array< u32 > ;
@group(0) @binding(2) var< storage , read > fieldSmooth:array< f32 > ;
@group(0) @binding(3) var outTex:texture_storage_2d< rgba8unorm , write > ;
fn hsv(h:f32,s:f32,v:f32)->vec3< f32 > {
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3< f32 > (v,t,pp);} if(i==1){return vec3< f32 > (q,v,pp);} if(i==2){return vec3< f32 > (pp,v,t);} if(i==3){return vec3< f32 > (pp,q,v);} if(i==4){return vec3< f32 > (t,pp,v);} return vec3< f32 > (v,pp,q);
}
2026-08-25 14:03:13 +09:00
fn rgb8(r:f32,g:f32,b:f32)->vec3< f32 > {return vec3< f32 > (r,g,b)/255.0;}
fn tri_phase(t0:f32)->f32{return 1.0-abs(2.0*fract(t0)-1.0);}
fn ramp5(t:f32,c0:vec3< f32 > ,c1:vec3< f32 > ,c2:vec3< f32 > ,c3:vec3< f32 > ,c4:vec3< f32 > )->vec3< f32 > {
if(t< 0.25 ) { return mix ( c0 , c1 , smoothstep ( 0 . 0 , 0 . 25 , t ) ) ; }
if(t< 0.50 ) { return mix ( c1 , c2 , smoothstep ( 0 . 25 , 0 . 50 , t ) ) ; }
if(t< 0.75 ) { return mix ( c2 , c3 , smoothstep ( 0 . 50 , 0 . 75 , t ) ) ; }
return mix(c3,c4,smoothstep(0.75,1.0,t));
}
2026-08-23 23:27:38 +09:00
fn current_palette(t0:f32)->vec3< f32 > {
let t=select(2.0-2.0*t0,2.0*t0,t0< =0.5);
2026-08-25 14:03:13 +09:00
if(t< 0.11 ) { return mix ( rgb8 ( 4 . 0 , 10 . 0 , 27 . 0 ) , rgb8 ( 12 . 0 , 53 . 0 , 79 . 0 ) , smoothstep ( 0 . 0 , 0 . 11 , t ) ) ; }
if(t< 0.25 ) { return mix ( rgb8 ( 12 . 0 , 53 . 0 , 79 . 0 ) , rgb8 ( 31 . 0 , 156 . 0 , 184 . 0 ) , smoothstep ( 0 . 11 , 0 . 25 , t ) ) ; }
if(t< 0.38 ) { return mix ( rgb8 ( 31 . 0 , 156 . 0 , 184 . 0 ) , rgb8 ( 91 . 0 , 226 . 0 , 234 . 0 ) , smoothstep ( 0 . 25 , 0 . 38 , t ) ) ; }
if(t< 0.50 ) { return mix ( rgb8 ( 91 . 0 , 226 . 0 , 234 . 0 ) , rgb8 ( 66 . 0 , 53 . 0 , 151 . 0 ) , smoothstep ( 0 . 38 , 0 . 50 , t ) ) ; }
if(t< 0.62 ) { return mix ( rgb8 ( 66 . 0 , 53 . 0 , 151 . 0 ) , rgb8 ( 139 . 0 , 49 . 0 , 170 . 0 ) , smoothstep ( 0 . 50 , 0 . 62 , t ) ) ; }
if(t< 0.73 ) { return mix ( rgb8 ( 139 . 0 , 49 . 0 , 170 . 0 ) , rgb8 ( 232 . 0 , 72 . 0 , 145 . 0 ) , smoothstep ( 0 . 62 , 0 . 73 , t ) ) ; }
if(t< 0.84 ) { return mix ( rgb8 ( 232 . 0 , 72 . 0 , 145 . 0 ) , rgb8 ( 255 . 0 , 137 . 0 , 64 . 0 ) , smoothstep ( 0 . 73 , 0 . 84 , t ) ) ; }
if(t< 0.93 ) { return mix ( rgb8 ( 255 . 0 , 137 . 0 , 64 . 0 ) , rgb8 ( 255 . 0 , 211 . 0 , 99 . 0 ) , smoothstep ( 0 . 84 , 0 . 93 , t ) ) ; }
return mix(rgb8(255.0,211.0,99.0),rgb8(255.0,250.0,223.0),smoothstep(0.93,1.0,t));
2026-08-23 23:27:38 +09:00
}
2026-08-24 20:30:26 +09:00
fn palette_color(phase:f32)->vec3< f32 > {
2026-08-25 14:03:13 +09:00
if(p.palette==0u){return current_palette(phase);}
if(p.palette==1u){return hsv(phase,0.92,1.0);}
if(p.palette==2u){let g=(18.0+237.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3< f32 > (g);}
let t=tri_phase(phase);
if(p.palette==3u){return ramp5(t,rgb8(0.0,0.0,2.0),rgb8(40.0,8.0,84.0),rgb8(141.0,36.0,101.0),rgb8(228.0,88.0,48.0),rgb8(252.0,255.0,166.0));}
if(p.palette==5u){return ramp5(t,rgb8(1.0,4.0,14.0),rgb8(7.0,36.0,92.0),rgb8(0.0,132.0,178.0),rgb8(116.0,232.0,240.0),rgb8(242.0,254.0,255.0));}
if(p.palette==7u){return ramp5(t,rgb8(3.0,6.0,20.0),rgb8(63.0,26.0,132.0),rgb8(10.0,146.0,174.0),rgb8(48.0,224.0,140.0),rgb8(229.0,255.0,184.0));}
if(p.palette==9u){return ramp5(t,rgb8(0.0,5.0,20.0),rgb8(4.0,42.0,82.0),rgb8(0.0,113.0,136.0),rgb8(34.0,176.0,162.0),rgb8(221.0,246.0,202.0));}
if(p.palette==10u){return ramp5(t,rgb8(5.0,4.0,2.0),rgb8(44.0,24.0,4.0),rgb8(132.0,83.0,10.0),rgb8(228.0,171.0,46.0),rgb8(255.0,245.0,198.0));}
if(p.palette==11u){return ramp5(t,rgb8(3.0,0.0,16.0),rgb8(250.0,0.0,132.0),rgb8(0.0,228.0,255.0),rgb8(126.0,255.0,0.0),rgb8(255.0,247.0,255.0));}
if(p.palette==13u){return ramp5(t,rgb8(0.0,0.0,14.0),rgb8(21.0,16.0,152.0),rgb8(0.0,188.0,255.0),rgb8(220.0,0.0,255.0),rgb8(255.0,255.0,255.0));}
if(p.palette==15u){return ramp5(t,rgb8(2.0,8.0,26.0),rgb8(18.0,85.0,210.0),rgb8(192.0,244.0,255.0),rgb8(255.0,134.0,36.0),rgb8(255.0,242.0,196.0));}
if(p.palette==17u){return ramp5(t,rgb8(20.0,1.0,14.0),rgb8(204.0,26.0,72.0),rgb8(255.0,226.0,182.0),rgb8(0.0,202.0,226.0),rgb8(226.0,249.0,255.0));}
if(p.palette==18u){return ramp5(t,rgb8(22.0,8.0,24.0),rgb8(171.0,36.0,122.0),rgb8(255.0,211.0,149.0),rgb8(45.0,204.0,176.0),rgb8(232.0,255.0,243.0));}
return current_palette(phase);
2026-08-24 20:30:26 +09:00
}
fn escaped_color(m:u32,sm:f32)->vec3< f32 > {
2026-08-25 14:03:13 +09:00
// Direct phase mapping: no iteration-band/anchor compensation.
let phase=fract(p.shift+sm*p.cycle);
let c=palette_color(phase);let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.30+0.70*pow(edge,0.40);let floorc=select(vec3< f32 > (0.0,0.0,0.0),vec3< f32 > (2.0/255.0),p.palette==2u);return mix(floorc,c,mixv);
2026-08-24 20:30:26 +09:00
}
2026-08-23 23:27:38 +09:00
fn base_color(i:u32)->vec3< f32 > {
2026-08-25 14:03:13 +09:00
let m=fieldMeta[i];return escaped_color(m,fieldSmooth[i]);
2026-08-23 23:27:38 +09:00
}
fn linearize(c:vec3< f32 > )->vec3< f32 > {return pow(c,vec3< f32 > (2.2));}
fn delinearize(c:vec3< f32 > )->vec3< f32 > {return pow(max(c,vec3< f32 > (0.0)),vec3< f32 > (1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
2026-08-25 14:03:13 +09:00
if(gid.x>=p.width||gid.y>=p.height){return;}
let i=gid.y*p.width+gid.x;let m=fieldMeta[i];let cls=(m>>28u)&3u;
2026-08-24 20:30:26 +09:00
if(cls==FIELD_UNKNOWN){
2026-08-25 14:03:13 +09:00
let reason=(m>>20u)&0xffu;
// Iteration-limit pixels are not a numeric hole: display them as opaque
// black so temporal history cannot paint stale exterior colour through.
if(reason==6u){textureStore(outTex,vec2< i32 > (gid.xy),vec4< f32 > (0.0,0.0,0.0,1.0));}
else{textureStore(outTex,vec2< i32 > (gid.xy),vec4< f32 > (0.0));}
return;
2026-08-24 20:30:26 +09:00
}
2026-08-25 14:03:13 +09:00
if(cls!=FIELD_ESCAPED){
// Certified / likely interior is real image content, not a temporal hole.
// Keep it opaque so a reprojected history frame can never leak exterior
// colour into an already-known Mandelbrot interior pixel.
textureStore(outTex,vec2< i32 > (gid.xy),vec4< f32 > (0.0,0.0,0.0,1.0));
return;
}
var c=escaped_color(m,fieldSmooth[i]);
2026-08-23 23:27:38 +09:00
if(p.edgeAA!=0u){
2026-08-25 14:03:13 +09:00
let x=i32(gid.x);let y=i32(gid.y);var sum=linearize(c);var cnt=1.0;
for(var oy=-1;oy< =1;oy+=1){for(var ox=-1;ox< =1;ox+=1){
if(ox==0&&oy==0){continue; }
let xx=x+ox;let yy=y+oy;
if(xx< 0 | | yy < 0 | | xx > =i32(p.width)||yy>=i32(p.height)){continue;}
let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];
if(((mj>>28u)&3u)==FIELD_ESCAPED){sum+=linearize(escaped_color(mj,fieldSmooth[j])); cnt+=1.0;}
}}
c=delinearize(sum/cnt);
2026-08-23 23:27:38 +09:00
}
textureStore(outTex,vec2< i32 > (gid.xy),vec4< f32 > (c,1.0));
}
2026-08-24 20:30:26 +09:00
`
2026-08-23 23:27:38 +09:00
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d< f32 > ;
@group(0) @binding(1) var b:texture_2d< f32 > ;
@group(0) @binding(2) var c:texture_2d< f32 > ;
@group(0) @binding(3) var d:texture_2d< f32 > ;
@group(0) @binding(4) var outTex:texture_storage_2d< rgba8unorm , write > ;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3< f32 > )->vec3< f32 > {return vec3< f32 > (to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3< f32 > )->vec3< f32 > {return vec3< f32 > (to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
2026-08-25 14:03:13 +09:00
let q=vec2< i32 > (gid.xy);let va=textureLoad(a,q,0);let vb=textureLoad(b,q,0);let vc=textureLoad(c,q,0);let vd=textureLoad(d,q,0);
let alpha=(va.a+vb.a+vc.a+vd.a)*0.25;
let premulLin=(lin3(va.rgb)*va.a+lin3(vb.rgb)*vb.a+lin3(vc.rgb)*vc.a+lin3(vd.rgb)*vd.a)*0.25;
if(alpha< =0.00001){textureStore(outTex,q,vec4< f32 > (0.0));return;}
let straightLin=premulLin/alpha;
textureStore(outTex,q,vec4< f32 > (srgb3(straightLin)*alpha,alpha));
2026-08-23 23:27:38 +09:00
}
`;
const PRESENT_WGSL=String.raw`
2026-08-25 14:03:13 +09:00
struct Params{
frontScaleX:f32,frontScaleY:f32,frontOffsetX:f32,frontOffsetY:f32,
histScaleX:f32,histScaleY:f32,histOffsetX:f32,histOffsetY:f32,
historyEnabled:f32,_p0:f32,_p1:f32,_p2:f32,
};
2026-08-23 23:27:38 +09:00
@group(0) @binding(0) var samp:sampler;
2026-08-25 14:03:13 +09:00
@group(0) @binding(1) var frontTex:texture_2d< f32 > ;
@group(0) @binding(2) var historyTex:texture_2d< f32 > ;
@group(0) @binding(3) var< uniform > p:Params;
2026-08-23 23:27:38 +09:00
struct VSOut{@builtin(position) pos:vec4< f32 > ,@location(0) uv:vec2< f32 > };
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array< vec2 < f32 > ,3>(vec2< f32 > (-1.0,-1.0),vec2< f32 > (3.0,-1.0),vec2< f32 > (-1.0,3.0));
var uv=array< vec2 < f32 > ,3>(vec2< f32 > (0.0,1.0),vec2< f32 > (2.0,1.0),vec2< f32 > (0.0,-1.0));
var o:VSOut;o.pos=vec4< f32 > (pos[i],0.0,1.0);o.uv=uv[i];return o;
}
2026-08-25 14:03:13 +09:00
fn mapped_uv(uv:vec2< f32 > ,scale:vec2< f32 > ,offset:vec2< f32 > )->vec2< f32 > {
return vec2< f32 > (0.5)+(uv-vec2< f32 > (0.5))*scale+offset;
}
fn inside(uv:vec2< f32 > )->bool{return all(uv>=vec2< f32 > (0.0))&&all(uv<=vec2<f32>(1.0)); }
2026-08-23 23:27:38 +09:00
@fragment fn fs(in:VSOut)->@location(0) vec4< f32 > {
2026-08-25 14:03:13 +09:00
let fuv=mapped_uv(in.uv,vec2< f32 > (p.frontScaleX,p.frontScaleY),vec2< f32 > (p.frontOffsetX,p.frontOffsetY));
var front=vec4< f32 > (0.0);
if(inside(fuv)){front=textureSampleLevel(frontTex,samp,fuv,0.0);}
if(p.historyEnabled< 0.5 | | front . a > =0.999){return front;}
let huv=mapped_uv(in.uv,vec2< f32 > (p.histScaleX,p.histScaleY),vec2< f32 > (p.histOffsetX,p.histOffsetY));
var hist=vec4< f32 > (0.0);
if(inside(huv)){hist=textureSampleLevel(historyTex,samp,huv,0.0);}
return front+hist*(1.0-front.a);
2026-08-23 23:27:38 +09:00
}
`;
2026-08-24 20:30:26 +09:00
2026-08-25 14:03:13 +09:00
const BLA_GPU_SHADOW_WGSL=COMMON+String.raw`
// Comparison-only BLA shadow kernel. It never writes visible Mandelbrot
// field/color/history resources. Each invocation computes one deterministic
// screen sample twice: (1) the existing FAST scaled perturbation recurrence,
// (2) the guarded BLA accelerator with checkpoint/replay. Any unsafe BLA
// candidate is rejected and replayed through the baseline recurrence.
struct ShadowParams{
maxIter:u32, refLen:u32, sampleCount:u32, levelCount:u32,
fullW:f32, refPixelX:f32, refPixelY:f32, spanMantHi:f32,
spanExp:i32, safety:f32, _p0:f32, _p1:f32,
};
struct RefPoint{ hi:vec2< f32 > , lo:vec2< f32 > };
struct BlaEntry{
aHi:vec2< f32 > , aLo:vec2< f32 > , bHi:vec2< f32 > , bLo:vec2< f32 > ,
q:f32, aAbs:f32, bAbs:f32, aRes:f32,
bRes:f32, skip:u32, start:u32, ok:u32,
};
struct LevelMeta{ offset:u32, count:u32 };
struct ShadowResult{
baseCode:u32, baseIter:u32, baseSteps:u32, blaCode:u32,
blaIter:u32, blaSteps:u32, blaApps:u32, blaSkipped:u32,
blaRejected:u32, blaReplays:u32, replaySteps:u32, maxSkip:u32,
internalFailure:u32, _r0:u32, _r1:u32, _r2:u32,
};
struct PState{ d:vec2< f32 > , w:vec2< f32 > , scaleExp:i32, n:u32 };
struct AdvanceResult{ s:PState, code:u32, iter:u32, steps:u32 };
struct BlaRunResult{
code:u32, iter:u32, steps:u32, apps:u32,
skipped:u32, rejected:u32, replays:u32, replaySteps:u32,
maxSkip:u32, internalFailure:u32,
};
@group(0) @binding(0) var< uniform > p:ShadowParams;
@group(0) @binding(1) var< storage , read > refs:array< RefPoint > ;
@group(0) @binding(2) var< storage , read > blas:array< BlaEntry > ;
@group(0) @binding(3) var< storage , read > levels:array< LevelMeta > ;
@group(0) @binding(4) var< storage , read > samples:array< vec2 < f32 > >;
@group(0) @binding(5) var< storage , read_write > results:array< ShadowResult > ;
const CODE_MAX_ITER:u32=0u;
const CODE_ESCAPED:u32=1u;
const CODE_REFERENCE_END:u32=2u;
const CODE_RANGE:u32=3u;
const CODE_CONTINUE:u32=4u;
const F32_MAX:f32=bitcast< f32 > (0x7f7fffffu);
const F32_MIN_NORMAL:f32=1.1754943508222875e-38;
const F32_MIN_SUB_BITS:u32=1u;
const F32_INF_BITS:u32=0x7f800000u;
const BAIL_GUARD:f32=1.75;
fn finite_f(x:f32)->bool{return x==x & & abs(x)< =F32_MAX;}
fn finite_v(v:vec2< f32 > )->bool{return finite_f(v.x)&&finite_f(v.y); }
fn next_up_pos(x0:f32)->f32{
let x=max(0.0,x0);
if(x!=x){return F32_MAX;}
if(x==0.0){return bitcast< f32 > (F32_MIN_SUB_BITS);}
let u=bitcast< u32 > (x);
if(u>=0x7f7fffffu){return F32_MAX;}
return bitcast< f32 > (u+1u);
}
fn next_down_pos(x0:f32)->f32{
let x=max(0.0,x0);
if(x!=x||x==0.0){return 0.0;}
let u=bitcast< u32 > (x);
if(u< =F32_MIN_SUB_BITS){return 0.0;}
return bitcast< f32 > (u-1u);
}
fn up_add(a:f32,b:f32)->f32{
let s=a+b;
if(!finite_f(s)){return F32_MAX;}
return next_up_pos(s);
}
fn up_mul_scale(a:f32,b:f32,e:i32)->f32{
if(!(a>0.0)& & !(b>0.0)){return 0.0;}
if(a< =0.0||b< =0.0){return 0.0;}
if(!finite_f(a)||!finite_f(b)){return F32_MAX;}
// WGSL frexp has indeterminate results for subnormal f32 inputs.
// Normalize any positive subnormal exactly by 2^24 before calling frexp.
var aa=a;var bb=b;var ee=e;
if(aa< F32_MIN_NORMAL ) { aa * = 16777216 . 0 ; ee- = 24;}
if(bb< F32_MIN_NORMAL ) { bb * = 16777216 . 0 ; ee- = 24;}
let fa=frexp(aa); let fb=frexp(bb);
let m=next_up_pos(fa.fract*fb.fract);
let y=ldexp(m,fa.exp+fb.exp+ee);
if(y==0.0){return bitcast< f32 > (F32_MIN_SUB_BITS);}
if(!finite_f(y)){return F32_MAX;}
return next_up_pos(y);
}
fn up_mul(a:f32,b:f32)->f32{return up_mul_scale(a,b,0);}
fn up_scale(a:f32,e:i32)->f32{
if(a< =0.0){return 0.0;}
let y=ldexp(a,e);
if(y==0.0){return bitcast< f32 > (F32_MIN_SUB_BITS);}
if(!finite_f(y)){return F32_MAX;}
return next_up_pos(y);
}
fn coef_mul(h:vec2< f32 > ,l:vec2< f32 > ,x:vec2< f32 > )->vec2< f32 > {
return cmul(h,x)+cmul(l,x);
}
fn state_z(s:PState)->vec2< f32 > {
let rp=refs[min(s.n,p.refLen)];
let delta=scaled_to_f32(s.w,s.scaleExp);
return rp.hi+(rp.lo+delta);
}
fn normalize_state(s0:PState)->PState{
var s=s0;
var mm=max(maxabs(s.w),maxabs(s.d));
var k=0u;
loop{
if(!(mm>65536.0)||k>=24u){break;}
s.w*=0.0000152587890625; s.d*=0.0000152587890625; s.scaleExp+=16; k+=1u;
mm=max(maxabs(s.w),maxabs(s.d));
}
k=0u;
loop{
if(!(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp)||k>=24u){break; }
s.w*=65536.0; s.d*=65536.0; s.scaleExp-=16; k+=1u;
mm=max(maxabs(s.w),maxabs(s.d));
}
return s;
}
fn advance_baseline(s0:PState,targetN:u32)->AdvanceResult{
var s=s0; var steps=0u;
loop{
if(s.n>=targetN||s.n>=p.maxIter){return AdvanceResult(s,CODE_CONTINUE,s.n,steps);}
let z=state_z(s); let mag=dot(z,z);
if(mag>4.0){return AdvanceResult(s,CODE_ESCAPED,s.n,steps);}
if(s.n>=p.refLen){return AdvanceResult(s,CODE_REFERENCE_END,s.n,steps);}
let r=refs[s.n];
let linear=2.0*(cmul(r.hi,s.w)+cmul(r.lo,s.w));
let sq=scaled_to_f32(cmul(s.w,s.w),s.scaleExp);
s.w=linear+sq+s.d; s.n+=1u; steps+=1u;
let mm=max(maxabs(s.w),maxabs(s.d));
if(mm>=1.0e30||mm!=mm){return AdvanceResult(s,CODE_RANGE,s.n,steps);}
if(mm>65536.0){s.w*=0.0000152587890625;s.d*=0.0000152587890625;s.scaleExp+=16;}
else if(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp){s.w*=65536.0; s.d*=65536.0;s.scaleExp-=16;}
if(s.scaleExp>126){return AdvanceResult(s,CODE_RANGE,s.n,steps);}
}
}
fn run_baseline(g:vec2< f32 > )->AdvanceResult{
let dx=(g.x-p.refPixelX)/p.fullW;
let dy=(p.refPixelY-g.y)/p.fullW;
var s=PState(vec2< f32 > (p.spanMantHi*dx,p.spanMantHi*dy),vec2< f32 > (0.0),p.spanExp,0u);
let r=advance_baseline(s,p.maxIter);
if(r.code!=CODE_CONTINUE){return r;}
return AdvanceResult(r.s,CODE_MAX_ITER,p.maxIter,r.steps);
}
fn get_bla(n:u32,lev:u32)->BlaEntry{
let lm=levels[lev];
let j=(n-1u)>>lev;
return blas[lm.offset+j];
}
fn has_bla(n:u32,lev:u32)->bool{
if(n< 1u | | lev > =p.levelCount){return false;}
let skip=1u< < lev ;
if(((n-1u)& (skip-1u))!=0u){return false;}
let lm=levels[lev]; let j=(n-1u)>>lev;
return j< lm.count ;
}
fn target_scaled(scaleExp:i32)->f32{
// This is a LOWER bound on the admissible error budget. The 128-ulp
// shrink covers the few f32 operations before the final nextDown.
let denom=max(1.0,p.fullW)*max(1.0,f32(p.maxIter));
let base=(abs(p.spanMantHi)*p.safety/denom)*(1.0-128.0*F32_U);
if(!(base>0.0)){return 0.0;}
let y=ldexp(base,p.spanExp-scaleExp);
if(!(y>0.0)||!finite_f(y)){return 0.0;}
return next_down_pos(y);
}
fn physical_error(errScaled:f32,s:PState,z:vec2< f32 > )->f32{
let propagated=up_scale(errScaled,s.scaleExp);
let delta=scaled_to_f32(s.w,s.scaleExp);
let recon=up_mul(8.0*F32_U,up_add(up_add(maxabs(z),maxabs(delta)),1.0e-30));
return up_add(propagated,recon);
}
fn run_bla(g:vec2< f32 > )->BlaRunResult{
let dx=(g.x-p.refPixelX)/p.fullW;
let dy=(p.refPixelY-g.y)/p.fullW;
var s=PState(vec2< f32 > (p.spanMantHi*dx,p.spanMantHi*dy),vec2< f32 > (0.0),p.spanExp,0u);
var anchor=s;
var errScaled=0.0;
var steps=0u; var apps=0u; var skipped=0u; var rejected=0u;
var replays=0u; var replaySteps=0u; var maxSkip=0u; var internalFailure=0u;
loop{
if(s.n>=p.maxIter){
if(errScaled>0.0){
let z=state_z(s); let e=physical_error(errScaled,s,z);
if(up_add(next_up_pos(length(z)),e)>=2.0){
let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps;
if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);}
s=rr.s;errScaled=0.0;anchor=s;
}
}
return BlaRunResult(CODE_MAX_ITER,p.maxIter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);
}
let z=state_z(s); let zUpper=next_up_pos(length(z));
if(errScaled>0.0){
let e=physical_error(errScaled,s,z);
if(up_add(zUpper,e)>=BAIL_GUARD){
let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps;
if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);}
s=rr.s;errScaled=0.0;anchor=s;continue;
}
}
if(dot(z,z)>4.0){
if(errScaled>0.0){
let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps;
if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);}
s=rr.s;errScaled=0.0;anchor=s;continue;
}
return BlaRunResult(CODE_ESCAPED,s.n,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);
}
if(s.n>=p.refLen){return BlaRunResult(CODE_REFERENCE_END,s.n,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);}
var found=false; var chosen:BlaEntry; var chosenW=vec2< f32 > (0.0); var chosenErr=0.0;
if(s.n>=1u& & p.levelCount>0u){
let za=length(s.w); let ca=length(s.d); let budget=target_scaled(s.scaleExp);
var li=i32(p.levelCount)-1;
loop{
if(li< 0 ) { break ; }
let lev=u32(li);
if(has_bla(s.n,lev)){
let b=get_bla(s.n,lev);
if(b.ok!=0u& & b.start==s.n& & b.skip>0u& & s.n+b.skip< =p.maxIter& & s.n+b.skip< =p.refLen& & budget>0.0){
let quad=up_add(up_add(up_mul(za,za),up_mul(za,ca)),up_mul(ca,ca));
let local=up_mul_scale(b.q,quad,s.scaleExp);
let carry=up_mul(b.aAbs,errScaled);
let coeffA=up_mul(up_mul(up_mul(b.aRes,za),4.0),F32_U);
let coeffB=up_mul(up_mul(up_mul(b.bRes,ca),4.0),F32_U);
let opMag=up_add(up_mul(b.aAbs,za),up_mul(b.bAbs,ca));
let roundErr=up_mul(up_mul(64.0,F32_U),opMag);
let eOut=up_add(up_add(up_add(carry,local),up_add(coeffA,coeffB)),roundErr);
if(finite_f(eOut)& & eOut< =budget){
let nw=coef_mul(b.aHi,b.aLo,s.w)+coef_mul(b.bHi,b.bLo,s.d);
if(finite_v(nw)){
var trial=PState(s.d,nw,s.scaleExp,s.n+b.skip);
var trialErr=eOut;
var mm=max(maxabs(trial.w),maxabs(trial.d)); var rk=0u;
loop{
if(!(mm>65536.0)||rk>=24u){break;}
trial.w*=0.0000152587890625;trial.d*=0.0000152587890625;trialErr*=0.0000152587890625;trial.scaleExp+=16;rk+=1u;
mm=max(maxabs(trial.w),maxabs(trial.d));
}
rk=0u;
loop{
if(!(mm>0.0&&mm<0.0000152587890625&&trial.scaleExp>p.spanExp)||rk>=24u){break; }
trial.w*=65536.0;trial.d*=65536.0;trialErr*=65536.0;trial.scaleExp-=16;rk+=1u;
mm=max(maxabs(trial.w),maxabs(trial.d));
}
if(trial.scaleExp< =126& & finite_v(trial.w)& & finite_v(trial.d)& & finite_f(trialErr)){
let ez=state_z(trial); let ee=physical_error(trialErr,trial,ez);
if(up_add(next_up_pos(length(ez)),ee)< BAIL_GUARD ) {
found=true;chosen=b;chosenW=trial.w;chosenErr=trialErr;
// Commit the normalized scale and d as well.
s=trial;
break;
}
}
}
}
}
rejected+=1u;
}
li-=1;
}
}
if(found){
errScaled=chosenErr;steps+=1u;apps+=1u;skipped+=chosen.skip-1u;maxSkip=max(maxSkip,chosen.skip);continue;
}
if(errScaled>0.0){
let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps;
if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);}
s=rr.s;errScaled=0.0;anchor=s;continue;
}
let rr=advance_baseline(s,s.n+1u);steps+=rr.steps;
if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);}
s=rr.s;anchor=s;
}
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
let i=gid.x;if(i>=p.sampleCount){return;}
let g=samples[i];
let b=run_baseline(g); let s=run_bla(g);
results[i]=ShadowResult(b.code,b.iter,b.steps,s.code,s.iter,s.steps,s.apps,s.skipped,s.rejected,s.replays,s.replaySteps,s.maxSkip,s.internalFailure,0u,0u,0u);
}
`;
// Canary stage: derived from the exact GPU-shadow BLA core. It does NOT
// replace the production FAST kernel. It recomputes a deterministic 1/64
// pixel lattice, compares candidate metadata with the already completed
// baseline fieldMeta, and writes only to a private candidate buffer.
// A separate apply pass is allowed only after CPU readback proves zero
// mismatches / zero numeric UNKNOWN / zero internal failures.
const BLA_GPU_CANARY_WGSL=BLA_GPU_SHADOW_WGSL
.replace('@group(0) @binding(5) var< storage , read_write > results:array< ShadowResult > ;', `@group(0) @binding(5) var< storage , read_write > candidateMeta:array< u32 > ;
@group(0) @binding(6) var< storage , read > sampleIndices:array< u32 > ;
@group(0) @binding(7) var< storage , read > baselineMeta:array< u32 > ;
@group(0) @binding(8) var< storage , read_write > canaryStats:array< atomic < u32 > >;`)
.replace(`@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
let i=gid.x;if(i>=p.sampleCount){return;}
let g=samples[i];
let b=run_baseline(g); let s=run_bla(g);
results[i]=ShadowResult(b.code,b.iter,b.steps,s.code,s.iter,s.steps,s.apps,s.skipped,s.rejected,s.replays,s.replaySteps,s.maxSkip,s.internalFailure,0u,0u,0u);
}`, `fn canary_meta(code:u32,iter:u32)->u32{
if(code==CODE_ESCAPED){return pack_meta(iter,FIELD_ESCAPED);}
if(code==CODE_MAX_ITER){return pack_unknown(p.maxIter,REASON_OPERATION_LIMIT);}
if(code==CODE_REFERENCE_END){return pack_unknown(iter,REASON_REFERENCE_END);}
if(code==CODE_RANGE){return pack_unknown(iter,REASON_RANGE);}
return pack_unknown(iter,REASON_ERROR_BOUND);
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
let i=gid.x;if(i>=p.sampleCount){return;}
let s=run_bla(samples[i]);
let cm=canary_meta(s.code,s.iter);
let bm=baselineMeta[sampleIndices[i]];
candidateMeta[i]=cm;
atomicAdd(&canaryStats[0],1u);
if(cm!=bm){atomicAdd(&canaryStats[1],1u); }
if(s.code==CODE_REFERENCE_END||s.code==CODE_RANGE){atomicAdd(&canaryStats[2],1u); }
if(s.internalFailure!=0u){atomicAdd(&canaryStats[3],s.internalFailure); }
atomicAdd(&canaryStats[4],s.apps);
atomicAdd(&canaryStats[5],s.skipped);
atomicAdd(&canaryStats[6],s.rejected);
atomicAdd(&canaryStats[7],s.replays);
atomicAdd(&canaryStats[8],s.replaySteps);
atomicMax(&canaryStats[9],s.maxSkip);
}`);
if(BLA_GPU_CANARY_WGSL===BLA_GPU_SHADOW_WGSL||!BLA_GPU_CANARY_WGSL.includes('candidateMeta'))throw new Error('BLA canary shader derivation failed');
const BLA_GPU_CANARY_APPLY_WGSL=String.raw`
struct Params{ count:u32,_p0:u32,_p1:u32,_p2:u32 };
@group(0) @binding(0) var< uniform > p:Params;
@group(0) @binding(1) var< storage , read > sampleIndices:array< u32 > ;
@group(0) @binding(2) var< storage , read > candidateMeta:array< u32 > ;
@group(0) @binding(3) var< storage , read_write > fieldMeta:array< u32 > ;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
let i=gid.x;if(i>=p.count){return;}
fieldMeta[sampleIndices[i]]=candidateMeta[i];
}
`;
2026-08-25 21:33:45 +09:00
// Simplified BLA probe kernel retained as the zero-restart control candidate.
2026-08-25 14:03:13 +09:00
const BLA_GPU_FAST_CANDIDATE_WGSL=COMMON+String.raw`
struct Params{maxIter:u32,refLen:u32,sampleCount:u32,levelCount:u32,fullW:f32,refPixelX:f32,refPixelY:f32,spanMant:f32,spanExp:i32,safety:f32,_p0:f32,_p1:f32};
struct RefPoint{hi:vec2< f32 > ,lo:vec2< f32 > };
struct Bla{aHi:vec2< f32 > ,aLo:vec2< f32 > ,bHi:vec2< f32 > ,bLo:vec2< f32 > ,q:f32,aAbs:f32,bAbs:f32,aRes:f32,bRes:f32,skip:u32,start:u32,ok:u32};
struct Lev{offset:u32,count:u32};struct Result{packed:u32,smoothBits:u32,steps:u32,aux:u32};struct S{d:vec2< f32 > ,w:vec2< f32 > ,e:i32,n: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 > bt:array< Bla > ;@group(0) @binding(3) var< storage , read > lm:array< Lev > ;@group(0) @binding(4) var< storage , read > pts:array< vec2 < f32 > >;@group(0) @binding(5) var< storage , read_write > out:array< Result > ;
const GUARD:f32=1.75;fn fin(x:f32)->bool{return x==x&&abs(x)<=bitcast<f32>(0x7f7fffffu); }fn fin2(v:vec2< f32 > )->bool{return fin(v.x)&&fin(v.y); }fn cof(h:vec2< f32 > ,l:vec2< f32 > ,x:vec2< f32 > )->vec2< f32 > {return cmul(h,x)+cmul(l,x);}fn zof(s:S)->vec2< f32 > {let r=refs[min(s.n,p.refLen)];return r.hi+(r.lo+scaled_to_f32(s.w,s.e));}
fn baseline(g:vec2< f32 > ,pre:u32)->Result{let dx=(g.x-p.refPixelX)/p.fullW;let dy=(p.refPixelY-g.y)/p.fullW;var s=S(vec2< f32 > (p.spanMant*dx,p.spanMant*dy),vec2< f32 > (0),p.spanExp,0u);var st=pre;loop{if(s.n>=p.maxIter){return Result(pack_unknown(p.maxIter,REASON_OPERATION_LIMIT),0u,st,1u);}let z=zof(s);let mag=dot(z,z);if(mag>4.0){return Result(pack_meta(s.n,FIELD_ESCAPED),bitcast< u32 > (smooth_escape(s.n,mag)),st,1u);}if(s.n>=p.refLen){return Result(pack_unknown(s.n,REASON_REFERENCE_END),0u,st,1u);}let r=refs[s.n];let lin=2.0*(cmul(r.hi,s.w)+cmul(r.lo,s.w));let sq=scaled_to_f32(cmul(s.w,s.w),s.e);s.w=lin+sq+s.d;s.n++;st++;let mm=max(maxabs(s.w),maxabs(s.d));if(mm>=1e30||mm!=mm){return Result(pack_unknown(s.n,REASON_RANGE),0u,st,1u);}if(mm>65536.0){s.w/=65536.0;s.d/=65536.0;s.e+=16;}else if(mm>0.0&&mm<1.0/65536.0&&s.e>p.spanExp){s.w*=65536.0; s.d*=65536.0;s.e-=16;}if(s.e>126){return Result(pack_unknown(s.n,REASON_RANGE),0u,st,1u);}}}
fn has(n:u32,l:u32)->bool{if(n< 1u | | l > =p.levelCount){return false;}let sk=1u< < l ; if ( ( ( n-1u ) & ( sk-1u ) ) ! = 0u ) { return false ; } return ( ( n-1u ) > >l)< lm [ l ] . count ; } fn get_bla ( n:u32 , l:u32 ) - > Bla{return bt[lm[l].offset+((n-1u)>>l)];}
fn budget(e:i32)->f32{let b=abs(p.spanMant)*p.safety/(max(1.0,p.fullW)*max(1.0,f32(p.maxIter)));if(!(b>0)){return 0;}let y=ldexp(b,p.spanExp-e);return select(0.0,y,y>0&&fin(y)); }fn perr(er:f32,s:S,z:vec2< f32 > )->f32{return abs(ldexp(er,s.e))+8.0*F32_U*(maxabs(z)+maxabs(scaled_to_f32(s.w,s.e))+1e-30);}
fn run(g:vec2< f32 > )->Result{let dx=(g.x-p.refPixelX)/p.fullW;let dy=(p.refPixelY-g.y)/p.fullW;var s=S(vec2< f32 > (p.spanMant*dx,p.spanMant*dy),vec2< f32 > (0),p.spanExp,0u);var er=0.0;var ap=false;var st=0u;loop{if(s.n>=p.maxIter){if(ap& & length(zof(s))+perr(er,s,zof(s))>=2.0){return baseline(g,st);}return Result(pack_unknown(p.maxIter,REASON_OPERATION_LIMIT),0u,st,0u);}let z=zof(s);if(ap& & length(z)+perr(er,s,z)>=GUARD){return baseline(g,st);}let mag=dot(z,z);if(mag>4.0){if(ap){return baseline(g,st);}return Result(pack_meta(s.n,FIELD_ESCAPED),bitcast< u32 > (smooth_escape(s.n,mag)),st,0u);}if(s.n>=p.refLen){return baseline(g,st);}var found=false;var ne=0.0;if(s.n>=1u){let za=length(s.w);let ca=length(s.d);let bd=budget(s.e);var li=i32(p.levelCount)-1;loop{if(li< 0 ) { break ; } let l = u32(li);if(has(s.n,l)){let b = get_bla(s.n,l);if(b.ok!=0u&&b.start==s.n&&b.skip > 0u& & s.n+b.skip< =p.maxIter& & s.n+b.skip< =p.refLen& & bd>0){let qd=za*za+za*ca+ca*ca;let local=abs(ldexp(b.q*qd,s.e));let eo=b.aAbs*er+local+b.aRes*za*4.0*F32_U+b.bRes*ca*4.0*F32_U;if(fin(eo)& & eo< =bd){let nw=cof(b.aHi,b.aLo,s.w)+cof(b.bHi,b.bLo,s.d);if(fin2(nw)){var t=S(s.d,nw,s.e,s.n+b.skip);var te=eo;var mm=max(maxabs(t.w),maxabs(t.d));var k=0u;loop{if(!(mm>65536.0)||k>=24u){break;}t.w/=65536.0;t.d/=65536.0;te/=65536.0;t.e+=16;k++;mm=max(maxabs(t.w),maxabs(t.d));}loop{if(!(mm>0.0&&mm<1.0/65536.0&&t.e>p.spanExp)||k>=48u){break; }t.w*=65536.0;t.d*=65536.0;te*=65536.0;t.e-=16;k++;mm=max(maxabs(t.w),maxabs(t.d));}if(t.e< =126&&fin2(t.w)&&fin2(t.d)&&fin(te)&&length(zof(t))+perr(te,t,zof(t))<GUARD){s=t; ne=te;found=true;break;}}}}}li--;}}if(found){er=ne;ap=true;st++;continue;}if(ap){return baseline(g,st);}let r=refs[s.n];let lin=2.0*(cmul(r.hi,s.w)+cmul(r.lo,s.w));let sq=scaled_to_f32(cmul(s.w,s.w),s.e);s.w=lin+sq+s.d;s.n++;st++;let mm=max(maxabs(s.w),maxabs(s.d));if(mm>=1e30||mm!=mm){return baseline(g,st);}if(mm>65536.0){s.w/=65536.0;s.d/=65536.0;s.e+=16;}else if(mm>0.0&&mm<1.0/65536.0&&s.e>p.spanExp){s.w*=65536.0; s.d*=65536.0;s.e-=16;}if(s.e>126){return baseline(g,st);}}}
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id:vec3< u32 > ){if(id.x< p.sampleCount ) { out [ id . x ] = run ( pts [ id . x ] ) ; } }
`;
2026-08-25 21:33:45 +09:00
// Production candidate with exact-checkpoint fallback. The approximation is
// never used to publish an uncertain result: on any guard/rebase/range event,
// the original FAST recurrence resumes from the last state reached without a
// BLA step instead of restarting the pixel at iteration zero.
const BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL=BLA_GPU_FAST_CANDIDATE_WGSL
.replace(
'fn baseline(g:vec2< f32 > ,pre:u32)->Result{let dx=(g.x-p.refPixelX)/p.fullW;let dy=(p.refPixelY-g.y)/p.fullW;var s=S(vec2< f32 > (p.spanMant*dx,p.spanMant*dy),vec2< f32 > (0),p.spanExp,0u);var st=pre;loop{if(s.n>=p.maxIter){return Result(pack_unknown(p.maxIter,REASON_OPERATION_LIMIT),0u,st,1u);}let z=zof(s);let mag=dot(z,z);if(mag>4.0){return Result(pack_meta(s.n,FIELD_ESCAPED),bitcast< u32 > (smooth_escape(s.n,mag)),st,1u);}if(s.n>=p.refLen){return Result(pack_unknown(s.n,REASON_REFERENCE_END),0u,st,1u);}let r=refs[s.n];let lin=2.0*(cmul(r.hi,s.w)+cmul(r.lo,s.w));let sq=scaled_to_f32(cmul(s.w,s.w),s.e);s.w=lin+sq+s.d;s.n++;st++;let mm=max(maxabs(s.w),maxabs(s.d));if(mm>=1e30||mm!=mm){return Result(pack_unknown(s.n,REASON_RANGE),0u,st,1u);}if(mm>65536.0){s.w/=65536.0;s.d/=65536.0;s.e+=16;}else if(mm>0.0&&mm<1.0/65536.0&&s.e>p.spanExp){s.w*=65536.0; s.d*=65536.0;s.e-=16;}if(s.e>126){return Result(pack_unknown(s.n,REASON_RANGE),0u,st,1u);}}}',
'fn baseline_from(s0:S,pre:u32)->Result{var s=s0;var st=pre;loop{if(s.n>=p.maxIter){return Result(pack_unknown(p.maxIter,REASON_OPERATION_LIMIT),0u,st,1u);}let z=zof(s);let mag=dot(z,z);if(mag>4.0){return Result(pack_meta(s.n,FIELD_ESCAPED),bitcast< u32 > (smooth_escape(s.n,mag)),st,1u);}if(s.n>=p.refLen){return Result(pack_unknown(s.n,REASON_REFERENCE_END),0u,st,1u);}let r=refs[s.n];let lin=2.0*(cmul(r.hi,s.w)+cmul(r.lo,s.w));let sq=scaled_to_f32(cmul(s.w,s.w),s.e);s.w=lin+sq+s.d;s.n++;st++;let mm=max(maxabs(s.w),maxabs(s.d));if(mm>=1e30||mm!=mm){return Result(pack_unknown(s.n,REASON_RANGE),0u,st,1u);}if(mm>65536.0){s.w/=65536.0;s.d/=65536.0;s.e+=16;}else if(mm>0.0&&mm<1.0/65536.0&&s.e>p.spanExp){s.w*=65536.0; s.d*=65536.0;s.e-=16;}if(s.e>126){return Result(pack_unknown(s.n,REASON_RANGE),0u,st,1u);}}}'
)
.replace('var er=0.0;var ap=false;var st=0u;loop','var er=0.0;var ap=false;var st=0u;var anchor=s;loop')
.split('return baseline(g,st);').join('return baseline_from(anchor,st);')
.replace('if(s.e>126){return baseline_from(anchor,st);}}}', 'if(s.e>126){return baseline_from(anchor,st);}anchor=s;}}');
if(BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL===BLA_GPU_FAST_CANDIDATE_WGSL||!BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL.includes('baseline_from(anchor,st)')||!BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL.includes('anchor=s;}}'))throw new Error('adaptive FAST BLA replay derivation failed');
2026-08-25 14:03:13 +09:00
2026-08-25 21:33:45 +09:00
// Adaptive FAST BLA production kernel. It uses the replay-safe candidate core
// and writes complete screen strips. If a BLA chain becomes unsafe, the exact
// FAST recurrence resumes at its last checkpoint; BLA rejection never becomes
// UNKNOWN by itself.
const BLA_GPU_FAST_FRAME_WGSL=BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL
2026-08-25 14:03:13 +09:00
.replace(
'struct Params{maxIter:u32,refLen:u32,sampleCount:u32,levelCount:u32,fullW:f32,refPixelX:f32,refPixelY:f32,spanMant:f32,spanExp:i32,safety:f32,_p0:f32,_p1:f32};',
'struct Params{width:u32,height:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,levelCount:u32,outputStride:u32,outputBase:u32,_u0:u32,_u1:u32,_u2:u32,fullW:f32,refPixelX:f32,refPixelY:f32,spanMant:f32,spanExp:i32,safety:f32,_p0:f32,_p1:f32};'
)
.replace(
'@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 > bt:array< Bla > ;@group(0) @binding(3) var< storage , read > lm:array< Lev > ;@group(0) @binding(4) var< storage , read > pts:array< vec2 < f32 > >;@group(0) @binding(5) var< storage , read_write > out:array< Result > ;',
'@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 > bt:array< Bla > ;@group(0) @binding(3) var< storage , read > lm:array< Lev > ;@group(0) @binding(4) var< storage , read_write > fieldMeta:array< u32 > ;@group(0) @binding(5) var< storage , read_write > fieldSmooth:array< f32 > ;@group(0) @binding(6) var< storage , read_write > unresolved:array< atomic < u32 > >;'
)
.replace(
'@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id:vec3< u32 > ){if(id.x< p.sampleCount ) { out [ id . x ] = run ( pts [ id . x ] ) ; } } ' ,
`fn store_result(i:u32,r:Result){
fieldMeta[i]=r.packed;fieldSmooth[i]=bitcast< f32 > (r.smoothBits);
let cls=(r.packed>>28u)&3u;
if(cls==FIELD_UNKNOWN){let reason=(r.packed>>20u)&255u; atomicAdd(&unresolved[0],1u); if(reason>=1u&&reason<=6u){atomicAdd(&unresolved[reason],1u); }}
}
@compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
if(gid.x>=p.width||gid.y>=p.height){return;}
let g=vec2< f32 > (f32(p.tileX+gid.x)+0.5,f32(p.tileY+gid.y)+0.5);
let i=p.outputBase+gid.y*p.outputStride+gid.x;
store_result(i,run(g));
}`
);
2026-08-25 21:33:45 +09:00
if(BLA_GPU_FAST_FRAME_WGSL===BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL||!BLA_GPU_FAST_FRAME_WGSL.includes('store_result'))throw new Error('adaptive FAST BLA shader derivation failed');
2026-08-25 14:03:13 +09:00
// Screen path: collect UNKNOWN statistics in the bucket histogram pass rather
// than using per-UNKNOWN global atomics in the primary perturbation kernel.
2026-08-24 20:30:26 +09:00
const DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u); \n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u); }\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u); }\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u); }\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u); }\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u); }\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u); }\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}');
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); }`);
2026-08-25 14:03:13 +09:00
if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('post-stats shader derivation failed');
2026-08-24 20:30:26 +09:00
2026-08-25 21:33:45 +09:00
const FAST_PERTURB_POSTSTATS_WGSL=FAST_PERTURB_WGSL.replace('fn mark_fast_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u); \n if(reason>=1u & & reason< =6u){atomicAdd(&unresolved[reason],1u); }\n}','fn mark_fast_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;\n}');
const BLA_GPU_FAST_FRAME_POSTSTATS_WGSL=BLA_GPU_FAST_FRAME_WGSL.replace('if(cls==FIELD_UNKNOWN){let reason=(r.packed>>20u)&255u; atomicAdd(&unresolved[0],1u); if(reason>=1u&&reason<=6u){atomicAdd(&unresolved[reason],1u); }}','if(cls==FIELD_UNKNOWN){}');
if(FAST_PERTURB_POSTSTATS_WGSL===FAST_PERTURB_WGSL || BLA_GPU_FAST_FRAME_POSTSTATS_WGSL===BLA_GPU_FAST_FRAME_WGSL)throw new Error('screen post-stats shader derivation failed');
// One O(pixels) reduction is shared by Direct, Accurate DS, FAST, Deep, and
// BLA. Workgroup-local atomics replace up to two contended global atomics for
// every UNKNOWN pixel, and reason zero remains visible in the total count.
const UNKNOWN_STATS_WGSL=COMMON+String.raw`
struct Params{pixelCount:u32,_p0:u32,_p1:u32,_p2:u32};
@group(0) @binding(0) var< uniform > p:Params;
@group(0) @binding(1) var< storage , read > fieldMeta:array< u32 > ;
@group(0) @binding(2) var< storage , read_write > unresolvedStats:array< atomic < u32 > >;
var< workgroup > localReasons:array< atomic < u32 > ,8>;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ,@builtin(local_invocation_id) lid:vec3< u32 > ){
if(lid.x< 8u ) { atomicStore ( & localReasons [ lid . x ] , 0u ) ; }
workgroupBarrier();
if(gid.x< p.pixelCount ) {
let packed=fieldMeta[gid.x];
if(((packed>>28u)& 3u)==FIELD_UNKNOWN){
atomicAdd(&localReasons[0],1u);
let reason=(packed&REASON_MASK)>>REASON_SHIFT;
if(reason>=1u & & reason< =6u){atomicAdd(&localReasons[reason],1u); }
}
}
workgroupBarrier();
if(lid.x< 8u ) { let count = atomicLoad(&localReasons[lid.x]);if(count > 0u){atomicAdd(&unresolvedStats[lid.x],count); }}
}
`;
const SYMMETRY_COPY_WGSL=String.raw`
struct Params{width:u32,height:u32,_p0:u32,_p1:u32};
@group(0) @binding(0) var< uniform > p:Params;
@group(0) @binding(1) var< storage , read_write > fieldMeta:array< u32 > ;
@group(0) @binding(2) var< storage , read_write > fieldSmooth:array< f32 > ;
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3< u32 > ){
let half=p.height/2u;if(gid.x>=p.width||gid.y>=half){return;}
let src=gid.y*p.width+gid.x;let dst=(p.height-1u-gid.y)*p.width+gid.x;
fieldMeta[dst]=fieldMeta[src];fieldSmooth[dst]=fieldSmooth[src];
}
`;
const FAILURE_TILE_MAP_WGSL=String.raw`
struct Params{width:u32,height:u32,tileSize:u32,cols:u32,rows:u32,_p0:u32,_p1:u32,_p2:u32};
@group(0) @binding(0) var< uniform > p:Params;
@group(0) @binding(1) var< storage , read > fieldMeta:array< u32 > ;
@group(0) @binding(2) var< storage , read_write > failureMap:array< u32 > ;
var< workgroup > counts:array< atomic < u32 > ,6>;
@compute @workgroup_size(64)
fn main(@builtin(local_invocation_id) lid:vec3< u32 > ,@builtin(workgroup_id) wid:vec3< u32 > ){
if(lid.x< 5u ) { atomicStore ( & counts [ lid . x ] , 0u ) ; } if ( lid . x = =5u){atomicStore(&counts[5],0xffffffffu);}workgroupBarrier();
let x0=wid.x*p.tileSize;let y0=wid.y*p.tileSize;let area=p.tileSize*p.tileSize;var i=lid.x;loop{if(i>=area){break;}let x=x0+i%p.tileSize;let y=y0+i/p.tileSize;if(x< p.width & & y < p . height ) { let index = y*p.width+x;let packed = fieldMeta[index];if(((packed > >28u)& 3u)==0u){let reason=(packed>>20u)&255u; if(reason>=1u&&reason<=5u){atomicAdd(&counts[reason-1u],1u); atomicMin(&counts[5],index); }}}i+=64u;}workgroupBarrier();if(lid.x< 6u ) { failureMap [ ( wid . y * p . cols + wid . x ) * 6u + lid . x ] = atomicLoad ( & counts [ lid . x ] ) ; }
}
`;
2026-08-23 23:27:38 +09:00
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
2026-08-25 21:33:45 +09:00
version:'24.3.0-optimized-unknown-pipeline',DIRECT_F32_WGSL,ACTIVE_PREPARE_WGSL,DIRECT_ACTIVE_INIT_WGSL,DIRECT_ACTIVE_CONTINUE_WGSL,ACTIVE_COMPARE_WGSL,DIRECT_ACCURATE_SEED_WGSL,DIRECT_DS_CORRECT_WGSL,DIRECT_DS_QUEUE_BUILD_WGSL,DIRECT_DS_CORRECT_QUEUE_WGSL,FAST_PERTURB_WGSL,FAST_PERTURB_POSTSTATS_WGSL,DEEP_PERTURB_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,DEEP_ACTIVE_INIT_WGSL,DEEP_ACTIVE_RESUME_INIT_WGSL,DEEP_ACTIVE_CONTINUE_WGSL,PERTURB_CERTIFIED_INTERIOR_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,UNKNOWN_STATS_WGSL,SYMMETRY_COPY_WGSL,FAILURE_TILE_MAP_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL,BLA_GPU_SHADOW_WGSL,BLA_GPU_CANARY_WGSL,BLA_GPU_CANARY_APPLY_WGSL,BLA_GPU_FAST_CANDIDATE_WGSL,BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL,BLA_GPU_FAST_FRAME_WGSL,BLA_GPU_FAST_FRAME_POSTSTATS_WGSL
2026-08-23 23:27:38 +09:00
});
})();
< / script >
< script data-bundle = "app" >
(()=>{'use strict';
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
const $=s=>document.querySelector(s),canvas=$('#view');
const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
2026-08-25 14:03:13 +09:00
const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000,PALETTE_MAX=18,VALID_PALETTE_IDS=new Set([0,1,2,3,5,7,9,10,11,13,15,17,18]);
2026-08-23 23:27:38 +09:00
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
2026-08-25 14:03:13 +09:00
const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96;
2026-09-06 23:22:55 +09:00
const PIXEL_FRONTIER_PRODUCTION=true,PIXEL_FRONTIER_WORK=4000000,PIXEL_FRONTIER_MAX_ITER=150000;
2026-08-25 21:33:45 +09:00
const PIXEL_FRONTIER_PARAMS=new URLSearchParams(location.search),PIXEL_FRONTIER_CANDIDATE=PIXEL_FRONTIER_PARAMS.get('frontier-candidate')==='1'||PIXEL_FRONTIER_PARAMS.has('frontier-gate'),PIXEL_FRONTIER_ENABLED=PIXEL_FRONTIER_PRODUCTION||PIXEL_FRONTIER_CANDIDATE;
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,colorRevision:0,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,multiRefPasses:0,multiRefRecovered:0,frontierRounds:0,frontierPixels:0,frontierEscaped:0,frontierIter:0,frontierMinimumIter:0,frontierConverged:false,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,qualityStage:'idle',qualityIter:0,qualityFinalIter:0,previewMs:0,refineMs:0,refinePending:false,temporalFill:false,numericRecoveryPending:false,numericRecoveryRunning:false,gpuStage:'',gpuTileRows:0};
2026-08-25 14:03:13 +09:00
let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,refineTimer=0,lastWrittenHash='',navigationHash='';
2026-08-23 23:27:38 +09:00
const viewHistory=[];let viewHistoryIndex=-1;
2026-08-25 21:33:45 +09:00
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,multiRefBuilds:0,numericSubmits:0,correctionSubmits:0,gpuSyncWaits:0,stageCounts:{},stageMs:{},blaShadowBuilds:0,blaShadowFailures:0,blaShadowLatest:null,blaGpuShadowRuns:0,blaGpuShadowFailures:0,blaGpuShadowLatest:null,blaGpuCanaryRuns:0,blaGpuCanaryFailures:0,blaGpuCanaryApplied:0,blaGpuCanaryLatest:null,blaFastProbeRuns:0,blaFastEnabledFrames:0,blaFastBaselineFrames:0,blaFastProbeLatest:null,blaFastFailures:0,generationMetrics:[]};
function generationMetric(token=state.token){for(let i=runtime.generationMetrics.length-1;i>=0;i--)if(runtime.generationMetrics[i].token===token)return runtime.generationMetrics[i];return null}
2026-08-23 23:27:38 +09:00
// ── exact fixed-point view state ─────────────────────────────────────────
function one(bits=state.bits){return 1n< < BigInt ( bits ) }
function fromFrac(n,d=1n){return n*one()/d}
function roundDivSigned(v,d){const neg=v< 0n , a = neg?-v:v,q=(a+d/2n)/d;return neg ? -q:q }
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places< 0 ) { digits + = ' 0 ' . repeat ( -places ) ; places = 0}const den = 10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg ? -v:v }
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v< 0n ? -v:v ; return v = ==0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v< < BigInt ( d ) :v > >BigInt(-d)}
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v< 0n ; if ( neg ) v = -v;const bl = bitLen(v),keep=52;let top , exp ; if ( bl > keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
function log2FixedAt(v,bits){v=v< 0n ? -v:v ; if ( v = ==0n)return-Infinity;const bl = bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v > >BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a< 0n ; if ( neg ) a = -a;const q = (a<<52n)/b,v=Number(q)/4503599627370496;return neg ? -v:v }
function f32ToFixed(x,bits){
x=Math.fround(x);if(!Number.isFinite(x))throw new Error('non-finite f32 split component');if(x===0)return 0n;
const ab=new ArrayBuffer(4),dv=new DataView(ab);dv.setFloat32(0,x,false);const u=dv.getUint32(0,false),neg=(u>>>31)!==0,eb=(u>>>23)&255,frac=u&0x7fffff,m=BigInt(eb===0?frac:(0x800000|frac)),e=(eb===0?-149:eb-127-23),sh=bits+e;
let q=sh>=0?m< < BigInt ( sh ) :roundDivSigned ( m , 1n < < BigInt ( -sh ) ) ; return neg ? -q:q ;
}
function splitFixedF32(v,bits,count=3){const out=[];let r=v;for(let i=0;i< count ; i + + ) { const x = Math.fround(fixedNum(r,bits));out.push(x);if(x===0)continue;r-=f32ToFixed(x,bits)}return out }
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re< < =s;state.im< < =s;state.span< < =s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re< < s , im:state . frameView . im < < s , span:state . frameView . span < < s } } state . bits + = shift }
function ensurePrecision(){const bl=bitLen(state.span);if(bl< MIN_SPAN_BITS ) promoteState ( TARGET_SPAN_BITS-bl ) }
function fmtFixed(v,d=17){let neg=v< 0n ; if ( neg ) v = -v;const scale = 10n**BigInt(d),q=v*scale > >BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v< 0n ; if ( neg ) v = -v;const maxD = state.bits,scale=10n**BigInt(maxD),q=v*scale > >BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
2026-08-24 20:30:26 +09:00
function f32Ulp(x){x=Math.abs(Math.fround(x));if(!Number.isFinite(x))return Infinity;if(x===0)return Math.pow(2,-149);if(x< Math.pow ( 2 , -126 ) ) return Math . pow ( 2 , -149 ) ; return Math . pow ( 2 , Math . floor ( Math . log2 ( x ) ) -23 ) }
function directPixelRatio(snap,w){const cr=Math.abs(fixedNum(snap.re,snap.bits)),ci=Math.abs(fixedNum(snap.im,snap.bits)),sp=Math.abs(fixedNum(snap.span,snap.bits)),step=sp/Math.max(1,w),scale=Math.max(cr,ci,sp*.75,Math.pow(2,-126)),ulp=f32Ulp(scale);return step/ulp}
function fastNeedsExtended(snap,w){return !(directPixelRatio(snap,w)>=4)}
2026-08-25 14:03:13 +09:00
function accurateHybridSafe(snap=snapshot(),w=canvas.width){return directPixelRatio(snap,w)>=32}
2026-08-25 21:33:45 +09:00
function chooseBackend(snap=snapshot(),w=canvas.width){const accurate=state.renderMode==='accurate',accurateHybrid=accurate&&accurateHybridSafe(snap,w),needsExtended=!accurate&&fastNeedsExtended(snap,w),deep=accurate&&!accurateHybrid,fastExtended=needsExtended; return{backend:accurateHybrid?'accurate-direct-ds':deep?'deep':fastExtended?'fast-extended':'direct',deep,accurateHybrid,fastExtended,probe:false,reason:accurateHybrid?'accurate-direct-ds-safe-spacing':deep?'manual-accurate-deep':fastExtended?'manual-fast-extended':'manual-fast'}}
function pixelPrecisionBits(snap=snapshot(),w=canvas.width,guard=32){const pixelLog2=log2FixedAt(snap.span,snap.bits)-Math.log2(Math.max(1,w)),need=Math.ceil(Math.max(0,-pixelLog2))+Math.max(16,guard|0);return Math.max(96,Math.min(snap.bits+64,need))}
2026-08-25 14:03:13 +09:00
function deepNeeded(snap=snapshot(),w=canvas.width){return state.renderMode==='accurate'}
2026-08-24 20:30:26 +09:00
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 ───────────────────────────────────────────
2026-08-25 21:33:45 +09:00
const ADAPTIVE_ITER_QUANTUM=64;
function adaptiveIterationBonus(screenBits,q,quantum=ADAPTIVE_ITER_QUANTUM){const raw=48*Math.max(0,Number(q)||0)*Math.max(0,Number(screenBits)||0);return raw>0?Math.ceil(raw/quantum)*quantum:0}
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),w=Math.max(1,canvas.width||innerWidth||1),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),q=state.processMode==='power'?.72:state.processMode==='fine'?1.25:state.processMode==='validate'?1.5:1,bonus=adaptiveIterationBonus(screenBits,q);return Math.min(12000,Math.max(state.baseIter,state.baseIter+bonus))}
2026-08-25 14:03:13 +09:00
function previewIter(finalIter=maxIter()){
const z=Math.max(0,zoomExp()),m=state.processMode==='power'?.72:state.processMode==='fine'?1.28:state.processMode==='validate'?1.55:1;
// The old fixed 1100-iteration ceiling starved deep previews: around 10^12,
// finalIter is ~2700 while many visible boundary pixels need >1100 steps.
// Make the first pass track final quality as zoom deepens, with a mode-aware
// ceiling so responsiveness is still bounded on extreme views.
const legacy=Math.round(m*(420+24*z));
const fractionBase=Math.min(.84,.48+.03*z);
const fraction=state.processMode==='power'?Math.min(.68,fractionBase*.82):state.processMode==='fine'?Math.min(.90,fractionBase+.04):state.processMode==='validate'?Math.min(.94,fractionBase+.08):fractionBase;
const deepFloor=Math.round(finalIter*fraction);
const cap=state.processMode==='power'?1800:state.processMode==='fine'?4800:state.processMode==='validate'?6000:3600;
return Math.min(finalIter,Math.max(state.baseIter,Math.min(cap,Math.max(legacy,deepFloor))));
}
function canUseStableReprojection(){
if(!renderer?.historyReady||!state.frameView)return false;
// Covers normal wheel/pinch, one 2x toolbar zoom, and ordinary pans while
// avoiding grotesque reuse after a reset or a distant shared-URL jump.
return reprojectionSafe(renderer.presentTransform(),2.25,.45);
}
function qualityStages(finalIter=maxIter()){
// Once a completed nearby frame exists, temporal reprojection is the
// preview. Do not replace it with a lower-iteration numerical frame: that
// was the source of the stressful black patches during small zoom/pan.
// Accurate mode also skips its expensive low-iteration Deep pass entirely;
// the final Deep result is computed once and swapped in atomically.
2026-08-25 21:33:45 +09:00
// Deep FAST cannot reuse a preview built from a shorter reference key. A
// separate preview therefore repeats the expensive perturbation frame and
// reference build without reducing final latency. Compute final once.
if(state.renderMode==='accurate'||canUseStableReprojection()||fastNeedsExtended(snapshot(),canvas.width))return[finalIter];
2026-08-25 14:03:13 +09:00
const p=previewIter(finalIter);
return finalIter>p?[p,finalIter]:[p];
}
function refinementDelay(stageIndex){
// Full-quality latency matters more than leaving a long idle gap after the
// first preview. Keep a short interaction settling window only.
if(stageIndex< =1)return state.processMode==='validate'?300:state.processMode==='fine'?240:state.processMode==='power'?420:160;
return state.processMode==='validate'?240:state.processMode==='fine'?200:360;
}
function sameViewSnapshot(a,b){return !!a& & !!b& & a.bits===b.bits& & a.re===b.re& & a.im===b.im& & a.span===b.span}
2026-08-25 21:33:45 +09:00
function canUseRealAxisSymmetry(snap){return !!snap& & snap.im===0n}
2026-08-25 14:03:13 +09:00
function reprojectionSafe(t,maxZoom=1.30,maxShift=.18){const s=Math.abs(Number(t?.scaleX));if(!Number.isFinite(s)||s< =0)return false;const z=Math.max(s,1/s),shift=Math.hypot(Number(t?.offsetX)||0,Number(t?.offsetY)||0);return z< =maxZoom& & shift< =maxShift}
2026-08-24 20:30:26 +09:00
function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))}
function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)}
2026-08-25 14:03:13 +09:00
function fract01(x){return x-Math.floor(x)}
function colorStyleSnapshot(){return{palette:state.palette,cycle:state.cycle,shift:state.shift,hq:state.hq}}
function colorStyleKey(style=colorStyleSnapshot()){return[style.palette,style.cycle,style.shift,style.hq?1:0].join(':')}
2026-08-23 23:27:38 +09:00
function pixelBudget(){const low=Number(navigator.deviceMemory||8)< =4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
'use strict';
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}
2026-08-25 21:33:45 +09:00
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v< < BigInt ( d ) :roundShift ( v , -d ) }
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n< < B , BAIL = 16n*ONE;let zr = 0n,zi=0n,zr2=0n,zi2=0n,escape=0,n=0;const rr = new Float64Array ( iter + 1 ) , ri = new Float64Array ( iter + 1 ) ; for ( ; n < iter & & ! escape ; n + + ) { rr [ n ] = fixedNum ( zr , bits ) ; ri [ n ] = fixedNum ( zi , bits ) ; const zri = roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2 > BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n< < B , BAIL = 16n*ONE;let zr = 0n,zi=0n,zr2=0n,zi2=0n;for(let n = 0;n<iter;n++){const zri = roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2 > BAIL)return n+1}return 0}
function referenceCandidates(re,im,span,w,h){if(span< =0n||w< =0||h< =0)return[{re,im}];const out=[{re,im}],den=8n,bh=BigInt(h),bw=BigInt(w);for(let gy=-3;gy< =3;gy++)for(let gx=-3;gx< =3;gx++){if(!gx&&!gy)continue; out.push({re:re+span*BigInt(gx)/den,im:im+span*bh*BigInt(gy)/(den*bw)})}return out}
2026-08-24 20:30:26 +09:00
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k< list.length ; k + + ) { const c = list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score > best.score){best={...c,escape,score}}if(escape===0)break}return best}
2026-08-25 21:33:45 +09:00
function samePacked(a,b){const ah=Math.fround(a),bh=Math.fround(b);return Object.is(ah,bh)& & Object.is(Math.fround(a-ah),Math.fround(b-bh))}
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+32,R=re< < 32n , I = im<<32n,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,zr2=0n,zi2=0n,escape=0,mismatch=false,checked=0;for(let n = 0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!samePacked(fixedNum(zr,bits),ref.rr[n])||!samePacked(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zri = roundShift(2n*zr*zi,bits);zi=zri+I;zr=zr2-zi2+R;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2 > BAIL)escape=n+1}return{mismatch,checked}}
2026-08-23 23:27:38 +09:00
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i< =refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
2026-08-25 21:33:45 +09:00
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceBits=d.bits,sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,fixed=d.fixedRe!=null&&d.fixedIm!=null,baseRe=fixed?BigInt(d.fixedRe):sourceRe,baseIm=fixed?BigInt(d.fixedIm):sourceIm,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),limit=Math.min(MAX_REF-1,d.iter),bits=d.targetBits,centerRe=requant(baseRe,sourceBits,bits),centerIm=requant(baseIm,sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,limit),chosen=fixed?{re:baseRe,im:baseIm,escape:centerRef.escape,score:centerRef.escape||limit}:chooseReference(sourceBits,sourceRe,sourceIm,sourceSpan,w,h,limit,centerRef.escape),re=requant(chosen.re,sourceBits,bits),im=requant(chosen.im,sourceBits,bits),ref=(chosen.re===baseRe&&chosen.im===baseIm)?centerRef:orbit(bits,re,im,limit),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen); postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,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-25 21:33:45 +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;if(d.type!=='error'&&d.checkpointMismatch&&p.attempt<3){p.attempt++; p.message.targetBits+=32;this.worker.postMessage(p.message);return}this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,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 targetBits=pixelPrecisionBits(snap,width,40),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'pixel-guarded-perturb-v24.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail}),message={type:'build',id,key,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter};this.pending.set(id,{resolve,reject,key,promise,message,attempt:0,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage(message);return promise}
requestFixed(snap,iter,width,height,fixedSource){if(!fixedSource)return this.request(snap,iter,width,height);const targetBits=pixelPrecisionBits(snap,width,40),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,fixedSource.re,fixedSource.im,'pixel-fixed-reference-v24.3'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded fixed reference request');if(!this.ensure())return Promise.reject(new Error('Reference Worker unavailable'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail}),message={type:'build',id,key,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter,fixedRe:fixedSource.re.toString(),fixedIm:fixedSource.im.toString()};this.pending.set(id,{resolve,reject,key,promise,message,attempt:0,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage(message);return promise}
2026-08-23 23:27:38 +09:00
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
}
const refs=new ReferenceService();
2026-08-25 21:33:45 +09:00
const tileReferencePool=Array.from({length:3},()=>new ReferenceService());
2026-08-23 23:27:38 +09:00
2026-08-25 21:33:45 +09:00
function fastReferenceBits(snap,w=canvas.width){return pixelPrecisionBits(snap,w,32)}
2026-08-24 20:30:26 +09:00
function fastReferenceWorkerSource(){return String.raw`
'use strict';
function bitLen(v){v=v< 0n ? -v:v ; return v = ==0n?0:v.toString(2).length}
function roundShift(v,b){if(b< =0)return v;const neg=v< 0n , a = neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half) > >BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v< 0n ; if ( neg ) v = -v;const bl = bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v > >BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v< < BigInt ( d ) :roundShift ( v , -d ) }
2026-08-25 21:33:45 +09:00
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n< < B , BAIL = 16n*ONE;let zr = 0n,zi=0n,zr2=0n,zi2=0n,escape=0,n=0;const rr = new Float64Array ( iter + 1 ) , ri = new Float64Array ( iter + 1 ) ; for ( ; n < iter & & ! escape ; n + + ) { rr [ n ] = fixedNum ( zr , bits ) ; ri [ n ] = fixedNum ( zi , bits ) ; const zri = roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2 > BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n< < B , BAIL = 16n*ONE;let zr = 0n,zi=0n,zr2=0n,zi2=0n;for(let n = 0;n<iter;n++){const zri = roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2 > BAIL)return n+1}return 0}
function referenceCandidates(re,im,span,w,h){if(span< =0n||w< =0||h< =0)return[{re,im}];const out=[{re,im}],den=8n,bh=BigInt(h),bw=BigInt(w);for(let gy=-3;gy< =3;gy++)for(let gx=-3;gx< =3;gx++){if(!gx&&!gy)continue; out.push({re:re+span*BigInt(gx)/den,im:im+span*bh*BigInt(gy)/(den*bw)})}return out}
2026-08-24 20:30:26 +09:00
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k< list.length ; k + + ) { const c = list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score > best.score){best={...c,escape,score}}if(escape===0)break}return best}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i< =refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),bits=d.targetBits,centerRe=requant(sourceRe,d.sourceBits,bits),centerIm=requant(sourceIm,d.sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,d.iter),chosen=chooseReference(d.sourceBits,sourceRe,sourceIm,sourceSpan,w,h,d.iter,centerRef.escape),re=requant(chosen.re,d.sourceBits,bits),im=requant(chosen.im,d.sourceBits,bits),ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,d.iter),refs=packRefs(ref.rr,ref.ri,ref.refLen); postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error& & error.stack||error)})}}
`}
class FastReferenceService{
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}}
2026-08-25 21:33:45 +09:00
request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap,width),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.3'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail});this.pending.set(id,{resolve,reject,key,promise,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter});return promise}
2026-08-24 20:30:26 +09:00
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');this.cache.clear()}
}
const fastRefs=new FastReferenceService();
2026-08-25 21:33:45 +09:00
function precisionFallbackWorkerSource(){return String.raw`
'use strict';
const ITER_MASK=0x000fffff,FIELD_ESCAPED=1,REASON_SHIFT=20;
function roundDiv(v,d){const neg=v< 0n , a = neg?-v:v,q=(a+d/2n)/d;return neg ? -q:q }
function roundShift(v,b){const neg=v< 0n , a = neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half) > >BigInt(b);return neg?-q:q}
function bitLen(v){v=v< 0n ? -v:v ; return v = ==0n?0:v.toString(2).length}
function fixedNum(v,b){if(v===0n)return 0;let neg=v< 0n ; if ( neg ) v = -v;const bl = bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v > >BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v< < BigInt ( d ) :roundShift ( v , -d ) }
function smooth(n,mag,bits){const m=Math.max(4.0000005,fixedNum(mag,bits)),u=Math.log2(m);return Math.fround(n+1-Math.log2(Math.max(1e-20,.5*u)))}
function pixel(s,index,bits){const B=BigInt(bits),ONE=1n< < B , w = BigInt(s.width),x=BigInt(index%s.width),y=BigInt(Math.floor(index/s.width)),den=2n*w,re=requant(BigInt(s.re),s.bits,bits),im=requant(BigInt(s.im),s.bits,bits),span=requant(BigInt(s.span),s.bits,bits),cr=re+roundDiv(span*(2n*x+1n-w),den),ci=im+roundDiv(span*(BigInt(s.height)-2n*y-1n),den),bail=4n*ONE;let zr = 0n,zi=0n,zr2=0n,zi2=0n;for(let n = 0;n<s.iter;n++){zi=roundShift(2n*zr*zi,bits)+ci;zr=zr2-zi2+cr;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);const mag = zr2+zi2;if(mag > bail)return{escape:n+1,smooth:smooth(n+1,mag,bits)}}return{escape:0,smooth:0}}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='solve')return;try{const indices=new Uint32Array(d.indices),meta=new Uint32Array(indices.length),smoothOut=new Float32Array(indices.length),accepted=new Uint8Array(indices.length),s={bits:d.bits,re:d.re,im:d.im,span:d.span,width:d.width,height:d.height,iter:d.iter},baseBits=d.targetBits;for(let i=0;i< indices.length ; i + + ) { for ( let attempt = 0;attempt<4&&!accepted[i];attempt++){const bits = baseBits+attempt*32,a=pixel(s,indices[i],bits),b=pixel(s,indices[i],bits+32);if(a.escape&&a.escape===b.escape&&Object.is(a.smooth,b.smooth)){meta[i]=(a.escape&ITER_MASK)|(FIELD_ESCAPED<<28);smoothOut[i]=a.smooth;accepted[i]=1}else if ( ! a . escape & & ! b . escape ) { meta [ i ] = d . terminalClass > =0?((d.iter&ITER_MASK)|((d.terminalClass&3)<<28)):((d.iter&ITER_MASK)|(6<<REASON_SHIFT)); accepted[i]=1}}}postMessage({type:'solved',id:d.id,meta:meta.buffer,smooth:smoothOut.buffer,accepted:accepted.buffer},[meta.buffer,smoothOut.buffer,accepted.buffer])}catch(error){postMessage({type:'error',id:d.id,error:String(error& & error.stack||error)})}}
`}
class PrecisionFallbackService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.failed=false}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([precisionFallbackWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else p.resolve({indices:p.indices,meta:new Uint32Array(d.meta),smooth:new Float32Array(d.smooth),accepted:new Uint8Array(d.accepted)})};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'precision fallback worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter,width,height,indices,terminalClass=-1){if(this.pending.size)this.cancel('superseded precision fallback');if(!this.ensure())return Promise.reject(new Error('precision fallback Workerを作成できません'));const id=++this.serial,copy=Uint32Array.from(indices),targetBits=pixelPrecisionBits(snap,width,40);return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,indices:Uint32Array.from(indices)});this.worker.postMessage({type:'solve',id,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter,terminalClass,indices:copy.buffer},[copy.buffer])})}
cancel(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancel('destroyed')}
}
const precisionFallback=new PrecisionFallbackService();
const precisionFallbackPool=Array.from({length:Math.max(2,Math.min(4,Number(navigator.hardwareConcurrency)||4))},()=>new PrecisionFallbackService());
2026-08-25 14:03:13 +09:00
// ── BLA shadow laboratory ────────────────────────────────────────────────
// Safety rule for v24.2.49: BLA never participates in visible rendering.
// A separate worker builds a real hierarchical BLA table from the exact same
// packed reference orbit used by the existing perturbation kernel and compares
// its result against baseline perturbation on deterministic screen samples.
// Any mismatch therefore blocks promotion instead of creating a black/UNKNOWN
// pixel or changing the image.
function blaShadowWorkerSource(){return String.raw`
'use strict';
function cabs(ar,ai){return Math.hypot(ar,ai)}
function cmul(ar,ai,br,bi){return[ar*br-ai*bi,ar*bi+ai*br]}
function cadd(ar,ai,br,bi){return[ar+br,ai+bi]}
function finite2(a,b){return Number.isFinite(a)& & Number.isFinite(b)}
function ldexp(v,e){if(!Number.isFinite(v)||v===0)return v;return v*Math.pow(2,e)}
function unpackRefs(buf,refLen){const dv=new DataView(buf),rr=new Float64Array(refLen+1),ri=new Float64Array(refLen+1);for(let i=0;i< =refLen;i++){const o=i*16;rr[i]=dv.getFloat32(o,true)+dv.getFloat32(o+8,true);ri[i]=dv.getFloat32(o+4,true)+dv.getFloat32(o+12,true)}return{rr,ri}}
function item(start,skip,ar,ai,br,bi,dr,di,er,ei,fr,fi){return{start,skip,ar,ai,br,bi,dr,di,er,ei,fr,fi,q:Math.max(cabs(dr,di),.5*cabs(er,ei),cabs(fr,fi))}}
function buildTable(rr,ri,refLen){const levels=[];let l0=[];for(let m=1;m< refLen ; m + + ) l0 . push ( item ( m , 1 , 2 * rr [ m ] , 2 * ri [ m ] , 1 , 0 , 1 , 0 , 0 , 0 , 0 , 0 ) ) ; levels . push ( l0 ) ; let skip = 1;while(skip*2<=(refLen-1)&&levels.length<20){const prev = levels[levels.length-1],next=[];for(let j = 0;j+1<prev.length;j+=2){const x = prev[j],y=prev[j+1];if(x.start+skip!==y.start)continue;let t = cmul(y.ar,y.ai,x.ar,x.ai),A=t;t=cmul(y.ar,y.ai,x.br,x.bi);let B = cadd(t[0],t[1],y.br,y.bi);t=cmul(y.ar,y.ai,x.dr,x.di);let D0 = t;let ax2 = cmul(x.ar,x.ai,x.ar,x.ai),dyax2=cmul(y.dr,y.di,ax2[0],ax2[1]),D=cadd(D0[0],D0[1],dyax2[0],dyax2[1]);t=cmul(y.ar,y.ai,x.er,x.ei);let E = t;let axbx = cmul(x.ar,x.ai,x.br,x.bi),twoDyAxBx=cmul(2*y.dr,2*y.di,axbx[0],axbx[1]);E=cadd(E[0],E[1],twoDyAxBx[0],twoDyAxBx[1]);let eyax = cmul(y.er,y.ei,x.ar,x.ai);E=cadd(E[0],E[1],eyax[0],eyax[1]);t=cmul(y.ar,y.ai,x.fr,x.fi);let F = t;let bx2 = cmul(x.br,x.bi,x.br,x.bi),dybx2=cmul(y.dr,y.di,bx2[0],bx2[1]);F=cadd(F[0],F[1],dybx2[0],dybx2[1]);let eybx = cmul(y.er,y.ei,x.br,x.bi);F=cadd(F[0],F[1],eybx[0],eybx[1]);F=cadd(F[0],F[1],y.fr,y.fi);const vals = [...A,...B,...D,...E,...F];if(vals.every(Number.isFinite))next.push(item(x.start,skip*2,A[0],A[1],B[0],B[1],D[0],D[1],E[0],E[1],F[0],F[1]))}if(!next.length)break;levels.push(next);skip*=2}return levels }
const F32_MIN_SUB=1.401298464324817e-45;
function nextUpF32(x){let y=Math.fround(x);if(!Number.isFinite(y))return y;if(y===0)return F32_MIN_SUB;const a=new Float32Array(1),u=new Uint32Array(a.buffer);a[0]=y;if(y>0)u[0]++;else u[0]--;return a[0]}
function upF32(x){if(!(x>=0)||!Number.isFinite(x))return Math.fround(x);let y=Math.fround(x);if(y< x ) y = nextUpF32(y);return y }
function splitF32(x){const h=Math.fround(x),l=Math.fround(x-h);return[h,l]}
// Future GPU table format is deliberately NOT raw f32 A/B. Safety testing
// found raw coefficients can diverge badly from the scaled perturbation path.
// A/B are stored as hi+lo and all scalar error bounds are rounded upward.
function flattenTable(levels){let total=0;for(const l of levels)total+=l.length;const stride=64,buf=new ArrayBuffer(Math.max(1,total)*stride),dv=new DataView(buf),offsets=[],counts=[];let n=0;for(const lev of levels){offsets.push(n);counts.push(lev.length);for(const b of lev){const [arh,arl]=splitF32(b.ar),[aih,ail]=splitF32(b.ai),[brh,brl]=splitF32(b.br),[bih,bil]=splitF32(b.bi);const q=upF32(b.q),aAbs=upF32(cabs(b.ar,b.ai)),bAbs=upF32(cabs(b.br,b.bi)),aRes=upF32(cabs(b.ar-(arh+arl),b.ai-(aih+ail))),bRes=upF32(cabs(b.br-(brh+brl),b.bi-(bih+bil)));const ok=[arh,arl,aih,ail,brh,brl,bih,bil,q,aAbs,bAbs,aRes,bRes].every(Number.isFinite)&&q>=b.q&&aAbs>=cabs(b.ar,b.ai)&&bAbs>=cabs(b.br,b.bi); const o=n*stride;dv.setFloat32(o,arh,true);dv.setFloat32(o+4,aih,true);dv.setFloat32(o+8,arl,true);dv.setFloat32(o+12,ail,true);dv.setFloat32(o+16,brh,true);dv.setFloat32(o+20,bih,true);dv.setFloat32(o+24,brl,true);dv.setFloat32(o+28,bil,true);dv.setFloat32(o+32,q,true);dv.setFloat32(o+36,aAbs,true);dv.setFloat32(o+40,bAbs,true);dv.setFloat32(o+44,aRes,true);dv.setFloat32(o+48,bRes,true);dv.setUint32(o+52,b.skip,true);dv.setUint32(o+56,b.start,true);dv.setUint32(o+60,ok?1:0,true);n++}}return{buffer:buf,offsets,counts,total,stride}}
function at(levels,n,d){const skip=1< < d ; if ( n < 1 | | ( ( n-1 ) & ( skip-1 ) ) ! = = 0 ) return null ; const j = (n-1) > >d,lev=levels[d];return lev& & j< lev.length ? lev [ j ] :null }
function baseline(rr,ri,refLen,dcr,dci,maxIter){let wr=0,wi=0,n=0,physical=0;while(n< maxIter ) { const m = Math.min(n,refLen),zr=rr[m]+wr,zi=ri[m]+wi;if(zr*zr+zi*zi > 4)return{cls:1,iter:n,wr,wi,physical};if(n>=refLen)return{cls:0,iter:n,wr,wi,physical};const lr=2*(rr[n]*wr-ri[n]*wi),li=2*(rr[n]*wi+ri[n]*wr),sq=[wr*wr-wi*wi,2*wr*wi];wr=lr+sq[0]+dcr;wi=li+sq[1]+dci;n++;physical++}return{cls:0,iter:maxIter,wr,wi,physical}}
function shadow(rr,ri,levels,refLen,dcr,dci,maxIter,pixelSpacing,safety,allowBla=true){
let wr=0,wi=0,n=0,physical=0,apps=0,skipped=0,rejected=0,maxSkip=0,replaySteps=0,replays=0,err=0;
let anchorWr=0,anchorWi=0,anchorN=0;
const ca=cabs(dcr,dci),L=Math.max(1,maxIter),BAIL_GUARD=1.75;
function replayTo(target){
wr=anchorWr;wi=anchorWi;n=anchorN;err=0;replays++;
while(n< target ) {
const zr=rr[n]+wr,zi=ri[n]+wi;
if(zr*zr+zi*zi>4)return{escaped:true,iter:n};
const lr=2*(rr[n]*wr-ri[n]*wi),li=2*(rr[n]*wi+ri[n]*wr),sqR=wr*wr-wi*wi,sqI=2*wr*wi;
wr=lr+sqR+dcr;wi=li+sqI+dci;n++;physical++;replaySteps++;
}
anchorWr=wr;anchorWi=wi;anchorN=n;
return{escaped:false};
}
while(n< maxIter ) {
const m=Math.min(n,refLen),zr=rr[m]+wr,zi=ri[m]+wi,zmag=cabs(zr,zi);
// Once approximation uncertainty approaches the bailout circle, replay
// from the last exact baseline checkpoint. Never turn BLA uncertainty
// into UNKNOWN and never trust a skip across an escape decision.
if(err>0& & zmag+err>=BAIL_GUARD){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};continue;}
if(zr*zr+zi*zi>4){if(err>0){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};continue;}return{cls:1,iter:n,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};}
if(n>=refLen)return{cls:0,iter:n,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};
let chosen=null;
if(n>=1& & allowBla){
const za=cabs(wr,wi);
for(let d=levels.length-1;d>=0;d--){
const b=at(levels,n,d);if(!b||n+b.skip>Math.min(maxIter,refLen)||!Number.isFinite(b.q))continue;
const local=b.q*(za*za+za*ca+ca*ca),budget=cabs(b.br,b.bi)*pixelSpacing/L*safety,eOut=cabs(b.ar,b.ai)*err+local;
if(!(Number.isFinite(eOut)&&eOut<=budget)){rejected++; continue;}
const az=cmul(b.ar,b.ai,wr,wi),bc=cmul(b.br,b.bi,dcr,dci),nw=cadd(az[0],az[1],bc[0],bc[1]),end=n+b.skip,ezr=rr[end]+nw[0],ezi=ri[end]+nw[1];
if(cabs(ezr,ezi)+eOut>=BAIL_GUARD){rejected++;continue;}
chosen={b,nw,eOut};break;
}
}
if(chosen){wr=chosen.nw[0];wi=chosen.nw[1];err=chosen.eOut;n+=chosen.b.skip;physical++;apps++;skipped+=chosen.b.skip-1;if(chosen.b.skip>maxSkip)maxSkip=chosen.b.skip;continue;}
// A normal perturbation step is an exact checkpoint only if no earlier BLA
// uncertainty remains. Otherwise replay first, then continue baseline.
if(err>0){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};continue;}
const lr=2*(rr[n]*wr-ri[n]*wi),li=2*(rr[n]*wi+ri[n]*wr),sqR=wr*wr-wi*wi,sqI=2*wr*wi;
wr=lr+sqR+dcr;wi=li+sqI+dci;n++;physical++;anchorWr=wr;anchorWi=wi;anchorN=n;
}
if(err>0& & cabs(rr[Math.min(n,refLen)]+wr,ri[Math.min(n,refLen)]+wi)+err>=2){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};}
return{cls:0,iter:maxIter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};
}
function samplePoints(w,h,count){const side=Math.max(3,Math.ceil(Math.sqrt(Math.max(1,count)))),out=[];for(let j=0;j< side ; j + + ) for ( let i = 0;i<side;i++){if(out.length > =count)break;const x=(i+.5)*w/side,y=(j+.5)*h/side;out.push([x,y])}return out}
self.onmessage=e=>{const d=e.data;if(!d||(d.type!=='validate'&&d.type!=='build'))return; const t0=performance.now();try{const {rr,ri}=unpackRefs(d.refs,d.refLen),tb0=performance.now(),levels=buildTable(rr,ri,d.refLen),packed=flattenTable(levels),tableBuildMs=performance.now()-tb0,span=ldexp(d.spanMant,d.spanExp),pixelSpacing=Math.abs(span)/Math.max(1,d.width),buildOnly=d.type==='build',pts=buildOnly?[]:samplePoints(d.width,d.height,d.samples||49),referenceFull=d.refLen>=d.maxIter;let classMismatch=0,escapeIterationMismatch=0,logical=0,physical=0,apps=0,skipped=0,rejected=0,maxSkip=0,maxEndpointDelta=0,replaySteps=0,replays=0;for(const [x,y] of pts){const dcr=(x-d.refPixelX)*span/d.width,dci=(d.refPixelY-y)*span/d.width,b=baseline(rr,ri,d.refLen,dcr,dci,d.maxIter),sh=shadow(rr,ri,levels,d.refLen,dcr,dci,d.maxIter,pixelSpacing,d.safety||0.015625,referenceFull);logical+=b.physical;physical+=sh.physical;apps+=sh.apps;skipped+=sh.skipped;rejected+=sh.rejected;replaySteps+=sh.replaySteps||0;replays+=sh.replays||0;maxSkip=Math.max(maxSkip,sh.maxSkip);if(b.cls!==sh.cls)classMismatch++;if(b.cls===1&&sh.cls===1&&b.iter!==sh.iter)escapeIterationMismatch++; if(b.cls===sh.cls& & b.iter===sh.iter){const de=Math.hypot(b.wr-sh.wr,b.wi-sh.wi);if(Number.isFinite(de))maxEndpointDelta=Math.max(maxEndpointDelta,de)}}const eligibleForBla=referenceFull&&span!==0,gatePassed=eligibleForBla&&(buildOnly||(classMismatch===0&&escapeIterationMismatch===0&&!Number.isNaN(maxEndpointDelta))); const stats={samples:pts.length,classMismatch,escapeIterationMismatch,logicalIterations:logical,physicalSteps:physical,blaApplications:apps,blaSkippedIterations:skipped,blaRejected:rejected,blaReplaySteps:replaySteps,blaReplays:replays,maxSkip,skipRate:logical?Math.max(0,(logical-physical)/logical):0,candidateSkipRate:logical?skipped/logical:0,maxEndpointDelta,tableLevels:levels.length,tableEntries:packed.total,tableStride:packed.stride,gpuPacking:'A/B hi+lo; bounds upward-rounded',unsafeRawF32Rejected:true,gpuShadowEligible:gatePassed,tableBuildMs,totalMs:performance.now()-t0,spanUnderflow:span===0,referenceFull,eligibleForBla,buildOnly,productionEligible:false,gatePassed};postMessage({type:'validated',id:d.id,key:d.key,stats,table:packed.buffer,levelOffsets:packed.offsets,levelCounts:packed.counts},[packed.buffer])}catch(error){postMessage({type:'error',id:d.id,error:String(error& & error.stack||error)})}}
`}
class BlaShadowService{
2026-08-25 21:33:45 +09:00
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.latest=null;this.cache=new Map();this.maxCache=4;this.failed=false}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([blaShadowWorkerSource()],{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'){runtime.blaShadowFailures++;p.reject(new Error(d.error));return}const result={...d,source:p.source,createdAt:performance.now()};this.latest=result;if(p.cacheKey){this.cache.set(p.cacheKey,result);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value)}runtime.blaShadowLatest=result.stats;runtime.blaShadowBuilds++;p.resolve(result);updateStats()};this.worker.onerror=e=>{this.failed=true;runtime.blaShadowFailures++;for(const p of this.pending.values())p.reject(new Error(e.message||'BLA shadow worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request({ctx,snap,iter,width,height,refPixel,samples=49,safety=1/64}){if(!ctx?.refs||!ctx.refLen)return Promise.reject(new Error('BLA shadow requires perturbation reference'));if(!this.ensure())return Promise.reject(new Error('BLA shadow Workerを作成できません'));if(this.pending.size)this.cancelPending('superseded BLA shadow request');if(!this.ensure())return Promise.reject(new Error('BLA shadow Workerを再起動できません'));const id=++this.serial,se=spanMantExp(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,ctx.refLen,samples,safety].join(':');const copy=ctx.refs.slice(0),source={bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y};return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source});this.worker.postMessage({type:'validate',id,key,refs:copy,refLen:ctx.refLen,maxIter:iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y,spanMant:se.mant,spanExp:se.exp,samples,safety},[copy])})}
requestBuild({ctx,snap,iter,width,height,refPixel,safety=1/64}){if(!ctx?.refs||!ctx.refLen)return Promise.reject(new Error('BLA build requires perturbation reference'));const cacheKey=[ctx.key||'',ctx.refLen,'bla-table-v24.3'].join(':');const hit=this.cache.get(cacheKey);if(hit)return Promise.resolve({...hit,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y},cacheHit:true});if(!this.ensure())return Promise.reject(new Error('BLA Workerを作成できません'));if(this.pending.size)this.cancelPending('superseded BLA build request');if(!this.ensure())return Promise.reject(new Error('BLA Workerを再起動できません'));const id=++this.serial,se=spanMantExp(snap),key=[cacheKey,'build',safety].join(':'),copy=ctx.refs.slice(0),source={bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y};return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source,cacheKey});this.worker.postMessage({type:'build',id,key,refs:copy,refLen:ctx.refLen,maxIter:iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y,spanMant:se.mant,spanExp:se.exp,safety},[copy])})}
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');if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}this.latest=null;this.cache.clear()}
2026-08-25 14:03:13 +09:00
}
const blaShadow=new BlaShadowService();let blaShadowTimer=0,numericRecoveryTimer=0;
function blaSamplePoints(w,h,count){const side=Math.max(3,Math.ceil(Math.sqrt(Math.max(1,count)))),out=[];for(let j=0;j< side ; j + + ) for ( let i = 0;i<side;i++){if(out.length > =count)break;out.push([(i+.5)*w/side,(j+.5)*h/side])}return out}
function blaCanaryGrid(w,h){const points=[],indices=[];for(let y=5;y< h ; y + = 8 ) for ( let x = 3;x<w;x+=8){points.push([x+.5,y+.5]);indices.push(y*w+x)}return{points,indices}}
function scheduleBlaShadowValidation(snap,iter,ctx,refPixel,token,kind='cpu'){
// Production modes do not spend CPU time re-validating BLA after every
// completed frame. The heavy shadow/canary machinery is diagnostics-only.
if(state.processMode!=='validate'||!ctx?.refs||!refPixel)return;if(blaShadowTimer)clearTimeout(blaShadowTimer);
blaShadowTimer=setTimeout(()=>{blaShadowTimer=0;if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot()))return;
const gpuGateRun=kind==='fast'&&state.processMode==='validate',samples=gpuGateRun?289:49;
blaShadow.request({ctx,snap,iter,width:canvas.width,height:canvas.height,refPixel,samples,safety:1/64}).then(async result=>{
if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot()))return;
if(gpuGateRun& & result.stats.gpuShadowEligible& & renderer){
const gpu=await renderer.runBlaGpuShadow({shadow:result,ctx,snap,iter,refPixel,safety:1/64});
if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot()))return;
if(gpu?.gatePassed& & renderer.blaGpuGate?.armed){await renderer.runBlaGpuCanary({shadow:result,ctx,snap,iter,refPixel,safety:1/64,token});}
}
}).catch(e=>{if(!/cancelled|superseded/i.test(String(e&&e.message||e))){runtime.blaShadowFailures++; console.warn('BLA shadow validation skipped:',e)}})
},700)
}
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-25 21:33:45 +09:00
this.frame=null;this.deepCtx=null;this.fastCtx=null;this.blaFastCtx=null;this.exportWs=[];this.blaFastCandidatePipeline=null;this.blaFastFramePipeline=null;this.blaFastCompilePromise=null;this.blaFastWarmScheduled=false;this.blaFastDisabled=true;this.blaFastOptIn=false;this.blaGpuShadowPipeline=null;this.blaGpuShadowCompilePromise=null;this.blaGpuShadowDisabled=false;this.blaGpuCanaryPipeline=null;this.blaGpuCanaryApplyPipeline=null;this.blaGpuCanaryCompilePromise=null;this.blaGpuCanaryDisabled=false;this.blaGpuGate={consecutive:0,totalSamples:0,distinctViews:0,viewKeys:[],armed:false,lastReason:'not-tested'};this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.activeGpuStage='';this.lastCompletedGpuStage='';this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false;this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});this.reprojectSampler=device.createSampler({magFilter:'nearest',minFilter:'nearest'});
2026-08-23 23:27:38 +09:00
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error& & e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
2026-08-25 21:33:45 +09:00
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;const where=this.activeGpuStage||this.lastCompletedGpuStage||'unknown-stage';state.gpuStage=where;state.gpuError='device lost @ '+where+': '+this.lossReason;if(renderer===this){renderer=null;state.gpuInitFailed=true;markDirty(false);setTimeout(()=>{state.gpuInitFailed=false;initRenderer().then(()=>markDirty(false))},1200)}});
2026-08-25 14:03:13 +09:00
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'premultiplied'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>'line '+x.lineNum+':'+x.linePos+' '+x.message).join('\n'))}return m}
resetBlaGpuGate(reason='reset'){
this.blaGpuGate={consecutive:0,totalSamples:0,distinctViews:0,viewKeys:[],armed:false,lastReason:reason};
runtime.blaGpuCanaryLatest={gateArmed:false,reason};
}
updateBlaGpuGate(stats,viewKey=''){
const pass=!!(stats&&stats.gatePassed&&stats.exactMismatch===0&&stats.blaDerivedUnknown===0&&stats.internalFailure===0&&stats.samples>=289);
if(!pass){this.resetBlaGpuGate(stats?.error||'gpu-shadow-mismatch');return false}
this.blaGpuGate.consecutive++;this.blaGpuGate.totalSamples+=stats.samples;this.blaGpuGate.lastReason='gpu-shadow-pass';
if(viewKey&&!this.blaGpuGate.viewKeys.includes(viewKey)){if(this.blaGpuGate.viewKeys.length<8)this.blaGpuGate.viewKeys.push(viewKey); this.blaGpuGate.distinctViews=this.blaGpuGate.viewKeys.length}
this.blaGpuGate.armed=this.blaGpuGate.consecutive>=3&&this.blaGpuGate.totalSamples>=867&&this.blaGpuGate.distinctViews>=3;
return this.blaGpuGate.armed;
}
warmBlaFastPipelines(){
if(this.blaFastCandidatePipeline& & this.blaFastFramePipeline)return Promise.resolve({probe:this.blaFastCandidatePipeline,frame:this.blaFastFramePipeline});
if(this.blaFastDisabled)return Promise.resolve(null);
if(this.blaFastCompilePromise)return this.blaFastCompilePromise;
this.blaFastCompilePromise=(async()=>{
let scope=false;
try{
if(this.device.pushErrorScope){this.device.pushErrorScope('validation');scope=true}
2026-08-25 21:33:45 +09:00
const [pm,fm]=await Promise.all([this.module('bla-fast-replay-probe',G.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL),this.module('bla-fast-frame-post-stats',G.BLA_GPU_FAST_FRAME_POSTSTATS_WGSL)]);
2026-08-25 14:03:13 +09:00
const make=async module=>this.device.createComputePipelineAsync?this.device.createComputePipelineAsync({layout:'auto',compute:{module,entryPoint:'main'}}):this.device.createComputePipeline({layout:'auto',compute:{module,entryPoint:'main'}});
const [probe,frame]=await Promise.all([make(pm),make(fm)]);
if(scope& & this.device.popErrorScope){const e=await this.device.popErrorScope();scope=false;if(e)throw e}
this.blaFastCandidatePipeline=probe;this.blaFastFramePipeline=frame;return{probe,frame};
}catch(e){
try{if(scope& & this.device.popErrorScope)await this.device.popErrorScope()}catch{}
this.blaFastDisabled=true;runtime.blaFastFailures++;runtime.blaFastProbeLatest={enabled:false,reason:'compile-failed',error:String(e&&e.message||e)}; console.warn('Adaptive FAST BLA disabled:',e);updateStats();return null;
}finally{this.blaFastCompilePromise=null}
})();
return this.blaFastCompilePromise;
}
scheduleBlaFastWarmup(){if(this.blaFastWarmScheduled||this.blaFastCandidatePipeline||this.blaFastCompilePromise||this.blaFastDisabled)return;this.blaFastWarmScheduled=true;const run=()=>{this.blaFastWarmScheduled=false;if(renderer===this&&!state.rendering&&!this.blaFastCandidatePipeline&&!this.blaFastDisabled)this.warmBlaFastPipelines()}; if(typeof requestIdleCallback==='function')requestIdleCallback(run,{timeout:2500});else setTimeout(run,1200)}
2026-08-25 21:33:45 +09:00
destroyBlaFastContext(){if(!this.blaFastCtx)return;destroy(this.blaFastCtx.tableB);destroy(this.blaFastCtx.levelsB);this.blaFastCtx=null;this.clearBindGroupCache()}
2026-08-25 14:03:13 +09:00
setBlaFastContext(shadow){
const key=shadow?.key||'';if(this.blaFastCtx& & this.blaFastCtx.key===key)return this.blaFastCtx;this.destroyBlaFastContext();
const levelCount=Math.min(20,shadow?.levelOffsets?.length||0);if(!levelCount||!shadow?.table?.byteLength)return null;
const levelData=new Uint32Array(levelCount*2);for(let i=0;i< levelCount ; i + + ) { levelData [ 2 * i ] = shadow . levelOffsets [ i ] > >>0;levelData[2*i+1]=shadow.levelCounts[i]>>>0}
const d=this.device,B=GPUBufferUsage,tableB=buf(d,shadow.table.byteLength,B.STORAGE|B.COPY_DST,'bla-fast-table'),levelsB=buf(d,levelData.byteLength,B.STORAGE|B.COPY_DST,'bla-fast-levels');d.queue.writeBuffer(tableB,0,shadow.table);d.queue.writeBuffer(levelsB,0,levelData);
this.blaFastCtx={key,tableB,levelsB,levelCount,tableBytes:shadow.table.byteLength};return this.blaFastCtx;
}
blaFastProbeParams(sampleCount,iter,snap,refPixel,safety=1/64){const se=spanMantExp(snap);return writeU32F32(48,d=>{d.setUint32(0,iter,true);d.setUint32(4,this.fastCtx.refLen,true);d.setUint32(8,sampleCount,true);d.setUint32(12,this.blaFastCtx.levelCount,true);d.setFloat32(16,canvas.width,true);d.setFloat32(20,Math.fround(refPixel.x),true);d.setFloat32(24,Math.fround(refPixel.y),true);d.setFloat32(28,Math.fround(se.mant),true);d.setInt32(32,se.exp,true);d.setFloat32(36,Math.fround(safety),true)})}
blaFastFrameParams(w,h,tileX,tileY,iter,snap,refPixel,outputStride,outputBase,safety=1/64){const se=spanMantExp(snap);return writeU32F32(80,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,tileX,true);d.setUint32(12,tileY,true);d.setUint32(16,iter,true);d.setUint32(20,this.fastCtx.refLen,true);d.setUint32(24,this.blaFastCtx.levelCount,true);d.setUint32(28,outputStride,true);d.setUint32(32,outputBase,true);d.setFloat32(48,canvas.width,true);d.setFloat32(52,Math.fround(refPixel.x),true);d.setFloat32(56,Math.fround(refPixel.y),true);d.setFloat32(60,Math.fround(se.mant),true);d.setInt32(64,se.exp,true);d.setFloat32(68,Math.fround(safety),true)})}
async runBlaFastProbe({shadow,snap,iter,refPixel,safety=1/64,samples=64}){
if(this.blaFastDisabled||!this.blaFastCandidatePipeline)return{enabled:false,reason:'pipeline-not-ready'};
if(!this.fastCtx||this.fastCtx.refLen< iter ) return { enabled:false , reason: ' short-reference ' } ;
const bc=this.setBlaFastContext(shadow);if(!bc)return{enabled:false,reason:'table'};
const n=Math.max(16,samples|0),pts=blaSamplePoints(canvas.width,canvas.height,n),pd=new Float32Array(n*2);for(let i=0;i< n ; i + + ) { pd [ 2 * i ] = Math . fround ( pts [ i ] [ 0 ] ) ; pd [ 2 * i + 1 ] = Math . fround ( pts [ i ] [ 1 ] ) }
const d=this.device,B=GPUBufferUsage,pbuf=buf(d,48,B.UNIFORM|B.COPY_DST,'bla-fast-probe-params'),ptsB=buf(d,pd.byteLength,B.STORAGE|B.COPY_DST,'bla-fast-probe-points'),outB=buf(d,n*16,B.STORAGE|B.COPY_SRC,'bla-fast-probe-results'),readB=buf(d,n*16,B.COPY_DST|B.MAP_READ,'bla-fast-probe-read');let mapped=false;
try{
d.queue.writeBuffer(pbuf,0,this.blaFastProbeParams(n,iter,snap,refPixel,safety));d.queue.writeBuffer(ptsB,0,pd);
2026-08-25 21:33:45 +09:00
const bg=d.createBindGroup({layout:this.blaFastCandidatePipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:bc.tableB}},{binding:3,resource:{buffer:bc.levelsB}},{binding:4,resource:{buffer:ptsB}},{binding:5,resource:{buffer:outB}}]}),e=d.createCommandEncoder({label:'adaptive-bla-fast-probe'}),pass=e.beginComputePass({label:'adaptive-bla-fast-probe'});pass.setPipeline(this.blaFastCandidatePipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(n/64));pass.end();e.copyBufferToBuffer(outB,0,readB,0,n*16);const t0=performance.now();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;await readB.mapAsync(GPUMapMode.READ);mapped=true;const raw=new Uint32Array(readB.getMappedRange().slice(0));let fallback=0,logical=0,physical=0,unknown=0;for(let i=0;i< n ; i + + ) { const o = i*4,packed=raw[o],steps=raw[o+2],aux=raw[o+3],cls=(packed > >>28)&3,it=packed&ITER_MASK; if(aux)fallback++;if(cls===FIELD_UNKNOWN)unknown++;logical+=Math.max(1,it);physical+=steps}const fallbackRate=fallback/n,operationReduction=logical?1-physical/logical:0,baselineMs=Number(this.fastBaselineGpuMs)||0,tableBuildMs=shadow.cacheHit?0:Number(shadow.stats?.tableBuildMs)||0,estimatedFrameMs=baselineMs*Math.max(0,1-operationReduction),estimatedNetMs=estimatedFrameMs+tableBuildMs+gpuMs,operationGate=fallbackRate< =0.125&&operationReduction>=0.5,netGate=baselineMs>0&&estimatedNetMs<baselineMs*.95,enabled=operationGate&&netGate; const out={enabled,reason:enabled?'probe-pass':!operationGate?'probe-cost':'probe-net-time',samples:n,fallback,fallbackRate,logicalSteps:logical,physicalSteps:physical,operationReduction,gpuMs,baselineMs,tableBuildMs,estimatedFrameMs,estimatedNetMs,netGate,unknown,tableBytes:bc.tableBytes};runtime.blaFastProbeRuns++;runtime.blaFastProbeLatest=out;updateStats();return out;
2026-08-25 14:03:13 +09:00
}finally{if(mapped)try{readB.unmap()}catch{};[pbuf,ptsB,outB,readB].forEach(destroy)}
}
2026-08-25 21:33:45 +09:00
encodeBlaFastNumeric(encoder,{pbuf,meta,smooth,w,h}){const bg=this.cachedBindGroup(this.blaFastFramePipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:this.blaFastCtx.tableB}},{binding:3,resource:{buffer:this.blaFastCtx.levelsB}},{binding:4,resource:{buffer:meta}},{binding:5,resource:{buffer:smooth}}]),pass=encoder.beginComputePass({label:'adaptive-bla-fast'});pass.setPipeline(this.blaFastFramePipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
2026-08-25 14:03:13 +09:00
async ensureBlaGpuCanaryPipelines(){
if(this.blaGpuCanaryPipeline&&this.blaGpuCanaryApplyPipeline)return{compare:this.blaGpuCanaryPipeline,apply:this.blaGpuCanaryApplyPipeline};
if(this.blaGpuCanaryDisabled||!this.blaGpuGate.armed)return null;
if(this.blaGpuCanaryCompilePromise)return this.blaGpuCanaryCompilePromise;
this.blaGpuCanaryCompilePromise=(async()=>{
let scope=false;
try{
if(this.device.pushErrorScope){this.device.pushErrorScope('validation');scope=true}
const [cm,am]=await Promise.all([this.module('bla-gpu-canary-compare',G.BLA_GPU_CANARY_WGSL),this.module('bla-gpu-canary-apply',G.BLA_GPU_CANARY_APPLY_WGSL)]);
const compare=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}}),apply=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
if(scope& & this.device.popErrorScope){const e=await this.device.popErrorScope();scope=false;if(e)throw e}
this.blaGpuCanaryPipeline=compare;this.blaGpuCanaryApplyPipeline=apply;return{compare,apply};
}catch(e){
try{if(scope& & this.device.popErrorScope)await this.device.popErrorScope()}catch{}
this.blaGpuCanaryDisabled=true;this.resetBlaGpuGate('canary-compile-failed');runtime.blaGpuCanaryFailures++;runtime.blaGpuCanaryLatest={gateArmed:false,compileFailed:true,error:String(e&&e.message||e)}; console.warn('BLA GPU canary disabled:',e);updateStats();return null;
}finally{this.blaGpuCanaryCompilePromise=null}
})();
return this.blaGpuCanaryCompilePromise;
}
async ensureBlaGpuShadowPipeline(){
if(this.blaGpuShadowPipeline)return this.blaGpuShadowPipeline;
if(this.blaGpuShadowDisabled)return null;
if(this.blaGpuShadowCompilePromise)return this.blaGpuShadowCompilePromise;
this.blaGpuShadowCompilePromise=(async()=>{
try{
this.device.pushErrorScope?.('validation');
const m=await this.module('bla-gpu-shadow',G.BLA_GPU_SHADOW_WGSL);
const pipe=this.device.createComputePipeline({layout:'auto',compute:{module:m,entryPoint:'main'}});
if(this.device.popErrorScope){const e=await this.device.popErrorScope();if(e)throw e}
this.blaGpuShadowPipeline=pipe;return pipe;
}catch(e){
try{if(this.device.popErrorScope)await this.device.popErrorScope()}catch{}
this.blaGpuShadowDisabled=true;runtime.blaGpuShadowFailures++;
runtime.blaGpuShadowLatest={gatePassed:false,compileFailed:true,error:String(e&&e.message||e),productionEligible:false};
console.warn('BLA GPU shadow disabled:',e);updateStats();return null;
}finally{this.blaGpuShadowCompilePromise=null}
})();
return this.blaGpuShadowCompilePromise;
}
async runBlaGpuShadow({shadow,ctx,snap,iter,refPixel,safety=1/64}){
await this.ready;
if(!shadow?.stats?.gatePassed||!shadow?.stats?.gpuShadowEligible)return{skipped:true,reason:'cpu-gate'};
if(!ctx?.refs||ctx.refLen< iter ) return { skipped:true , reason: ' short-reference ' } ;
const pipeline=await this.ensureBlaGpuShadowPipeline();if(!pipeline)return{skipped:true,reason:'pipeline'};
const sampleCount=Math.max(1,shadow.stats.samples|0),pts=blaSamplePoints(canvas.width,canvas.height,sampleCount),sampleData=new Float32Array(sampleCount*2);
for(let i=0;i< sampleCount ; i + + ) { sampleData [ 2 * i ] = Math . fround ( pts [ i ] [ 0 ] ) ; sampleData [ 2 * i + 1 ] = Math . fround ( pts [ i ] [ 1 ] ) ; }
const levelCount=Math.min(20,shadow.levelOffsets?.length||0),levelData=new Uint32Array(Math.max(1,levelCount)*2);
for(let i=0;i< levelCount ; i + + ) { levelData [ 2 * i ] = shadow . levelOffsets [ i ] > >>0;levelData[2*i+1]=shadow.levelCounts[i]>>>0;}
const B=GPUBufferUsage,d=this.device,resultStride=64,resultBytes=resultStride*sampleCount;
const pbuf=buf(d,48,B.UNIFORM|B.COPY_DST,'bla-shadow-params'),refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-reference'),tableB=buf(d,shadow.table.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-table'),levelsB=buf(d,levelData.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-levels'),samplesB=buf(d,sampleData.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-samples'),resultsB=buf(d,resultBytes,B.STORAGE|B.COPY_SRC,'bla-shadow-results'),readB=buf(d,resultBytes,B.COPY_DST|B.MAP_READ,'bla-shadow-readback');
const se=spanMantExp(snap),mantHi=Math.fround(se.mant),params=writeU32F32(48,dv=>{dv.setUint32(0,iter,true);dv.setUint32(4,ctx.refLen,true);dv.setUint32(8,sampleCount,true);dv.setUint32(12,levelCount,true);dv.setFloat32(16,canvas.width,true);dv.setFloat32(20,Math.fround(refPixel.x),true);dv.setFloat32(24,Math.fround(refPixel.y),true);dv.setFloat32(28,mantHi,true);dv.setInt32(32,se.exp,true);dv.setFloat32(36,Math.fround(safety),true)});
let mapped=false,scopePushed=false;
try{
d.queue.writeBuffer(pbuf,0,params);d.queue.writeBuffer(refsB,0,ctx.refs);d.queue.writeBuffer(tableB,0,shadow.table);d.queue.writeBuffer(levelsB,0,levelData);d.queue.writeBuffer(samplesB,0,sampleData);
if(d.pushErrorScope){d.pushErrorScope('validation');scopePushed=true;}this.activeGpuStage='bla-shadow-comparison';
const bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:refsB}},{binding:2,resource:{buffer:tableB}},{binding:3,resource:{buffer:levelsB}},{binding:4,resource:{buffer:samplesB}},{binding:5,resource:{buffer:resultsB}}]}),enc=d.createCommandEncoder({label:'bla-shadow-comparison'}),pass=enc.beginComputePass({label:'bla-shadow-comparison'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(sampleCount/64));pass.end();enc.copyBufferToBuffer(resultsB,0,readB,0,resultBytes);d.queue.submit([enc.finish()]);await d.queue.onSubmittedWorkDone();this.lastCompletedGpuStage='bla-shadow-comparison';this.activeGpuStage='';
if(scopePushed& & d.popErrorScope){const e=await d.popErrorScope();scopePushed=false;if(e)throw e}
await readB.mapAsync(GPUMapMode.READ);mapped=true;const raw=new Uint32Array(readB.getMappedRange().slice(0));
let classMismatch=0,escapeIterationMismatch=0,exactMismatch=0,blaDerivedUnknown=0,internalFailure=0,baseSteps=0,blaSteps=0,apps=0,skipped=0,rejected=0,replays=0,replaySteps=0,maxSkip=0;
const rows=[];
for(let i=0;i< sampleCount ; i + + ) {
const o=i*16,baseCode=raw[o],baseIter=raw[o+1],bs=raw[o+2],blaCode=raw[o+3],blaIter=raw[o+4],bsteps=raw[o+5],ba=raw[o+6],sk=raw[o+7],rej=raw[o+8],rep=raw[o+9],rsteps=raw[o+10],mx=raw[o+11],inf=raw[o+12];
const cm=baseCode!==blaCode,em=baseCode===1&&blaCode===1&&baseIter!==blaIter,ex=cm||baseIter!==blaIter;
if(cm)classMismatch++;if(em)escapeIterationMismatch++;if(ex)exactMismatch++;if((blaCode===2||blaCode===3)&&blaCode!==baseCode)blaDerivedUnknown++; internalFailure+=inf;baseSteps+=bs;blaSteps+=bsteps;apps+=ba;skipped+=sk;rejected+=rej;replays+=rep;replaySteps+=rsteps;maxSkip=Math.max(maxSkip,mx);
if(ex&&rows.length<8)rows.push({sample:i,point:pts[i],baseCode,baseIter,blaCode,blaIter,baseSteps:bs,blaSteps:bsteps});
}
const stats={samples:sampleCount,classMismatch,escapeIterationMismatch,exactMismatch,blaDerivedUnknown,internalFailure,baseSteps,blaSteps,blaApplications:apps,blaSkippedIterations:skipped,blaRejected:rejected,blaReplays:replays,replaySteps,maxSkip,operationReduction:baseSteps?Math.max(0,1-blaSteps/baseSteps):0,gatePassed:exactMismatch===0&&blaDerivedUnknown===0&&internalFailure===0,productionEligible:false,mismatches:rows};
runtime.blaGpuShadowRuns++;runtime.blaGpuShadowLatest=stats;this.updateBlaGpuGate(stats,shadow.key||'');updateStats();return stats;
}catch(e){
this.activeGpuStage='';if(scopePushed& & d.popErrorScope)try{await d.popErrorScope()}catch{};scopePushed=false;runtime.blaGpuShadowFailures++;const stats={gatePassed:false,error:String(e&&e.message||e),productionEligible:false}; runtime.blaGpuShadowLatest=stats;this.resetBlaGpuGate('gpu-shadow-error');console.warn('BLA GPU shadow failed:',e);updateStats();return stats;
}finally{
if(mapped)try{readB.unmap()}catch{};for(const x of [pbuf,refsB,tableB,levelsB,samplesB,resultsB,readB])destroy(x);
}
}
async runBlaGpuCanary({shadow,ctx,snap,iter,refPixel,safety=1/64,token=state.token}){
await this.ready;
if(state.processMode!=='validate'||state.renderMode!=='fast')return{skipped:true,reason:'validate-fast-only'};
if(!state.fieldView?.fastExtended||state.fieldView.iter!==iter)return{skipped:true,reason:'fast-extended-frame-required'};
if(!this.blaGpuGate.armed)return{skipped:true,reason:'gate-not-armed',gate:{...this.blaGpuGate}};
if(!shadow?.stats?.gatePassed||!shadow?.stats?.gpuShadowEligible||!ctx?.refs||ctx.refLen< iter ) return { skipped:true , reason: ' cpu-gate ' } ;
if(token!==state.token||!sameViewSnapshot(snap,snapshot())||!this.frame)return{skipped:true,reason:'stale-view'};
const pipes=await this.ensureBlaGpuCanaryPipelines();if(!pipes)return{skipped:true,reason:'pipeline'};
const f=this.frame,grid=blaCanaryGrid(f.w,f.h),sampleCount=grid.points.length;if(!sampleCount)return{skipped:true,reason:'empty-grid'};
const sampleData=new Float32Array(sampleCount*2),indexData=new Uint32Array(sampleCount);
for(let i=0;i< sampleCount ; i + + ) { sampleData [ 2 * i ] = Math . fround ( grid . points [ i ] [ 0 ] ) ; sampleData [ 2 * i + 1 ] = Math . fround ( grid . points [ i ] [ 1 ] ) ; indexData [ i ] = grid . indices [ i ] > >>0}
const levelCount=Math.min(20,shadow.levelOffsets?.length||0),levelData=new Uint32Array(Math.max(1,levelCount)*2);
for(let i=0;i< levelCount ; i + + ) { levelData [ 2 * i ] = shadow . levelOffsets [ i ] > >>0;levelData[2*i+1]=shadow.levelCounts[i]>>>0}
const d=this.device,B=GPUBufferUsage,statsBytes=64;
const pbuf=buf(d,48,B.UNIFORM|B.COPY_DST,'bla-canary-params'),refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-reference'),tableB=buf(d,shadow.table.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-table'),levelsB=buf(d,levelData.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-levels'),samplesB=buf(d,sampleData.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-samples'),indicesB=buf(d,indexData.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-indices'),candidateB=buf(d,sampleCount*4,B.STORAGE|B.COPY_SRC,'bla-canary-candidate-meta'),statsB=buf(d,statsBytes,B.STORAGE|B.COPY_SRC|B.COPY_DST,'bla-canary-stats'),readB=buf(d,statsBytes,B.COPY_DST|B.MAP_READ,'bla-canary-readback'),applyP=buf(d,16,B.UNIFORM|B.COPY_DST,'bla-canary-apply-params');
const se=spanMantExp(snap),mantHi=Math.fround(se.mant),params=writeU32F32(48,dv=>{dv.setUint32(0,iter,true);dv.setUint32(4,ctx.refLen,true);dv.setUint32(8,sampleCount,true);dv.setUint32(12,levelCount,true);dv.setFloat32(16,f.w,true);dv.setFloat32(20,Math.fround(refPixel.x),true);dv.setFloat32(24,Math.fround(refPixel.y),true);dv.setFloat32(28,mantHi,true);dv.setInt32(32,se.exp,true);dv.setFloat32(36,Math.fround(safety),true)});
let mapped=false,scope=false;
try{
d.queue.writeBuffer(pbuf,0,params);d.queue.writeBuffer(refsB,0,ctx.refs);d.queue.writeBuffer(tableB,0,shadow.table);d.queue.writeBuffer(levelsB,0,levelData);d.queue.writeBuffer(samplesB,0,sampleData);d.queue.writeBuffer(indicesB,0,indexData);d.queue.writeBuffer(statsB,0,new Uint32Array(statsBytes/4));
if(d.pushErrorScope){d.pushErrorScope('validation');scope=true}this.activeGpuStage='bla-canary-compare';
const bg=d.createBindGroup({layout:pipes.compare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:refsB}},{binding:2,resource:{buffer:tableB}},{binding:3,resource:{buffer:levelsB}},{binding:4,resource:{buffer:samplesB}},{binding:5,resource:{buffer:candidateB}},{binding:6,resource:{buffer:indicesB}},{binding:7,resource:{buffer:f.meta}},{binding:8,resource:{buffer:statsB}}]}),enc=d.createCommandEncoder({label:'bla-canary-compare'}),pass=enc.beginComputePass({label:'bla-canary-compare'});pass.setPipeline(pipes.compare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(sampleCount/64));pass.end();enc.copyBufferToBuffer(statsB,0,readB,0,statsBytes);d.queue.submit([enc.finish()]);await d.queue.onSubmittedWorkDone();this.lastCompletedGpuStage='bla-canary-compare';this.activeGpuStage='';
if(scope& & d.popErrorScope){const e=await d.popErrorScope();scope=false;if(e)throw e}
await readB.mapAsync(GPUMapMode.READ);mapped=true;const raw=new Uint32Array(readB.getMappedRange().slice(0));readB.unmap();mapped=false;
const stats={samples:sampleCount,checked:raw[0]||0,exactMismatch:raw[1]||0,blaDerivedUnknown:raw[2]||0,internalFailure:raw[3]||0,blaApplications:raw[4]||0,blaSkippedIterations:raw[5]||0,blaRejected:raw[6]||0,blaReplays:raw[7]||0,replaySteps:raw[8]||0,maxSkip:raw[9]||0,gateArmed:true,applied:false,metaOnly:true,productionEligible:false};
stats.gatePassed=stats.checked===sampleCount&&stats.exactMismatch===0&&stats.blaDerivedUnknown===0&&stats.internalFailure===0;
runtime.blaGpuCanaryRuns++;runtime.blaGpuCanaryLatest=stats;
if(!stats.gatePassed){this.resetBlaGpuGate('canary-mismatch');stats.gateArmed=false;runtime.blaGpuCanaryLatest=stats;runtime.blaGpuCanaryFailures++;updateStats();return stats}
if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot())||this.frame!==f){stats.skippedApply='stale-view';updateStats();return stats}
// Only now may the candidate metadata touch the production field. Every
// value was compared bit-for-bit against that same fieldMeta first, so
// this write is semantically idempotent. smooth/color remain baseline.
d.queue.writeBuffer(applyP,0,new Uint32Array([sampleCount,0,0,0]));
if(d.pushErrorScope){d.pushErrorScope('validation');scope=true}this.activeGpuStage='bla-canary-apply';
const abg=d.createBindGroup({layout:pipes.apply.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:applyP}},{binding:1,resource:{buffer:indicesB}},{binding:2,resource:{buffer:candidateB}},{binding:3,resource:{buffer:f.meta}}]}),ae=d.createCommandEncoder({label:'bla-canary-apply'}),ap=ae.beginComputePass({label:'bla-canary-apply'});ap.setPipeline(pipes.apply);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(sampleCount/64));ap.end();d.queue.submit([ae.finish()]);await d.queue.onSubmittedWorkDone();this.lastCompletedGpuStage='bla-canary-apply';this.activeGpuStage='';
if(scope& & d.popErrorScope){const e=await d.popErrorScope();scope=false;if(e)throw e}
stats.applied=true;runtime.blaGpuCanaryApplied+=sampleCount;runtime.blaGpuCanaryLatest=stats;updateStats();return stats;
}catch(e){
this.activeGpuStage='';try{if(scope& & d.popErrorScope)await d.popErrorScope()}catch{};this.resetBlaGpuGate('canary-runtime-error');runtime.blaGpuCanaryFailures++;const stats={gatePassed:false,gateArmed:false,error:String(e&&e.message||e),productionEligible:false}; runtime.blaGpuCanaryLatest=stats;console.warn('BLA GPU canary failed:',e);updateStats();return stats;
}finally{if(mapped)try{readB.unmap()}catch{};for(const x of [pbuf,refsB,tableB,levelsB,samplesB,indicesB,candidateB,statsB,readB,applyP])destroy(x)}
2026-08-23 23:27:38 +09:00
}
2026-08-25 14:03:13 +09:00
2026-08-23 23:27:38 +09:00
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
2026-08-25 21:33:45 +09:00
const [dm,um,sm,cm,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('unknown-stats',G.UNKNOWN_STATS_WGSL),this.module('real-axis-symmetry-copy',G.SYMMETRY_COPY_WGSL),this.module('color',G.COLOR_WGSL),this.module('present',G.PRESENT_WGSL)]);
2026-08-23 23:27:38 +09:00
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
2026-08-25 21:33:45 +09:00
this.unknownStats=this.device.createComputePipeline({layout:'auto',compute:{module:um,entryPoint:'main'}});
this.symmetryCopy=this.device.createComputePipeline({layout:'auto',compute:{module:sm,entryPoint:'main'}});
2026-08-23 23:27:38 +09:00
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
2026-08-25 14:03:13 +09:00
this.configure();
2026-08-23 23:27:38 +09:00
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
2026-08-25 21:33:45 +09:00
makeCompute(module){return this.device.createComputePipelineAsync?this.device.createComputePipelineAsync({layout:'auto',compute:{module,entryPoint:'main'}}):this.device.createComputePipeline({layout:'auto',compute:{module,entryPoint:'main'}})}
async ensureAccuratePipelines(){if(this.accurateDirectSeed&&this.accurateDirectDs&&this.accurateQueueBuild&&this.accurateDirectDsQueued)return; if(this.accuratePipelinePromise)return this.accuratePipelinePromise;this.accuratePipelinePromise=(async()=>{const modules=await Promise.all([this.module('accurate-direct-seed',G.DIRECT_ACCURATE_SEED_WGSL),this.module('accurate-direct-ds',G.DIRECT_DS_CORRECT_WGSL),this.module('accurate-ds-queue-build',G.DIRECT_DS_QUEUE_BUILD_WGSL),this.module('accurate-direct-ds-queued',G.DIRECT_DS_CORRECT_QUEUE_WGSL)]);[this.accurateDirectSeed,this.accurateDirectDs,this.accurateQueueBuild,this.accurateDirectDsQueued]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.accuratePipelinePromise}finally{this.accuratePipelinePromise=null}}
async ensureFastPipelines(){if(this.fast&&this.fastPostStats)return; if(this.fastPipelinePromise)return this.fastPipelinePromise;this.fastPipelinePromise=(async()=>{const [base,screen]=await Promise.all([this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('fast-perturb-post-stats',G.FAST_PERTURB_POSTSTATS_WGSL)]);[this.fast,this.fastPostStats]=await Promise.all([this.makeCompute(base),this.makeCompute(screen)])})();try{return await this.fastPipelinePromise}finally{this.fastPipelinePromise=null}}
async ensureDeepPipelines(){if(this.deep&&this.deepPostStats)return; if(this.deepPipelinePromise)return this.deepPipelinePromise;this.deepPipelinePromise=(async()=>{const [base,screen]=await Promise.all([this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL)]);[this.deep,this.deepPostStats]=await Promise.all([this.makeCompute(base),this.makeCompute(screen)])})();try{return await this.deepPipelinePromise}finally{this.deepPipelinePromise=null}}
async ensureCorrectionPipelines(){await this.ensureDeepPipelines();if(this.correct&&this.correctQueued&&this.deepBucketHist&&this.deepBucketPrefix&&this.deepBucketScatter&&this.failureTileMap)return; if(this.correctionPipelinePromise)return this.correctionPipelinePromise;this.correctionPipelinePromise=(async()=>{const modules=await Promise.all([this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('failure-tile-map',G.FAILURE_TILE_MAP_WGSL)]);[this.deepBucketHist,this.deepBucketPrefix,this.deepBucketScatter,this.correct,this.correctQueued,this.failureTileMap]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.correctionPipelinePromise}finally{this.correctionPipelinePromise=null}}
async ensureAaPipeline(){if(this.aaResolve)return;if(this.aaPipelinePromise)return this.aaPipelinePromise;this.aaPipelinePromise=(async()=>{this.aaResolve=await this.makeCompute(await this.module('aa-resolve',G.AA_RESOLVE_WGSL))})();try{return await this.aaPipelinePromise}finally{this.aaPipelinePromise=null}}
async ensureDeepActivePipelines(){if(this.activePrepare&&this.deepActiveInit&&this.deepActiveResumeInit&&this.deepActiveContinue)return; if(this.deepActivePipelinePromise)return this.deepActivePipelinePromise;this.deepActivePipelinePromise=(async()=>{const modules=await Promise.all([this.module('active-prepare-production',G.ACTIVE_PREPARE_WGSL),this.module('deep-active-init-production',G.DEEP_ACTIVE_INIT_WGSL),this.module('deep-active-resume-init-production',G.DEEP_ACTIVE_RESUME_INIT_WGSL),this.module('deep-active-continue-production',G.DEEP_ACTIVE_CONTINUE_WGSL)]);[this.activePrepare,this.deepActiveInit,this.deepActiveResumeInit,this.deepActiveContinue]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.deepActivePipelinePromise}finally{this.deepActivePipelinePromise=null}}
async ensureActiveShadowPipelines(){await this.ensureDeepActivePipelines();if(this.directActiveInit&&this.directActiveContinue&&this.activeCompare&&this.perturbCertifiedInterior)return; if(this.activeShadowPipelinePromise)return this.activeShadowPipelinePromise;this.activeShadowPipelinePromise=(async()=>{const modules=await Promise.all([this.module('direct-active-init-shadow',G.DIRECT_ACTIVE_INIT_WGSL),this.module('direct-active-continue-shadow',G.DIRECT_ACTIVE_CONTINUE_WGSL),this.module('active-compare-shadow',G.ACTIVE_COMPARE_WGSL),this.module('perturb-certified-interior-shadow',G.PERTURB_CERTIFIED_INTERIOR_WGSL)]);[this.directActiveInit,this.directActiveContinue,this.activeCompare,this.perturbCertifiedInterior]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.activeShadowPipelinePromise}finally{this.activeShadowPipelinePromise=null}}
clearBindGroupCache(){if(this.bindGroupCache)this.bindGroupCache.clear()}
bindResourceId(resource){const object=resource?.buffer||resource;if(!object||typeof object!=='object')return String(object);if(!this.bindResourceIds){this.bindResourceIds=new WeakMap();this.nextBindResourceId=1}let id=this.bindResourceIds.get(object);if(!id){id=this.nextBindResourceId++;this.bindResourceIds.set(object,id)}return id}
cachedTextureView(texture){if(!this.textureViews)this.textureViews=new WeakMap();let view=this.textureViews.get(texture);if(!view){view=texture.createView();this.textureViews.set(texture,view)}return view}
colorTarget(f=this.frame){if(!f)throw new Error('color target requested without a frame');if(f.back===f.history){const old=f.history;f.back=f.spare;f.spare=old}return f.back}
cachedBindGroup(pipeline,entries){if(!this.bindGroupCache)this.bindGroupCache=new Map();const key=this.bindResourceId(pipeline)+'|'+entries.map(e=>e.binding+':'+this.bindResourceId(e.resource)+':'+(e.resource?.offset||0)+':'+(e.resource?.size||0)).join('|');let bg=this.bindGroupCache.get(key);if(!bg){bg=this.device.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries});this.bindGroupCache.set(key,bg)}return bg}
frameDestroy(){if(!this.frame)return;this.clearBindGroupCache();for(const ring of Object.values(this.frame.readbackRings||{}))for(const b of ring.buffers)destroy(b);for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead','failureMap','failureParams','numericParams','statsParams','symmetryParams','colorParams','presentParams'])destroy(this.frame[k]);for(const t of new Set([this.frame.front,this.frame.back,this.frame.history,this.frame.spare]))destroy(t);this.frame=null;this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false}
2026-08-23 23:27:38 +09:00
ensureFrame(w,h){
2026-08-25 21:33:45 +09:00
const n=w*h;if(this.frame& & this.frame.w===w& & this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,colorUsage=T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,front=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'front-color'}),back=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'back-color'}),history=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'stable-history'});
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,deepActiveCapacity:0,deepActiveState:null,deepActiveQueueA:null,deepActiveQueueB:null,deepActiveCountA:null,deepActiveCountB:null,deepActiveIndirect:null,deepActiveRead:null,failureMap:null,failureMapBytes:0,readbackRings:{},failureParams:buf(d,32,B.UNIFORM|B.COPY_DST,'failure-map-params'),numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),statsParams:buf(d,16,B.UNIFORM|B.COPY_DST,'unknown-stats-params'),symmetryParams:buf(d,16,B.UNIFORM|B.COPY_DST,'symmetry-copy-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,48,B.UNIFORM|B.COPY_DST,'present-params'),front,back,history,spare:history};return this.frame;
2026-08-23 23:27:38 +09:00
}
2026-08-25 21:33:45 +09:00
frameReadback(kind,size){const f=this.frame;if(!f)throw new Error('frame readback requested without a frame');let ring=f.readbackRings[kind];if(!ring){const B=GPUBufferUsage;ring=f.readbackRings[kind]={cursor:0,buffers:Array.from({length:3},(_,i)=>buf(this.device,size,B.COPY_DST|B.MAP_READ,kind+'-readback-'+i))}}for(let tries=0;tries< ring.buffers.length ; tries + + ) { const i = ring.cursor++%ring.buffers.length,b=ring.buffers[i];if(!b.mapState||b.mapState==='unmapped')return b } throw new Error ( kind + ' readback ring is saturated ' ) }
2026-08-24 20:30:26 +09:00
ensureDeepBucketState(f=this.frame){if(!f)return null;if(!f.deepBucketState){const d=this.device,B=GPUBufferUsage;f.deepBucketState=buf(d,DEEP_BUCKET_STATE_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-bucket-state')}return f}
ensureDeepQueueWorkspace(f=this.frame){if(!f)return null;this.ensureDeepBucketState(f);if(f.deepQueue& & f.deepQueueStats& & f.deepIndirect)return f;const d=this.device,B=GPUBufferUsage;f.deepQueueStats=buf(d,SPARSE_QUEUE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue-stats');f.deepQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue');f.deepIndirect=buf(d,SPARSE_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'deep-correction-indirect');return f}
2026-08-25 21:33:45 +09:00
ensureDeepActiveWorkspace(capacity,f=this.frame){if(!f)return null;if(f.deepActiveCapacity>=capacity& & f.deepActiveState)return f;this.clearBindGroupCache();for(const k of ['deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead'])destroy(f[k]);const d=this.device,B=GPUBufferUsage;f.deepActiveCapacity=capacity;f.deepActiveState=buf(d,capacity*32,B.STORAGE|B.COPY_DST,'deep-active-state');f.deepActiveQueueA=buf(d,capacity*4,B.STORAGE|B.COPY_DST,'deep-active-queue-a');f.deepActiveQueueB=buf(d,capacity*4,B.STORAGE|B.COPY_DST,'deep-active-queue-b');f.deepActiveCountA=buf(d,4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-count-a');f.deepActiveCountB=buf(d,4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-count-b');f.deepActiveIndirect=buf(d,12,B.STORAGE|B.INDIRECT|B.COPY_DST,'deep-active-indirect');f.deepActiveRead=buf(d,4,B.COPY_DST|B.MAP_READ,'deep-active-count-readback');return f}
ensureFailureMap(bytes,f=this.frame){if(!f)return null;if(f.failureMap& & f.failureMapBytes>=bytes)return f;destroy(f.failureMap);const d=this.device,B=GPUBufferUsage;f.failureMap=buf(d,bytes,B.STORAGE|B.COPY_SRC|B.COPY_DST,'numerical-failure-tile-map');f.failureMapBytes=bytes;return f}
exportTileSize(){const l=this.device.limits||{},memory=Number(navigator.deviceMemory||8),storage=Number(l.maxStorageBufferBindingSize||0),maxBuffer=Number(l.maxBufferSize||0);return memory< =4||(storage& & storage< 2 * 1024 * 1024 ) | | ( maxBuffer & & maxBuffer < 8 * 1024 * 1024 ) ? 256:512 }
exportRingDepth(tile=this.exportTileSize()){const memory=Number(navigator.deviceMemory||8),limit=Number(this.device.limits?.maxBufferSize||0);return memory< =4||(limit& & limit< 32 * 1024 * 1024 ) | | tile < 512 ? 2:3 }
destroyExportWorkspace(ws){if(!ws)return;this.clearBindGroupCache();for(const k of ['meta','smooth','cbuf','tex','read'])destroy(ws[k]);for(const b of ws.unresolveds)destroy(b);for(const b of ws.pbufs)destroy(b);for(const t of ws.samples)destroy(t)}
ensureExportWorkspace(slot=0,S=this.exportTileSize()){
const old=this.exportWs[slot];if(old& & old.size===S)return old;if(old)this.destroyExportWorkspace(old);const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,n=S*S,bpr=S*4,pixelBytes=bpr*S,label='export-'+slot+'-';
const ws={slot,size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,label+'unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,label+'numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,label+'color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:label+'sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:label+'resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,label+'readback')};this.exportWs[slot]=ws;return ws;
2026-08-23 23:27:38 +09:00
}
2026-08-25 21:33:45 +09:00
exportWorkspaceDestroy(){for(const ws of this.exportWs)this.destroyExportWorkspace(ws);this.exportWs=[]}
2026-08-23 23:27:38 +09:00
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}}
2026-08-25 21:33:45 +09:00
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null;this.clearBindGroupCache()}
2026-08-24 20:30:26 +09:00
setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return; this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}}
2026-08-25 21:33:45 +09:00
destroyFastContext(){this.destroyBlaFastContext();if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null;this.clearBindGroupCache()}
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)})}
2026-08-25 21:33:45 +09:00
directActiveParams(w,h,iter,snap,targetIter=iter,chunkSize=128){const data=this.directParams(w,h,w,h,0,0,iter,snap);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(64,targetIter,true);d.setUint32(68,chunkSize,true)})}
accurateDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,outputStride=fullW,outputBase=0,lateThreshold=24,sx=.5,sy=.5,queueOffset=0,queueCount=0){const re=fixedNum(snap.re,snap.bits),im=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),inv=1/Math.max(1,fullW),reHi=Math.fround(re),imHi=Math.fround(im),spHi=Math.fround(sp),invHi=Math.fround(inv);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,lateThreshold,outputStride,outputBase,queueOffset,queueCount].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,reHi,true);d.setFloat32(52,Math.fround(re-reHi),true);d.setFloat32(56,imHi,true);d.setFloat32(60,Math.fround(im-imHi),true);d.setFloat32(64,spHi,true);d.setFloat32(68,Math.fround(sp-spHi),true);d.setFloat32(72,invHi,true);d.setFloat32(76,Math.fround(inv-invHi),true);d.setFloat32(80,sx,true);d.setFloat32(84,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,queueOffset=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true);d.setUint32(92,queueOffset,true)})}
deepActiveParams(w,h,iter,snap,refPixel,targetIter=iter,chunkSize=128,strict=0,selectionCount=0){const data=this.deepParams(w,h,w,h,0,0,iter,snap,.5,.5,strict,refPixel.x,refPixel.y,selectionCount,w,0);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(84,targetIter,true);d.setUint32(88,chunkSize,true)})}
deepActiveTileParams(w,h,fullW,fullH,tileX,tileY,iter,snap,refPixel,targetIter=iter,chunkSize=128,strict=0,outputStride=fullW,outputBase=0){const data=this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,strict,refPixel.x,refPixel.y,0,outputStride,outputBase);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(84,targetIter,true);d.setUint32(88,chunkSize,true)})}
certifiedInteriorParams(count,ctx,snap,width,refPixel){const se=spanMantExp(snap),hi=Math.fround(se.mant);return writeU32F32(48,d=>{d.setUint32(0,count,true);d.setUint32(4,ctx.refLen,true);d.setInt32(8,se.exp,true);d.setFloat32(16,hi,true);d.setFloat32(20,Math.fround(se.mant-hi),true);d.setFloat32(24,width,true);d.setFloat32(28,Math.fround(refPixel.x),true);d.setFloat32(32,Math.fround(refPixel.y),true)})}
2026-08-25 14:03:13 +09:00
fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0,unknownOnly=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
colorParamsData(w,h,_iter=state.fieldView?.iter??maxIter(),style=colorStyleSnapshot()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,style.palette,true);d.setUint32(12,style.hq?1:0,true);d.setFloat32(16,style.cycle,true);d.setFloat32(20,style.shift,true)})}
2026-08-25 21:33:45 +09:00
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const pipeline=this.deepPostStats,bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const bg=this.cachedBindGroup(this.direct,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeAccurateDirectSeed(encoder,{pbuf,meta,smooth,w,h}){const bg=this.cachedBindGroup(this.accurateDirectSeed,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]),pass=encoder.beginComputePass({label:'accurate-direct-seed'});pass.setPipeline(this.accurateDirectSeed);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeAccurateDirectDs(encoder,{pbuf,meta,smooth,w,h}){const bg=this.cachedBindGroup(this.accurateDirectDs,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]),pass=encoder.beginComputePass({label:'accurate-direct-ds'});pass.setPipeline(this.accurateDirectDs);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeAccurateQueueBuild(encoder,{pbuf,meta,queueStats,queue,w,h}){const bg=this.cachedBindGroup(this.accurateQueueBuild,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:queueStats}},{binding:3,resource:{buffer:queue}}]),pass=encoder.beginComputePass({label:'accurate-ds-queue-build'});pass.setPipeline(this.accurateQueueBuild);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeAccurateDirectDsQueued(encoder,{pbuf,meta,smooth,queueStats,queue,count}){const bg=this.cachedBindGroup(this.accurateDirectDsQueued,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]),pass=encoder.beginComputePass({label:'accurate-direct-ds-queued'});pass.setPipeline(this.accurateDirectDsQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(count/64));pass.end()}
encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const bg=this.cachedBindGroup(this.fast,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass();pass.setPipeline(this.fast);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeFastPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const bg=this.cachedBindGroup(this.fastPostStats,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]),pass=encoder.beginComputePass();pass.setPipeline(this.fastPostStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeUnknownStats(encoder,{meta,unresolved,n,params=this.frame?.statsParams}){const d=this.device;if(!params)throw new Error('unknown stats params are missing');d.queue.writeBuffer(params,0,new Uint32Array([n,0,0,0]));encoder.clearBuffer(unresolved,0,28);const bg=this.cachedBindGroup(this.unknownStats,[{binding:0,resource:{buffer:params}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass({label:'unknown-post-stats'});pass.setPipeline(this.unknownStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(n/256));pass.end()}
encodeSymmetryCopy(encoder,{meta,smooth,w,h,params=this.frame?.symmetryParams}){if(h< 2 ) return ; const d = this.device;if(!params)throw new Error ( ' symmetry params are missing ' ) ; d . queue . writeBuffer ( params , 0 , new Uint32Array ( [ w , h , 0 , 0 ] ) ) ; const bg = this.cachedBindGroup(this.symmetryCopy,[{binding:0,resource:{buffer:params}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]),pass=encoder.beginComputePass({label:'real-axis-symmetry-copy'});pass.setPipeline(this.symmetryCopy);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(Math.floor(h/2)/8));pass.end()}
encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const bg=this.cachedBindGroup(this.deepBucketHist,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHist);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const bg=this.cachedBindGroup(this.deepBucketHistStats,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
encodeDeepBucketPrefix(encoder,{bucketState,queueStats,indirect}){const bg=this.cachedBindGroup(this.deepBucketPrefix,[{binding:0,resource:{buffer:bucketState}},{binding:1,resource:{buffer:queueStats}},{binding:2,resource:{buffer:indirect}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketPrefix);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end()}
encodeDeepBucketScatter(encoder,{pbuf,meta,bucketState,queueStats,queue,w,h}){const bg=this.cachedBindGroup(this.deepBucketScatter,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
encodeQueuedDeepCorrection(encoder,{pbuf,meta,smooth,unresolved,queueStats,queue,indirect}){const bg=this.cachedBindGroup(this.correctQueued,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{binding:5,resource:{buffer:queueStats}},{binding:6,resource:{buffer:queue}}]),pass=encoder.beginComputePass();pass.setPipeline(this.correctQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()}
2026-08-23 23:27:38 +09:00
encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
2026-08-25 21:33:45 +09:00
numericInFlightBudgetMs(){return state.pointerActive||state.wheelActive||performance.now()-state.lastInteraction< 180 ? 24:40 }
numericBurstLimit(predictedStripMs){return Math.max(1,Math.min(8,Math.floor(this.numericInFlightBudgetMs()/Math.max(1,Number(predictedStripMs)||20))))}
2026-08-25 14:03:13 +09:00
initialNumericRows(w,iter,deep){
// Structural workload cap, not a zoom threshold. The first submission is
// intentionally conservative; subsequent strips self-tune from measured
// queue completion time. This keeps one GPU submission bounded even when
// maxIter reaches 12000.
2026-08-25 21:33:45 +09:00
const workBudget=deep?4000000:48000000;
2026-08-25 14:03:13 +09:00
return Math.max(1,Math.min(128,Math.floor(workBudget/Math.max(1,w*iter))));
}
adaptNumericRows(rows,elapsedMs,remaining,deep){
if(remaining< =0)return rows;
// Never grow beyond the initial work-bound. Timing feedback may only
// reduce a strip if this adapter is slower than expected, so the
// worst-case pixel-iteration cap remains invariant across the frame.
const targetMs=deep?18:22,ms=Math.max(.25,Number(elapsedMs)||targetMs);
if(ms< =targetMs*1.25)return Math.max(1,Math.min(rows,remaining));
const factor=Math.max(.5,Math.min(1,Math.sqrt(targetMs/ms)));
return Math.max(1,Math.min(rows,remaining,Math.round(rows*factor)));
}
async submitStage(encoder,stage,token,counter='numeric'){
this.activeGpuStage=stage;state.gpuStage=stage;
const t0=performance.now();this.device.queue.submit([encoder.finish()]);
await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++;
const ms=performance.now()-t0;this.lastCompletedGpuStage=stage;this.activeGpuStage='';
2026-08-25 21:33:45 +09:00
if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++;const stageKey=String(stage).replace(/\d+/g,'#');runtime.stageCounts[stageKey]=(runtime.stageCounts[stageKey]||0)+1;runtime.stageMs[stageKey]=(runtime.stageMs[stageKey]||0)+ms;
2026-08-25 14:03:13 +09:00
return token===undefined||token===state.token?ms:null;
}
submitStageDeferred(encoder,stage,counter='numeric'){this.activeGpuStage=stage;state.gpuStage=stage;this.device.queue.submit([encoder.finish()]);if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++;return stage}
async waitStageBurst(stage,token,t0,count=1){await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++;const ms=(performance.now()-t0)/Math.max(1,count);this.lastCompletedGpuStage=stage;this.activeGpuStage='';return token===undefined||token===state.token?ms:null}
2026-08-25 21:33:45 +09:00
deepActiveBandRows(w,h){const binding=Number(this.adapterLimits.maxStorageBufferBindingSize||64*1024*1024),buffer=Number(this.adapterLimits.maxBufferSize||binding),bytes=Math.max(32,Math.min(64*1024*1024,binding,buffer));return Math.max(1,Math.min(h,Math.floor(bytes/Math.max(32,w*32))))}
encodeDeepActiveInit(e,{pbuf,stateB,queue,count,w,h}){const d=this.device,bg=this.cachedBindGroup(this.deepActiveInit,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:stateB}},{binding:2,resource:{buffer:queue}},{binding:3,resource:{buffer:count}}]),pass=e.beginComputePass({label:'deep-active-init-production'});pass.setPipeline(this.deepActiveInit);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDeepActiveResumeInit(e,{pbuf,stateB,queue,count}){const d=this.device,bg=this.cachedBindGroup(this.deepActiveResumeInit,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:stateB}},{binding:2,resource:{buffer:queue}}]),pass=e.beginComputePass({label:'deep-active-resume-init-production'});pass.setPipeline(this.deepActiveResumeInit);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(count/64));pass.end()}
async readDeepActiveCount(buffer){await buffer.mapAsync(GPUMapMode.READ,0,4);const n=new Uint32Array(buffer.getMappedRange(0,4))[0]||0;buffer.unmap();return n}
async beginOperationLimitContinuation(snap,token,ctx,refPixel,indices,strict=false){
await this.ready;await this.ensureDeepActivePipelines();if(!ctx)throw new Error('active continuation reference context is missing');const f=this.ensureFrame(canvas.width,canvas.height),limit=Math.min(Number(this.adapterLimits.maxStorageBufferBindingSize||Infinity),Number(this.adapterLimits.maxBufferSize||Infinity)),stateBytes=f.n*32;if(stateBytes>limit)throw new Error('active continuation state exceeds adapter buffer limit: '+stateBytes+' > '+limit);if(indices.length>f.n)throw new Error('active continuation candidate overflow');this.setDeepContext(ctx);this.ensureDeepActiveWorkspace(f.n,f);const d=this.device,queues=[f.deepActiveQueueA,f.deepActiveQueueB],counts=[f.deepActiveCountA,f.deepActiveCountB],count=indices.length;d.queue.writeBuffer(queues[0],0,indices);d.queue.writeBuffer(counts[0],0,new Uint32Array([count]));d.queue.writeBuffer(counts[1],0,new Uint32Array([0]));d.queue.writeBuffer(f.numericParams,0,this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER+1,snap,refPixel,0,1,strict?1:0,count));const e=d.createCommandEncoder({label:'all-pixel-active-init'});this.encodeDeepActiveResumeInit(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queue:queues[0],count});const ms=await this.submitStage(e,'all-pixel active init '+count,token,'correction');if(ms===null)return null;return{f,queues,counts,input:0,active:count,initial:count,progress:0,maxDispatchWork:0,dispatches:0,pixelIterations:0,strict:!!strict};
}
async continueOperationLimitActive(session,snap,target,token,ctx,refPixel){
if(!session||!ctx)throw new Error('active continuation session/context is missing');this.setDeepContext(ctx);const {f,queues,counts}=session,d=this.device;while(session.active>0& & session.progress< target ) { if ( token ! = = state . token ) return null ; const remaining = target-session.progress,chunk=Math.max(1,Math.min(256,remaining,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,session.active))));d.queue.writeBuffer(f.numericParams,0,this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER+1,snap,refPixel,target,chunk,session.strict?1:0));const e = d.createCommandEncoder({label:'all-pixel-active-'+session.progress+'-'+target}),before=session.active;session.input=this.encodeDeepActiveShadowChunks(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queues,counts,indirect:f.deepActiveIndirect,meta:f.meta,smooth:f.smooth,input:session.input,passes:1});e.copyBufferToBuffer(counts[session.input],0,f.deepActiveRead,0,4);const ms = await this . submitStage ( e , ' all-pixel active ' + ( session . progress + chunk ) + ' / ' + target + ' · ' + before + ' px ' , token , ' correction ' ) ; if ( ms = ==null)return null ; session . active = await this . readDeepActiveCount ( f . deepActiveRead ) ; const work = before*chunk;session.progress+=chunk;session.dispatches++;session.pixelIterations+=work;session.maxDispatchWork=Math.max(session.maxDispatchWork,work);runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,work)}const statsEncoder = d.createCommandEncoder({label:'all-pixel-active-post-stats'});this.encodeUnknownStats(statsEncoder,{meta:f.meta,unresolved:f.unresolved,n:f.n});const statsMs = await this . submitStage ( statsEncoder , ' all-pixel active post-stats ' , token , ' correction ' ) ; return statsMs = ==null?null:session;
}
async computeDeepActiveFrame(snap,iter,token,forceStrict,referencePixel){
const f=this.ensureFrame(canvas.width,canvas.height),d=this.device,mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h,bandRows=this.deepActiveBandRows(f.w,computeH),workPerDispatch=PIXEL_FRONTIER_WORK,maxPassesPerSubmit=1,refPixel=referencePixel||{x:f.w*.5,y:f.h*.5};
this.ensureDeepActiveWorkspace(f.w*bandRows,f);let lastUi=0;
for(let y=0;y< computeH ; y + = bandRows ) {
if(token!==state.token)return false;const th=Math.min(bandRows,computeH-y),capacity=f.w*th,queues=[f.deepActiveQueueA,f.deepActiveQueueB],counts=[f.deepActiveCountA,f.deepActiveCountB];let input=0,active=capacity,progress=0;
let chunk=Math.max(1,Math.min(256,Math.floor(workPerDispatch/Math.max(1,active))));runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,active*chunk);d.queue.writeBuffer(f.numericParams,0,this.deepActiveTileParams(f.w,th,f.w,f.h,0,y,iter,snap,refPixel,iter,chunk,forceStrict?1:0,f.w,y*f.w));
let e=d.createCommandEncoder({label:'deep-active-band-init-'+y});if(y===0){e.clearBuffer(f.meta);e.clearBuffer(f.smooth)}e.clearBuffer(counts[0]);e.clearBuffer(counts[1]);this.encodeDeepActiveInit(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queue:queues[0],count:counts[0],w:f.w,h:th});let passes=Math.min(maxPassesPerSubmit,Math.ceil((iter-progress)/chunk));input=this.encodeDeepActiveShadowChunks(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queues,counts,indirect:f.deepActiveIndirect,meta:f.meta,smooth:f.smooth,input,passes});progress=Math.min(iter,progress+passes*chunk);e.copyBufferToBuffer(counts[input],0,f.deepActiveRead,0,4);let ms=await this.submitStage(e,'deep active rows '+y+'..'+(y+th-1)+' iter '+progress+'/'+iter,token,'numeric');if(ms===null)return false;active=await this.readDeepActiveCount(f.deepActiveRead);
while(active>0& & progress< iter ) {
if(token!==state.token)return false;chunk=Math.max(1,Math.min(256,Math.floor(workPerDispatch/Math.max(1,active)),iter-progress));passes=Math.min(maxPassesPerSubmit,Math.ceil((iter-progress)/chunk));runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,active*chunk);d.queue.writeBuffer(f.numericParams,0,this.deepActiveTileParams(f.w,th,f.w,f.h,0,y,iter,snap,refPixel,iter,chunk,forceStrict?1:0,f.w,y*f.w));e=d.createCommandEncoder({label:'deep-active-band-'+y+'-'+progress});input=this.encodeDeepActiveShadowChunks(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queues,counts,indirect:f.deepActiveIndirect,meta:f.meta,smooth:f.smooth,input,passes});progress=Math.min(iter,progress+passes*chunk);e.copyBufferToBuffer(counts[input],0,f.deepActiveRead,0,4);ms=await this.submitStage(e,'deep active rows '+y+'..'+(y+th-1)+' iter '+progress+'/'+iter,token,'numeric');if(ms===null)return false;active=await this.readDeepActiveCount(f.deepActiveRead);const now=performance.now();if(now-lastUi>100){lastUi=now;state.lastEngine='WebGPU · 正確 分割計算 '+Math.round(100*(y+th*progress/Math.max(1,iter))/computeH)+'%';state.gpuTileRows=th;updateStats()}}
}
const e=d.createCommandEncoder({label:'deep-active-post-stats'});if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms=await this.submitStage(e,'deep active UNKNOWN post-stats',token,'numeric');if(ms===null)return false;runtime.gpuFrames++;return true
}
2026-08-25 14:03:13 +09:00
async computePerturbFrameTiled(snap,iter,deep,token,forceStrict,referencePixel,fastExtended,colorStyle,colorKey,blaPlan=null){
const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;
d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-08-25 21:33:45 +09:00
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,useBla=!deep&&!!blaPlan?.enabled&&!!this.blaFastFramePipeline&&!!this.blaFastCtx,mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h,previous=state.fieldView,currentReferenceKey=(deep?this.deepCtx:this.fastCtx)?.key||'',reusePreview=!useBla&&!!previous&&sameViewSnapshot(previous,snap)&&previous.w===f.w&&previous.h===f.h&&previous.iter<iter&&previous.referenceKey===currentReferenceKey&&previous.deep===deep&&previous.fastExtended===fastExtended;
let rows=this.initialNumericRows(f.w,iter,deep),lastUi=0,burstCount=0,burstStart=performance.now(),lastStage='',predictedStripMs=deep?18:22,burstLimit=this.numericBurstLimit(predictedStripMs),frameNumericMs=0;
2026-08-25 14:03:13 +09:00
// When the distributed probe reports a very clean BLA view, allow only a
// modest 2x strip growth. Even a pathological local fallback therefore
// stays far below the old unbounded full-frame dispatch.
if(useBla&&blaPlan.operationReduction>=.75&&blaPlan.fallbackRate<=.0625)rows=Math.min(256,rows*2);
2026-08-25 21:33:45 +09:00
for(let y=0;y< computeH ; ) {
2026-08-25 14:03:13 +09:00
if(token!==state.token)return false;
2026-08-25 21:33:45 +09:00
const th=Math.min(rows,computeH-y),base=y*f.w;
const params=useBla?this.blaFastFrameParams(f.w,th,0,y,iter,snap,{x:refX,y:refY},f.w,base,blaPlan.safety||1/64):(deep?this.deepParams(f.w,th,f.w,f.h,0,y,iter,snap,.5,.5,forceStrict?1:0,refX,refY,reusePreview?1:0,f.w,base):this.fastParams(f.w,th,f.w,f.h,0,y,iter,snap,.5,.5,refX,refY,f.w,base,reusePreview?1:0));
2026-08-25 14:03:13 +09:00
d.queue.writeBuffer(f.numericParams,0,params);
const e=d.createCommandEncoder({label:(deep?'deep':useBla?'bla-fast':'fast')+'-numeric-strip-'+y});
// A fresh numeric frame starts from an explicit zero/UNKNOWN sentinel.
// If any strip or pixel is accidentally left untouched, the coverage
// repair below can detect and recompute it instead of publishing stale
// metadata or a rectangular transparent/black hole.
2026-08-25 21:33:45 +09:00
if(y===0&&!reusePreview){e.clearBuffer(f.meta); e.clearBuffer(f.smooth);}
if(deep)this.encodeDeepPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:th});
2026-08-25 14:03:13 +09:00
else if(useBla)this.encodeBlaFastNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:th});
2026-08-25 21:33:45 +09:00
else this.encodeFastPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:th});
2026-08-25 14:03:13 +09:00
lastStage=(deep?'deep':useBla?'bla-fast':'fast')+' primary rows '+y+'..'+(y+th-1);
this.submitStageDeferred(e,lastStage,'numeric');
y+=th;burstCount++;state.gpuTileRows=th;
2026-08-25 21:33:45 +09:00
if(burstCount>=burstLimit||y>=computeH){
const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;frameNumericMs+=ms*burstCount;
rows=this.adaptNumericRows(rows,ms,computeH-y,deep);predictedStripMs=.65*predictedStripMs+.35*ms;burstLimit=this.numericBurstLimit(predictedStripMs);burstCount=0;burstStart=performance.now();
const now=performance.now();if(now-lastUi>100){lastUi=now;state.lastEngine='WebGPU · '+(deep?'正確':useBla?'高速BLA':'高速拡張')+' 計算 '+Math.round(100*y/computeH)+'%';updateStats()}
2026-08-25 14:03:13 +09:00
}
}
2026-08-25 21:33:45 +09:00
if(deep){
const e=d.createCommandEncoder({label:'deep-unknown-post-stats'});if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
const ms=await this.submitStage(e,'deep UNKNOWN post-stats',token,'numeric');if(ms===null)return false;
}else{
2026-08-25 14:03:13 +09:00
// Coverage contract: zero metadata means a pixel was never written by
// the tiled primary path. Re-run only those pixels through the original
// FAST recurrence before colouring. This pass is O(pixels) for the
// trivial guard but performs iterations only for actual coverage holes.
d.queue.writeBuffer(f.numericParams,0,this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY,f.w,0,2));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));
const e=d.createCommandEncoder({label:'fast-coverage-repair-and-color'});
2026-08-25 21:33:45 +09:00
if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});
2026-08-25 14:03:13 +09:00
this.encodeFastNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});
2026-08-25 21:33:45 +09:00
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),cp=e.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
const ms=await this.submitStage(e,'fast coverage + color',token,'numeric');if(ms===null)return false;frameNumericMs+=ms;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;
2026-08-25 14:03:13 +09:00
}
2026-08-25 21:33:45 +09:00
if(!deep){if(useBla)runtime.blaFastEnabledFrames++;else{runtime.blaFastBaselineFrames++;this.fastBaselineGpuMs=frameNumericMs;}}
2026-08-25 14:03:13 +09:00
runtime.gpuFrames++;return true;
2026-08-23 23:27:38 +09:00
}
2026-08-25 14:03:13 +09:00
async computeAccurateDirectHybrid(snap,iter,token,colorStyle,colorKey){
2026-08-25 21:33:45 +09:00
const f=this.ensureFrame(canvas.width,canvas.height),d=this.device,mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h;
2026-08-25 14:03:13 +09:00
// Pass 1: cheap f32 seed. Analytically proven main-cardioid/period-2 pixels
// are final, and only very early escapes (< 24 ) are trusted . Everything
// else remains UNKNOWN for the DS pass below.
2026-08-25 21:33:45 +09:00
d.queue.writeBuffer(f.numericParams,0,this.accurateDirectParams(f.w,computeH,f.w,f.h,0,0,iter,snap,f.w,0,24));
let e=d.createCommandEncoder({label:'accurate-direct-seed-frame'});this.encodeAccurateDirectSeed(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:computeH});
2026-08-25 14:03:13 +09:00
this.submitStageDeferred(e,'accurate direct seed','numeric');
2026-08-25 21:33:45 +09:00
// Pass 2: compact only UNKNOWN indices, then run bounded queue slices. The
// original full-screen guarded DS scan remains the corruption fallback.
const qf=this.ensureDeepQueueWorkspace(f);e=d.createCommandEncoder({label:'accurate-ds-queue-build'});e.clearBuffer(qf.deepQueueStats);this.encodeAccurateQueueBuild(e,{pbuf:f.numericParams,meta:f.meta,queueStats:qf.deepQueueStats,queue:qf.deepQueue,w:f.w,h:computeH});let qms=await this.submitStage(e,'accurate DS queue build',token,'numeric');if(qms===null)return false;let q=await this.readDeepQueueStats();if(token!==state.token)return false;let queueValid=q.overflow===0&&q.selected===q.enqueued&&q.invalidIndex===0&&q.staleEntry===0;
if(queueValid& & q.selected>0){d.queue.writeBuffer(qf.deepQueueStats,12,new Uint32Array([q.selected]));let offset=0,batch=Math.max(64,Math.min(32768,Math.floor(4000000/Math.max(1,iter)))),lastUi=0;while(offset< q.selected ) { if ( token ! = = state . token ) return false ; const count = Math.min(batch,q.selected-offset);d.queue.writeBuffer(f.numericParams,0,this.accurateDirectParams(f.w,computeH,f.w,f.h,0,0,iter,snap,f.w,0,24,.5,.5,offset,count));e=d.createCommandEncoder({label:'accurate-ds-queue-'+offset});this.encodeAccurateDirectDsQueued(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,queueStats:qf.deepQueueStats,queue:qf.deepQueue,count});const ms = await this . submitStage ( e , ' accurate DS queue ' + offset + ' . . ' + ( offset + count-1 ) , token , ' numeric ' ) ; if ( ms = ==null)return false ; offset + = count ; const target = 18,factor=Math.max(.5,Math.min(1.5,target/Math.max(.25,ms)));batch=Math.max(64,Math.min(32768,Math.round(batch*factor)));const now = performance.now();if(now-lastUi > 100){lastUi=now;state.lastEngine='WebGPU · 正確DS queue '+Math.round(100*offset/q.selected)+'%';updateStats()}}q=await this.readDeepQueueStats();if(token!==state.token)return false;queueValid=q.selected===q.enqueued&&q.enqueued===q.dispatchCount&&q.dispatchCount===q.processed&&q.overflow===0&&q.invalidIndex===0&&q.staleEntry===0; runtime.accurateQueuedPixels=(runtime.accurateQueuedPixels||0)+q.processed}
if(!queueValid){runtime.accurateQueueFallbacks=(runtime.accurateQueueFallbacks||0)+1;let rows=this.initialNumericRows(f.w,iter,true),y=0;while(y< computeH ) { if ( token ! = = state . token ) return false ; const th = Math.min(rows,computeH-y),base=y*f.w;d.queue.writeBuffer(f.numericParams,0,this.accurateDirectParams(f.w,th,f.w,f.h,0,y,iter,snap,f.w,base,24));e=d.createCommandEncoder({label:'accurate-ds-fallback-strip-'+y});this.encodeAccurateDirectDs(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:th});const ms = await this . submitStage ( e , ' accurate DS fallback rows ' + y + ' . . ' + ( y + th-1 ) , token , ' numeric ' ) ; if ( ms = ==null)return false ; y + = th ; rows = this.adaptNumericRows(rows,ms,computeH-y,true)}}
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));e=d.createCommandEncoder({label:'accurate-direct-stats-and-color'});if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),cp=e.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();const cms=await this.submitStage(e,'accurate direct stats + color',token,'numeric');if(cms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuFrames++;return true;
2026-08-25 14:03:13 +09:00
}
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null,blaPlan=null,accurateHybrid=false){
2026-08-25 21:33:45 +09:00
await this.ready;if(accurateHybrid)await this.ensureAccuratePipelines();else if(deep)await this.ensureDeepPipelines();else if(fastExtended)await this.ensureFastPipelines();const f=this.ensureFrame(canvas.width,canvas.height),d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);
2026-08-25 14:03:13 +09:00
if(accurateHybrid)return this.computeAccurateDirectHybrid(snap,iter,token,colorStyle,colorKey);
if(deep){this.setDeepContext(deepContext);return this.computePerturbFrameTiled(snap,iter,true,token,forceStrict,referencePixel,false,colorStyle,colorKey)}
if(fastExtended){this.setFastContext(fastContext);return this.computePerturbFrameTiled(snap,iter,false,token,false,referencePixel,true,colorStyle,colorKey,blaPlan)}
// Direct f32 remains the exact v24.2.42 single-pass path. It is only used
// while absolute f32 pixel coordinates retain sufficient separation.
d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-08-25 21:33:45 +09:00
const mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h;d.queue.writeBuffer(f.numericParams,0,this.directParams(f.w,computeH,f.w,f.h,0,0,iter,snap));
2026-08-25 14:03:13 +09:00
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const encoder=d.createCommandEncoder({label:'mandelbrot-direct-frame'});
2026-08-25 21:33:45 +09:00
this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:computeH});
if(mirror)this.encodeSymmetryCopy(encoder,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});
this.encodeUnknownStats(encoder,{meta:f.meta,unresolved:f.unresolved,n:f.n});
const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
2026-08-25 14:03:13 +09:00
const ms=await this.submitStage(encoder,'direct primary',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuFrames++;return true;
}
2026-08-25 21:33:45 +09:00
async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder(),bg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuRecolors++;return true}
2026-08-23 23:27:38 +09:00
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
2026-08-25 21:33:45 +09:00
commitHistory(view=state.frameView){if(!this.frame||!view)return false;const f=this.frame;if(f.history!==f.front){f.history=f.front;this.clearBindGroupCache()}this.historyView=view;this.historyColorKey=this.frontColorKey;this.historyReady=true;const metric=generationMetric();if(metric&&!metric.historyCommitMs)metric.historyCommitMs=performance.now()-metric.startedAt; return true}
2026-08-25 14:03:13 +09:00
invalidateHistory(){this.historyView=null;this.historyColorKey='';this.historyReady=false}
2026-08-25 21:33:45 +09:00
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const f=this.frame,d=this.device,pb=f.presentParams,interactive=state.pointerActive||state.wheelActive,finite=Number.isFinite(transform?.scaleX)&&Number.isFinite(transform?.scaleY)&&Number.isFinite(transform?.offsetX)&&Number.isFinite(transform?.offsetY)&&transform.scaleX>0&&transform.scaleY>0,ft=finite?transform:{scaleX:1,scaleY:1,offsetX:0,offsetY:0},historyCandidate=!!(state.temporalFill&&this.historyReady&&this.historyView&&this.historyColorKey===colorStyleKey()),rawHistory=historyCandidate?this.presentTransform(this.historyView):{scaleX:1,scaleY:1,offsetX:0,offsetY:0},useHistory=!!(historyCandidate&&reprojectionSafe(rawHistory,2.25,.45)),ht=useHistory?rawHistory:{scaleX:1,scaleY:1,offsetX:0,offsetY:0}; d.queue.writeBuffer(pb,0,new Float32Array([ft.scaleX,ft.scaleY,ft.offsetX,ft.offsetY,ht.scaleX,ht.scaleY,ht.offsetX,ht.offsetY,useHistory?1:0,0,0,0]));const bg=this.cachedBindGroup(this.present,[{binding:0,resource:interactive?this.reprojectSampler:this.sampler},{binding:1,resource:this.cachedTextureView(f.front)},{binding:2,resource:this.cachedTextureView(f.history)},{binding:3,resource:{buffer:pb}}]),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:0,g:0,b:0,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
2026-08-23 23:27:38 +09:00
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i< indices.length ; i + + ) e . copyBufferToBuffer ( this . frame . meta , indices [ i ] * 4 , r , i * 4 , 4 ) ; d . queue . submit ( [ e . finish ( ) ] ) ; await r . mapAsync ( GPUMapMode . READ ) ; const out = new Uint32Array ( r . getMappedRange ( ) . slice ( 0 ) ) ; r . unmap ( ) ; destroy ( r ) ; return out }
2026-08-25 14:03:13 +09:00
async readMetaAll(){if(!this.frame)return new Uint32Array();const f=this.frame,d=this.device,B=GPUBufferUsage,r=buf(d,f.n*4,B.COPY_DST|B.MAP_READ,'field-meta-readback'),e=d.createCommandEncoder({label:'field-meta-readback'});e.copyBufferToBuffer(f.meta,0,r,0,f.n*4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
2026-08-25 21:33:45 +09:00
async readFieldAll(){if(!this.frame)return{meta:new Uint32Array(),smoothBits:new Uint32Array()};const f=this.frame,d=this.device,B=GPUBufferUsage,bytes=f.n*4,r=buf(d,bytes*2,B.COPY_DST|B.MAP_READ,'field-meta-smooth-readback'),e=d.createCommandEncoder({label:'field-meta-smooth-readback'});e.copyBufferToBuffer(f.meta,0,r,0,bytes);e.copyBufferToBuffer(f.smooth,0,r,bytes,bytes);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const raw=r.getMappedRange(),meta=new Uint32Array(raw.slice(0,bytes)),smoothBits=new Uint32Array(raw.slice(bytes,bytes*2));r.unmap();destroy(r);return{meta,smoothBits}}
async readNumericalFailureTiles(size){await this.ready;await this.ensureCorrectionPipelines();if(!this.frame)return[];const f=this.frame,d=this.device,B=GPUBufferUsage,cols=Math.ceil(f.w/size),rows=Math.ceil(f.h/size),stride=6,bytes=cols*rows*stride*4;this.ensureFailureMap(bytes,f);d.queue.writeBuffer(f.failureParams,0,new Uint32Array([f.w,f.h,size,cols,rows,0,0,0]));const read=buf(d,bytes,B.COPY_DST|B.MAP_READ,'failure-map-readback'),e=d.createCommandEncoder({label:'failure-tile-map'}),bg=d.createBindGroup({layout:this.failureTileMap.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.failureParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.failureMap}}]}),pass=e.beginComputePass({label:'failure-tile-map'});pass.setPipeline(this.failureTileMap);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(cols,rows);pass.end();e.copyBufferToBuffer(f.failureMap,0,read,0,bytes);d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();await read.mapAsync(GPUMapMode.READ);const raw=new Uint32Array(read.getMappedRange().slice(0));read.unmap();destroy(read);runtime.failureMapReadBytes=(runtime.failureMapReadBytes||0)+bytes;const tiles=[];for(let ty=0;ty< rows ; ty + + ) for ( let tx = 0;tx<cols;tx++){const o = (ty*cols+tx)*stride,reasons={errorBound:raw[o]||0,escapeUncertain:raw[o+1]||0,referenceEnd:raw[o+2]||0,rebaseGap:raw[o+3]||0,range:raw[o+4]||0},numeric=reasons.errorBound+reasons.escapeUncertain+reasons.referenceEnd+reasons.rebaseGap+reasons.range;if(!numeric)continue;const x = tx*size,y=ty*size,w=Math.min(size,f.w-x),h=Math.min(size,f.h-y),representative=raw[o+5],cx=representative<f.n?representative%f.w+.5:x+w*.5,cy=representative<f.n?Math.floor(representative/f.w)+.5:y+h*.5;tiles.push({x,y,w,h,numeric,sumX:cx*numeric,sumY:cy*numeric,minX:x,minY:y,maxX:x+w-1,maxY:y+h-1,representative,reasons})}return tiles . sort ( ( a , b ) = > b.numeric-a.numeric)}
async applyPrecisionFallback(result,token){if(!this.frame||!result)return null;const f=this.frame,d=this.device,{indices,meta,smooth,accepted}=result;let applied=0,start=-1;const flush=end=>{if(start< 0 | | end < = start ) return ; const pixel = indices[start];d.queue.writeBuffer(f.meta,pixel*4,meta.subarray(start,end));d.queue.writeBuffer(f.smooth,pixel*4,smooth.subarray(start,end));applied+=end-start;start=-1};for(let i = 0;i<indices.length;i++){if(!accepted[i]){flush(i);continue}if(start<0)start=i;else if ( indices [ i ] ! = = indices [ i-1 ] + 1 ) { flush ( i ) ; start = i}}flush(indices.length);if(token!==state.token)return null ; const e = d.createCommandEncoder({label:'precision-fallback-post-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms = await this . submitStage ( e , ' precision fallback post-stats ' , token , ' correction ' ) ; return ms = ==null?null:{applied,ms}}
2026-08-25 14:03:13 +09:00
async recoverUnknownTile(snap,iter,token,deepContext,tile,referencePixel=null){
2026-08-25 21:33:45 +09:00
await this.ready;await this.ensureCorrectionPipelines();const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!deepContext)throw new Error('tile deep reference context is missing');this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-08-25 14:03:13 +09:00
const refX=referencePixel?.x??(tile.x+tile.w*.5),refY=referencePixel?.y??(tile.y+tile.h*.5);let rows=this.initialNumericRows(tile.w,iter,true);
2026-08-25 21:33:45 +09:00
for(let sy=0;sy< tile.h ; ) { if ( token ! = = state . token ) return null ; const th = Math.min(rows,tile.h-sy),gy=tile.y+sy,base=gy*f.w+tile.x;d.queue.writeBuffer(f.numericParams,0,this.deepParams(tile.w,th,f.w,f.h,tile.x,gy,iter,snap,.5,.5,1,refX,refY,2,f.w,base));const e = d.createCommandEncoder({label:'tile-multireference-recovery-'+gy});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:tile.w,h:th});const ms = await this . submitStage ( e , ' multi-ref correction rows ' + gy + ' . . ' + ( gy + th-1 ) , token , ' correction ' ) ; if ( ms = ==null)return null ; sy + = th ; rows = this.adaptNumericRows(rows,ms,tile.h-sy,true)}const statsEncoder = d.createCommandEncoder({label:'tile-recovery-post-stats'});this.encodeUnknownStats(statsEncoder,{meta:f.meta,unresolved:f.unresolved,n:f.n});const statsMs = await this . submitStage ( statsEncoder , ' tile recovery post-stats ' , token , ' correction ' ) ; if ( statsMs = ==null)return null ; runtime . correctionPasses + + ; runtime . multiRefBuilds + + ; return this . readUnresolvedStats ( )
2026-08-25 14:03:13 +09:00
}
2026-08-25 21:33:45 +09:00
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,r=this.frameReadback('unknown-stats',UNRESOLVED_BYTES),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
2026-08-23 23:27:38 +09:00
async readUnresolved(){return(await this.readUnresolvedStats()).total}
2026-08-25 21:33:45 +09:00
async readDeepQueueStats(){if(!this.frame?.deepQueueStats)return{selected:0,overflow:0,enqueued:0,dispatchCount:0,invalidIndex:0,staleEntry:0,processed:0};const d=this.device,r=this.frameReadback('deep-queue-stats',SPARSE_QUEUE_STATS_BYTES),e=d.createCommandEncoder({label:'deep-queue-stats-readback'});e.copyBufferToBuffer(this.frame.deepQueueStats,0,r,0,SPARSE_QUEUE_STATS_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return{selected:a[0]||0,overflow:a[1]||0,enqueued:a[2]||0,dispatchCount:a[3]||0,invalidIndex:a[4]||0,staleEntry:a[5]||0,processed:a[6]||0}}
async correctUnknownFrame(snap,iter,token,referencePixel=null,numericCount=0){
await this.ready;await this.ensureCorrectionPipelines();const f=this.ensureDeepQueueWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
const 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,2,f.w,0));
let e=d.createCommandEncoder({label:'deep-sparse-queue-build'});e.clearBuffer(f.deepBucketState);e.clearBuffer(f.deepQueueStats);e.clearBuffer(f.deepIndirect);
this.encodeDeepBucketHistogram(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,w:f.w,h:f.h});
this.encodeDeepBucketPrefix(e,{bucketState:f.deepBucketState,queueStats:f.deepQueueStats,indirect:f.deepIndirect});
this.encodeDeepBucketScatter(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,queueStats:f.deepQueueStats,queue:f.deepQueue,w:f.w,h:f.h});
let totalMs=await this.submitStage(e,'queued DS build',token,'correction');if(totalMs===null)return false;
const built=await this.readDeepQueueStats();if(token!==state.token)return false;
const buildValid=built.selected===built.enqueued&&built.overflow===0&&built.invalidIndex===0; if(!buildValid){console.warn('Sparse correction build invariant failed; using bounded full scan',built);return this.correctUnknownFrameScan(snap,iter,token,referencePixel)}
const maxBatch=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,iter)));let offset=0;
while(offset< built.selected ) { if ( token ! = = state . token ) return false ; const count = Math.min(maxBatch,built.selected-offset),params=this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,2,f.w,0,offset);d.queue.writeBuffer(f.numericParams,0,params);d.queue.writeBuffer(f.deepQueueStats,12,new Uint32Array ( [ count ] ) ) ; d . queue . writeBuffer ( f . deepIndirect , 0 , new Uint32Array ( [ Math . ceil ( count / 64 ) , 1 , 1 ] ) ) ; e = d.createCommandEncoder({label:'deep-sparse-correction-'+offset});this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});const ms = await this . submitStage ( e , ' queued DS correction ' + ( offset + 1 ) + ' . . ' + ( offset + count ) + ' / ' + built . selected , token , ' correction ' ) ; if ( ms = ==null)return false ; totalMs + = ms ; runtime . frontierMaxDispatchWork = Math.max(runtime.frontierMaxDispatchWork||0,count*Math.max(1,iter));offset+=count}
d.queue.writeBuffer(f.deepQueueStats,12,new Uint32Array([built.selected]));e=d.createCommandEncoder({label:'deep-sparse-correction-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const statsMs=await this.submitStage(e,'queued DS post-stats',token,'correction');if(statsMs===null)return false;totalMs+=statsMs;
const q=await this.readDeepQueueStats();if(token!==state.token)return false;
const valid=q.selected===q.enqueued&&q.enqueued===q.dispatchCount&&q.dispatchCount===q.processed&&q.overflow===0&&q.invalidIndex===0&&q.staleEntry===0;
runtime.correctionPasses++;runtime.lastSparseQueue={...q,valid,ms:totalMs,batches:Math.ceil(built.selected/maxBatch)};
if(valid)return{mode:'queue',queue:q};
console.warn('Sparse correction invariant failed; using bounded full scan',q);
return this.correctUnknownFrameScan(snap,iter,token,referencePixel)
2026-08-25 14:03:13 +09:00
}
2026-08-25 21:33:45 +09:00
async correctUnknownFrameScan(snap,iter,token,referencePixel=null){const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,maxPixels=Math.max(1,Math.floor(4000000/Math.max(1,iter))),tileW=Math.min(f.w,maxPixels),rows=Math.max(1,Math.min(128,Math.floor(maxPixels/tileW)));for(let y=0;y< f.h ; y + = rows ) { const th = Math.min(rows,f.h-y);for(let x = 0;x<f.w;x+=tileW){if(token!==state.token)return false ; const tw = Math.min(tileW,f.w-x),base=y*f.w+x;d.queue.writeBuffer(f.numericParams,0,this.deepParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,0,refX,refY,2,f.w,base));const e = d.createCommandEncoder({label:'deep-correction-fallback-'+x+'-'+y});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:tw,h:th});const ms = await this . submitStage ( e , ' DS fallback tile ' + x + ' , ' + y + ' ' + tw + ' x ' + th , token , ' correction ' ) ; if ( ms = ==null)return false } } const e = d.createCommandEncoder({label:'deep-correction-fallback-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms = await this . submitStage ( e , ' DS fallback post-stats ' , token , ' correction ' ) ; if ( ms = ==null)return false ; runtime . correctionPasses + + ; return { mode: ' scan ' } }
async refineOperationLimitIndices(snap,iter,token,deepContext,referencePixel,indices){
await this.ready;await this.ensureCorrectionPipelines();if(!deepContext)throw new Error('frontier reference context is missing');
const f=this.ensureDeepQueueWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setDeepContext(deepContext);
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,chunkSize=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,iter)));let processed=0,maxDispatchWork=0;
for(let start=0;start< indices.length ; start + = chunkSize ) {
if(token!==state.token)return null;const chunk=indices.subarray?indices.subarray(start,Math.min(indices.length,start+chunkSize)):indices.slice(start,start+chunkSize),count=chunk.length;if(!count)continue;
d.queue.writeBuffer(f.deepQueue,0,chunk);d.queue.writeBuffer(f.deepQueueStats,0,new Uint32Array([count,0,count,count,0,0,0,0]));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array([Math.ceil(count/64),1,1,0]));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,0,f.w,0));
const e=d.createCommandEncoder({label:'pixel-frontier-'+iter+'-'+start});e.clearBuffer(f.unresolved);this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});const ms=await this.submitStage(e,'pixel frontier '+(start+1)+'..'+(start+count)+'/'+indices.length+' @ '+iter,token,'correction');if(ms===null)return null;
const q=await this.readDeepQueueStats();if(token!==state.token)return null;const valid=q.selected===count&&q.enqueued===count&&q.dispatchCount===count&&q.processed===count&&q.overflow===0&&q.invalidIndex===0&&q.staleEntry===0; if(!valid)throw new Error('pixel frontier queue invariant failed: '+JSON.stringify(q));processed+=count;maxDispatchWork=Math.max(maxDispatchWork,count*iter);
}
const e=d.createCommandEncoder({label:'pixel-frontier-post-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms=await this.submitStage(e,'pixel frontier post-stats',token,'correction');if(ms===null)return null;runtime.frontierDispatches=(runtime.frontierDispatches||0)+Math.ceil(indices.length/Math.max(1,chunkSize));runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,maxDispatchWork);return{stats:await this.readUnresolvedStats(),processed,maxDispatchWork,chunkSize};
}
async extendUnknownFrame(snap,iter,token,deepContext,referencePixel=null){await this.ready;await this.ensureCorrectionPipelines();const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!deepContext)throw new Error('extended deep reference context is missing');this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,1,f.w,0));const e=d.createCommandEncoder({label:'targeted-deep-unknown-recovery'});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
encodeDirectActiveShadowChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input,passes}){const d=this.device;for(let k=0;k< passes ; k + + ) { const output = 1-input;e.clearBuffer(counts[output]);let bg = d.createBindGroup({layout:this.activePrepare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:counts[input]}},{binding:1,resource:{buffer:indirect}}]}),pass=e.beginComputePass({label:'active-indirect-prepare-shadow'});pass.setPipeline(this.activePrepare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end();bg=d.createBindGroup({layout:this.directActiveContinue.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:stateB}},{binding:2,resource:{buffer:queues[input]}},{binding:3,resource:{buffer:counts[input]}},{binding:4,resource:{buffer:queues[output]}},{binding:5,resource:{buffer:counts[output]}},{binding:6,resource:{buffer:meta}},{binding:7,resource:{buffer:smooth}}]});pass=e.beginComputePass({label:'direct-active-chunk-shadow'});pass.setPipeline(this.directActiveContinue);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end();input=output}return input }
async runDirectActiveShadow(snap,iter,{chunkSize=128,previewIter=Math.min(iter,Math.max(chunkSize,Math.floor(iter*.35)))}={}){
await this.ready;await this.ensureActiveShadowPipelines();if(!this.frame)throw new Error('active shadow requires a baseline frame');previewIter=Math.max(0,Math.min(iter,previewIter|0));if(previewIter>=iter)previewIter=0;const f=this.frame,d=this.device,B=GPUBufferUsage,n=f.n,stateB=buf(d,n*16,B.STORAGE|B.COPY_DST,'direct-active-state-shadow'),queues=[buf(d,n*4,B.STORAGE|B.COPY_DST,'direct-active-queue-a'),buf(d,n*4,B.STORAGE|B.COPY_DST,'direct-active-queue-b')],counts=[buf(d,4,B.STORAGE|B.COPY_DST,'direct-active-count-a'),buf(d,4,B.STORAGE|B.COPY_DST,'direct-active-count-b')],indirect=buf(d,12,B.STORAGE|B.INDIRECT|B.COPY_DST,'direct-active-indirect'),meta=buf(d,n*4,B.STORAGE|B.COPY_DST,'direct-active-meta'),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST,'direct-active-smooth'),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'direct-active-params'),compareP=buf(d,16,B.UNIFORM|B.COPY_DST,'direct-active-compare-params'),stats=buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'direct-active-compare-stats'),read=buf(d,16,B.COPY_DST|B.MAP_READ,'direct-active-compare-readback'),owned=[stateB,...queues,...counts,indirect,meta,smooth,pbuf,compareP,stats,read];let mapped=false;
try{const firstTarget=previewIter||iter;d.queue.writeBuffer(pbuf,0,this.directActiveParams(f.w,f.h,iter,snap,firstTarget,chunkSize));d.queue.writeBuffer(compareP,0,new Uint32Array([n,0,0,0]));let e=d.createCommandEncoder({label:'direct-active-shadow-initial'});e.clearBuffer(counts[0]);e.clearBuffer(counts[1]);e.clearBuffer(stats);let bg=d.createBindGroup({layout:this.directActiveInit.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:stateB}},{binding:2,resource:{buffer:queues[0]}},{binding:3,resource:{buffer:counts[0]}},{binding:4,resource:{buffer:meta}},{binding:5,resource:{buffer:smooth}}]}),pass=e.beginComputePass({label:'direct-active-init-shadow'});pass.setPipeline(this.directActiveInit);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();let input=this.encodeDirectActiveShadowChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input:0,passes:Math.ceil(firstTarget/chunkSize)});d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();
if(previewIter){d.queue.writeBuffer(pbuf,0,this.directActiveParams(f.w,f.h,iter,snap,iter,chunkSize));e=d.createCommandEncoder({label:'direct-active-shadow-resume'});input=this.encodeDirectActiveShadowChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input,passes:Math.ceil((iter-previewIter)/chunkSize)})}else e=d.createCommandEncoder({label:'direct-active-shadow-compare'});
bg=d.createBindGroup({layout:this.activeCompare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:compareP}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:f.meta}},{binding:4,resource:{buffer:f.smooth}},{binding:5,resource:{buffer:stats}}]});pass=e.beginComputePass({label:'direct-active-compare-shadow'});pass.setPipeline(this.activeCompare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(n/256));pass.end();e.copyBufferToBuffer(stats,0,read,0,16);d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();await read.mapAsync(GPUMapMode.READ);mapped=true;const a=new Uint32Array(read.getMappedRange().slice(0));const result={metaMismatch:a[0]||0,smoothMismatch:a[1]||0,knownToUnknown:a[2]||0,gatePassed:!(a[0]||a[1]||a[2]),pixels:n,iterations:iter,chunkSize,previewIter,resumedFromPreview:previewIter>0,productionEligible:false};runtime.activeShadowLatest=result;return result;
}finally{if(mapped)read.unmap();for(const b of owned)destroy(b)}
}
encodeDeepActiveShadowChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input,passes}){const d=this.device;for(let k=0;k< passes ; k + + ) { const output = 1-input;e.clearBuffer(counts[output]);let bg = d.createBindGroup({layout:this.activePrepare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:counts[input]}},{binding:1,resource:{buffer:indirect}}]}),pass=e.beginComputePass({label:'deep-active-indirect-prepare-shadow'});pass.setPipeline(this.activePrepare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end();bg=d.createBindGroup({layout:this.deepActiveContinue.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:stateB}},{binding:3,resource:{buffer:queues[input]}},{binding:4,resource:{buffer:counts[input]}},{binding:5,resource:{buffer:queues[output]}},{binding:6,resource:{buffer:counts[output]}},{binding:7,resource:{buffer:meta}},{binding:8,resource:{buffer:smooth}}]});pass=e.beginComputePass({label:'deep-active-chunk-shadow'});pass.setPipeline(this.deepActiveContinue);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end();input=output}return input }
async runDeepActiveShadow({ctx,snap,iter,width,height,refPixel,chunkSize=128,previewIter=Math.min(iter,Math.max(chunkSize,Math.floor(iter*.35))),strict=false}){
await this.ready;await this.ensureDeepPipelines();await this.ensureActiveShadowPipelines();this.setDeepContext(ctx);previewIter=Math.max(0,Math.min(iter,previewIter|0));if(previewIter>=iter)previewIter=0;const d=this.device,B=GPUBufferUsage,n=width*height,stateB=buf(d,n*32,B.STORAGE|B.COPY_DST,'deep-active-state-shadow'),queues=[buf(d,n*4,B.STORAGE|B.COPY_DST,'deep-active-queue-a'),buf(d,n*4,B.STORAGE|B.COPY_DST,'deep-active-queue-b')],counts=[buf(d,4,B.STORAGE|B.COPY_DST,'deep-active-count-a'),buf(d,4,B.STORAGE|B.COPY_DST,'deep-active-count-b')],indirect=buf(d,12,B.STORAGE|B.INDIRECT|B.COPY_DST,'deep-active-indirect'),meta=buf(d,n*4,B.STORAGE|B.COPY_DST,'deep-active-meta'),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST,'deep-active-smooth'),baselineMeta=buf(d,n*4,B.STORAGE|B.COPY_DST,'deep-active-baseline-meta'),baselineSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST,'deep-active-baseline-smooth'),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_DST,'deep-active-baseline-unresolved'),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'deep-active-params'),baselineP=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'deep-active-baseline-params'),compareP=buf(d,16,B.UNIFORM|B.COPY_DST,'deep-active-compare-params'),stats=buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-compare-stats'),read=buf(d,16,B.COPY_DST|B.MAP_READ,'deep-active-compare-readback'),owned=[stateB,...queues,...counts,indirect,meta,smooth,baselineMeta,baselineSmooth,unresolved,pbuf,baselineP,compareP,stats,read];let mapped=false;
try{const firstTarget=previewIter||iter;d.queue.writeBuffer(pbuf,0,this.deepActiveParams(width,height,iter,snap,refPixel,firstTarget,chunkSize,strict?1:0));d.queue.writeBuffer(baselineP,0,this.deepParams(width,height,width,height,0,0,iter,snap,.5,.5,strict?1:0,refPixel.x,refPixel.y));d.queue.writeBuffer(compareP,0,new Uint32Array([n,0,0,0]));d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));let e=d.createCommandEncoder({label:'deep-active-shadow-initial'});e.clearBuffer(counts[0]);e.clearBuffer(counts[1]);e.clearBuffer(stats);this.encodeDeepNumeric(e,{pbuf:baselineP,meta:baselineMeta,smooth:baselineSmooth,unresolved,w:width,h:height});let bg=d.createBindGroup({layout:this.deepActiveInit.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:stateB}},{binding:2,resource:{buffer:queues[0]}},{binding:3,resource:{buffer:counts[0]}}]}),pass=e.beginComputePass({label:'deep-active-init-shadow'});pass.setPipeline(this.deepActiveInit);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(width/8),Math.ceil(height/8));pass.end();let input=this.encodeDeepActiveShadowChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input:0,passes:Math.ceil(firstTarget/chunkSize)});d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();
if(previewIter){d.queue.writeBuffer(pbuf,0,this.deepActiveParams(width,height,iter,snap,refPixel,iter,chunkSize,strict?1:0));e=d.createCommandEncoder({label:'deep-active-shadow-resume'});input=this.encodeDeepActiveShadowChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input,passes:Math.ceil((iter-previewIter)/chunkSize)})}else e=d.createCommandEncoder({label:'deep-active-shadow-compare'});
bg=d.createBindGroup({layout:this.activeCompare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:compareP}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:baselineMeta}},{binding:4,resource:{buffer:baselineSmooth}},{binding:5,resource:{buffer:stats}}]});pass=e.beginComputePass({label:'deep-active-compare-shadow'});pass.setPipeline(this.activeCompare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(n/256));pass.end();e.copyBufferToBuffer(stats,0,read,0,16);d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();await read.mapAsync(GPUMapMode.READ);mapped=true;const a=new Uint32Array(read.getMappedRange().slice(0)),result={metaMismatch:a[0]||0,smoothMismatch:a[1]||0,knownToUnknown:a[2]||0,gatePassed:!(a[0]||a[1]||a[2]),pixels:n,iterations:iter,chunkSize,previewIter,resumedFromPreview:previewIter>0,strict:!!strict,productionEligible:false};runtime.deepActiveShadowLatest=result;return result;
}finally{if(mapped)read.unmap();this.clearBindGroupCache();for(const b of owned)destroy(b)}
}
async runCertifiedInteriorShadow({ctx,snap,width,height,refPixel,samples=289}){await this.ready;await this.ensureActiveShadowPipelines();this.setDeepContext(ctx);const side=Math.max(2,Math.ceil(Math.sqrt(Math.max(4,samples)))),indices=[],points=[];for(let gy=0;gy< side ; gy + + ) for ( let gx = 0;gx<side;gx++){const x = Math.min(width-1,Math.max(0,Math.round((gx+.5)*width/side-.5))),y=Math.min(height-1,Math.max(0,Math.round((gy+.5)*height/side-.5))),index=y*width+x;if(indices.includes(index))continue;indices.push(index);points.push(x+.5,y+.5)}const count = indices.length,d=this.device,B=GPUBufferUsage,pdata=new Float32Array ( points ) , pbuf = buf(d,48,B.UNIFORM|B.COPY_DST,'certified-interior-params'),pointsB=buf(d,pdata.byteLength,B.STORAGE|B.COPY_DST,'certified-interior-points'),flagsB=buf(d,count*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'certified-interior-flags'),read=buf(d,count*4,B.COPY_DST|B.MAP_READ,'certified-interior-readback'),owned=[pbuf,pointsB,flagsB,read];let mapped = false;try{d.queue.writeBuffer(pbuf,0,this.certifiedInteriorParams(count,ctx,snap,width,refPixel));d.queue.writeBuffer(pointsB,0,pdata);const e = d.createCommandEncoder({label:'certified-interior-shadow'}),bg=d.createBindGroup({layout:this.perturbCertifiedInterior.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:pointsB}},{binding:3,resource:{buffer:flagsB}}]}),pass=e.beginComputePass({label:'certified-interior-shadow'});pass.setPipeline(this.perturbCertifiedInterior);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(count/64));pass.end();e.copyBufferToBuffer(flagsB,0,read,0,count*4);d.queue.submit([e.finish()]);await d . queue . onSubmittedWorkDone ( ) ; await read . mapAsync ( GPUMapMode . READ ) ; mapped = true;const flags = new Uint32Array ( read . getMappedRange ( ) . slice ( 0 ) ) , provenIndices = [];for(let i = 0;i<count;i++)if(flags[i]===1)provenIndices.push(indices[i]);return{samples:count,proven:provenIndices.length,provenIndices,intervalArithmetic:true,productionEligible:false}}finally{if(mapped)read.unmap();for(const b of owned ) destroy ( b ) } }
2026-08-23 23:27:38 +09:00
2026-08-24 20:30:26 +09:00
async renderTileMeta({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
2026-08-25 21:33:45 +09:00
await this.ready;const fastExtended=!deep&&!!fastContext; if(deep){await this.ensureDeepPipelines();if(correctUnknown)await this.ensureCorrectionPipelines();this.setDeepContext(deepContext)}else if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-08-24 20:30:26 +09:00
if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
2026-08-23 23:27:38 +09:00
const read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
}
2026-08-25 21:33:45 +09:00
async renderTileRGBA({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true,workspaceSlot=0,workspaceSize=this.exportTileSize()}){
await this.ready;const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext; if(deep){await this.ensureDeepPipelines();if(correctUnknown)await this.ensureCorrectionPipelines();this.setDeepContext(deepContext)}else if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
2026-08-24 20:30:26 +09:00
if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const ca=this.colorParamsData(w,h,iter),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
2026-08-23 23:27:38 +09:00
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y< h ; y + + ) out . set ( raw . subarray ( y * bpr , y * bpr + w * 4 ) , y * w * 4 ) ; const stats = new Uint32Array ( raw . buffer , raw . byteOffset + pixelBytes , UNRESOLVED_BYTES / 4 ) , unresolvedCount = stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
2026-08-25 21:33:45 +09:00
async renderTileRGBA2x({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true,workspaceSlot=0,workspaceSize=this.exportTileSize()}){
await this.ready;await this.ensureAaPipeline();const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext; if(deep){await this.ensureDeepPipelines();if(correctUnknown)await this.ensureCorrectionPipelines();this.setDeepContext(deepContext)}else if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h,iter);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
2026-08-24 20:30:26 +09:00
for(let si=0;si< 4 ; si + + ) { const [ sampleX , sampleY ] = offsets [ si ] , pbuf = ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array ( UNRESOLVED_BYTES / 4 ) ) ; if ( deep ) { const ref = referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if ( fastExtended ) { const ref = referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg = d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
2026-08-23 23:27:38 +09:00
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si< 4 ; si + + ) encoder . copyBufferToBuffer ( ws . unresolveds [ si ] , 0 , ws . read , pixelBytes + si * UNRESOLVED_BYTES , UNRESOLVED_BYTES ) ; d . queue . submit ( [ encoder . finish ( ) ] ) ; await ws . read . mapAsync ( GPUMapMode . READ , 0 , pixelBytes + statsBytes ) ; const raw = new Uint8Array ( ws . read . getMappedRange ( 0 , pixelBytes + statsBytes ) ) , out = new Uint8ClampedArray ( w * h * 4 ) ; for ( let y = 0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount = 0,corrected=0;for(let si = 0;si<4;si++){const stats = new Uint32Array ( raw . buffer , raw . byteOffset + pixelBytes + si * UNRESOLVED_BYTES , UNRESOLVED_BYTES / 4 ) ; unresolvedCount + = stats [ 0 ] | | 0 ; corrected + = stats [ 7 ] | | 0 } ws . read . unmap ( ) ; return { rgba:out , unresolved:unresolvedCount , corrected } ;
}
2026-08-25 14:03:13 +09:00
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext();this.destroyFastContext()}
}
function shouldApplySparseCorrection(numeric,n){if(numeric< =0)return false;if(state.processMode==='power')return numeric>=Math.max(32,Math.floor(n*.0005));return true}
function numericalFailureCount(stats){const r=stats&&stats.reasons||{}; return (r.errorBound||0)+(r.escapeUncertain||0)+(r.referenceEnd||0)+(r.rebaseGap||0)+(r.range||0)}
// A broad failure is cheaper to normalize once with the full-frame Deep pass;
// sparse/moderate failures go directly to local references.
function shouldPromoteFastToDeep(stats,n){const failed=numericalFailureCount(stats);return failed>=Math.max(60000,Math.floor(n*.08))}
function tileRecoveryConfig(){const z=zoomExp(),deep=z>=32;if(state.processMode==='power')return{size:deep?224:320,maxRefs:4,minFailed:deep?4:48};if(state.processMode==='fine')return{size:deep?128:192,maxRefs:20,minFailed:deep?1:8};if(state.processMode==='validate')return{size:deep?112:160,maxRefs:32,minFailed:1};return{size:deep?160:224,maxRefs:12,minFailed:deep?1:12}}
2026-08-25 21:33:45 +09:00
function isOperationLimitMeta(m){return ((m>>>28)& 3)===FIELD_UNKNOWN& & ((m>>>REASON_SHIFT)& 255)===6}
function operationLimitIndices(meta){const out=[];for(let i=0;i< meta.length ; i + + ) if ( isOperationLimitMeta ( meta [ i ] ) ) out . push ( i ) ; return Uint32Array . from ( out ) }
function numericalFailureIndices(meta){const out=[];for(let i=0;i< meta.length ; i + + ) { if ( ( ( meta [ i ] > >>28)&3)!==FIELD_UNKNOWN)continue; const reason=(meta[i]>>>REASON_SHIFT)&255; if(reason>=1& & reason< =5)out.push(i)}return Uint32Array.from(out)}
function operationLimitFrontier(meta,w,h){const out=[];for(let y=0;y< h ; y + + ) { const row = y*w;for(let x = 0;x<w;x++){const i = row+x;if(!isOperationLimitMeta(meta[i]))continue;let edge = false;for(let oy = -1;oy<=1&&!edge;oy++){const yy = y+oy;if(yy<0||yy > =h)continue;for(let ox=-1;ox< =1;ox++){if(ox===0&&oy===0)continue; const xx=x+ox;if(xx< 0 | | xx > =w)continue;if(((meta[yy*w+xx]>>>28)&3)===FIELD_ESCAPED){edge=true; break}}}if(edge)out.push(i)}}return Uint32Array.from(out)}
function pixelEscapeAdvance(before,after,w,h){let escaped=0,outsideEnvelope=0;for(let i=0;i< after.length ; i + + ) { if ( ( ( after [ i ] > >>28)&3)!==FIELD_ESCAPED||((before[i]>>>28)&3)===FIELD_ESCAPED)continue; escaped++;const x=i%w,y=Math.floor(i/w);let adjacent=false;for(let oy=-1;oy< =1&&!adjacent; oy++){const yy=y+oy;if(yy< 0 | | yy > =h)continue;for(let ox=-1;ox< =1;ox++){if(!ox&&!oy)continue; const xx=x+ox;if(xx>=0&&xx<w&&((before[yy*w+xx]>>>28)&3)===FIELD_ESCAPED){adjacent=true; break}}}if(!adjacent)outsideEnvelope++}return{escaped,outsideEnvelope,pixelStable:outsideEnvelope===0}}
function pixelFrontierIterationFloor(snap,w,initialIter){const pixelBits=Math.max(1,Math.log2(Math.max(1,w))-log2FixedAt(snap.span,snap.bits)),scaled=Math.ceil(pixelBits*256/ADAPTIVE_ITER_QUANTUM)*ADAPTIVE_ITER_QUANTUM;return Math.min(PIXEL_FRONTIER_MAX_ITER,Math.max(initialIter,scaled))}
2026-08-25 14:03:13 +09:00
function tileReferenceSnapshot(snap,tile,fullW,fullH){const cx=tile.numeric?tile.sumX/tile.numeric:tile.x+tile.w*.5,cy=tile.numeric?tile.sumY/tile.numeric:tile.y+tile.h*.5,bw=Math.max(1,tile.maxX-tile.minX+1),bh=Math.max(1,tile.maxY-tile.minY+1),refW=Math.max(8,Math.min(tile.w,Math.ceil(bw*1.75))),refH=Math.max(8,Math.min(tile.h,Math.ceil(bh*1.75))),den=2n*BigInt(fullW),nx=BigInt(Math.round(2*cx-fullW)),ny=BigInt(Math.round(fullH-2*cy)),re=snap.re+roundDivSigned(snap.span*nx,den),im=snap.im+roundDivSigned(snap.span*ny,den),localSpan=(()=>{const q=roundDivSigned(snap.span*BigInt(refW),BigInt(fullW));return q>0n?q:1n})();return{snap:{bits:snap.bits,re,im,span:localSpan},width:refW,height:refH}}
2026-08-25 21:33:45 +09:00
async function recoverNumericalUnknownTiles(r,snap,iter,token,initialStats){
const initialFailed=numericalFailureCount(initialStats),cfg=tileRecoveryConfig();
if(initialFailed< cfg.minFailed ) return { stats:initialStats , passes:0 , recovered:0 , attempted:0 } ;
state.lastEngine='WebGPU · 数値失敗領域を局所referenceで解析';updateStats();
let passes=0,attempted=0,stats=initialStats,budget=cfg.maxRefs;
const sizes=[cfg.size,Math.max(56,Math.floor(cfg.size*.5))];
for(let round=0;round< sizes.length & & budget > 0;round++){
stats=await r.readUnresolvedStats();if(token!==state.token)return null;
const failed=numericalFailureCount(stats);if(failed< cfg.minFailed ) break ; const roundStartFailed = failed;
const tiles=await r.readNumericalFailureTiles(sizes[round]);if(token!==state.token)return null;if(!tiles.length)break;
const roundCap=round===0?Math.min(budget,Math.max(1,Math.ceil(cfg.maxRefs*.67))):budget;let used=0;
for(let cursor=0;cursor< tiles.length & & used < roundCap & & budget > 0;){
if(token!==state.token)return null;const count=Math.min(tileReferencePool.length,tiles.length-cursor,roundCap-used,budget),batch=tiles.slice(cursor,cursor+count);cursor+=count;
state.lastEngine='CPU · 局所reference '+(passes+1)+'– '+(passes+count)+'/'+cfg.maxRefs+' を並列準備';updateStats();
const prepared=await Promise.all(batch.map(async(tile,i)=>{const local=tileReferenceSnapshot(snap,tile,canvas.width,canvas.height),fixed={re:local.snap.re,im:local.snap.im},ctx=await tileReferencePool[i].requestFixed(local.snap,iter,local.width,local.height,fixed);return{tile,ctx}}));if(token!==state.token)return null;
const currentMeta=await r.readMetaAll();if(token!==state.token)return null;const currentFailures=numericalFailureIndices(currentMeta);
for(const {tile,ctx} of prepared){if(ctx.checkpointMismatch)throw new Error('tile reference guard checkpoint mismatch');const tileIndices=[];for(const index of currentFailures){const x=index%canvas.width,y=Math.floor(index/canvas.width);if(x>=tile.x&&x<tile.x+tile.w&&y>=tile.y&&y<tile.y+tile.h)tileIndices.push(index)}if(!tileIndices.length)continue; const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height),next=await r.refineOperationLimitIndices(snap,iter,token,ctx,refPixel,Uint32Array.from(tileIndices));if(!next||token!==state.token)return null;stats=next.stats;attempted+=tileIndices.length;passes++;used++;budget--;state.multiRefPasses=passes;}
}
if(!used||numericalFailureCount(stats)>=roundStartFailed)break;
}
if(passes){stats=await r.readUnresolvedStats();if(token!==state.token)return null}
const recovered=Math.max(0,initialFailed-numericalFailureCount(stats));state.multiRefRecovered=recovered;state.unresolved=stats.total;state.unknownReasons=stats.reasons;return{stats,passes,recovered,attempted};
}
async function recoverResidualPrecision(r,snap,iter,token,initialStats,terminalClass=-1){
const failed=numericalFailureCount(initialStats);if(failed< =0)return{stats:initialStats,attempted:0,applied:0};const meta=await r.readMetaAll();if(token!==state.token)return null;const indices=Array.from(numericalFailureIndices(meta));if(!indices.length)return{stats:initialStats,attempted:0,applied:0};const perWorker=Math.max(16,Math.ceil(indices.length/precisionFallbackPool.length)),bits=pixelPrecisionBits(snap,canvas.width,40);let attempted=0,appliedCount=0;
try{for(let start=0;start< indices.length ; start + = perWorker * precisionFallbackPool . length ) { if ( token ! = = state . token ) return null ; const jobs = [];for(let worker = 0;worker<precisionFallbackPool.length;worker++){const a = start+worker*perWorker,b=Math.min(indices.length,a+perWorker);if(a<b)jobs.push(precisionFallbackPool[worker].request(snap,iter,canvas.width,canvas.height,indices.slice(a,b),terminalClass))}state.lastEngine='CPU · 画素精度 ' + bits + ' bit以上を ' + jobs . length + ' workerで補修 · ' + Math . min ( indices . length , start + perWorker * precisionFallbackPool . length ) + ' / ' + indices . length + ' px ' ; updateStats ( ) ; const solvedBatch = await Promise . all ( jobs ) ; if ( token ! = = state . token ) return null ; for ( const solved of solvedBatch ) { const applied = await r . applyPrecisionFallback ( solved , token ) ; if ( ! applied | | token ! = = state . token ) return null ; attempted + = solved . indices . length ; appliedCount + = applied . applied } } const stats = await r . readUnresolvedStats ( ) ; state . precisionFallbackAttempted = (state.precisionFallbackAttempted||0)+attempted;state.precisionFallbackApplied=(state.precisionFallbackApplied||0)+appliedCount;return{stats,attempted,applied:appliedCount}
}catch(e){if(!/cancelled|superseded/i.test(String(e& & e.message||e)))console.warn('precision fallback skipped:',e);return{stats:initialStats,attempted,applied:appliedCount,error:String(e& & e.message||e)}}
}
async function refinePixelFrontier(r,snap,initialIter,token,initialStats){
if(!PIXEL_FRONTIER_ENABLED)return{stats:initialStats,iter:initialIter,rounds:0,processed:0,escaped:0,converged:false,applied:false};
const maxIter=Math.max(initialIter,PIXEL_FRONTIER_MAX_ITER),minimumIter=pixelFrontierIterationFloor(snap,canvas.width,initialIter),initialMeta=await r.readMetaAll();if(token!==state.token)return null;state.frontierMinimumIter=minimumIter;let previousMeta=initialMeta,candidates=operationLimitIndices(initialMeta),iter=initialIter,stats=initialStats,rounds=0,processed=0,escaped=0,stableRounds=0,converged=candidates.length===0,session=null,fixedSource=null;
while(!converged& & iter< maxIter ) {
if(token!==state.token)return null;const target=Math.min(maxIter,Math.max(iter+1,iter*2));state.lastEngine='WebGPU · 未脱出全画素 active-list '+candidates.length+' px · '+target+' 反復';state.frontierPixels=candidates.length;updateStats();let ctx=fixedSource?await refs.requestFixed(snap,target,canvas.width,canvas.height,fixedSource):await refs.request(snap,target,canvas.width,canvas.height);if(token!==state.token)return null;if(ctx.checkpointMismatch)throw new Error('all-pixel active reference guard checkpoint mismatch');
// A persistent perturbation state is valid only while its reference orbit
// origin is unchanged and reaches the new target. If it does not, rebuild
// the candidate list from the already-published metadata and restart once
// with a newly selected long reference; no pixel is silently discarded.
if(ctx.refLen< target ) { ctx = await refs . request ( snap , target , canvas . width , canvas . height , true ) ; if ( token ! = = state . token ) return null ; const meta = await r . readMetaAll ( ) ; if ( token ! = = state . token ) return null ; candidates = operationLimitIndices(meta);session=null;fixedSource=null;if(!candidates.length){converged=true;break}}
const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(!session){session=await r.beginOperationLimitContinuation(snap,token,ctx,refPixel,candidates,state.processMode==='validate');if(!session||token!==state.token)return null;fixedSource=ctx.source}
const workBefore=session.pixelIterations,dispatchBefore=session.dispatches,pass=await r.continueOperationLimitActive(session,snap,target,token,ctx,refPixel);if(!pass||token!==state.token)return null;processed+=pass.pixelIterations-workBefore;runtime.frontierDispatches=(runtime.frontierDispatches||0)+(pass.dispatches-dispatchBefore);stats=await r.readUnresolvedStats();if(token!==state.token)return null;let restart=false,failed=numericalFailureCount(stats);if(failed>0){const failedMeta=await r.readMetaAll();if(token!==state.token)return null;const failedIndices=numericalFailureIndices(failedMeta),corrected=await r.refineOperationLimitIndices(snap,target,token,ctx,refPixel,failedIndices);if(!corrected||token!==state.token)return null;stats=corrected.stats;restart=true}if(numericalFailureCount(stats)>512){const beforeLocal=numericalFailureCount(stats),mr=await recoverNumericalUnknownTiles(r,snap,target,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;restart=true;if(numericalFailureCount(stats)>=beforeLocal)state.multiRefRecovered=0}if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,target,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats;restart=true}const after=await r.readMetaAll();if(token!==state.token)return null;const advance=pixelEscapeAdvance(previousMeta,after,canvas.width,canvas.height);rounds++;escaped+=advance.escaped;iter=target;stableRounds=advance.pixelStable?stableRounds+1:0;previousMeta=after;state.frontierRounds=rounds;state.frontierEscaped=escaped;state.frontierIter=iter;
// A quiet pair of doubling rounds is only meaningful after every remaining
// operation-limit pixel has actually reached the budget derived from one
// screen pixel. This prevents an early quiet interval from hiding a later
// visible escape component. Recovery may repopulate an emptied GPU queue,
// so convergence is decided from the post-recovery metadata.
const recoveredCandidates=operationLimitIndices(after);if(!recoveredCandidates.length)converged=true;else if(iter>=minimumIter&&stableRounds>=2)converged=true; else if(restart||session.active===0){candidates=recoveredCandidates;session=null;fixedSource=null;converged=false}
}
stats=await r.readUnresolvedStats();if(token!==state.token)return null;
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats}
state.frontierConverged=converged;state.frontierIter=iter;state.unresolved=stats.total;state.unknownReasons=stats.reasons;runtime.frontierFrames=(runtime.frontierFrames||0)+1;runtime.frontierPixels=(runtime.frontierPixels||0)+processed;runtime.frontierEscaped=(runtime.frontierEscaped||0)+escaped;return{stats,iter,minimumIter,rounds,processed,escaped,converged,applied:escaped>0};
}
2026-08-25 14:03:13 +09:00
function scheduleFastNumericRecovery(r,snap,iter,token,initialStats,initialRefPixel){
if(numericalFailureCount(initialStats)< =0)return false;
if(numericRecoveryTimer){clearTimeout(numericRecoveryTimer);numericRecoveryTimer=0;}
state.numericRecoveryPending=true;
// The primary FAST image is already visible. Give input/recolour handlers a
// short turn, then repair only if the view is still current.
numericRecoveryTimer=setTimeout(()=>{numericRecoveryTimer=0;runFastNumericRecovery(r,snap,iter,token,initialStats,initialRefPixel);},60);
return true;
}
async function runFastNumericRecovery(r,snap,iter,token,initialStats,initialRefPixel){
if(token!==state.token||renderer!==r||state.dirty||!sameViewSnapshot(snap,snapshot())){state.numericRecoveryPending=false;return;}
state.numericRecoveryPending=false;state.numericRecoveryRunning=true;state.drawState='REFINING';
let stats=initialStats,referencePixel=initialRefPixel,ctx=null;
try{
// First try the inexpensive local-reference recovery on the existing FAST
// field. It often removes isolated perturbation failures without touching
// the rest of the frame.
2026-08-25 21:33:45 +09:00
if(numericalFailureCount(stats)>0& & !shouldPromoteFastToDeep(stats,canvas.width*canvas.height)){
2026-08-25 14:03:13 +09:00
state.lastEngine='WebGPU · 背景で数値穴を局所補修';updateStats();
const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return;stats=mr.stats;
}
// If local references were insufficient, perform the old full Deep path
// in the background. The visible FAST/front texture is not replaced until
// Deep + correction + recolour has completed, so this can no longer create
// a black rectangle or extend the interaction-to-first-image latency.
if(numericalFailureCount(stats)>0){
state.lastEngine='WebGPU · 背景で正確系補修';updateStats();
const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height);
if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;}
else{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('background reference guard checkpoint mismatch');}
const ok=await r.computeFrame(snap,iter,true,ctx,token,state.processMode==='validate',referencePixel,false,null);if(!ok||token!==state.token)return;
stats=await r.readUnresolvedStats();if(token!==state.token)return;
const n=canvas.width*canvas.height;
if(shouldApplySparseCorrection(numericalFailureCount(stats),n)){
2026-08-25 21:33:45 +09:00
const corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel,numericalFailureCount(stats));if(!corrected||token!==state.token)return;state.correctionPasses++;
stats=await r.readUnresolvedStats();if(token!==state.token)return;
2026-08-25 14:03:13 +09:00
}
if(numericalFailureCount(stats)>0){const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return;stats=mr.stats;}
2026-08-25 21:33:45 +09:00
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return;stats=pf.stats;}
2026-08-25 14:03:13 +09:00
}
state.unresolved=stats.total;state.unknownReasons=stats.reasons;
const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return;
state.frameView=snap;state.fieldView={...state.fieldView,...snap,iter,w:canvas.width,h:canvas.height,backend:ctx?'deep-background-recovery':state.fieldView?.backend||'fast-extended',progressive:true};
// Keep temporal fill active until every numeric UNKNOWN is gone. Max-iter
// non-escape pixels are not numeric failures and may still be black.
if(numericalFailureCount(stats)===0){r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});r.commitHistory(snap);state.temporalFill=false;state.drawState='COVERED';}
else{state.temporalFill=!!r.historyReady;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});state.drawState='REFINED';}
}catch(e){if(token===state.token){state.gpuError='背景数値補修: '+String(e&&e.message||e); console.warn('background numeric recovery failed:',e);}}
finally{
2026-08-25 21:33:45 +09:00
if(token===state.token){state.numericRecoveryRunning=false;state.numericRecoveryPending=false;const metric=generationMetric(token);if(metric){metric.numericQuiescentMs=performance.now()-metric.startedAt;metric.finalUnknown=state.unresolved;metric.finalReasons={...(state.unknownReasons||{})};metric.finalNumericalFailures=numericalFailureCount({reasons:metric.finalReasons})}updateStats();if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor); }
2026-08-25 14:03:13 +09:00
}
2026-08-23 23:27:38 +09:00
}
2026-08-24 20:30:26 +09:00
function referencePixelForSource(src,snap,w,h){if(!src)return{x:w*.5,y:h*.5};const b=Math.max(snap.bits,src.bits),span=align(snap.span,snap.bits,b),dr=align(src.re,src.bits,b)-align(snap.re,snap.bits,b),di=align(src.im,src.bits,b)-align(snap.im,snap.bits,b);return{x:w*.5+fixedRatio(dr,span)*w,y:h*.5-fixedRatio(di,span)*w}}
2026-08-25 21:33:45 +09:00
function referenceOriginMatchesView(src,snap){return !!src& & !!snap& & src.bits===snap.bits& & (src.viewRe??src.re)===snap.re& & (src.viewIm??src.im)===snap.im& & src.span===snap.span}
function reusableReferencePixel(ctx,snap,iter,w,h,requiredBits){const src=ctx&&ctx.source; if(!ctx||!src||!referenceOriginMatchesView(src,snap)||ctx.refLen< iter | | ( ctx . precisionBits | | 0 ) < requiredBits | | ctx . checkpointMismatch ) return null ; const pixel = referencePixelForSource(src,snap,w,h),margin=.125*Math.max(w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x < -margin | | pixel . x > w+margin||pixel.y < -margin | | pixel . y > h+margin)return null;return{ctx,pixel}}
function reusableDeepReference(snap,iter,w,h){return reusableReferencePixel(renderer& & renderer.deepCtx,snap,iter,w,h,pixelPrecisionBits(snap,w,40))}
function reusableFastReference(snap,iter,w,h){return reusableReferencePixel(renderer& & renderer.fastCtx,snap,iter,w,h,fastReferenceBits(snap,w))}
2026-08-23 23:27:38 +09:00
// ── GPU startup / rendering orchestration ────────────────────────────────
2026-08-25 14:03:13 +09:00
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=true;state.gpuUnavailable=true;state.gpuError='WebGPU非対応 · 重いfallbackは無効';updateStats();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=true;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません · fallback無効';updateStats();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuUnavailable=false;state.gpuError='WebGPU初期化失敗: '+String(e& & e.message||e)+' · fallback無効';updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
2026-08-25 21:33:45 +09:00
function cancelRender(){const metric=generationMetric();if(metric&&metric.status==='running'){metric.status='cancelled'; metric.cancelMs=performance.now()-metric.startedAt}state.token++;state.rendering=false;state.recolorPending=false;state.refinePending=false;state.numericRecoveryPending=false;state.numericRecoveryRunning=false;runtime.blaShadowLatest=null;runtime.blaGpuShadowLatest=null;if(refineTimer){clearTimeout(refineTimer);refineTimer=0}if(blaShadowTimer){clearTimeout(blaShadowTimer);blaShadowTimer=0}if(numericRecoveryTimer){clearTimeout(numericRecoveryTimer);numericRecoveryTimer=0}blaShadow.cancelPending('render cancelled');refs.cancelPending('render cancelled');for(const service of tileReferencePool)service.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled');precisionFallback.cancel('render cancelled');for(const service of precisionFallbackPool)service.cancel('render cancelled')}
2026-08-23 23:27:38 +09:00
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
2026-08-25 14:03:13 +09:00
async function renderQualityStage(r,snap,iter,token,isFinal=false){
let decision=chooseBackend(snap,canvas.width,isFinal),deep=decision.deep,accurateHybrid=!!decision.accurateHybrid,fastExtended=decision.fastExtended;
state.backendDecision=decision;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false,blaPlan=null;
if(deep){
const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height);
if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true}
else{state.lastEngine='WebGPU · '+(isFinal?'最終':'preview')+' 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch')}
}else if(fastExtended){
const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);
if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true}
else{state.lastEngine='WebGPU · '+(isFinal?'最終':'preview')+' 高速拡張 reference準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height)}
}
if(fastExtended& & isFinal& & !r.blaFastDisabled){
if(r.blaFastCandidatePipeline& & r.blaFastFramePipeline& & fastCtx?.refLen>=iter){
try{
const shadow=await blaShadow.requestBuild({ctx:fastCtx,snap,iter,width:canvas.width,height:canvas.height,refPixel:referencePixel,safety:1/64});if(token!==state.token)return null;
if(shadow.stats.gatePassed& & shadow.stats.gpuShadowEligible){blaPlan=await r.runBlaFastProbe({shadow,snap,iter,refPixel:referencePixel,safety:1/64,samples:64});if(blaPlan?.enabled){blaPlan.safety=1/64;decision={...decision,backend:'fast-bla',reason:'adaptive-bla-probe-pass',probe:true};state.backendDecision=decision;}}else{runtime.blaFastProbeLatest={enabled:false,reason:'cpu-gate'};}
}catch(e){runtime.blaFastFailures++;runtime.blaFastProbeLatest={enabled:false,reason:'probe-error',error:String(e&&e.message||e)}; blaPlan=null;console.warn('Adaptive FAST BLA probe skipped:',e)}
}else if(fastCtx?.refLen< iter ) { runtime . blaFastProbeLatest = {enabled:false,reason:'short-reference'};}
else if(!r.blaFastCompilePromise){setTimeout(()=>{if(renderer===r& & !r.blaFastDisabled)r.warmBlaFastPipelines()},250)}
}
state.lastEngine='WebGPU · '+(isFinal?'最終品質':'即時preview')+' '+(accurateHybrid?'正確DS':deep?'正確':blaPlan?.enabled?'高速BLA':fastExtended?'高速拡張':'高速');updateStats();
let ok=await r.computeFrame(snap,iter,deep,ctx,token,isFinal&&state.processMode==='validate',referencePixel,fastExtended,fastCtx,blaPlan,accurateHybrid); if(!ok||token!==state.token)return null;
if(deep){
if(isFinal){
let stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;
const n=canvas.width*canvas.height;
if(shouldApplySparseCorrection(numericalFailureCount(stats),n)){
2026-08-25 21:33:45 +09:00
const corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel,numericalFailureCount(stats));if(!corrected||token!==state.token)return null;state.correctionPasses=1;
stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;
2026-08-25 14:03:13 +09:00
}
if(numericalFailureCount(stats)>0){const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;state.unresolved=stats.total;state.unknownReasons=stats.reasons;}
2026-08-25 21:33:45 +09:00
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats;state.unresolved=stats.total;state.unknownReasons=stats.reasons;}
2026-08-25 14:03:13 +09:00
}
const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;
}else if(accurateHybrid){
2026-08-25 21:33:45 +09:00
// The shared post-pass also exposes rare DS range failures; never publish
// an Accurate frame as complete merely because this backend has no inline
// unresolved atomics.
let stats=await r.readUnresolvedStats();if(token!==state.token)return null;if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats,FIELD_INTERIOR_LIKELY);if(!pf||token!==state.token)return null;stats=pf.stats;if(pf.applied>0){const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null}}state.unresolved=stats.total;state.unknownReasons=stats.reasons;
2026-08-25 14:03:13 +09:00
}else if(isFinal& & fastExtended){
2026-08-25 21:33:45 +09:00
// A FAST frame is not publishable while any numeric UNKNOWN remains.
// Repair locally first, then solve every residual pixel at precision
// derived from span / canvasWidth. This computes the pixels instead of
// disguising them with colour or deferring correctness to a zoom cutoff.
let stats=await r.readUnresolvedStats();if(token!==state.token)return null;const initialFailed=numericalFailureCount(stats);
if(initialFailed>0){const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;}
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats;}
state.unresolved=stats.total;state.unknownReasons=stats.reasons;
if(initialFailed>0){const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;}
}else if(isFinal){
2026-08-25 14:03:13 +09:00
const stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;
}
2026-08-25 21:33:45 +09:00
let effectiveIter=iter,frontierResult=null;
if(isFinal& & PIXEL_FRONTIER_ENABLED){frontierResult=await refinePixelFrontier(r,snap,iter,token,{total:state.unresolved,reasons:{...(state.unknownReasons||{})}});if(!frontierResult||token!==state.token)return null;effectiveIter=frontierResult.iter;if(!frontierResult.converged)throw new Error('全画素反復が安全上限 '+PIXEL_FRONTIER_MAX_ITER+' までに収束しなかったためframeを公開しません');if(frontierResult.applied){const painted=await r.recolor(token,effectiveIter);if(!painted||token!==state.token)return null}}
if(isFinal){const remaining=numericalFailureCount({reasons:state.unknownReasons||{}});if(remaining>0)throw new Error('画素精度補修後も数値未確定が '+remaining+' px 残ったためframeを公開しません')}
state.frameView=snap;state.fieldView={...snap,iter:effectiveIter,baseIter:iter,frontier:frontierResult,w:canvas.width,h:canvas.height,deep,accurateHybrid,fastExtended,backend:frontierResult?.applied?'pixel-frontier-'+decision.backend:decision.backend,referenceKey:(deep?ctx:fastExtended?fastCtx:null)?.key||'',referenceReused,progressive:true};
2026-08-25 14:03:13 +09:00
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});
return{decision,referenceReused,shadowContext:deep?ctx:(fastExtended?fastCtx:null),referencePixel};
}
function numericFrameComplete(){return numericalFailureCount({reasons:state.unknownReasons||{}})===0& & !state.numericRecoveryPending& & !state.numericRecoveryRunning}
function scheduleQualityRefinement(snap,token,stages,index){
if(index>=stages.length||token!==state.token)return;
state.refinePending=true;if(refineTimer)clearTimeout(refineTimer);
refineTimer=setTimeout(()=>{refineTimer=0;runQualityRefinement(snap,token,stages,index)},refinementDelay(index));
}
2026-08-25 21:33:45 +09:00
function prefetchFinalReference(snap,finalIter,token,decision){if(token!==state.token||!decision||finalIter< =state.qualityIter)return;let work=null;if(decision.deep)work=refs.request(snap,finalIter,canvas.width,canvas.height);else if(decision.fastExtended)work=fastRefs.request(snap,finalIter,canvas.width,canvas.height);if(work){runtime.referencePrefetches=(runtime.referencePrefetches||0)+1;work.catch(e=>{if(!/cancelled|superseded/i.test(String(e& & e.message||e)))console.warn('final reference prefetch skipped:',e)})}}
2026-08-25 14:03:13 +09:00
async function runQualityRefinement(snap,token,stages,index){
if(token!==state.token||state.pointerActive||state.wheelActive||state.dirty||!sameViewSnapshot(snap,snapshot())){state.refinePending=false;return}
const r=renderer;if(!r){state.refinePending=false;return}
const iter=stages[index],isFinal=index===stages.length-1,t0=performance.now();state.rendering=true;state.refinePending=false;state.drawState='REFINING';state.qualityStage=isFinal?'final':'refining';state.qualityIter=iter;updateStats();
try{
const result=await renderQualityStage(r,snap,iter,token,isFinal);if(!result||token!==state.token)return;
2026-08-25 21:33:45 +09:00
state.refineMs=performance.now()-t0;state.rendering=false;state.drawState=isFinal?'COVERED':'REFINED';state.lastRender=state.refineMs;state.lastEngine='WebGPU · '+(isFinal?'最終品質':'中間品質')+' '+(result.decision.accurateHybrid?'正確DS':result.decision.deep?'正確':result.decision.backend==='fast-bla'?'高速BLA':result.decision.fastExtended?'高速拡張':'高速');const metric=generationMetric(token);if(metric&&isFinal){metric.finalMs=performance.now()-metric.startedAt; metric.status='final';metric.finalUnknown=state.unresolved;metric.finalReasons={...(state.unknownReasons||{})};metric.finalNumericalFailures=numericalFailureCount({reasons:metric.finalReasons});metric.numericSubmits=runtime.numericSubmits-metric.numericSubmitsStart;metric.correctionSubmits=runtime.correctionSubmits-metric.correctionSubmitsStart;metric.gpuSyncWaits=runtime.gpuSyncWaits-metric.gpuSyncWaitsStart}
2026-08-25 14:03:13 +09:00
if(isFinal){if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false;}else{state.temporalFill=!!r.historyReady;}r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});r.scheduleBlaFastWarmup();scheduleBlaShadowValidation(snap,iter,result.shadowContext,result.referencePixel,token,result.decision.fastExtended?'fast':result.decision.deep?'deep':'cpu');}
updateStats();
if(!isFinal)scheduleQualityRefinement(snap,token,stages,index+1);
}catch(e){if(token!==state.token)return;state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e); state.lastEngine='WebGPU refinement error';updateStats();console.error(e)}
}
2026-08-23 23:27:38 +09:00
async function renderFrame(){
2026-08-25 14:03:13 +09:00
const token=++state.token,snap=snapshot(),finalIter=maxIter(),holdStable=canUseStableReprojection(),stages=qualityStages(finalIter),iter=stages[0],isSingleFinal=stages.length===1,t0=performance.now();
2026-08-25 21:33:45 +09:00
const generation={token,startedAt:t0,viewKey:[snap.bits,snap.re,snap.im,snap.span].join(':'),width:canvas.width,height:canvas.height,previewIter:iter,finalIter,numericSubmitsStart:runtime.numericSubmits,correctionSubmitsStart:runtime.correctionSubmits,gpuSyncWaitsStart:runtime.gpuSyncWaits,status:'running'};runtime.generationMetrics.push(generation);if(runtime.generationMetrics.length>64)runtime.generationMetrics.shift();
state.rendering=true;state.dirty=false;state.drawState=isSingleFinal&&holdStable?'REPROJECTED':'COVERING'; state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.multiRefPasses=0;state.multiRefRecovered=0;state.frontierRounds=0;state.frontierPixels=0;state.frontierEscaped=0;state.frontierIter=iter;state.frontierMinimumIter=PIXEL_FRONTIER_ENABLED?pixelFrontierIterationFloor(snap,canvas.width,iter):iter;state.frontierConverged=false;state.qualityStage=isSingleFinal?'final':'preview';state.qualityIter=iter;state.qualityFinalIter=finalIter;state.previewMs=0;state.refineMs=0;state.refinePending=false;runtime.renderStarts++;runtime.stageCounts={};runtime.stageMs={};runtime.frontierMaxDispatchWork=0;runtime.blaFastProbeLatest=null;updateStats();
2026-08-23 23:27:38 +09:00
try{
const r=renderer||await initRenderer();if(token!==state.token)return;
2026-08-25 14:03:13 +09:00
if(!r){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU必須 · fallback無効';updateStats();return}
state.temporalFill=!!r.historyReady;
// Paint the old completed frame at the new view immediately. The numeric
// buffers may now be rewritten strip by strip, but the user keeps seeing
// a coherent image until the complete new colour frame is ready.
if(holdStable){r.presentFrame(r.presentTransform());state.drawState='REPROJECTED';updateStats()}
const result=await renderQualityStage(r,snap,iter,token,isSingleFinal);if(!result||token!==state.token)return;
2026-08-25 21:33:45 +09:00
const elapsed=performance.now()-t0;generation.primaryMs=elapsed;generation.backend=result.decision.backend;generation.referenceReused=!!result.referenceReused;generation.primaryUnknown=state.unresolved;generation.primaryReasons={...(state.unknownReasons||{})};generation.primaryNumericalFailures=numericalFailureCount({reasons:generation.primaryReasons});state.lastRender=elapsed;state.rendering=false;state.drawState=isSingleFinal?'COVERED':'REFINED';
2026-08-25 14:03:13 +09:00
if(isSingleFinal){state.refineMs=elapsed;state.previewMs=0;state.lastEngine='WebGPU · 最終品質 '+(result.decision.accurateHybrid?'正確DS':result.decision.deep?'正確':result.decision.backend==='fast-bla'?'高速BLA':result.decision.fastExtended?'高速拡張':'高速');if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false;}else{state.temporalFill=!!r.historyReady;}r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});state.qualityStage='final';r.scheduleBlaFastWarmup();scheduleBlaShadowValidation(snap,iter,result.shadowContext,result.referencePixel,token,result.decision.fastExtended?'fast':result.decision.deep?'deep':'cpu');}
2026-08-25 21:33:45 +09:00
else{state.previewMs=elapsed;state.lastEngine='WebGPU · 即時preview '+(result.decision.accurateHybrid?'正確DS':result.decision.deep?'正確':result.decision.backend==='fast-bla'?'高速BLA':result.decision.fastExtended?'高速拡張':'高速');prefetchFinalReference(snap,finalIter,token,result.decision);}
if(isSingleFinal){generation.finalMs=elapsed;generation.status='final';generation.finalUnknown=state.unresolved;generation.finalReasons={...(state.unknownReasons||{})};generation.numericSubmits=runtime.numericSubmits-generation.numericSubmitsStart;generation.correctionSubmits=runtime.correctionSubmits-generation.correctionSubmitsStart;generation.gpuSyncWaits=runtime.gpuSyncWaits-generation.gpuSyncWaitsStart}updateStats();
2026-08-25 14:03:13 +09:00
if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor);
if(!isSingleFinal)scheduleQualityRefinement(snap,token,stages,1);
2026-08-25 21:33:45 +09:00
}catch(e){generation.status=token===state.token?'error':'cancelled';generation.error=String(e&&e.message||e); if(token!==state.token)return;state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e); state.lastEngine='WebGPU error';updateStats();console.error(e)}
2026-08-25 14:03:13 +09:00
}
function requestRecolor(){
state.colorRevision++;state.recolorPending=true;
// Keep the old completed frame valid until the replacement colour pass has
// actually finished. Invalidating history at slider-down caused apparent
// non-response/flicker when GPU work was already in flight.
if(!state.rendering&&!state.recoloring&&!state.numericRecoveryRunning)queueMicrotask(recolor);
}
async function recolor(){
if(!renderer||!state.fieldView){state.recolorPending=true;return false}
if(state.rendering||state.recoloring||state.numericRecoveryRunning){state.recolorPending=true;return false}
state.recoloring=true;let painted=false;
try{
while(state.recolorPending& & !state.rendering& & !state.numericRecoveryRunning& & renderer& & state.fieldView){
state.recolorPending=false;const revision=state.colorRevision,token=state.token,ok=await renderer.recolor(token);
if(!ok||token!==state.token){state.recolorPending=true;continue}
renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;
// Only promote a colour frame to stable history if it still represents
// the newest slider value. Rapid dragging therefore collapses to the
// latest value instead of replaying stale colour states.
if(revision===state.colorRevision&&state.qualityStage==='final'&&!state.refinePending&&numericFrameComplete()){renderer.commitHistory(state.frameView); state.temporalFill=false}
updateStats();
2026-08-24 20:30:26 +09:00
}
2026-08-25 14:03:13 +09:00
return painted
}catch(e){console.error(e);return false}
finally{state.recoloring=false;if(state.recolorPending& & !state.rendering& & !state.numericRecoveryRunning)queueMicrotask(recolor)}
2026-08-23 23:27:38 +09:00
}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform()); updateStats();return}if(state.dirty&&!state.rendering)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}}
2026-08-25 14:03:13 +09:00
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b< 64 | | sp < = 0n ) return false ; state . bits = b;state.re=re;state.im=im;state.span=sp;if(p.has('pal')){const pal = Math.max(0,Math.min(PALETTE_MAX,Number(p.get('pal'))|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;}if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';if(p.has('rm'))state.renderMode=p.get('rm')==='accurate'?'accurate':'fast';ensurePrecision();return true } catch { return false } }
2026-08-24 20:30:26 +09:00
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()};
2026-08-25 14:03:13 +09:00
async function copyShareUrl(){
saveHash(false);const url=location.href;
try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(url);toast('共有URLをコピーしました');return}}
catch{}
try{const ta=document.createElement('textarea');ta.value=url;ta.setAttribute('readonly','');ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();const ok=document.execCommand&&document.execCommand('copy'); ta.remove();if(ok){toast('共有URLをコピーしました');return}}catch{}
try{if(navigator.share){await navigator.share({title:'MANDELBROT DEEP ZOOM',url});toast('共有しました');return}}catch(e){if(e?.name==='AbortError')return}
try{prompt('共有URLをコピーしてください',url)}catch{}toast('共有URLを表示しました')
}
$('#share').onclick=copyShareUrl;
2026-08-24 20:30:26 +09:00
let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0;
2026-08-25 14:03:13 +09:00
function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色相変化停止':'色相変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')}
2026-08-24 20:30:26 +09:00
function stopColorAuto(){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton()}
2026-08-25 14:03:13 +09:00
function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;/* Hue animation advances shift directly. No cycle or phase compensation is applied. */state.shift=fract01(state.shift+.09*dt);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;requestRecolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)}
2026-08-24 20:30:26 +09:00
$('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf)colorAutoRaf=requestAnimationFrame(colorAutoStep); else if(!state.colorAuto)stopColorAuto()};
2026-08-25 14:03:13 +09:00
$('#palette').addEventListener('change',e=>{const pal=Math.max(0,Math.min(PALETTE_MAX,Number(e.target.value)|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;requestRecolor();saveHash(false)});
function applyCycleControl(e){state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);requestRecolor()}
function applyShiftControl(e){if(state.colorAuto)stopColorAuto();state.shift=Math.max(0,Math.min(1,Number(e.target.value)||0));$('#shiftO').textContent=state.shift.toFixed(2);requestRecolor()}
$('#cycle').addEventListener('input',applyCycleControl);$('#cycle').addEventListener('change',()=>saveHash(false));
$('#shift').addEventListener('input',applyShiftControl);$('#shift').addEventListener('change',()=>saveHash(false));
2026-08-24 20:30:26 +09:00
$('#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)}}
2026-08-25 21:33:45 +09:00
async function yieldExportUi(lastYield,force=false){if(!force& & performance.now()-lastYield< 24 ) return lastYield ; if ( globalThis . scheduler ? . yield ) await scheduler . yield ( ) ; else await new Promise ( resolve = > setTimeout(resolve,0));return performance . now ( ) }
2026-08-23 23:27:38 +09:00
async function runExport(){
if(exportJob.active)return;
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-25 21:33:45 +09:00
const tile=r.exportTileSize(),ringDepth=r.exportRingDepth(tile),totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
2026-08-23 23:27:38 +09:00
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
2026-08-25 21:33:45 +09:00
let done=0,unresolvedSamples=0,lastYield=performance.now();
2026-08-23 23:27:38 +09:00
try{
for(let y=0;y< h ; y + = tile ) {
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
2026-08-25 21:33:45 +09:00
for(let x=0;x< w ; ) {
if(exportJob.cancelled)throw new Error('cancelled');lastYield=await yieldExportUi(lastYield);if(exportJob.cancelled)throw new Error('cancelled');
const batch=[];for(let slot=0;slot< ringDepth & & x < w ; slot + + , x + = tile ) { const tileX = x,tw=Math.min(tile,w-tileX),args={snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX,tileY:y,w:tw,h:th,forceStrict:strict,workspaceSlot:slot,workspaceSize:tile};batch.push({tileX,tw,promise:ss===1?r.renderTileRGBA({...args,sampleX:.5,sampleY:.5,edgeAA:false}):r.renderTileRGBA2x(args)})}
const settled=await Promise.allSettled(batch.map(item=>item.promise)),failed=settled.find(item=>item.status==='rejected');if(failed)throw failed.reason;const results=settled.map(item=>item.value);
for(let i=0;i< batch.length ; i + + ) { const { tileX , tw } = batch [ i ] , result = results[i],data=result.rgba;unresolvedSamples+=result.unresolved||0;for(let row = 0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+tileX*4);done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 ' + Math . round ( 100 * done / totalTiles ) + ' % ' + ( unresolvedSamples ? ' · 未確定sample ' + unresolvedSamples: ' ' ) }
2026-08-23 23:27:38 +09:00
}
2026-08-25 21:33:45 +09:00
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);lastYield=await yieldExportUi(lastYield);
2026-08-23 23:27:38 +09:00
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
2026-08-25 21:33:45 +09:00
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':fastExtended?'reduced-reference + fast-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + '+ringDepth+'-slot staging ring + streaming-png':'gpu-tile + '+ringDepth+'-slot staging ring + streaming-png',exportTileSize:tile,exportRingDepth:ringDepth,iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,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-25 14:03:13 +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.accurateHybrid?'WebGPU 正確DS ' :decision . deep ? ' WebGPU 正確 ' :decision . fastExtended ? ' WebGPU 高速 ' : ' WebGPU 高速 ' ) : ( state . gpuInitFailed ? ' WebGPU エラー ' :state . gpuError ? ' WebGPU 復旧中 ' : ' 起動中 ' ) ; $ ( ' # 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){const nf = numericalFailureCount({reasons:state.unknownReasons||{}}),op=state.unknownReasons?.operationLimit||0;status+=' · 未確定 ' + state . unresolved + ( nf | | op ? ' ( 数値 ' + nf + ' / 上限 ' + op + ' ) ' : ' ' ) ; } if ( state . correctionPasses ) status + = ' · DS補正 ' ; if ( state . multiRefPasses ) status + = ' · 局所ref ' + state . multiRefPasses + ' / 回収 ' + state . multiRefRecovered ; if ( state . qualityStage = =='preview')status+=' · preview ' + state . qualityIter + ' / ' + state . qualityFinalIter ; if ( state . refinePending ) status + = ' · 最終品質待機 ' ; if ( state . numericRecoveryPending ) status + = ' · 数値穴補修待機 ' ; if ( state . numericRecoveryRunning ) status + = ' · 数値穴補修中 ' ; if ( state . qualityStage = =='refining')status+=' · 精細化 ' + state . qualityIter + ' / ' + state . qualityFinalIter ; if ( state . qualityStage = =='final')status+=' · 最終 ' + state . qualityIter ; if ( state . temporalFill ) status + = ' · 前フレーム保持 ' ; if ( runtime . blaShadowLatest & & ! runtime . blaShadowLatest . buildOnly ) { const bs = runtime.blaShadowLatest;status+=' · BLA影 ' + bs . samples + ' 点 ' + ( ! bs . eligibleForBla ? ( bs . referenceFull ? ' 非対象 ( span ) ' : ' 非対象 ( ref短 ) ' ) :bs . gatePassed ? ' 一致 ' : ' 差異 ' + ( bs . classMismatch + bs . escapeIterationMismatch ) ) + ' / skip ' + ( 100 * bs . skipRate ) . toFixed ( 1 ) + ' % ' ; } if ( runtime . blaGpuShadowLatest ) { const gs = runtime.blaGpuShadowLatest;status+=' · GPU影 ' + ( gs . gatePassed ? ' 一致 ' :gs . compileFailed ? ' コンパイル不可 ' :gs . error ? ' 実行不可 ' : ' 差異 ' + ( gs . exactMismatch ? ? ' ? ' ) ) ; if ( renderer ? . blaGpuGate ) status + = ' gate ' + renderer . blaGpuGate . distinctViews + ' / 3 ' ; } if ( runtime . blaGpuCanaryLatest ) { const cs = runtime.blaGpuCanaryLatest;status+=' · BLA canary ' + ( cs . applied ? ( cs . checked + ' px一致 ' ) :cs . gateArmed ? ' 待機 ' :cs . error ? ' 停止 ' : ' 未解禁 ' ) ; } if ( runtime . blaFastProbeLatest ) { const bp = runtime.blaFastProbeLatest;status+=' · FAST BLA ' + ( bp . enabled ? ' ON ' : ' OFF ' ) + ( Number . isFinite ( bp . operationReduction ) ? ' / 削減 ' + ( 100 * bp . operationReduction ) . toFixed ( 0 ) + ' % ' : ' ' ) + ( Number . isFinite ( bp . fallbackRate ) ? ' / fallback ' + ( 100 * bp . fallbackRate ) . toFixed ( 0 ) + ' % ' : ' ' ) ; } if ( state . gpuError ) { const ge = state.gpuError.length > 120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status}
2026-08-23 23:27:38 +09:00
2026-08-25 21:33:45 +09:00
async function waitForTestState(predicate,label,timeout=120000){const start=performance.now();for(;;){if(predicate())return true;if(performance.now()-start>=timeout)throw new Error(label+' timeout');if(state.dirty&&!state.rendering)schedule(); await new Promise(resolve=>setTimeout(resolve,20))}}
async function sha256View(view){const bytes=new Uint8Array(view.buffer,view.byteOffset,view.byteLength),digest=await crypto.subtle.digest('SHA-256',bytes),out=[];for(const b of new Uint8Array(digest))out.push(b.toString(16).padStart(2,'0'));return out.join('')}
2026-08-23 23:27:38 +09:00
globalThis.__MANDEL_TEST__={
2026-08-25 21:33:45 +09:00
async ensureGpuReady(){const r=renderer||await initRenderer();return{ready:!!r,navigatorGpu:!!navigator.gpu,renderer:!!renderer,gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError}},
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard',renderMode='fast',fixed=false}){const ready=await this.ensureGpuReady();if(!ready.ready)throw new Error('WebGPU initialization failed: '+JSON.stringify(ready));cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fixed?BigInt(re):fromDec(re);state.im=fixed?BigInt(im):fromDec(im);state.span=fixed?BigInt(span):fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.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');if(!renderer?.frame||!state.fieldView)throw new Error('test render completed without a GPU frame: '+JSON.stringify({gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError,drawState:state.drawState,engine:state.lastEngine}));return{width:canvas.width,height:canvas.height,state:this.state()}},
2026-08-23 23:27:38 +09:00
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
2026-08-25 14:03:13 +09:00
async runBlaShadow({samples=289,safety=1/64}={}){const snap=snapshot(),iter=maxIter(),w=canvas.width,h=canvas.height;let ctx=null,refPixel=null;if(renderer?.deepCtx&&renderer.deepCtx.source?.iter===iter){ctx=renderer.deepCtx; refPixel=referencePixelForSource(ctx.source,snap,w,h)}else if(renderer?.fastCtx&&renderer.fastCtx.source?.iter===iter){ctx=renderer.fastCtx; refPixel=referencePixelForSource(ctx.source,snap,w,h)}else{ctx=await refs.request(snap,iter,w,h);refPixel=referencePixelForSource(ctx.source,snap,w,h)}const result=await blaShadow.request({ctx,snap,iter,width:w,height:h,refPixel,samples,safety});return{...result.stats,tableBytes:result.table.byteLength,levelOffsets:result.levelOffsets,levelCounts:result.levelCounts}},
async runBlaGpuShadow({samples=49,safety=1/64}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),w=canvas.width,h=canvas.height,ctx=await fastRefs.request(snap,iter,w,h),refPixel=referencePixelForSource(ctx.source,snap,w,h),cpu=await blaShadow.request({ctx,snap,iter,width:w,height:h,refPixel,samples,safety});if(!cpu.stats.gpuShadowEligible)return{cpu:cpu.stats,gpu:{skipped:true,reason:'cpu-gate'}};const gpu=await renderer.runBlaGpuShadow({shadow:cpu,ctx,snap,iter,refPixel,safety});return{cpu:cpu.stats,gpu}},
2026-08-25 21:33:45 +09:00
async runActiveDirectShadow({chunkSize=128,previewIter}={}){if(!renderer||!renderer.frame)throw new Error('WebGPU baseline frame unavailable');const decision=state.backendDecision||chooseBackend();if(decision.deep||decision.fastExtended||decision.accurateHybrid)throw new Error('active Direct shadow requires the Direct f32 baseline');const options={chunkSize:Math.max(64,Math.min(256,chunkSize|0))};if(previewIter!==undefined)options.previewIter=previewIter;return renderer.runDirectActiveShadow(snapshot(),state.fieldView?.iter??maxIter(),options)},
async runActiveDeepShadow({width=160,height=112,chunkSize=128,previewIter,strict=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),w=Math.max(8,Math.min(512,width|0)),h=Math.max(8,Math.min(512,height|0)),ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');const refPixel=referencePixelForSource(ctx.source,snap,w,h),options={ctx,snap,iter,width:w,height:h,refPixel,chunkSize:Math.max(64,Math.min(256,chunkSize|0)),strict:!!strict};if(previewIter!==undefined)options.previewIter=previewIter;return renderer.runDeepActiveShadow(options)},
async runCertifiedInteriorShadow({width=320,height=220,samples=289}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),w=Math.max(8,Math.min(1024,width|0)),h=Math.max(8,Math.min(1024,height|0)),ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');const refPixel=referencePixelForSource(ctx.source,snap,w,h),gpu=await renderer.runCertifiedInteriorShadow({ctx,snap,width:w,height:h,refPixel,samples:Math.max(4,Math.min(4096,samples|0))});if(!gpu.proven){const result={...gpu,oracleChecked:0,falsePositive:0,inconclusive:0,gatePassed:true,productionEligible:false};runtime.certifiedInteriorShadowLatest=result;return result}const solved=await precisionFallback.request(snap,iter,w,h,gpu.provenIndices,FIELD_INTERIOR_PROVEN);let falsePositive=0,inconclusive=0;for(let i=0;i< solved.indices.length ; i + + ) { if ( ! solved . accepted [ i ] ) { inconclusive + + ; continue } if ( ( ( solved . meta [ i ] > >>28)& 3)===FIELD_ESCAPED)falsePositive++}const oracleBits=pixelPrecisionBits(snap,w,40),result={...gpu,oracleChecked:solved.indices.length,falsePositive,inconclusive,gatePassed:falsePositive===0&&inconclusive===0,oracleBits:[oracleBits,oracleBits+32],productionEligible:false}; runtime.certifiedInteriorShadowLatest=result;return result},
async compileActiveShadow(){if(!renderer)throw new Error('WebGPU renderer unavailable');await renderer.ensureActiveShadowPipelines();await renderer.ensureCorrectionPipelines();return{direct:true,deep:true,certifiedInterior:true,pixelFrontier:true,frontierWorkLimit:PIXEL_FRONTIER_WORK,productionEligible:PIXEL_FRONTIER_PRODUCTION,candidateEnabled:PIXEL_FRONTIER_CANDIDATE}},
async waitForPrimary({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&!!state.fieldView,'primary',timeout); return this.state()},
async waitForFinal({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&!state.refinePending&&state.qualityStage==='final'&&state.qualityIter===state.qualityFinalIter,'final',timeout); return this.state()},
async waitForNumericComplete({timeout=120000}={}){await this.waitForFinal({timeout});await waitForTestState(()=>!state.numericRecoveryPending& & !state.numericRecoveryRunning,'numeric recovery',timeout);const result=this.state();if(result.numericalFailures)throw new Error('numeric recovery quiesced with '+result.numericalFailures+' unresolved failures');return result},
async fieldHashes(){if(!renderer?.frame)throw new Error('WebGPU field unavailable');const field=await renderer.readFieldAll();return{pixels:field.meta.length,metaSha256:await sha256View(field.meta),smoothSha256:await sha256View(field.smoothBits)}},
gpuDiagnostics:()=>renderer?{adapterInfo:{...renderer.adapterInfo},adapterLimits:{...renderer.adapterLimits},compilation:renderer.compilation.map(x=>({...x})),uncapturedErrors:[...renderer.uncapturedErrors],lossReason:renderer.lossReason||'',activeGpuStage:renderer.activeGpuStage,lastCompletedGpuStage:renderer.lastCompletedGpuStage}:null,
2026-08-25 14:03:13 +09:00
blaGpuGate:()=>renderer?{...renderer.blaGpuGate}:null,
async runBlaGpuCanary({safety=1/64}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');if(!renderer.blaGpuGate?.armed)return{skipped:true,reason:'gate-not-armed',gate:renderer.blaGpuGate?{...renderer.blaGpuGate}:null};const snap=snapshot(),iter=maxIter(),w=canvas.width,h=canvas.height,ctx=await fastRefs.request(snap,iter,w,h),refPixel=referencePixelForSource(ctx.source,snap,w,h),cpu=await blaShadow.request({ctx,snap,iter,width:w,height:h,refPixel,samples:289,safety});if(!cpu.stats.gpuShadowEligible)return{cpu:cpu.stats,canary:{skipped:true,reason:'cpu-gate'}};const canary=await renderer.runBlaGpuCanary({shadow:cpu,ctx,snap,iter,refPixel,safety,token:state.token});return{cpu:cpu.stats,canary}},
2026-08-25 21:33:45 +09:00
stabilityPolicy:()=>({adaptiveIterationQuantum:ADAPTIVE_ITER_QUANTUM,pixelFrontierProduction:PIXEL_FRONTIER_PRODUCTION,pixelFrontierCandidate:PIXEL_FRONTIER_CANDIDATE,pixelFrontierEnabled:PIXEL_FRONTIER_ENABLED,pixelFrontierWorkLimit:PIXEL_FRONTIER_WORK,pixelFrontierMaxIter:PIXEL_FRONTIER_MAX_ITER,pixelFrontierMinimumIter:pixelFrontierIterationFloor(snapshot(),canvas.width,maxIter()),blaFastProductionOptIn:!!renderer?.blaFastOptIn,blaFastDisabled:renderer?!!renderer.blaFastDisabled:true,stableHistoryReady:!!renderer?.historyReady,fastBackgroundDeepOnFailureOnly:true,deepProductionActive:false,deepCorrectionDispatchWork:PIXEL_FRONTIER_WORK}),
state:()=>{const snap=snapshot(),se=spanMantExp(snap),reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),spanExp:se.exp,directPixelRatio:directPixelRatio(snap,canvas.width),width:canvas.width,height:canvas.height,iter:maxIter(),effectiveIter:state.fieldView?.iter??maxIter(),renderMode:state.renderMode,palette:state.palette,cycle:state.cycle,shift:state.shift,colorRevision:state.colorRevision,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,qualityStage:state.qualityStage,qualityIter:state.qualityIter,qualityFinalIter:state.qualityFinalIter,previewMs:state.previewMs,refineMs:state.refineMs,refinePending:state.refinePending,temporalFill:state.temporalFill,numericRecoveryPending:state.numericRecoveryPending,numericRecoveryRunning:state.numericRecoveryRunning,historyReady:!!renderer?.historyReady,multiRefPasses:state.multiRefPasses,multiRefRecovered:state.multiRefRecovered,frontierRounds:state.frontierRounds,frontierPixels:state.frontierPixels,frontierEscaped:state.frontierEscaped,frontierIter:state.frontierIter,frontierMinimumIter:state.frontierMinimumIter,frontierConverged:state.frontierConverged,frontierMaxDispatchWork:runtime.frontierMaxDispatchWork||0,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version,gpuStage:state.gpuStage,gpuTileRows:state.gpuTileRows,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,stageCounts:{...runtime.stageCounts},stageMs:{...runtime.stageMs},blaShadowBuilds:runtime.blaShadowBuilds,blaShadowFailures:runtime.blaShadowFailures,blaShadow:runtime.blaShadowLatest,blaGpuShadowRuns:runtime.blaGpuShadowRuns,blaGpuShadowFailures:runtime.blaGpuShadowFailures,blaGpuShadow:runtime.blaGpuShadowLatest,blaFastProbeRuns:runtime.blaFastProbeRuns,blaFastEnabledFrames:runtime.blaFastEnabledFrames,blaFastBaselineFrames:runtime.blaFastBaselineFrames,blaFastProbe:runtime.blaFastProbeLatest,blaFastDisabled:!!renderer?.blaFastDisabled,generationMetrics:runtime.generationMetrics.slice(-16).map(x=>({...x}))}},
2026-08-24 20:30:26 +09:00
async panPixels(dx,dy){pan(dx,dy);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule(); await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test pan timeout');return this.state()},
2026-08-25 21:33:45 +09:00
setBlaFastEnabled(enabled=true){if(!renderer)return false;renderer.blaFastOptIn=!!enabled;renderer.blaFastDisabled=!renderer.blaFastOptIn;if(renderer.blaFastOptIn&&renderer.blaFastCandidatePipeline&&renderer.blaFastFramePipeline)renderer.blaFastDisabled=false; runtime.blaFastProbeLatest=null;return !renderer.blaFastDisabled},
2026-08-25 14:03:13 +09:00
async compareCurrentMetaToBaseline(){if(!renderer||!renderer.frame)throw new Error('WebGPU frame unavailable');const snap=snapshot(),iter=state.fieldView?.iter??maxIter(),w=canvas.width,h=canvas.height,fastExtended=fastNeedsExtended(snap,w);let fastCtx=null;if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const baseline=await renderer.renderTileMeta({snap,iter,deep:false,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:false}),current=await renderer.readMetaAll();let mismatch=0,first=-1,ha=2166136261>>>0,hb=2166136261>>>0;for(let i=0;i< current.length ; i + + ) { const a = current[i] > >>0,b=baseline[i]>>>0;if(a!==b){mismatch++;if(first< 0 ) first = i}ha=Math.imul((ha^a) > >>0,16777619)>>>0;hb=Math.imul((hb^b)>>>0,16777619)>>>0}return{pixels:current.length,mismatch,first,currentChecksum:ha>>>0,baselineChecksum:hb>>>0,backend:state.fieldView?.backend||null,unresolved:baseline.unresolved||0}},
2026-08-25 21:33:45 +09:00
async compareCurrentToFrontierBaseline({strict=true}={}){if(!renderer?.frame)throw new Error('WebGPU frame unavailable');const snap=snapshot(),iter=state.fieldView?.iter??maxIter(),w=canvas.width,h=canvas.height,view=state.fieldView||{},deep=true,exactRequired=!!view.deep,ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch)throw new Error('frontier baseline reference guard checkpoint mismatch');const rows=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,w*iter))),baseline=new Uint32Array(w*h);let maxDispatchWork=0;for(let y=0;y< h ; y + = rows ) { const th = Math.min(rows,h-y),part=await renderer . renderTileMeta ( { snap , iter , deep , deepContext:ctx , fullW:w , fullH:h , tileX:0 , tileY:y , w , h:th , forceStrict: ! ! strict , correctUnknown:true } ) ; baseline . set ( part , y * w ) ; maxDispatchWork = Math.max(maxDispatchWork,w*th*iter)}const current = await renderer . readMetaAll ( ) , disputes = [];let mismatch = 0,classMismatch=0,escapeIterationMismatch=0,first=-1,escaped=0,baselineEscaped=0;for(let i = 0;i<current.length;i++){const a = current[i] > >>0,b=baseline[i]>>>0,ac=(a>>>28)&3,bc=(b>>>28)&3; if(ac===FIELD_ESCAPED)escaped++;if(bc===FIELD_ESCAPED)baselineEscaped++;const sameClass=(ac===FIELD_ESCAPED)===(bc===FIELD_ESCAPED),sameIter=ac!==FIELD_ESCAPED||(a&ITER_MASK)===(b&ITER_MASK); if(!sameClass)classMismatch++;else if(!sameIter)escapeIterationMismatch++;if(!sameClass||(exactRequired&&!sameIter)){mismatch++; disputes.push(i);if(first< 0 ) first = i}}let oracleChecked = 0,oracleInconclusive=0,oracleCurrentMismatch=0,oracleBaselineMismatch=0;if(disputes.length){const solved = await precisionFallback . request ( snap , iter , w , h , disputes , -1 ) ; for ( let j = 0;j<solved.indices.length;j++){if(!solved.accepted[j]){oracleInconclusive++;continue}oracleChecked++;const i = solved.indices[j],o=solved.meta[j] > >>0,a=current[i]>>>0,b=baseline[i]>>>0,oc=(o>>>28)&3,ac=(a>>>28)&3,bc=(b>>>28)&3,currentSame=(oc===FIELD_ESCAPED)===(ac===FIELD_ESCAPED)&&(!exactRequired||oc!==FIELD_ESCAPED||(o&ITER_MASK)===(a&ITER_MASK)),baselineSame=(oc===FIELD_ESCAPED)===(bc===FIELD_ESCAPED)&&(!exactRequired||oc!==FIELD_ESCAPED||(o&ITER_MASK)===(b&ITER_MASK)); if(!currentSame)oracleCurrentMismatch++;if(!baselineSame)oracleBaselineMismatch++}}const oracleGate=oracleCurrentMismatch===0&&oracleInconclusive===0; return{pixels:current.length,mismatch,classMismatch,escapeIterationMismatch,exactRequired,first,escaped,baselineEscaped,iter,baseline:'deep-gold',oracleChecked,oracleInconclusive,oracleCurrentMismatch,oracleBaselineMismatch,oracleGate,maxDispatchWork,workLimit:PIXEL_FRONTIER_WORK,gatePassed:oracleGate& & maxDispatchWork< =PIXEL_FRONTIER_WORK}},
2026-08-25 14:03:13 +09:00
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w):!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w); let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep& & correctUnknown}))},
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w),fastExtended=!deep&&fastNeedsExtended(snap,w); let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
2026-08-23 23:27:38 +09:00
};
2026-08-25 21:33:45 +09:00
async function runAutonomousFrontierGate(){
const mode=new URLSearchParams(location.search).get('frontier-gate');if(!mode)return;const profileOnly=mode.startsWith('profile-'),caseMode=profileOnly?mode.slice(8):mode,cases=[{id:'reset',re:'-0.5',im:'0',span:'3.4',bits:256,baseIter:256,adaptive:false,processMode:'validate',renderMode:'fast'},{id:'seahorse',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',bits:448,baseIter:512,adaptive:false,processMode:'validate',renderMode:'accurate'},{id:'real-axis',re:'-0.75',im:'0',span:'0.02',bits:384,baseIter:512,adaptive:false,processMode:'validate',renderMode:'accurate'},{id:'reported',re:'-29466147000382485924219538765424656674342940730553342389512209954887705938978413587',im:'5179018789925933872641942859702187326563010594882200888329237785122225251258352',span:'250637324516887125119843167990565159991251418774166207381437546036409',bits:273,baseIter:350,adaptive:true,processMode:'standard',renderMode:'accurate',fixed:true}],selected=caseMode==='all'?cases.filter(x=>!x.fixed):[cases.find(x=>x.id===caseMode)||cases[0]],records=[];
try{const gpu=await globalThis.__MANDEL_TEST__.ensureGpuReady();if(!gpu.ready)throw new Error('WebGPU readiness gate failed: '+JSON.stringify(gpu));for(const view of selected){const started=performance.now();await globalThis.__MANDEL_TEST__.setView(view);await globalThis.__MANDEL_TEST__.waitForNumericComplete({timeout:240000});const comparison=profileOnly?{gatePassed:true,profileOnly:true}:await globalThis.__MANDEL_TEST__.compareCurrentToFrontierBaseline({strict:true}),current=globalThis.__MANDEL_TEST__.state(),diagnostics=globalThis.__MANDEL_TEST__.gpuDiagnostics(),errors=(diagnostics?.compilation||[]).flatMap(x=>x.messages||[]).filter(x=>x.type==='error'),pass=comparison.gatePassed&¤t.numericalFailures===0&¤t.frontierConverged&¤t.frontierMaxDispatchWork<=PIXEL_FRONTIER_WORK&&!errors.length&&!(diagnostics?.uncapturedErrors?.length)&&!diagnostics?.lossReason; records.push({id:view.id,elapsedMs:performance.now()-started,pass,comparison,state:current,diagnostics})}const result={pass:records.every(x=>x.pass),production:PIXEL_FRONTIER_PRODUCTION,workLimit:PIXEL_FRONTIER_WORK,gpu,records};document.documentElement.setAttribute('data-frontier-gate',result.pass?'pass':'fail');document.body.textContent=JSON.stringify(result)}catch(error){const result={pass:false,error:String(error&&error.stack||error),gpu:{navigatorGpu:!!navigator.gpu,renderer:!!renderer,gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError},state:globalThis.__MANDEL_TEST__?.state?.(),diagnostics:globalThis.__MANDEL_TEST__?.gpuDiagnostics?.(),records}; document.documentElement.setAttribute('data-frontier-gate','error');document.body.textContent=JSON.stringify(result)}
}
2026-08-23 23:27:38 +09:00
// ── boot / teardown ──────────────────────────────────────────────────────
2026-08-25 21:33:45 +09:00
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();blaShadow.destroy();refs.destroy();fastRefs.destroy();precisionFallback.destroy();for(const service of precisionFallbackPool)service.destroy();if(renderer)renderer.destroy()},{once:true});
2026-08-24 20:30:26 +09:00
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-25 21:33:45 +09:00
setTimeout(runAutonomousFrontierGate,0);
2026-08-23 23:27:38 +09:00
})();
< / script >
2026-08-22 20:55:27 +09:00
< / body >
< / html >