Compare commits

...

3 commits

Author SHA1 Message Date
83b874c2a0 b 2026-08-25 21:33:45 +09:00
b653d77d4e q 2026-08-25 14:03:13 +09:00
0e1c1f04fd slim 2026-08-24 20:30:26 +09:00
180 changed files with 3646 additions and 17625 deletions

206
AUDIT.md
View file

@ -1,206 +0,0 @@
# v18 数値・性能監査
> **Archive notice:** この監査はv18時点の履歴資料です。現行renderer v23の受入値ではありません。v23のsource/配布baselineは`audit/v23-source-baseline.json`、実行時browser baselineは`audit/v23-browser-baseline.json`へ分離します。
## 目的
v7〜v17で積み重なった補正処理を「残っているから残す」のではなく、正しさ・速度・相互作用を測定して再判定した。
絶対時間は実行環境に依存するため、**誤判定率・同条件での比率・処理の計算量**を主な判断材料とする。
## 結論
| 処理 | v18判断 | 理由 |
|---|---|---|
| Arbitrary precision + Reference Orbit | 維持 | deep zoomの座標精度と基準軌道に必須 |
| Perturbation + Rebasing | 維持 | 全pixelをBigIntで直接計算するより圧倒的に有利 |
| BLA | 維持・監視強化 | deep pixel kernelの主高速化手段。ただし適切なεは画面依存 |
| Persistent Worker / Reference | 維持 | warm stateの価値が大きい |
| 固定距離によるReference維持 | 改廃 | 距離だけではrecenterの損得を判定できない |
| Pilot render | 縮小して維持 | コスト予測専用なら有用。画素の代用には不正確 |
| 数学的iteration上限を途中で切るwork cap | **廃止** | 黒/外部分類を壊す |
| Pilotのnearest-neighborで未解決pixelを補完 | **廃止** | 境界で誤差が大きい |
| coarse periodic interior mask | **廃止** | 効く画面が偏り、pilotコストも増える |
| 全strip strict verification | **廃止** | deep zoomで隠れた巨大ボトルネック |
| 1点の誤差で全画面strict再描画 | **廃止** | 局所誤差に対して再計算範囲が過大 |
| 複数の解像度feedback controller | **統合** | controller同士が競合して粗いまま固定されることがあった |
| Dither | **廃止** | detail detectorの勾配を人工的に増やし、余計な高精細tileを誘発 |
| Adaptive detail refinement/cache | 維持 | idle時へ遅延すれば費用対効果が良い |
| Autoの黒/色境界追従 | 維持 | kernelと独立で軽く、誤航行抑制に有用 |
| Synthetic Worker Wisdom | **廃止** | 実際のWASM+BLA負荷と相関しにくい |
| Real BLA Wisdom | 維持 | 実kernelを使ってWorker数を選べる |
## 1. v17のiteration work capは数学的に不正確
v17はdeep base描画で、表示用global iterationより小さい`computeIter`を使い、そこまで発散しなければ事実上黒候補として扱っていた。
Seahorse付近、48×30、global 3490 / cap 2870をBigInt直接計算で照合した。
- capまで残ったpixel: **1440 / 1440**
- その後3490までに発散したpixel: **1066**
- cap survivor中のlate escape: **74.03%**
つまり「2870で未発散だから黒」とみなすのは、この画面では4分の3近くを誤分類する。
**v18:** `computeIter = colorIter`へ戻した。負荷制限は数学的iteration数ではなく、BLA operation / perturbation operationの回数に対して行う。上限到達は`UNRESOLVED`であり黒ではない。少数なら局所再計算、多ければそのstripだけ無制限BLAへ戻す。
Raw: `audit/raw/workcap_truth_v17.json`
## 2. Pilot画像による未解決pixel補完は不正確
128×78のfull BLAと56×34 pilotのnearest-neighbor補完を比較。
- 黒/外部分類の不一致: **451 / 9984 = 4.52%**
- escape iteration差が8超: **43.71%**
境界付近では低解像度pilotを「計算結果」として使えない。
**v18:** pilotはコスト推定にだけ使う。画面へ表示せず、未解決pixelの色・黒判定にも使わない。
Raw: `audit/raw/pilot_fill_audit.json`
## 3. Interior detectionは「追加反復」ではなく早期終了で入れる
以前の黒専用look-aheadは、黒pixelへ追加iterationを課すため重かった。v18では逆に、内部と判定できたpixelを**早く終了**する。
実装:
- main cardioid / period-2 bulbの解析判定
- 軌道微分の収縮判定
- BLA jump時はBLAの線形係数で微分も更新
96×72、2500 iterationでInterior detection OFF/ONを比較。黒/外部分類不一致は全テスト0。
| 場所 | OFF | ON | 分類不一致 |
|---|---:|---:|---:|
| period-3 wide | 766.7 ms | 6.0 ms | 0 |
| period-3 mid | 438.7 ms | 2.3 ms | 0 |
| period-2近傍 | 364.7 ms | 1.0 ms | 0 |
| mixed interior/exterior | 282.8 ms | 16.2 ms | 0 |
| cardioid cusp近傍 | 329.9 ms | 1.8 ms | 0 |
これは「黒を正しくするために重くする処理」ではなく、黒の多い画面を軽くする処理として残す価値が高い。
Raw: `audit/raw/interior_safety_v18.json`
## 4. BLA εは単一固定値にできない
Seahorse z≈14、160×98で保守的BLAを基準に比較。
| ε | 時間 | class mismatch | iteration差>1 |
|---|---:|---:|---:|
| 2^-23 | 約299 ms | 1.00% | 5.23% |
| 2^-32 | 約786 ms | 0.383% | 1.16% |
| 2^-40 | 約1218 ms | 0.083% | 0.198% |
一方、同じSeahorseでもz≈20では2^-23が基準と一致したテストがある。したがって「常に厳密」「常に緩い」の両方が非効率。
**v18:** fast BLAを使い、1フレームあたり固定少数点だけsafe BLAと照合する。誤差が出たstripだけsafe BLAで再計算する。
- 通常の検証はstrict perturbationではない
- 1stripごとに何点もstrict計算しない
- 1点の不一致で全画面をstrict再描画しない
Raw: `audit/raw/audit_v18_kernel.txt`
## 5. per-strip strict verificationは隠れたボトルネックだった
旧設計では各stripのBLA後にstrict perturbationを複数点実行していた。非常に深い場所ではBLA本体が数msでも、strict検証が毎strip積み重なりwall timeを支配する。
v18の設計:
1. fast BLAでstrip計算
2. **フレーム全体で固定個数**だけsafe BLAと比較
3. mismatchがあれば**そのstripだけ**safe BLAで再計算
4. status failureの少数pixelだけexact fallback
これで検証コストが「strip数 × deep iteration」に増えにくくなった。
## 6. Reference recenterは距離だけで決めない
z≈14ではrecenterが有利なケースが多かった。
- 0.8画面横移動: first-frameで約**91 ms節約**
- 0.8横+0.5縦: 約**40 ms節約**
しかしz≈20では、0.5画面横移動でrecenterが約**14.5 ms損**するケースもあった。
同じ「0.5画面離れた」でも損得が逆転するため、固定距離閾値は合理的でない。
**v18:**
- Referenceは基本的に維持
- 実測kernel MPPが基準より悪化したときだけ、予測損失とReference rebuild EMAを比較
- rebuildの方が安いと予測できる場合だけrecenter
- 極端に離れた場合だけhard safety limitを使う
Raw: `audit/raw/reference_recenter_audit.json`, `audit/raw/reference_recenter_audit_z20.json`
## 7. Resolution controlを一本化
過去版では以下が同時に解像度へ作用していた。
- pilot推定
- renderPerf MPP
- deepScale
- Device WisdomのresScale
- 品質段階ごとのmin/max width
複数feedbackが同時に動くため、負荷変化後に過剰縮小したり、逆に最低幅に張り付いたりしやすかった。
**v18:** deep base resolutionは一つの時間予算モデルで決定する。pilotまたは最近の実測MPPを入力にし、detail tileは別のidle品質レイヤーとして扱う。
## 8. Ditherをdetail判定の前に入れない
Ditherはグラデーションbandingには効くが、画像のRGB勾配を人工的に増やす。detail detectorの入力へ先に適用すると「存在しない細部」を検出して追加tileを計算する。
**v18:** runtime ditherは削除。Canvasのhigh-quality scalingは維持。再導入するならdetail選定後の表示/export段階だけにする。
## 9. Real BLA Wisdomは残す
旧Synthetic Wisdomは単純なJS浮動小数点loopでWorker数を選んでいた。実際の負荷はWASM、BLA table、reference memory、postMessageを含むため別物。
**v18:** 長時間idle時のみ実際のBLA mini renderを1〜4 Workerで測定し、実スループットからWorker数を決める。解像度feedbackとは切り離した。
## 10. v17→v18 end-to-end確認
同一のclean Chromium 144 headless、780×441 viewportで、UI操作に相当するdeep base passを比較した。動的解像度を含む**実際のbase-frame pipeline比較**であり、pixel-for-pixel kernel benchmarkではない。
| Scene | v17 | v18 | 改善 |
|---|---:|---:|---:|
| Seahorse z≈14 | 1059 ms | 288 ms | **3.68×** |
| Seahorse z≈20 | 1226 ms | 238 ms | **5.15×** |
| Seahorse z≈100 | 983 ms | 474 ms | **2.07×** |
| period-3 z≈20 | 1931 ms | 781 ms | **2.47×** |
v18のclean runではruntime exception 0。
Raw: `audit/raw/v17-final-browser-benchmark.json`, `audit/raw/v18-final-browser-benchmark.json`, `audit/benchmarks.json`
## 11. 残した処理
次はデータ上、削る理由が弱いので維持した。
- BigInt fixed-point coordinate
- Reference Orbit cache + precision promotion
- Perturbation / Rebasing
- BLA
- Series Approximation(主にfallback側)
- Persistent Worker
- smooth coloring LUT
- Auto black/colour boundary tracking + safe rollback
- world-space HQ tile cache
- progressive detail refinement
- high-quality Canvas scaling
## 12. 今後のボトルネック
v18ではpixel hot-loopの補正処理を大幅に整理した。さらに深い地点で残る主候補はReference OrbitのBigInt生成である。
特にReferenceが長く、かつ高bit精度になるケースでは、次の候補は:
1. Reference Orbit専用Worker
2. 次Referenceの先読み / double buffer
3. Reference orbitの圧縮・周期利用
ただしz≈20程度ではReference生成が常に主因ではないため、常時複雑化するよりtelemetryで必要なときだけ有効化する方がよい。

View file

@ -1,182 +0,0 @@
# v24.2.3 crossover-benchmark audit
## 判定
静的contract、Node数値モデル、build/reproducibility gateはPASS。実WebGPU adapterでのv24.2.3 compile/dispatchはこのコンテナではadapterを取得できないため未実行であり、`tests/webgpu-acceptance.html`を外部実機gateとして残す。
## 主な修正
1. 深部の黒面積/黒点
- UNKNOWN reasonを保持。
- guarded f32 primary後にUNKNOWN限定double-single correctionを追加。
- interior黒とUNKNOWN表示を分離。
2. 標準描画範囲
- direct/deep overlapを導入。
- current-view quality/speed probeを本描画前に実行。
- deepが15%以上高速な場合だけdeepを採用。
3. Multi-reference
- 全画面モデルで費用対効果が低いため撤去。
4. Export
- 1×/2×2 deep ExportにもDS correctionを統合。
- 2×2はsample別unresolved counterを使用。
5. 旧BLA
- membership反転の既知反例があるため引き続き未搭載。
## CPU numeric model results
### Primary guarded perturbation
canonical 17×11 corpus:
- z14: false escaped 0 / false bounded 0 / UNKNOWN 0
- z20: 0 / 0 / 0
- z100: 0 / 0 / 0
- period3: 0 / 0 / 0
- e280: 0 / 0 / 0
- e400: 0 / 0 / 0
- swirly: false escaped 0 / false bounded 0 / UNKNOWN 49 of 187
Dense known regression pixels `(25,12)`, `(26,15)`, `(27,25)`はprimaryでは誤分類せずUNKNOWNへ退避。
### Sparse double-single correction
`swirly-seahorses-z12`, 61×39, 2000 iter:
- oracle guard mismatch: 0
- primary UNKNOWN: 478 / 2379
- primary false escaped: 0
- primary false bounded: 0
- corrected: 478
- remaining: 0
- correction false escaped: 0
- correction false bounded: 0
- escape iteration mismatch: 5
- max escape iteration difference: 374
DS passはfinite-iteration visual classification recoveryとして有効だが、escape iteration equalityは保証しない。
## Router contract
- direct coordinate floor: ratio 8
- no-probe direct margin: ratio 128
- overlap: 8–128
- deep required speed advantage: 15%
- corrected deep probe UNKNOWN max: 0.5%
- class disagreement tolerance: 0
- current view probe required; depth profile aloneではrouteしない
## Performance/迂遠処理監査
- full-frame high-precision CPU renderer: なし
- multi-reference reference build loop: なし
- redundant primary→same-f32 strict retry: なし
- BLA precompute/evaluate: なし
- deep correction: full dispatchだがKNOWN pixelは即return。追加reference生成なし。
- Export: reusable 512² workspace、1 readback/tile。
- 2×2 Export: 4 numerical samplesを1 command stream内で処理し、1 resolve/1 readback。
- presentation/recolor: GPU field常駐。
## Real WebGPU acceptance
`tests/webgpu-acceptance.html`は以下を要求する。
- production 6 WGSL + benchmark DS Direct 1本の計7 WGSL modules compile/pipeline creation
- stable corpus corrected UNKNOWN = 0 on sampled pixels
- BigInt P/P+64 guard mismatch = 0
- false escaped / false bounded = 0
- dense 3 regression pixelsをDS correctionでKNOWNへ回収
- overlap view-specific routerがquality gateを通過し、15% speed ruleどおりroute
- 1× / 2×2 Export unresolved = 0
- uncaptured WebGPU validation/runtime error = 0
この環境では`navigator.gpu`/adapterを実利用できないため、上記hardware gateのみ未判定。
## v24.2.3 crossover追加監査
- DS Direct WGSLはproduction routerへ未接続。
- Round-depth benchmark: z0,2,4,6,8,10,12,14,16,20,30,40。
- CPU BigInt oracle sweep: `audit/v24-crossover-quality.json`。
- Seahorse / 1600×900相当ではf32 material degradation ≈ z6、DS ≈ z10。
- corrected Deep CPU modelはz8で1/315 false escapeを検出したため、依然membership certificateではない。
- 実GPU timingはこのcontainerではWebGPU adapter不成立のため未測定。実機では診断情報→Backend比較で取得する。
## v24.2.3 z12 benchmark hang hotfix
ユーザー実機でBackend比較が `z12 · deep` から終了しない事象を確認。原因はproduction rendererの無限loopではなく、benchmarkが現在の全画面をDeep primary + DS correctionでwarmup 1回 + 2〜5回反復し、各回を単一の大規模GPU jobとして待っていたこと。1600×900級・z12では1測定だけでも最大10億回規模の反復候補となり、queue completion待ちが極端に長くなり得た。
修正:
- speed benchmarkを代表3tile(最大256×144)へ縮小。
- actual full-resolution座標を維持したtileX/tileYで測定。
- aggregate `ms/pixel` からcurrent canvasのfull-frame時間を換算。
- quality gateは従来どおり5 spatial tiles。
- GPU batch/reference buildに15 s watchdog。timeout時は明示的errorとし、GPU deviceを破棄して再初期化可能にする。
- progress表示を `deep sample 2/3` のように細分化。
- crossover JSON formatをv2へ更新し、`speedTiles`, `speedPixels`, `msPerPixel`, `fullMedianMs`を記録。
このhotfixはproduction backend routing/rendering mathを変更しない。
## v24.2.4 Hybrid experiment preparation
- Added benchmark-only `DIRECT_DS_GUARDED_WGSL`; production router is unchanged.
- Guard metric: approximate `|dz/dc|`; threshold sweep `1e12 / 1e13 / 1e14`.
- Added `benchmarkHybrid()` / `renderTileMetaHybrid()` using the existing Deep double-single correction only for DS-marked UNKNOWN pixels.
- Added z6/z8/z10/z12 experiment UI and JSON export.
- Added CPU BigInt P/P+64 risk-model gate. At risk=1e13, observed DS class errors are all caught at z6/z8/z10 with correction rates 2.8% / 11.1% / 34.6%; z12 correction rate is 91.2%, intentionally indicating the Full-Deep crossover.
- Added real-WebGPU acceptance smoke for the Hybrid diagnostic path.
## v24.2.5 Sparse Direct experiment (Audit)
- The previous sparse-Full-Deep diagnostic is superseded and is not exposed in the UI.
- Added `DIRECT_TS_CORRECT_WGSL`, a reference-free three-term f32 expansion correction shader that only processes DS sensitivity UNKNOWN pixels.
- Added `benchmarkSparseDirect()` / `renderTileMetaSparseDirect()` and a z6/z8/z10/z12 risk sweep.
- Full Deep remains the quality/timing baseline; the Sparse Direct candidate itself does not build or bind a reference orbit.
- Production backend routing is unchanged.
- Root `index.html` is now a single-file bundle with no external script dependency, specifically to keep diagnostic UI controls functional when the HTML is opened from an archive/temp extraction.
- Static single-file UI contract and real-browser click smoke are separate gates; real WebGPU arithmetic still requires a WebGPU-capable browser.
- Sparse Direct coordinate packing uses exact residual subtraction from BigInt fixed-point into three f32 components; this removed the CPU TD-model class errors caused by the earlier host-Number split.
## v24.2.6 Fixed96 experiment audit
- v24.2.5 three-term f32 expansion correction is removed from the active experiment because the real-GPU quality gate produced no viable candidate at z6/z8/z10/z12.
- New correction shader: `DIRECT_FIXED96_CORRECT_WGSL`.
- Arithmetic is signed Q8.88 in three `u32` limbs; no floating-point expansion is used by the correction pass.
- Exact BigInt ViewState coordinates are converted directly to Q8.88 tile base + pixel step on the host; no JS `Number` coordinate round-trip is used.
- `tests/v24-fixed96-limb-model.mjs` verifies the limb multiplication/round-shift algorithm against BigInt over 20,000 deterministic products.
- `tests/v24-fixed96-model.mjs` verifies the DS sensitivity mask + Q8.88 correction against the BigInt P/P+64 oracle on a 1600×900-equivalent Seahorse grid. Safe thresholds exist at z6/z8/z10/z12 in the CPU preflight.
- Experiment output reports every risk threshold, including rejected candidates, with correction rate, remaining UNKNOWN, class mismatch and timing. The former `best risk = none` information loss is removed.
- Production routing/rendering remains unchanged.
- Root `index.html` remains a single-file bundle; the Fixed96 button handler is embedded and has no sibling-JS dependency when opened from an extracted ZIP.
- Real-GPU WGSL compilation/timing remains the decisive acceptance gate.
## v24.2.7 Fixed96 range hotfix audit
- Root cause: `fx_bad_range()` incorrectly required the Q8.88 top integer byte to be `0x00` or `0xff`, rejecting valid values with magnitude >= 1.
- Symptom match: real-GPU experiment reported `correction ~= remain` across z6-z12.
- Fix: remove the invalid sign-extension test; use a certain-escape component test at `|component| > 2` before fixed-point magnitude squaring.
- Added BigInt coordinate range/step-underflow checks before GPU packing.
- Added regression tests for valid Q8.88 values above 1, 20,000 limb multiplication cases, and full limb-orbit iteration equality against exact BigInt Q8.88.
## v24.2.8 Fixed96 true sparse queue audit
Purpose: remove the v24.2.7 full-screen Fixed96 dispatch and make the diagnostic correction genuinely proportional to risky-pixel count.
Implementation checks:
- DS risk shader appends local pixel indices to a storage queue with atomics.
- queue capacity equals tile pixel count; overflow is still counted and treated as fatal.
- a dedicated finalize shader writes indirect dispatch arguments from the GPU-written queue count.
- Fixed96 uses `@workgroup_size(64)` and `dispatchWorkgroupsIndirect`; it no longer launches 8x8 workgroups over the full tile.
- Fixed96 validates queue indices and verifies the queued pixel is still UNKNOWN(reason 7).
- benchmark and quality paths both run `fixed96QueueIntegrity()` and fail closed on any counter/invariant mismatch.
- static queue-integrity model covers zero, 20, 65-entry, overflow, wrong-indirect, processed mismatch and stale-entry cases.
- Q8.88 limb multiplication/orbit tests remain unchanged and passing; this revision changes scheduling only.
Real-GPU timing remains the decisive performance gate.

View file

@ -1,51 +0,0 @@
# WASM build reproducibility
v23では、`src/`を監査可能なkernel source、`src/abi.json`を公開ABI、
`toolchain.lock.json`を固定toolchain契約として扱います。
## 互換payloadを展開する
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/extract-wasm.ps1
```
現在配布している8 payloadを`dist/wasm/`へ展開し、byte数とSHA-256を
`manifest.json`へ記録します。COLOR SIMD/scalarがbyte単位で同一ならStandaloneでは
Base64文字列を1本へ自動集約し、異なる場合は各payloadを保持します。どちらの場合もmanifest上の2 ABI名は維持します。
## sourceから再生成する
clang 17.0.6をPATHへ置き、次を実行します。
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-wasm.ps1
```
生成先は`build/wasm-v23/`です。`src/shallow_kernel.c`、
`src/deep_kernel.c`、`src/bla_kernel_v18.c`、`src/color_kernel.c`をそれぞれ
SIMD/scalarでcompileし、timestampを含まないsource manifestを生成します。
配布payloadへ昇格するときだけ`-Promote`を指定します。昇格前に
`tests/kernel-golden.mjs`が、画素中心座標、escape iteration、magnitude、
smooth補正、SIMD/scalar同値性を検査します。golden失敗時はコピーも
`kernels.js`再生成も行いません。
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-wasm.ps1 -Promote
```
## 信頼境界
clang 17.0.6とNode.js v22.18.0を作業領域内で使用し、復元sourceのcompile、
generated-WASM golden、SIMD/scalar同値性、画素中心契約を実行済みです。tool配布archive自体の
URL/SHA-256は現repositoryに保持されていないため、「公式archiveのhash照合」は再監査可能な証拠には
含めません。再監査できる信頼境界はversion/flags、source hash、生成manifest、golden結果です。
Provenance status: tool archive hash not retained.
golden合格後に8 payloadと`kernels.js`を昇格し、Hosted/Standalone成果物も再生成しました。
実測結果と生成manifestのSHA-256は`audit/v23-source-baseline.json`へ記録しています。
省略なしの再検証は、toolchainをPATHへ追加した状態で次を実行します。
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/test-all.ps1 -RequireToolchain
```

View file

@ -1,55 +0,0 @@
# Mandelbrot Deep Zoom v23 完了監査
## 判定
`IMPROVEMENT_PROPOSAL.md`のうち、Phase 0Aと計測結果に依存しないPhase 1〜3Aの実装項目はコード、build、非ブラウザ試験へ反映した。source/ABI/数値/pixel mapping/WASM Module clone/配布物の保存済みgateは合格している。v22成果物が残っていないため、v22比較項目は`not-verifiable`でありpassには含めない。
ただし、固定実機を必要とするbrowser acceptanceは実行環境にin-app Browserが存在しないため未実施である。性能修正後に再開した接続監査でも3回連続のgoal turnで利用可能Browser 0件となり、2026-08-22T10:48:56.1197927Zに外部状態待ちのblockerとして再確定した。Node固定sceneではdeep BLAの高いMPPを確認し、標準deepを約1.4秒の実測時間予算へ変更済みだが、これはCanvas、Worker、入力、downloadを含むbrowser gateの代替ではない。またPhase 3BとPhase 4は、提案書自身がbrowser telemetryで支配コストまたは誤差を確認した場合だけ実施すると定義した条件付き項目であり、trigger判定も未実施である。したがって本監査は「実装・非ブラウザgate合格、実機受入blocked」と判定する。
## 要件トレーサビリティ
| 提案 | 実装結果 | 主な証拠 |
|---|---|---|
| 6.1 イベント駆動 | 常時RAFを廃止。paint/statsをinvalidate時だけ予約し、非表示時はscheduler・render・refine・Exportを停止 | `script.js`の`scheduleFrame`、`requestScheduler`、`stopSchedulers`、`visibilitychange`。`tests/source-contract.ps1` |
| 6.2 Canvas/計算grid分離 | 処理モード別Screen pixel budgetとeffective DPRを導入。Previewは110ms予算、cold deepは幅48px以下。標準shallowは2〜4MP、標準deepは実測MPPから約1.4秒予算へ調整、精細は4〜8MP | `screenPixelBudget`、`adaptStandardDeepBudget`、`resize`、`targetSize`、`RENDER_PROFILE` |
| 6.3 gesture/取消 | wheel 110ms、pointer 90ms settle。操作中は再投影。省電力はPreviewで停止し、標準はCovered後の追加処理なし。精細の追加反復はescape境界候補だけをdeep 384/shallow 4,096点まで実行。非共有環境は短いtile/chunk | gesture handlers、`nextQualityDue`、`scheduleUnknownContinuation` |
| 6.4 deep遅延生成 | main deep WASM、Module、Worker、referenceを数値条件付近まで生成しない。1 Workerから実測増員。実job wisdomを端末・kernel keyで永続化 | `prewarmDeepAssets`、`prepareDeepModules`、`addDeepWorker`、`deepWisdomStorageKey` |
| 6.5 WASM/memory | compile済みdeep/BLA/color Moduleを一度だけ作りWorkerへclone。field buffer返却、byte ledger、Worker退役を実装。1 module/1 shared memory統合はPhase 3Bの条件付き項目 | `compileKernelPair`、Worker `init`、`memoryLedger`、`retireDeepAssets`、`tests/module-clone.mjs` |
| 6.6 byte cache | fixed countを廃止し、mobile 96 MiB/desktop 192 MiBのmanaged byte ledgerとLRUへ変更 | `rendererMemoryBudget`、`detailCacheBudget`、`trimDetailCache` |
| 6.7 Hosted/Standalone | content hash外部WASM+streaming compileのHosted、3ファイル直開きのStandaloneを生成。deep requestは深部到達前0の契約 | `hosted-loader.js`、`scripts/build-hosted.ps1`、`scripts/build-standalone.ps1`、`dist/` |
| 6.8 再現build | 4 kernel source、ABI、固定clang flags、SHA-256 manifest、SIMD/scalar goldenを整備 | `src/`、`toolchain.lock.json`、`BUILD_REPRODUCIBILITY.md`、`build/wasm-v23/manifest.json` |
| 7.1 画素中心 | JS、BigInt、shallow/deep/BLA WASM、detail、Exportを`x+0.5,y+0.5`へ統一しrenderer v23化 | `tests/pixel-mapping.mjs` 851 sample、`tests/pixel-contract.mjs` 1,440 sample |
| 7.2 adaptive AA | 全域sample後、数値fieldの分類・分散・confidenceでquadtree 2×/4× subsample。linear RGBでresolve | `scoreDetailTiles`、`resolveSubsampleField` |
| 7.3 field/color分離 | state、smooth、iteration、confidenceを保持。境界subsample fieldも保持。palette変更は再着色だけ | `makeField`、`colorizeField`、`recolorCurrentField` |
| 7.4 BLA/detail精度 | Covered/detailで同一precision profile。高scoreまたはValidated detailはguarded direct。同一world座標の数値gateと視覚gateを分離 | `blaProfile`、`sendDeepDetailRect`、pixel tests |
| 7.5 未確定分離 | `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を分離。反復を局所継続し、未確定数を表示・Export metadataへ記録 | field constants、`scheduleUnknownContinuation`、`runValidation` |
| 7.6 誤差ベース精度 | pixel step/ULP/orbit telemetry+hysteresisでengine選択。round-to-nearest fixed point。P/P+64 directとreference checkpointを照合し、不一致時はglobal reference再構築 | `deepEngineNeeded`、`roundShift`、`highPrecisionDirectPixelAsync`、`verifyReferenceCheckpoints` |
| 7.7 超深部 | 上限と未確定を診断表示。scaled BLA、multi-reference、可変参照長はPhase 4のtelemetry条件付き研究 | 診断UI、下記「条件付き項目」 |
| 7.8 Export | Quick snapshotと独立tiled Exportを分離。1×/2×/4×/custom、AA、Balanced/Validated、進捗、取消、全tile後encode、JSON sidecar | `runExport`、`tests/browser-benchmark.html` |
| 8/9 改廃・UX | 連続`q`、固定DPR/HQ幅、RGB detail、固定deep閾値、eager pool、件数cacheを廃止。ViewSpec、history、正確座標、初回UI、mobile status、Advanced、accessibilityを実装 | `index.html`、`script.js`、`tests/source-contract.ps1` |
## Gate結果
| Gate | 結果 |
|---|---|
| JavaScript本体/生成Worker/browser harness構文、mock DOM初期化 | pass |
| deep/BLA/color `WebAssembly.Module` structured clone・instantiate | pass |
| 256/320-bit direct agreement、analytic interior整数証明 | pass |
| reference P/P+64 checkpoint agreement・故意の不一致検出 | pass |
| Node固定scene計測/予算式契約 | pass。shallow 2 sceneと、出典付きのswirly-seahorses混在境界sceneでdeep BLAを測定し、deep標準を固定画素数から実測時間適応へ変更。保存値は端末負荷に依存する特性値であり、時間上限そのものをassertするgateではない |
| 全backend pixel mapping・2×/4× tile seam | pass、851 sample |
| 配布shallow SIMD pixel contract | pass、1,440 sample、mismatch 0 |
| kernel source/ABI、payload checksum、Hosted/Standalone build | pass |
| 実ブラウザ performance/visual/interaction/Export/accessibility | not-run。Browser bindingなし |
機械可読なsource基準値は`audit/v23-source-baseline.json`、未実施browser gateは`audit/v23-browser-baseline.json`に記録した。
追加のbrowser非依存監査は`audit/v23-browserless-baseline.json`へ保存した。固定Node.js v22.18.0で7 executable tests、source/document contract、配布hashを再実行してpassしたが、scopeは`browserless-current-artifacts`、`fullAcceptance: false`である。v22比較は`not-verifiable`、source-WASM buildはprior evidence reused、runtimeは測定値であり性能合格ではない。
## 条件付き項目
Phase 3Bの1 shared memory統合、immutable reference/BLA table共有、WASM hot loopのatomic cancel、およびPhase 4のdouble-double、interval/error ball、scaled BLA、multi-reference、WebGPUは無条件要件ではない。`IMPROVEMENT_PROPOSAL.md` 3.3、7.7、Phase 3B/4の規定どおり、固定browser計測でcopy、memory、10^-280以深、glitch、reference生成のどれが支配的か判明した場合だけ着手する。現在はModule clone、buffer pool、短いchunk、guarded directで安全な前段を実装済みだが、telemetryがないため条件成立を主張しない。
## Browser受入の再開条件
`tests/browser-benchmark.html`をHTTPで開けるin-app Browserを接続し、mobile/desktop/4Kを要求DPRの別contextで、Hosted/Standalone別に実行する。runnerはraw pixel golden、30回以上のPreview、Covered/実Refined、idle write、long task、managed/取得可能ならUA memory、deep request、非zero wheel paint、再着色、Export完了/取消/再現性、keyboard操作を採取する。observed GPU memory、visible focus、page zoom/pinch、screen-reader順序の外部証拠と統合し、全profile・全buildが揃った時点で固定実機の最終受入を判定できる。

View file

@ -1,62 +0,0 @@
# v24.2.3 Crossover Benchmark
## 目的
標準 `f32 Direct` が十分に速いのにBigInt referenceを伴うDeepへ早く切り替わっている可能性を検証する。Deepを速度目的ではなく「品質上必要になった地点」でのみ使えるよう、`f32 Direct / experimental DS Direct / corrected Deep`を同じ深度列で比較する。
## 品質モデル
- Seahorse center: `-0.7453983606667815 + 0.1125046349959942i`
- viewport: 1600×900。21×15点を画面全域から間引き、隣接21 pixelの座標分解能も別途検査。
- 深度: z0,2,4,6,8,10,12,14,16,20,30,40。`span = 3.4×10^-z`。
- oracle: BigInt fixed-point P / P+64一致。
- Deep: guarded rescaled f32 perturbation + UNKNOWNのみDS correction。
- DS Direct: GPU候補と同じdouble-single arithmeticのCPU `Math.fround`モデル。
| z | f32 class error | f32 adjacent unique | DS class error | DS adjacent unique | Deep error/UNKNOWN |
|---:|---:|---:|---:|---:|---:|
| 0 | 0.00% | 21/21 | 0.00% | 21/21 | 0 / 0 |
| 2 | 0.63% | 21/21 | 0.00% | 21/21 | 0 / 0 |
| 4 | 0.63% | 21/21 | 0.00% | 21/21 | 0 / 0 |
| 6 | 1.59% | 2/21 | 0.32% | 21/21 | 0 / 0 |
| 8 | 1.27% | 1/21 | 0.63% | 21/21 | 1 / 0 |
| 10 | 3.17% | 1/21 | 3.81% | 21/21 | 0 / 0 |
| 12 | 17.14% | 1/21 | 8.57% | 21/21 | 0 / 0 |
| 14 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 |
| 16 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 |
| 20 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 |
| 30 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 |
| 40 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 |
## 暫定結論
1. **f32 Directはz4付近までは実用候補。z6では隣接21 pixelが2値まで潰れ、品質上の限界が明確。** 現在の `ratio < 8` 強制Deepは1600px幅・この中心ではおおむねz4前後で発動するため、少なくとも「Deepを速度で選ぶ」には早すぎる。
2. **DS Directはz8まで中間backendとして有望。z10ではclass errorが約3.8%に増え、本命Deepへ移るべき。** 隣接pixel分解能自体はz12まで維持するが、反復誤差が先に品質を制限する。
3. **Deepは廃止できない。** f32はz6、DSもz10付近から品質が崩れる。Deepは極端な固定費を持つが、より深い領域では唯一の実用候補。
4. Deep CPUモデルにはz8で1/315のfalse escapeが残るため、Deepもmembership certificateではない。これは既存の`membershipCertified:false`方針と整合する。
## 実GPU速度ベンチ
このコンテナではWebGPU adapterを取得できないため、GPU時間は測定できない。代わりに本体の診断情報へ **Backend比較** を追加した。
v24.2.2では各深度を現在のcanvas全画面でwarmup+複数回測定していたため、z12 Deepで巨大dispatchが長時間GPU queueを占有するケースがあった。v24.2.3では速度計測を現在のview座標上の代表3タイル(各最大256×144)に限定し、合計時間から `ms/pixel` を算出して現在の全画面へ換算する。品質は別の5タイルで比較する。各GPU batch/reference buildには15秒watchdogがあり、永久待ちはしない。
実機では同じ深度列を次の3方式により測定する。
- `f32`: 標準numeric kernel。
- `DS`: experimental whole-frame double-single direct kernel。production routerには未接続。
- `Deep warm`: reference cache済みのDeep primary + DS correction。
- `Deep cold`: `Deep warm + verified BigInt reference build`。
各候補のqualityはactual full-resolution座標上の5領域でcorrected Deepと比較し、class mismatch / escape-iteration mismatch / UNKNOWN率をJSONへ記録する。速度JSONにはspeedTiles / speedPixels / msPerPixel / fullMedianMsを保存する。Deep自体の絶対品質は上記BigInt CPU oracle reportを参照する。
## 次のrouter候補
実GPU結果が得られたら、backendは「速度だけ」ではなく次で選ぶ。
```text
f32 quality OK → f32/DS/Deep のうち最速
f32 NG, DS quality OK → DS/Deep のうち最速
DS NG → Deep
```
DeepにBLAがない現在、同じiteration数で`f32 Direct`よりDeep numeric kernelが速くなる理由はほぼない。Deepの役割は速度ではなく座標/反復精度であり、速度比較の本命は **DS cold cost vs Deep cold/warm cost** になる。

View file

@ -1,70 +0,0 @@
# v24.2.8 DS + True Sparse Fixed96 Direct Experiment
## Purpose
The previous three-term f32 expansion correction failed the real-GPU quality gate on every tested depth. v24.2.6 keeps the DS sensitivity gate but replaces the correction arithmetic with deterministic integer fixed-point operations.
Pipeline:
```text
DS Direct + |dz/dc| risk gate
-> risky pixel indices compacted into GPU u32 queue
-> GPU finalize writes indirect workgroup count
-> dispatchWorkgroupsIndirect
-> signed Q8.88 Direct runs only queued pixels
-> compare against corrected Full Deep
```
The Fixed96 correction does not bind or build a deep reference.
## Arithmetic
- width: 96 bits
- format: signed Q8.88 two's complement
- storage: 3 × `u32`
- multiplication: exact 32×32→64 decomposition from 16-bit partial products, 96×96 accumulation into six 32-bit limbs, then round-and-shift by 88 bits
- coordinates: packed directly from the BigInt ViewState into Q8.88; JavaScript `Number` is not used as an intermediate
Q8.88 is intended only for the z6–z12 transition experiment. It is not proposed as an unlimited deep renderer.
## Measurements
For z6 / z8 / z10 / z12 and risk `1e11 / 1e12 / 1e13 / 1e14`, the UI reports every candidate row:
- extrapolated Fixed96 full-frame time,
- Full Deep cold estimate,
- speedup,
- correction rate,
- remaining UNKNOWN,
- escaped/bounded class mismatch,
- actual Fixed96 dispatch rate,
- queue integrity status,
- status/failure reason.
A failed candidate is never collapsed into a single `none` row.
## CPU preflight
`tests/v24-fixed96-model.mjs` compares the DS risk gate + exact Q8.88 Direct model with the BigInt P/P+64 oracle on a 1600×900-equivalent Seahorse sample grid. `tests/v24-fixed96-limb-model.mjs` separately verifies the six-limb multiplication algorithm against BigInt for 20,000 deterministic cases.
These are preflight tests. Real-GPU shader compilation, timing and class comparison remain decisive.
## Production status
Diagnostic only. The production router remains unchanged.
## v24.2.7 range hotfix
v24.2.6 contained a shader bug in `fx_bad_range()`: the top Q8.88 integer byte was treated as if it had to be pure sign-extension. Valid values such as `+1.2` were therefore rejected. Escaping Mandelbrot orbits commonly pass through component magnitudes above 1, so selected correction pixels were counted as `remaining` instead of being written back.
v24.2.7 removes that invalid test. While the prior state is inside the bailout disk, Q8.88 multiplication is bounded. After each iteration the shader first tests `|Re(z)| > 2 || |Im(z)| > 2` as a mathematically certain escape, and only squares components when both are within ±2. This prevents both the false range rejection and large-value fixed-point wraparound.
## v24.2.8 true sparse queue
v24.2.7 still launched a 2D Fixed96 workgroup over the whole benchmark tile and returned immediately for non-risk pixels. On SIMD/SIMT hardware this was not sparse enough: a few risky lanes could keep most of a workgroup occupied by the long integer loop.
v24.2.8 changes the execution topology, not the Q8.88 arithmetic. The guarded DS pass atomically appends each risky local pixel index to a queue. A one-invocation finalize shader writes `ceil(queueCount/64),1,1` to an indirect-argument buffer. The Fixed96 kernel is one-dimensional with `@workgroup_size(64)` and reads exactly one queue entry per invocation. No CPU map/readback occurs between these passes.
Every timing and quality tile validates queue invariants before its result is accepted. Overflow, wrong indirect arguments, invalid/stale indices or any disagreement between selected/enqueued/processed/corrected counts aborts the experiment rather than producing a misleading timing row.

View file

@ -1,69 +0,0 @@
# v24.2.6 DS + sparse Deep Hybrid Experiment
## 目的
v24.2.3実機計測では、Seahorse trackでDS Directはz10/z12でもFull Deepより速い一方、分類品質が不足した。次の実験ではDS Direct全画面を捨てず、数値的に危険なpixelだけDeep補正へ送ることで、z6〜z10の中間帯を高速化できるか測る。
production routerは変更しない。本ファイルの実験結果が採用条件を満たした場合にのみ次版で `f32 → DS/Hybrid → Full Deep` のrouterを検討する。
## 実験backend
`DS guarded direct → UNKNOWN only → existing Deep DS correction`
DS guarded directはorbitと同時に複素微分 `D = dz/dc` を近似追跡する。
```
D[n+1] = 2 z[n] D[n] + 1
```
`|D| > riskLimit` になったpixelはDS Directの分類を採用せず `FIELD_UNKNOWN / ds-sensitivity` とする。そのpixelだけ既存のBigInt reference + double-single perturbation correctionで再評価する。
この判定はmembership証明ではなく、DS Directの不安定化を早期に検出する実験用heuristicである。
## sweep
- 深度: z6 / z8 / z10 / z12
- riskLimit: `1e12`, `1e13`, `1e14`
- speed: actual full-resolution座標上の代表3tile(各最大256×144)
- quality: 5 spatial tiles
- quality reference: corrected Full Deep
- cold time: verified BigInt reference buildをHybrid / Full Deep双方へ加算
診断情報 → **Hybrid実験を開始** を押すとダイアログ表示後に自動開始する。ダイアログ内の **再実行** で同じ設定を再度測定でき、結果はJSON保存できる。
## CPU事前モデル
1600×900相当、Seahorse、31×21点、BigInt P/P+64 oracle。`riskLimit=1e13`:
| depth | raw DS class error | Deep補正対象 | DS class error捕捉率 |
|---:|---:|---:|---:|
| z6 | 4 / 651 | 18 / 651 = 2.8% | 100% |
| z8 | 4 / 651 | 72 / 651 = 11.1% | 100% |
| z10 | 10 / 651 | 225 / 651 = 34.6% | 100% |
| z12 | 51 / 651 | 594 / 651 = 91.2% | 100% |
このため事前仮説は次の通り。
- z6: Hybrid有望。
- z8: Hybrid有望。
- z10: 補正率約35%でもFull Deepより速いかが主要な計測点。
- z12: 約91%をDeepへ送るためHybridの意味が薄く、Full Deepへ切り替える候補。
## 採用gate
Hybridをproduction候補にするのは、各viewで以下を全て満たす場合のみ。
1. corrected Full Deepとのescape/bounded class mismatch = 0。
2. correction後remaining UNKNOWN = 0。
3. Hybrid cold estimateがFull Deep coldより10%以上高速。
4. correction対象率 < 60%。60%以上ならFull Deepを優先。
5. 実GPU acceptanceでuncaptured validation/runtime error = 0。
最終production routerへ昇格する前には、BigInt oracleでdense boundary corpusを別途再検証する。
## 注意
- `riskLimit=1e13`はCPU事前モデルから得た候補で、固定仕様ではない。
- `|dz/dc|`はcondition proxyであり厳密なroundoff error boundではない。
- Hybrid correctionはreference buildを必要とする。ただしv24.2.3実機値ではreference buildはDeep全時間の小部分だったため、主要比較はGPU numeric costとなる。
- z12でHybrid補正率が高いことを「失敗」とは扱わない。Full Deepへ切り替えるべき境界を検出することも実験目的である。

View file

@ -1,118 +0,0 @@
# v24.2.3 crossover benchmark implementation report
## ファイルの役割
- `index.html`: UI、canvas、Export dialog。WebGPU実装ロジックは持たない。
- `script.js`: BigInt ViewState、reference Worker、WebGPU resource orchestration、backend router、screen/Export pipeline。
- `gpu-kernels.js`: production 6 WGSL source + benchmark-only `DS Direct` 1本。DS Directはproduction routerから参照しない。
- `tests/v24-sparse-correction-model.mjs`: WGSLと同じf32/double-single式をCPU `Math.fround`で再現し、BigInt oracleと比較する品質gate。
- `tests/v24-backend-policy.mjs`: view-specific route policyの純粋ロジック検査。
- `tests/webgpu-acceptance.*`: 実GPU compile/dispatch/quality/export gate。
- `AUDIT_V24.md`: 現releaseの検査結果と未検証事項。
- `MIGRATION_V24.md`: v23からの設計変更と、廃案を含む理由。
## 今回作成/変更した理由
深部primaryのUNKNOWNを黒として残すことが閲覧品質を下げていたため、追加referenceではなく同じreference上のdouble-single correctionへ変更した。またdirect利用範囲を固定閾値だけで狭めず、actual view上のqualityとGPU速度を測ってdeep採用を限定するためrouterを変更した。
## 機能検査
`npm test`で以下を自動実行する。
- JS syntax
- WGSL reserved identifiers
- source/tree/index contracts
- reference Worker
- direct f32 model
- backend router
- guarded deep model
- sparse DS correction model
- BLA disabled contract
- geometry/export contract
- exact deep coordinate formatter
- streaming PNG
- real-GPU acceptance source contract
## 性能設計
- overlap probeは本描画前に1回/current-view。
- speed probeは最大224×160。
- quality probeは最大72×54を5領域。
- backend profileは補助診断であり、現在viewのquality probeを省略しない。
- DS correctionはUNKNOWNのみ数値ループへ入り、KNOWN pixelは即return。
- Export workspaceは512×512固定で再利用。
## 既知の制約
- double-singleはWGSLの実装差を含めmembership certificateとして扱わない。
- 実GPU acceptanceはWebGPU adapterを取得できる環境で別途実行が必要。
## v24.2.3追加作業
- `DIRECT_DS_WGSL`: 全画面double-single Directの速度/品質候補。
- `runCrossoverBenchmark()`: z0〜z40のround-depth自動比較。
- `Backend比較` dialog: 実GPUでf32 / DS / Deep warm / Deep coldを同一解像度で計時しJSON保存。
- `tests/v24-crossover-quality.mjs`: 1600×900相当のBigInt P/P+64 oracle品質掃引。
- `CROSSOVER_BENCHMARK.md`: 実装意図、CPU品質結果、次routerの判定基準。
現時点ではDS Directは診断専用であり、本体のbackend選択は変更していない。実GPU速度結果なしにproductionへ昇格させないための措置。
## v24.2.4 Hybrid experiment preparation
- Added benchmark-only `DIRECT_DS_GUARDED_WGSL`; production router is unchanged.
- Guard metric: approximate `|dz/dc|`; threshold sweep `1e12 / 1e13 / 1e14`.
- Added `benchmarkHybrid()` / `renderTileMetaHybrid()` using the existing Deep double-single correction only for DS-marked UNKNOWN pixels.
- Added z6/z8/z10/z12 experiment UI and JSON export.
- Added CPU BigInt P/P+64 risk-model gate. At risk=1e13, observed DS class errors are all caught at z6/z8/z10 with correction rates 2.8% / 11.1% / 34.6%; z12 correction rate is 91.2%, intentionally indicating the Full-Deep crossover.
- Added real-WebGPU acceptance smoke for the Hybrid diagnostic path.
## v24.2.5 Sparse Direct experiment (Implementation)
- The previous sparse-Full-Deep diagnostic is superseded and is not exposed in the UI.
- Added `DIRECT_TS_CORRECT_WGSL`, a reference-free three-term f32 expansion correction shader that only processes DS sensitivity UNKNOWN pixels.
- Added `benchmarkSparseDirect()` / `renderTileMetaSparseDirect()` and a z6/z8/z10/z12 risk sweep.
- Full Deep remains the quality/timing baseline; the Sparse Direct candidate itself does not build or bind a reference orbit.
- Production backend routing is unchanged.
- Root `index.html` is now a single-file bundle with no external script dependency, specifically to keep diagnostic UI controls functional when the HTML is opened from an archive/temp extraction.
- Static single-file UI contract and real-browser click smoke are separate gates; real WebGPU arithmetic still requires a WebGPU-capable browser.
- Sparse Direct coordinate packing uses exact residual subtraction from BigInt fixed-point into three f32 components; this removed the CPU TD-model class errors caused by the earlier host-Number split.
## v24.2.6 Fixed96 experiment implementation
Purpose: test a deterministic reference-free correction tier between DS Direct and Full Deep without relying on WGSL float-expansion behavior.
Implemented:
- `DIRECT_FIXED96_CORRECT_WGSL`: signed Q8.88 Direct iteration using 3×u32 values.
- exact 32×32→64 multiplication from 16-bit partial products;
- 96×96→192 accumulation and rounded Q8.88 reduction;
- BigInt→Q8.88 tile coordinate packing;
- DS risk gate + Fixed96 UNKNOWN-only correction benchmark;
- per-risk failure reporting instead of collapsing all failures to `none`;
- single-file ZIP UI contract;
- CPU oracle and limb-arithmetic tests.
Not implemented: production router integration. The experiment must first pass on a real WebGPU adapter.
## v24.2.7 Fixed96 hotfix
The Fixed96 experiment's real-GPU `remaining` counter exposed an implementation bug rather than an arithmetic-quality limitation. The correction shader's range check confused the 8-bit Q8.88 integer field with redundant sign-extension. The hotfix removes that check and makes bailout ordering overflow-safe: component-certain escape first, exact fixed-point magnitude only inside the ±2 component box.
## v24.2.8 — Fixed96 GPU compact queue
### Files changed
- `gpu-kernels.js`: DS risk queue output, indirect-args finalize shader, 1D queue-indexed Fixed96 correction kernel.
- `script.js`: queue/indirect buffers, indirect dispatch encoding, per-sample queue integrity verification, queue/dispatch diagnostics in the experiment result.
- `tests/v24-fixed96-contract.mjs`: static true-sparse contract.
- `tests/v24-fixed96-queue-model.mjs`: fail-closed queue counter/indirect argument model.
- `tests/webgpu-acceptance.js`: real-GPU Fixed96 smoke now requires queue integrity and dispatch rate equality.
### Performance intent
The previous implementation's numerical correction fraction was sparse, but its GPU invocation topology was not. v24.2.8 makes integer iteration work proportional to the actual queue length. The DS guard remains a full-tile pass because it is the cheap classifier; only the expensive Q8.88 loop is sparse.

View file

@ -1,566 +0,0 @@
# マンデルブロ集合ビューワー 軽量・高精細化 改善提案
- 対象baseline: Mandelbrot ∞ Zoom v22
- 文書status: v22に対する実装前提案。採用項目はrenderer v23へ実装済みであり、現行状態と残存gateは`IMPLEMENTATION_REPORT.md`と`COMPLETION_AUDIT.md`を正とする。
- Document status: historical-v22 / implemented-v23
- 証拠境界: v22成果物とGit snapshotは現workspaceに残っていないため、v22の行番号・bytesは履歴記録であり再監査可能なbaselineではない。以後の比較基準はversion付きJSONとSHA-256を必須とする。
- 作成日: 2026-08-22
- 対象ファイル: `index.html`, `script.js`, `kernels.js`, `src/bla_kernel_v18.c`
- 性格: 設計判断を保存する履歴文書兼、修正版の品質契約。効果値は、version付き監査JSONの引用を除き検証すべき目標または仮説である。
## 1. 結論
現行実装は、任意精度座標、基準軌道、摂動法、リベース、BLA、SIMD、局所フォールバックまで備えている。数値エンジンの骨格を捨てて全面的にGPU化するより、次の順で周辺設計を改める方が、軽量性と高精細を両立しやすい。
1. 待機中も続く全画面再描画を止め、イベント駆動にする。
2. 描画品質を曖昧な数値 `q` ではなく、Reprojected / Preview / Covered / Refined / Validated の完了条件で管理する。
3. 画面全域を、確定した表示pixel gridまでタイル描画してから、境界だけ適応的アンチエイリアスする。通常はnative DPRとし、画素予算でDPRを制限した場合は明示する。
4. 計算結果と彩色を分離し、色変更でフラクタル計算をやり直さない。
5. 深部WorkerとWASMを遅延生成し、固定最大長・Workerごとの重複メモリを減らす。
6. 画素中心、BLA精度、未解決画素、出力完了条件を明文化し、「高精細」の意味を計測・監査できる状態へ変える。
最優先はカーネルの細かな命令最適化ではない。現状の配布物は合計138,831 bytesとすでに小さい一方、待機中の常時描画、巨大なCanvas、Workerごとの固定配列は継続的なCPU/GPU・メモリ負荷になるためである。
## 2. 「軽量」と「高精細」の定義
本提案では、目標を次のように分ける。
| 軸 | 意味 | 主な指標 |
|---|---|---|
| 配布の軽さ | 初回ロード・解析・WASMコンパイルが小さい | 転送bytes、起動時に取得/compileする資産 |
| 実行の軽さ | 操作中と待機中にCPU/GPUを浪費しない | long task、idle CPU、Canvas draw回数、消費電力 |
| メモリの軽さ | DPR・Worker数・参照長が増えても予算内に収める | peak memory、cache bytes、Workerごとの固定領域 |
| ラスター精細度 | 表示ピクセルを実サンプルで覆い、境界を適切にAAする | sample coverage、resolved coverage、samples/pixel、継ぎ目 |
| 数値精度 | 座標・反復・BLA近似の誤差を検出し、未解決を黒と混同しない | 高精度referenceとの差、unresolved数、検証モード |
| 出力精細度 | 表示プレビューとは独立して、指定寸法を完了後に保存する | 出力寸法、AA、再現性、未完了pixel数 |
「Canvasが4Kサイズである」だけでは高精細とはみなさない。低解像度フレームを4K Canvasへ拡大した状態と、4K分のサンプルを実計算した状態を区別する。
本書のcoverageは次の2値へ分ける。sample coverageは宣言したiteration/precision policyで新ViewSpecのsampleを計算済みのtarget pixel比率であり、`UNRESOLVED`も計算済みsampleとして含む。resolved coverageは`ESCAPED`またはpolicy上の`INTERIOR_PROVEN`へ確定した比率である。Coveredはsample coverage 100%、Validatedはsample coverage 100%に加えてunresolved 0を要求する。
## 3. 現状評価
### 3.1 残す価値が高い部分
- BigInt固定小数点による中心・表示幅と、段階的な精度昇格 (`script.js:58-109`)
- 可変精度BigInt固定小数で生成し、Float64表現を摂動/BLAへ渡す基準軌道 (`script.js:608-705`)
- SIMDからscalar、さらにJS/BigIntへのフォールバック (`script.js:208-227`, `script.js:551-685`)
- 常駐Workerのwarm cache、少数検証、strip単位の局所補修 (`script.js:296-390`)
- main cardioid / period-2 bulbと収縮判定による内部早期終了 (`src/bla_kernel_v18.c:94-172`)
- 操作中の既存フレーム再投影 (`script.js:154-184`, `script.js:393-424`)
- smooth coloring、world-space detail cacheという考え方 (`script.js:187-206`, `script.js:393-531`)
既存のv18監査では、v17比のbase-frame改善が2.07〜5.15倍で、テストした内部早期終了は黒/外部分類不一致0だった (`AUDIT.md:60-82`, `AUDIT.md:163-176`)。この実績があるため、BLAや内部早期終了を一律に廃止する理由はない。
提案作成時の現行コードはv22だったのに、性能監査と最終ベンチの表題・データはv18だった。既存値は設計判断の参考には使えるが、v22の受入基準にはできない。当初は同じsceneのv22再計測を必要条件としたが、現在は不変なv22成果物が残っていないため比較を`not-verifiable`とし、version固定したv23以後のbaselineだけを回帰判定へ使う。
### 3.2 主要なギャップ
| 優先度 | ギャップ | 根拠 | 影響 |
|---|---|---|---|
| P0 | 静止中も永久に`requestAnimationFrame`を回す | `script.js:722-729` | idle CPU/GPU、電池、発熱 |
| P0 | 1フレームで全Canvasと全detail cacheを再合成し、DOM統計も更新する | `script.js:413-423`, `script.js:713-716` | 画面が変わらなくても継続負荷。ドラッグ中は同一RAF内で二重paintになる |
| P0 | 解像度制御が端末実測を使っていない | `renderPerf`は記録のみ。`targetSize()`は固定tier (`script.js:42-44`, `script.js:426-439`, `script.js:534-537`) | 小画面では過剰計算、大画面では不足 |
| P0 | HQ全画面は最大2100×2200で、選抜領域だけ2倍 | `script.js:429-461` | 4Kでは要求gridを全域計算しない。細線を基礎画像で見失うと精細化されない |
| P0 | 確認できるJS/direct/BLA経路が、画素中心ではなく整数格子点をサンプルする | `script.js:516-518`, `script.js:570-573`, `script.js:672-674`, `src/bla_kernel_v18.c:184-190` | 非対称なsample grid、解像度間対応の非標準化、backend差のリスク |
| P0 | detail fast BLAがbase HQより緩い | base HQはe-32、detailは通常e-28。`strictBoundary`は現在常にfalse (`script.js:350-359`, `script.js:504-525`) | 「精細化した領域の方が数値的に粗い」可能性 |
| P0 | PNGが現在の表示Canvasを即時保存する | `script.js:739` | 再投影中・低解像度・detail未完了でも高解像度出力に見える |
| P1 | 起動180ms後にdeep poolを必ず作る | `script.js:296-307`, `script.js:750` | 浅部だけ見る場合もWorker、参照配列、LUTを確保 |
| P1 | Workerごとに基準軌道とBLA表を重複保持・構築する | `script.js:249-259`, `src/bla_kernel_v18.c:3-23` | 深部使用後のpeak memoryが大きい |
| P1 | WorkerでstripごとにRGBAを新規確保し、transfer後にmain全画面配列とImageDataへ再コピーする | `script.js:265-269`, `script.js:378-382`, `script.js:398-400` | memory bandwidth・一時領域。transfer自体はzero-copy |
| P1 | palette/cycle/shift変更で数値計算まで再実行する | `script.js:397`, `script.js:468`, `script.js:744-747` | 深部の色調整が不必要に重い |
| P1 | deep切替が固定ズーム指数11.5 | `script.js:20`, `script.js:707-709` | 中心値、DPR、1画素幅に対し過剰または不足 |
| P1 | 有限反復表示として`maxIter`到達を黒にするため、精度優先時も証明済み内部と未確定を区別できない | `script.js:116-119`, `script.js:561-564`, `script.js:666-670` | Validated/Exportで確定度を説明できない。v17の途中work cap不具合とは別問題 |
| P2 | BLAが通常doubleのspanに依存し、約10^-280以深で無効になる | `script.js:364`, `script.js:508`, `src/bla_kernel_v18.c:175-190` | 超深部で性能が崖状に低下 |
### 3.3 メモリ上の具体例
`src/bla_kernel_v18.c`の固定配列だけを合計すると、1つのBLA WASM instanceあたり約15.6 MiBになる。
- 出力: `counts[65536]` + `mags[65536]` ≈ 0.75 MiB
- 参照軌道: `refs_r[150001]` + `refs_i[150001]` ≈ 2.29 MiB
- BLA double配列5本 × 300100 ≈ 11.45 MiB
- BLA length配列 × 300100 ≈ 1.14 MiB
深部job後に4 WorkerがBLA instanceを持てば、この固定領域だけで約62.5 MiBとなる。さらに各WorkerにはJS側の`RR/RI`、3パレットのLUT、deep/color WASM、出力scratchがある。
また、Worker内部の`RR/RI`とLUTはWASMより先に生成されるため、起動直後に4 Workerを作るだけでもtyped arrayだけで概算13 MiB超を確保する。固定最大配列を現在の`refLen`へ合わせる効果は大きい。
### 3.4 配布サイズ上の具体例
- `index.html`: 6,618 bytes
- `script.js`: 80,077 bytes
- `kernels.js`: 52,136 bytes
- 実行に必要な3ファイル合計: 138,831 bytes
- Base64部分: 51,808文字、復号後WASM合計38,845 bytes
絶対量はすでに小さい。Base64と復号後の約13KB差は非圧縮表現上の値であり、Brotli転送量が同量減るわけではない。外出しの主効果は独立cache、遅延取得、streaming compileであるため、先にidle負荷とメモリ重複を直す方がよい。なお`COLOR_SIMD_B64`と`COLOR_SCALAR_B64`は同一内容であり、現状は同じ2,510-byteバイナリを二重に内包している。
## 4. 推奨する新しい品質仕様
内部の連続値`q`を、利用者と実装の双方が判定できる状態機械へ置き換える。現状は初回`render(.3)`に対し分岐が`q < .3`なので、意図した最軽量tierではなく次のtierへ入る (`script.js:426-437`, `script.js:751`)。enum化はこの種の境界ずれも防ぐ。
| 状態 | 開始条件 | 解像度・計算 | 完了条件 | UI表示例 |
|---|---|---|---|---|
| Reprojected | wheel / pinch / pan継続中 | 既存フレームのtransformのみ | 入力停止待ち | プレビュー |
| Preview | 入力停止80〜120ms後 | 時間予算内の低解像度。近似BLA可 | 最優先tileを表示 | 高速描画 |
| Covered | Preview後 | 選択した処理モードのtarget gridを全域tile計算 | sample coverage 100%、再投影pixel 0。未確定数は別表示 | 全域描画 68% |
| Refined | Covered後のidle | 境界・不確実tileだけ2×/4× adaptive AA | 対象tile完了 | 境界AA完了 |
| Validated | 精度優先時またはValidated Export | 宣言した有限反復・精度policyを局所照合 | checks合格かつunresolved 0。残る場合は別状態「検証未完了・未確定N」 | 検証完了 / 検証未完了 |
重要な契約は次の通り。
- Previewは粗くてよいが、新しい未解決画素を「内部」と断定して黒にしない。
- Coveredはズーム深度に関係なく、確定したScreen Canvas gridを新しいViewSpecの実sampleだけで全域計算する。再投影した旧frameは描画中のpresentationにだけ使い、sample coverageへ数えない。計算済み`UNRESOLVED`はsample coverageへ数えるがresolved coverageへは数えない。
- native DPRは上限であり、標準・精細とも明示したpixel/memory budget内で使う。精細は時間適応で解像度を下げないが、memory/pixel上限は維持する。DPRを制限した場合はeffective DPRと実解像度を表示する。
- Refinedは単なる2倍補間ではなく、複数の実サンプルをlinear-lightでresolveする。
- Validatedは数学的なマンデルブロ集合所属証明ではなく、宣言した有限反復・precision policyの照合完了を表す。全sampleへ誤差境界または証明手続きを適用する将来モードだけを`Certified`と呼ぶ。
- 「高精細」と「数値検証完了」を同じチェックボックスにしない。
- Exportは表示Canvasとは独立したjobであり、view、寸法、反復方針、AA、precision tier、乱数seedを固定してから開始する。Balanced ExportとValidated Exportを区別し、前者は未確定数をmetadataへ記録して完了可能、後者だけをunresolved 0必須とする。
処理モードと描画状態は分ける。
| 処理モード | Coveredのtarget | 自動到達点 |
|---|---|---|
| 省電力 | 自動Coveredなし。Previewは最大約1MP | Preview |
| 標準 | shallowはmobile/低memory約2MP、desktop約4MP。deepはPreview実測から約1.4秒のCovered予算へeffective DPRを下げる | Covered。AA・未確定追加反復は自動実行しない |
| 精細 | mobile/低memory約4MP、desktop約8MP。上限時は実解像度を明示 | Refined。未確定追加反復は境界候補だけを上限付きで行う |
| 精度優先 | 指定gridとprecision policy | Validated。将来はCertifiedを選択可 |
Exportは処理モードではなく独立jobである。Balanced / Validatedのprecision tierを選び、それぞれRefined相当 / Validated相当の完了条件を適用する。
## 5. 推奨アーキテクチャ
```text
入力
↓
ViewState(BigInt座標・履歴・version)
↓
Render Scheduler(gesture、優先度、時間/メモリ予算、取消)
├─ f64 SIMD backend
├─ deep BLA / perturbation backend
└─ high-precision verifier / fallback
↓
Field Cache(escaped / interior likely / interior proven / unresolved、smooth value、confidence)
├─ Colorizer → Screen Compositor
└─ Adaptive AA / Export
```
責務を次の単位へ分ける。
- `view-model`: BigInt座標、pixel-to-world規約、URL version、戻る/進む
- `scheduler`: gesture settle、tile priority、generation cancel、visibility、品質状態
- `backend`: f64 / deep / verifierを同じtile protocolで実装
- `field-cache`: palette非依存のpacked数値結果、未解決tileだけの継続状態、byte-budget LRU
- `colorizer`: palette、tone mapping、最終ditherだけを担当
- `presenter`: 再投影、tile合成、coverage表示
- `exporter`: 指定寸法の独立tile render、進捗、取消、metadata
Workerへ送るjobは、最低限`viewVersion`, `tile`, `samplePattern`, `iterationPolicy`, `precisionPolicy`を持たせる。古いversionの結果は合成せず、1 tileを8〜12ms程度へ収めて取消後の滞留を限定する。
## 6. 軽量化の具体策
### 6.1 イベント駆動描画へ変更する — P0
現在の`loop()`は静止中も毎フレーム`paintFrame()`、`updateStats()`、次のRAF予約を行う。次へ変更する。
- `needsPaint` / `needsStats` / `scheduled`を設け、invalidate時だけRAFを1回予約する。
- pointermove/wheelはイベントごとに描かず、最新transformを1 RAFに集約する。
- render/tile完了時だけ該当rectを合成する。
- statsは状態変化時のみ、操作中でも最大4〜10Hzに制限する。
- `document.hidden`ではdetail、Wisdom、未表示tileを停止する。
- 静止完了後はRAF、Canvas write、DOM writeを0回/秒にする。
これは実装難度が低く、画質を落とさず、待機時負荷を大きく下げる最初の変更である。
### 6.2 表示Canvasと計算解像度を分離する — P0
現状は表示Canvasを`innerWidth × min(DPR, 2.5)`で確保する一方、frameは最大2100×2200である。4K・高DPRでは巨大な表示バッファへ小さいframeを拡大するだけになり得る。
極端な例として、3840×2160 CSS px・DPR 2.5では表示Canvasだけで9600×5400×4 bytes、約198 MiBになる。画素数上限がないため、source frameより大幅に大きいsurfaceを常時再描画し得る。
- Screen Canvasには処理モード別に約1〜8MPのbyte/pixel budgetを設ける。標準shallowは2〜4MP、精細・精度優先は4〜8MPを初期値とする。標準deepはcold Previewを極小gridで測り、実測MPPから約1.4秒以内を狙うpixel budgetへ下げる。effective DPRと実寸を表示し、精細を選んだ場合はこの時間適応を使わない。
- `effectiveDpr = min(deviceDpr, pixelBudget由来上限)`とする。
- Covered計算はタイルで選択モードのtarget Canvas gridを100%覆う。
- 画面より大きい4K/8K出力は表示Canvasを巨大化せず、ExporterのOffscreenCanvasまたはtile encoderで作る。
- `renderPerf`を本当に使い、Previewだけ`targetPixels = budgetMs / EMA(msPerPixel)`で決める。
- EMAはshallow/deepだけでなく、iteration帯、BLA有無、warm/coldを区別する。
Previewの目安は80〜120ms、停止後baseは400〜700msとし、その後はtileで漸進する。端末差を吸収するため、固定pixel幅ではなく時間予算から決める。
### 6.3 操作中の無駄な計算を止める — P0
- wheelをpointerと同じgestureとして扱い、入力停止80〜120msまでは再投影だけにする。
- pinch / wheel / +−を同じsettle処理へ統合する。
- SharedArrayBufferを使えるHosted版では、WASM hot loopが定期的に読むatomic cancel flagを設ける。
- SharedArrayBufferが使えない構成では、tile/chunkを小さくして取消後の最悪滞留時間を制限する。
- Preview中にwork capへ到達した画素は`UNRESOLVED`のまま残す。現状は低品質passで未解決が2%を超えると同stripを即時無制限再計算する (`script.js:265-266`) が、field/statusを導入するPhase 2以降はCoveredで継続し、Validatedでは必要に応じ高精度経路へ上げる。
- 浅部の`.58 → .84 → 1.0`という最大3回の全面再計算は、実測が遅い端末ではPreview → Coveredの2段へ減らす。
### 6.4 deep資産を遅延生成する — P0/P1
- Hosted版の初期表示ではshallow SIMDだけを取得・compileする。Standalone版は全Base64文字列をparseするが、deep payloadの復号・compileとWorker生成は遅延する。
- deep main-thread WASM、deep worker source、Worker poolは、数値条件が閾値へ近づいた時点で準備する。
- Workerは最初1〜2個だけ作り、実jobのthroughputから必要時だけ増やす。
- 現在のReal Wisdomは深部HQへ到達した各ページセッションのidle時に1回、最大4 Workerなら合計10回のmini renderを追加する (`script.js:333-348`, `script.js:698`)。通常jobから受動学習し、能動ベンチは診断時または不確実時だけにする。
- 学習結果はkernel versionと最小限の端末帯域特性をkeyに永続化する。schema versionと保存時刻を持たせ、既定TTL 30日で失効させる。storage拒否時はsession内学習だけで動作し、raw timing履歴や過剰なfingerprint属性を保存しない。
### 6.5 WASMとメモリを統合・動的化する — P1
- deep、BLA、smooth/colorを1 module・1 memoryへ統合する。
- `MAX_REF`、`MAX_BLA`の固定最大配列をやめ、現在の`refLen`に合わせて初期容量を決め、必要時だけ`memory.grow`する。linear memoryは縮まないため、大scene後の回収には容量bucket別instance poolの退役・再生成を使う。
- JS `RR/RI` → deep WASM → BLA WASMという重複copyを1回へ減らす。
- BLA tableは参照長`n`に対して概ね`2n`要素を動的確保する。
- Hosted版ではCOOP/COEPを設定する。共有対象はbuild完了後immutableなreference、key別BLA table、tileごとに非重複なfield領域へ限定する。table公開にはready barrierを設け、scratch・統計はWorker別offsetまたは別memoryにしてdata raceを避ける。
- 非共有版ではbuffer poolを使い、transferしたArrayBufferをWorkerへ返却して再利用する。
- `mags`をcolor WASMへcopyする経路も統合し、kernelからpacked fieldまたはRGBAを一度だけ出す。
### 6.6 キャッシュを件数ではなくbytesで管理する — P1
`DETAIL_TILE_LIMIT=96`は、tile寸法、DPR、Canvas実装の裏側を考慮しない。次へ置き換える。
- mobile 96 MiB、desktop 192 MiBを、JSから管理できるtyped array / WASM / cacheの暫定論理予算として計測開始する。
- frame、numeric field、tile canvasの推定bytes、reference、BLA、scratchをmemory ledgerへ登録する。ブラウザ内部のCanvas/GPU backing storeは正確に台帳化できないため、固定環境のprocess/UA memory観測を別指標にする。
- LRUはbytes、現viewportからの距離、再利用見込みでevictする。
- pan開始時の全frame cloneを避け、gesture中はimmutableな現frameを直接transformする。
- 論理予算超過時はcacheを捨てる。ブラウザ固有のmemory-pressure通知へ依存せず、最終解像度を黙って落とさない。
### 6.7 配布をHosted版とStandalone版へ分ける — P2
推奨する標準はHosted版である。
- `.wasm`とmodule Workerをhash付き外部資産にする。
- `compileStreaming` / `instantiateStreaming`、Brotli、immutable cacheを使う。
- SIMD版を先に取得し、失敗時だけscalar版を取得する。
- deep/BLA/colorは深部到達時だけ取得する。
- compile済み`WebAssembly.Module`をWorkerへstructured cloneする。
既存の「3ファイルを直接開ける」はStandalone互換buildとして残し、build時にWASMをinlineする。これにより、軽量な通常配信とオフライン可搬性を両立できる。
WebGPUは最初から必須にしない。まずpalette適用、linear-light resolve、tile合成へ使い、数値演算は低ズームPreviewだけの任意backendとして比較する。fp32を深部の最終結果へ使わない。
### 6.8 WASM buildを再現可能にする — P1
現状の`src/`にはBLA C sourceしかなく、他の埋込WASMに対応するsource、build command、toolchain manifestが見当たらない。バイナリだけでは、画素座標、丸め、SIMD/scalarの同値性を十分に監査できない。
- 全kernel sourceと生成scriptを保存する。
- clang等のversion、compile flags、exports ABIを固定する。
- 生成物のSHA-256をmanifestへ記録する。
- SIMD/scalarについて同じgolden vectorを実行する。
- `kernels.js`またはStandalone bundleは生成物とし、手編集しない。
## 7. 高精細・高精度化の具体策
### 7.1 pixel-to-world規約を画素中心へ統一する — P0
確認できるJS/direct/BLA実装では整数格子式が使われている。埋込WASMを含む全backend、detail、exportで、次の規約を共有する。
```text
cr = centerRe + span * ((x + 0.5) / width - 0.5)
ci = centerIm + span * ((height / 2 - (y + 0.5)) / width)
```
BigIntでは次の有理式にし、早期のdouble化を避ける。
```text
cr = centerRe + span * (2*x + 1 - width) / (2*width)
ci = centerIm + span * (height - 2*y - 1) / (2*width)
```
canonical座標は上の有理数とする。fixed-point化では分子の符号と対称なround-to-nearest、exact halfはaway-from-zeroへ丸める`roundDiv`をABI契約とし、積を先に計算してから1回だけ除算する。JS f64、shallow WASM、deep WASM、BLA、direct fallback、detail、exportについて、同一pixelがcanonical座標を各backendの表現可能範囲で再現するcross-backend testを追加する。「backend差0」は整数のfixed-point表現または分類・iteration契約に対して用い、f64座標は宣言ULP以内を基準にする。整数格子式だけで現在のtile seamが実証されたわけではないが、規約を統一して将来のbackend差を防ぐ。この変更は既存URLのview中心を変えない一方、画像sampleは半画素分変化し得るため、renderer/golden image versionを上げる。
### 7.2 Covered完了後にadaptive AAする — P0/P1
現行のRGBA勾配選別は、基礎サンプルに現れなかった細いフィラメントや微小島を検出できない。また、Canvasの`imageSmoothingQuality`は再拡大補間であり、真のフラクタルAAではない。
新しい流れは次の通り。
1. 画面全域を1 sample / target screen pixelで計算する。
2. kernelからescape iteration、smooth potential、内部/外部/未解決、confidence、可能なら距離推定値を出す。
3. 分類境界、値の分散、距離推定、tile中央・四隅probeからquadtreeを細分化する。
4. 必要なpixelだけ4/8/16 sampleへ増やす。
5. sampleをlinear RGBで平均し、最後にsRGB/P3へencodeする。
これにより、全面4倍SSAAより少ない計算量で、ジャギー、細線、微小島を改善できる。detail選定はpalette非依存となる。ただし中央・四隅probeや距離推定も、任意に細い未サンプル構造を完全には保証しない。Refinedは誤差推定に基づくadaptive AAであり、全pixel面積の数学的被覆証明ではない。Certifiedを名乗る場合は、別途保守的なdistance/error boundが必要になる。
### 7.3 数値fieldと色を分離する — P1
全画面について保持するのは、少なくとも次のpacked fieldに限定する。
- state: `ESCAPED`, `INTERIOR_LIKELY`, `INTERIOR_PROVEN`, `UNRESOLVED`
- smooth escape valueまたは固定小数へpackした値
- confidence / precision tier
`zr/zi`、参照位置、微分などの継続状態は全画面に持たず、未解決pixelまたは未解決tileだけに保持する。8MP全域へ完全な継続状態を持つと、memory budgetを容易に超えるためである。
adaptive AAを再着色可能にするには、境界pixelのsubsampleごとのstate/smooth値とsample patternを保持する。単一の平均smooth値だけでは、非線形paletteを正しく再resolveできない。palette、cycle、shift変更時はこのfieldから安価なcolor/AA resolveだけを再実行し、フラクタル反復はやり直さない。detail cache keyからstyleを外し、geometry/field cacheとcolor cacheを分離する。
現状の3 palette × 2048 phase × 64 mix LUTはmainと各Workerで約1.1 MiBずつ使う。1D palette LUT + 計算式または補間へ変えれば小さくできる。色はlinear RGBまたはOKLabで生成し、最後だけ8bit化する。
「昼夜」paletteは周期の先頭色と末尾色が一致していない (`script.js:189-197`)。phase wrap位置に筋が出ないよう、循環paletteはendpointを一致させ、非循環paletteはping-pong mappingにする。banding対策のditherを戻す場合は、detail/AA選定後の最終表示またはExportだけに固定seedで適用する。
### 7.4 BLA/detailの精度逆転をなくす — P0
- Covered/Refinedのfast εは、少なくともbase HQと同等にする。
- 境界scoreまたはconfidenceが悪いtileではεを厳しくするか、BLAを切ってstrict perturbation / double-doubleへ上げる。
- 現行detail検証の「黒分類不一致3.5%以下、平均RGB差48以下」 (`script.js:476-487`) は、品質保証ではなく視覚的coherence gateにすぎない。視覚gateと数値correctness gateを分離し、後者は同一world座標を高精度referenceで再評価して比較する。low/high rasterの異なるsample位置を直接0 mismatch要件にしない。
- fast BLAとsafe BLAは同じdouble参照軌道を使うため、「safe」を数学的厳密性の意味で使わない。
- Balancedでは少数probe方式を残せるが、Validatedでは疑わしいtileを独立した高精度reference実装またはdouble-doubleと比較する。少数probe合格だけで未検査pixelを保証済みとは呼ばない。
### 7.5 未解決を黒から分離し、反復を継続可能にする — P1
通常の有限反復表示で`maxIter`到達を黒にすること自体は一般的な表示規約であり、v17の「global iterationより手前で打ち切ったwork cap」とは別である。本提案では、Validated/Exportの確定度を説明するため、内部表現を次へ分ける。
- `INTERIOR_PROVEN`: 誤差境界付き解析判定、または周期吸引を証明できたもの
- `INTERIOR_LIKELY`: 現行のf64 cardioid/bulb判定や収縮heuristicなど、高信頼だが証明扱いしないもの
- `ESCAPED`: 発散が確認されたもの
- `UNRESOLVED`: iteration/work/precision上限へ達したもの
Coveredの描画中は未解決領域へ中立色または直前結果をpresentationとして重ねられる。旧frameの再投影はsample coverageへ数えないが、新ViewSpecでpolicy上限まで計算した`UNRESOLVED`はsample coverageへ数え、resolved coverageと未確定数で別に監査する。精細では未解決の境界tileだけ反復を段階的に継続する。これなら、旧work capを復活させずに計算量を局所化できる。
収縮による内部早期終了は既存監査で有効だったためBalancedでは`INTERIOR_LIKELY`として維持する。Validatedでは判定根拠をconfidenceへ記録し、policyが要求する場合は誤差境界付き解析判定・周期証明へ上げられないものを`UNRESOLVED`へ戻す。
### 7.6 engine選択とguard bitsを誤差ベースにする — P1
固定`DEEP_ZOOM_THRESHOLD=11.5`ではなく、少なくとも軸ごとに次を評価する。
- 1 pixelのworld-space幅
- `centerRe`, `centerIm`付近のf64 ULP
- 隣接pixel座標が別のdoubleになるか
- 直近orbitの誤差・glitch率
`pixelStep > safety × ulp(center)`を満たす間はf64を使い、境界付近にはhysteresisを置く。これにより、不要なdeep移行を減らしつつ量子化を防げる。
座標のBigInt精度と基準軌道の計算精度も分ける。基準軌道をP bitとP+32/64 bitのcheckpointで比較する。単一global referenceを使う間はreference単位で精度を上げる。tile単位で上げるにはlocal referenceを新設するか、そのtileだけdirect/double-double摂動へ昇格する。全画面を常時多倍長化しない。
`exactDeepPixel`は固定小数積を右shiftで量子化する高精度direct計算であり、数学的なexactではない (`script.js:602-605`)。正値は切り捨て、負値は算術shiftによる負方向丸めになる。`highPrecisionDirectPixel`へ改名し、round-to-nearestを使う。Certifiedでは区間演算でescapeを証明し、内部には別の周期吸引証明を使う。
### 7.7 超深部の性能崖を段階的に解消する — P2
- BLA APIのspan、offset、cMaxを通常doubleではなくmantissa + exponent bucketへする。
- 係数も必要に応じてscaled complexまたはdouble-doubleへ昇格する。
- glitch tileを分割し、tile中心にlocal referenceを作るmulti-reference perturbationを研究する。
- reference orbitをchunk化し、固定150,001点・最大140,000反復の上限をmemory budgetへ置き換える。
- 上限へ達した場合は「∞」を暗黙に名乗らず、現在の反復/参照/精度上限と未解決数を表示する。
multi-referenceやdouble-doubleは高難度である。version固定した現rendererのbrowser計測で、reference生成、glitch fallback、10^-280以深のどれが実際に支配的か確認してから実装する。v22欠落分を現renderer値で遡及的に代用しない。
### 7.8 Exportを独立pipelineにする — P0/P1
`PNG`を次の2操作へ分ける。
- Quick snapshot: 現在見えているCanvasを保存。Previewである可能性を明示する。
- High-quality export: 1× / 2× / 4× / custom、AA、Balanced / Validated、進捗、取消を指定する。Balancedは未確定sampleをmetadataへ記録して完了可能、Validatedはchecks合格かつunresolved 0だけをfinalとして出す。Certifiedは誤差境界経路を実装した後に追加する。
High-quality exportでは、固定したViewSpecをtile描画し、全tile完了後にだけencodeする。開始前に最大辺、sample数、予約bytesをpreflightし、予算超過・allocation失敗は解像度を黙って下げず明示失敗する。座標、span、bits、iteration policy、palette、AA、precision tier、backend、kernel hash、SIMD/scalar、色空間、encoder、renderer versionをPNG metadataまたはsidecar JSONへ記録する。同じ条件では規定許容差内で再現し、bit単位再現が必要な場合はscalar mathとencoderまで固定する。
## 8. 既存仕様の改廃
| 現行仕様 | 判断 | 新仕様 |
|---|---|---|
| 自動ズームを削除 | 維持 | 自動航法は戻さず、手動探索を軽く保つ |
| 深度だけで解像度を下げない | 趣旨を維持 | Coveredは深度非依存でtarget grid 100%。Previewだけ時間予算で縮小可 |
| `q`による固定4段階解像度 | 廃止 | 描画状態をReprojected / Preview / Covered / Refined / Validatedへ変更 |
| DPR 2.5、HQ 2100×2200、direct fallback幅390/620 | 置換 | 処理モード別pixel budgetを表示。Covered/Exportは選択したgridをtileで完遂 |
| 整数格子pixel座標 | 破壊的変更 | `(x+0.5, y+0.5)`へ統一し、renderer versionとgolden imageを更新 |
| RGB勾配で最大36枚だけ2× | 置換 | 数値field/confidence/distanceベースのadaptive quadtree AA |
| HQチェック1個 | 分割 | 表示品質、省電力、数値検証を別設定へ |
| palette変更で全再計算 | 廃止 | field cacheから即時再着色 |
| 固定z=11.5でdeep切替 | 廃止 | pixel step / ULP / orbit errorで切替 |
| 起動直後にdeep pool生成 | 廃止 | 数値的必要時に段階生成 |
| 深部HQ後、各ページセッションでReal Wisdomを能動測定 | 既定では廃止 | 実jobから受動学習。診断時だけ能動測定 |
| detail cache 96件固定 | 廃止 | 管理可能領域のbyte budget LRU |
| 表示Canvasの即時PNGだけ | 変更 | Quick snapshot + 独立High-quality export |
| Base64 WASM 3ファイル直開き | 互換buildへ | Hosted版を標準、Standalone版を併売 |
| SIMD → scalar → JS/BigInt fallback | 維持 | 全backendを同じtile/pixel-center contractへ統一 |
| Reference / perturbation / rebase / BLA | 維持・強化 | 局所精度昇格、動的memory、将来multi-reference |
| world-space cache | 維持・再設計 | style非依存field cache、quadtree key、byte budget |
| base iteration + adaptive slider | 変更 | Auto policyは維持。生値はAdvancedへ移し、未確定数を表示 |
| view hash / URL共有 | 強化 | version付きViewSpec、gesture settle保存、戻る/進む復元 |
| 「∞ Zoom」表記 | 廃止 | 有限の参照長・iteration・precision上限を持つため「Deep Zoom」とし、現在の上限と未確定数を併記 |
| UI初期非表示 | 変更 | 初回だけ操作ヒントを表示し、以後は利用者設定を復元 |
低優先度の整理対象として、未使用の`MILLION`、`last`、`sx/sy`、未使用`pilot`引数、常にfalseの`strictBoundary`、v22時点で同一内容だったCOLOR SIMD/scalar payloadがある。性能改善というより、仕様とコードのずれを減らすためにテスト後に削除する。実装後の再現buildではCOLOR variantsが異なるため、generatorは同一時だけ自動deduplicateする。
## 9. UX・再現性の変更
- 一般statusは「プレビュー / 全域描画 68% / 境界AA中 / 完了 / 未確定あり」とする。Covered等の内部用語は診断drawerだけに出す。
- bit数、BLA ε、Worker数、MPPは診断drawerへ移す。
- 深部座標表示を17桁固定 (`script.js:114`) にせず、spanから必要桁数を求める。画面上は短縮し、コピー時は正確な値を出す。
- URL hashをversion付きにし、見た目を再現するiteration profileも保存する。端末依存の解像度やWorker数は保存しない。
- gesture settleごとに`replaceState`、明示shareで`pushState`、`popstate/hashchange`でviewを復元する。座標/spanの正確値入力・コピーとUndo/Redoも同じViewSpecを使う。
- `user-scalable=no`を外し、bodyはpage zoom可能にする。Canvasだけに必要なtouch-actionを限定し、アプリのpinch zoomとブラウザpage zoomの範囲を分ける。
- rangeとlabelを関連付け、toast/statusへ`aria-live`、UI toggleへ`aria-expanded`、visible focus、44px以上のtarget、`prefers-reduced-motion/transparency`対応を付ける。
- 初回は操作ヒントを表示し、2回目以降はUI表示状態を保存する。
- 通常利用者には「省電力 / 標準 / 精細」を提示し、生の反復回数はAdvancedへ移す。
- 700px以下でも進捗、未確定数、Export状態へアクセスできるcompact status sheetを出す。現行のようにstatsとhintをすべて消さない (`index.html:21`)。
## 10. 実装ロードマップ
P0/P1/P2は利用者影響のseverityであり、実装順ではない。各項目はphase、依存先、mandatory / telemetry-conditional、未通過gateを追跡する。後段実装を先行できても、前段gateを通過したとは扱わない。
### Phase 0A — browser非依存の証拠基準化
- shallow/deep/colorを含む全kernel sourceを回収または復元し、固定toolchain、compile flags、生成script、payload checksumを揃える。
- z0 / z14 / z20 / z100 / period-3 interior / boundary / 10^-280付近をversion付き固定sceneにする。中心座標はspan相応のguard digitsを持つdecimalかexact rationalとし、precision kind、oracle、期待分類、iteration policyをscene schemaへ保存する。
- v22成果物が残っている場合は全対象hashとbaselineをimmutableに保存する。残っていない場合は「v22比較不能」と機械可読に記録し、後からv23値で代用しない。
- 現行`exactDeepPixel`とは独立した、複数精度一致と誤差上限を持つ高精度reference実装で、分類、iteration、smooth valueを比較する。
- source、ABI、pixel mapping、tile seam、SIMD/scalar、解析的内部証明、Module clone、配布物hash、文書内の生成値を同じcommandで検証する。
- v18監査はarchiveと明記し、現renderer baselineを別JSONにする。
Exit gate: 全kernelをsourceから再現buildでき、version付きsceneとoracleで数値baselineを再生成でき、全成果物hashと文書内の生成値が一致する。過去baseline欠落は明示的な`not-verifiable`でありpassにしない。
### Phase 0B — 固定browser baseline
- desktop、mobile相当、4Kでcold / warmを測る。
- first preview、Covered完了、Refined完了、idle job/draw、peak memory、long taskを記録する。
- Hosted / Standalone、SIMD / scalar、Worker / Workerなしを対象構成matrixに従って分けて実行する。
- viewportのCSS寸法だけでDPRを模擬しない。mobile / desktop / 4Kは要求DPRを持つ別browser contextまたは別実機runとし、各runの実viewport・実DPR・browser build・OS・電源・試行数を保存する。
- visualはraw pixelまたはlinear-light fieldのversion付きgoldenと比較し、hash文字列が生成できただけではpassにしない。
Exit gate: 対象profileごとのbrowser acceptanceが合格し、cold / warm、操作p95、visual、idle、long task、observed memory、Export、accessibilityのraw結果を保存する。binding不在などで未実施の場合、後続実装は進められるがbrowser性能・操作・表示の達成を主張しない。
### Phase 1A — Runtime quick wins
- event-driven paint/stats
- wheel gesture settle
- `visibilitychange`停止
- deep pool遅延生成
- Screen Canvas pixel budget
- 初回`q=.3`境界の修正と、既存`q`のままPreview時間予算へ`renderPerf`を接続
- Quick snapshotに品質状態を表示し、描画中の誤解を防止
- 固定件数cacheをbyte budgetへ置換
Exit gate: idle時に予約RAF/timer/background job 0、wheel中full render 0、浅部だけではdeep compile 0、Canvas/管理可能memoryが予算内。
### Phase 1B — Pixel contract
- 全kernelを画素中心規約へ変更し、cross-backend testを追加する。
- renderer/golden image versionを上げる。
- detail BLAをbase HQ以上へ修正する。
Exit gate: canonical rationalに対するfixed-point整数一致とf64の宣言ULP基準、tile境界回帰0、既存URLのview中心不変を確認する。
### Phase 2 — Tile/field pipeline
- packed field形式、subsample形式、論理byte budgetを先に固定する。
- buffer poolを導入し、field追加による一時的なpeak memory悪化を防ぐ。
- shallowもWorker化し、全backendを共通tile protocolへする。
- Reprojected / Preview / Covered / Refined状態機械を導入する。
- palette非依存field bufferと即時再着色を導入する。
- 処理モード別sample coverage 100%とresolved coverage/未確定数の分離を実装する。
- ULPベースのshallow/deep切替を導入する。
- `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を導入し、局所反復継続を実装する。
Exit gate: sample coverage 100%、再投影pixelの誤加算0、未確定数の独立計上、false `INTERIOR_PROVEN` 0、logical memory budget内。
### Phase 3A — Refine / Export
- Hosted / Standalone buildを分ける。
- 独立High-quality export、進捗、取消、metadataを実装する。
- adaptive AAとlinear-light resolveを導入する。
Exit gate: 指定寸法、完了待ち、取消、metadata、許容差内の再現性、Refinedのsample accountingが合格する。
### Phase 3B — 計測で必要ならWASM memory再編
- deep/BLA/color WASM統合と動的初期容量
- immutable reference / key別BLA table共有、Worker別scratch
- shared framebufferまたはbuffer poolによるcopy削減
- 大容量instanceの退役・再生成
Exit gate: 対象deep sceneで速度改善を確認し、data race 0、logical/observed peak memoryが予算内。
### Phase 4 — 計測後の高精度研究
- reference orbit専用Workerと次referenceの先読み
- double-double局所昇格
- error ball / interval verification
- scaled BLA
- multi-reference perturbation
- WebGPUによるcolor/resolve/composition、必要なら浅部Preview演算
Phase 4は、telemetryで支配コストまたは誤差が確認された項目だけ実施する。
## 11. 受入基準
browser時間値は、profileごとに固定したbrowser build・OS・viewport・実DPR・電源条件で、warmup後30回以上を採取してp95を算出する。単発値、同期event handler時間、異なるsceneを混ぜた分位点は代用しない。Visual golden、数値oracle、threshold policyには独立したversion IDを与え、threshold欠落時はpassにしない。
| 分野 | 基準案 |
|---|---|
| Idle | 最終更新2秒後に予約RAF/timer/background job 0、Canvas/DOM write 0回/秒。CPUは同環境blank baselineとの差を補助指標にする |
| 操作 | 最後の実入力eventから、その入力を反映した再投影frameのpaint完了までのp95 < 16ms、計測区間のmain thread 50ms超long task 0、継続wheel中に新規full renderを開始しない |
| Preview | settle期限到達からPreview paint完了まで、profile・sceneごとの30回以上でp95 < 120ms |
| Covered | target Screen Canvasのsample coverage 100%、effective DPRを表示し、再投影pixelをsample coverageへ数えない。resolved coverageとunresolved数を別記録。scene別時間budgetはPhase 0B後に確定 |
| Refined | Fine modeで実際に`REFINED`へ到達し、対象pixelは記録済み実sampleのlinear-light resolve。subsample accountingとversion付きgoldenを比較し、Canvas補間だけをAAと数えない |
| 数値 | version付きcorpus/oracle/threshold policyを必須とする。Validatedではcorpus上のfalse `ESCAPED` 0、false `INTERIOR_PROVEN` 0、unresolved 0。escape iteration差・smooth誤差・`INTERIOR_LIKELY` mismatchはpolicyの明示閾値で判定し、閾値未定義ならfail |
| Pixel mapping | canonical rationalとties-away-from-zeroの`roundDiv`を基準に、fixed-point backendは期待整数と一致、f64 backendは宣言ULP以内。tile境界に1px/半pxの回帰がない |
| Export | 指定寸法どおり、全tile完了前はfinal fileを出さない、取消可能、metadata必須項目とsample countを検査。Balancedはunresolved数を記録、Validatedは0。宣言したbackend/encoder条件とversion付きgoldenの許容差内で再現 |
| Memory | managed typed array/WASM/cacheは暫定mobile 96 MiB、desktop 192 MiB以内。Canvas/GPUを含むobserved peakは固定環境で別記録 |
| Startup | Hosted shallow表示ではdeep取得/compile 0。Standaloneはdeep復号/compileとWorker生成0 |
| 配布 | v22 baseline欠落のため初回比較は`not-verifiable`とする。v23の同一request集合・Content-Encoding・cache条件を新baselineとして固定し、以後の取得bytes・compile timeを回帰判定する。Hostedではdeep到達前のdeep request 0 |
| Accessibility | keyboard-onlyで全操作を実行し、visible focus、44px target、label/name、live status、page zoomとCanvas pinchの共存、reduced motion/transparencyの実動作が合格。静的属性の存在だけではpassにしない |
| Regression | version付き現行scene corpusをCIで実行し、速度・memory・分類・例外を履歴化する。各metricのwarn/fail閾値をpolicyへ保存する |
端末依存の時間値は、対象端末を固定して初めて合否に使う。絶対時間だけでなく、同一scene・同一pixel数の比率とcorrectnessを主要指標にする。browser非依存gateはsource/ABI/hash/数値oracle/pixel mapping/scene精度/document consistencyまでを証明し、paint、DPR、GPU、入力、download、実accessibilityの代替とはみなさない。
## 12. 推奨する最初の実装単位
最初の変更セットは、次の範囲に限定すると効果を測りやすい。
1. v22 baselineの有無を機械可読に確定し、現renderer baselineと全WASMの再現buildを用意する。
2. 常時RAFと毎frame DOM更新を廃止する。
3. wheelをgestureとしてデバウンスし、非表示時のjobを止める。
4. Screen Canvasへpixel budgetを設ける。
5. deep poolを必要時まで作らない。
6. 既存`q`のままPreview時間予算へ`renderPerf`を接続する。
7. version付き現renderer用のidle・gesture・startup・memory回帰テストを追加する。
この段階ではBLAカーネルやpixel contractをまだ変えない。まずidle負荷、入力応答、Canvas/初期memory、Preview時間を改善する。画素中心変更は、全kernelを再現buildできるPhase 1Bで行う。
## 付録A: 根拠データ
- 現行仕様: `README.md`
- 数値・性能監査: `AUDIT.md`
- end-to-end記録: `audit/final-browser-benchmark.json`, `audit/benchmarks.json`
- UI/スケジューラ/renderer: `index.html`, `script.js`
- BLA固定配列・演算: `src/bla_kernel_v18.c`
既存監査から今後も守るべき知見は次の通り。
- 数学的iterationを途中で切って黒扱いしない。
- pilot画像を未解決pixelの計算結果として使わない。
- 全stripを毎回strict検証しない。
- 1点の不一致で全画面を再計算せず、tile/strip単位で補修する。
- ditherはdetail検出前へ入れず、最終表示段だけにする。
- Reference recenterは固定距離でなく、実測損失とbuild費を比較する。
## 付録B: 用語
| 用語 | 本文での意味 |
|---|---|
| BLA | Bilinear Approximation。基準軌道上の複数反復を近似的にまとめて進める高速化 |
| perturbation | 高精度な基準軌道との差分だけを低精度で追跡する摂動法 |
| rebase | 差分が不安定になったとき、基準軌道上の別位置へ基準を移す処理 |
| DPR / effective DPR | OS/browserが示すdevice pixel比 / pixel budget適用後に実際に使う比率 |
| sample / resolved coverage | 選択したScreen Canvas gridのうち、新ViewSpecでpolicy上限まで実計算済みの割合 / `ESCAPED`またはpolicy上の`INTERIOR_PROVEN`へ確定した割合 |
| AA | Anti-Aliasing。1 pixel内の複数sampleをresolveして境界のジャギーを減らす処理 |
| SIMD | 1命令で複数データを処理するWASM最適化 |
| ULP | その浮動小数値付近で表現できる隣接値の間隔 |
| EMA / MPP | 指数移動平均 / millisecond per pixel。時間予算の推定に使う |
| cold / warm | 初回compile・cache未構築 / Worker・reference・BLA cache構築後 |
| confidence | 近似・検証・内部判定の根拠と確からしさを表すmetadata |
| Validated / Certified | 宣言した有限policyを照合済み / 全sampleへ誤差境界または証明手続きを適用済み |
## 付録C: 互換構成
| 構成 | 動作方針 | 主な制限 |
|---|---|---|
| Hosted + SIMD + Worker | 推奨構成。外部WASM、遅延取得、並列tile | Shared memoryは別途COOP/COEPが必要 |
| Hosted、SIMDなし | scalar WASMを必要時に取得 | 速度低下。数値policyは同一 |
| SharedArrayBufferなし | Transferable buffer poolと短いchunkを使用 | hot loop中のatomic cancel、read-only table共有なし |
| Workerなし | main-thread WASMを時間sliceして使用 | 並列なし。Covered/Exportの完了が遅い |
| OffscreenCanvasなし | Workerはfield/RGBA bufferを返し、main Canvasへ合成 | Export encodeと合成の一部がmain thread |
| Standalone / `file://` | 埋込WASM互換build。deepの復号・compileは遅延 | streaming、HTTP cache、COOP/COEP、Service Workerなし |
最低対応browser versionとoffline cache方針は、Phase 0Bで実機matrixを作って確定する。

View file

@ -1,157 +0,0 @@
# v23 → v24.2.3 WebGPU migration / crossover study
## 目的
v23のCPU/WASM deep pixel rendererとWorker poolを廃止し、画素単位のmassively parallel処理をWebGPUへ集約する。CPU高精度処理はBigInt ViewStateとreference orbit生成に限定する。
## 最終構成
```text
BigInt ViewState
│
├─ Direct安全域 ─────────────── WebGPU f32 direct
│
└─ Deep/overlap
│
├─ overlap: current-view GPU quality/speed probe
│ ├─ direct比較
│ └─ corrected deep比較
│
└─ deep: BigInt reference Worker
↓
guarded rescaled f32 perturbation
↓
UNKNOWN?
/ \
no yes
│ ↓
│ double-single correction
└────────────┘
↓
GPU field
↓
color / AA / present
```
## v24.1系からの品質修正
v24.1のguarded f32 perturbationは誤分類を避けるためUNKNOWNへ保守退避する設計だった。境界sceneではUNKNOWNが大きく、interiorに近い暗色表示と相まって黒面積増大/黒点として見えた。
### Multi-reference評価
最初にUNKNOWN macro tileへsecondary BigInt referenceを追加する方式をモデル化した。しかし複数referenceを追加しても`swirly-seahorses-z12`全画面モデルのUNKNOWN回収率が低く、reference生成・転送・再dispatchのコストに見合わなかった。そのためproductionへは採用しない。
### Sparse double-single correction
代わりにUNKNOWNだけをdouble-single complex perturbationで再評価する。reference orbitはprimaryと共用するため追加BigInt orbitは不要。
61×39 dense model / 2000 iter:
- primary UNKNOWN 478
- primary membership reversal 0
- correction recovered 478
- remaining UNKNOWN 0
- correction membership reversal 0
- escape iteration mismatch 5(最大374)
したがってcorrectionは表示品質passとして採用し、membership certificateには使わない。
## Backend router再設計
旧設計のzoom/ULP単一閾値はdeepを早く使いすぎる一方、閾値を下げるだけではdirectの境界誤分類リスクがある。
v24.2.1は三領域に分ける。
- ratio < 8: deep必須
- ratio >= 128: direct
- 8 <= ratio < 128: overlap
Overlapは本描画前にcurrent-view probeを行う。probeは中心のspeed tileと、画面5領域のquality tileをactual full-resolution pixel座標で評価する。directとDS補正済みdeepのescape/bounded分類差が0、補正後deep UNKNOWN率0.5%以下であることを要求する。そのうえでreference buildを含むdeep予測時間がdirectの85%未満ならdeep、それ以外はdirect。
深度binの過去profileだけでbackendを決定しない。
## BLAを戻さない理由
f64 BLAをf32係数へ量子化した旧試作は、dense swirly回帰でbounded/escaped membership反転を起こした。量子化誤差を含む安全な受入半径が未確立のため、BLA builder/evaluator/nodeはproduction treeに存在しない。
## Export
v24.2.1ではscreenだけでなくExportにもdeep correctionを適用する。
1×:
```text
primary deep → clear stats → DS correction → color → 1 readback
```
2×2:
```text
4 samples × (primary deep → DS correction → color)
→ GPU linear-light resolve
→ 1 readback / tile
```
4 sampleのunresolved counterを分離し、sidecarの`unresolvedSamples`へ合計する。
## 削除済み
- deep WASM renderer / Worker pool
- BLA WASM / GPU BLA試作
- BigInt direct production pixel renderer
- multi-reference production試作
- redundant strict retry queue
- Canvas2D deep hot path
- giant full-image Canvas Export
- Validated direct UI/Export
## Validation contract
productionは`membershipCertified:false`。Strictはprimary error-bound閾値を厳しくするpolicyで、任意精度direct証明ではない。DS correctionもvisual-quality recovery passである。
## v24.2.3 crossover study
BigInt Deepを廃止するのではなく、使用開始点を後ろへ送れるか検証するためDS Direct候補を追加した。DSはreference構築が不要なためcold latency削減の可能性があるが、WGSL double-singleのコストと品質はGPU依存である。したがってこのreleaseではbenchmark専用とし、実GPU結果を得てからrouterへの昇格を判断する。
## v24.2.4 diagnostic Hybrid tier
Crossover実機値でDS Directはz10/z12でもFull Deepより高速だったが、単独では分類品質が不足した。そこでproduction routerを変更する前に、DS Directのcondition proxy `|dz/dc|` を追跡し、危険pixelだけ既存Deep double-single correctionへ送るHybridをbenchmark専用で実装した。
候補routerはまだ採用しない。実験で `class mismatch=0`, `remaining UNKNOWN=0`, `Hybrid cold < 0.9 × Full Deep cold`, `correction rate < 60%` を満たすviewだけを将来のHybrid overlap候補とする。z12のCPU事前モデルではrisk=1e13で約91%が補正対象となるため、Full Deepへ切り替える境界を検出できる見込み。
## v24.2.5 Sparse Direct experiment (Migration)
- The previous sparse-Full-Deep diagnostic is superseded and is not exposed in the UI.
- Added `DIRECT_TS_CORRECT_WGSL`, a reference-free three-term f32 expansion correction shader that only processes DS sensitivity UNKNOWN pixels.
- Added `benchmarkSparseDirect()` / `renderTileMetaSparseDirect()` and a z6/z8/z10/z12 risk sweep.
- Full Deep remains the quality/timing baseline; the Sparse Direct candidate itself does not build or bind a reference orbit.
- Production backend routing is unchanged.
- Root `index.html` is now a single-file bundle with no external script dependency, specifically to keep diagnostic UI controls functional when the HTML is opened from an archive/temp extraction.
- Static single-file UI contract and real-browser click smoke are separate gates; real WebGPU arithmetic still requires a WebGPU-capable browser.
- Sparse Direct coordinate packing uses exact residual subtraction from BigInt fixed-point into three f32 components; this removed the CPU TD-model class errors caused by the earlier host-Number split.
## v24.2.6 experiment direction
The float-expansion sparse correction branch is superseded by an integer fixed-point experiment. The transition strategy under test is now:
```text
f32 Direct
-> DS Direct
-> DS risk gate + sparse Q8.88 Direct
-> Full Deep
```
Only the diagnostic branch is changed. Production remains on the existing direct/deep router until real-GPU timing and class gates justify promotion.
## v24.2.7 experiment correction
Do not interpret v24.2.6 Fixed96 `remaining-unknown` measurements as a Fixed96 feasibility result. They were dominated by an invalid Q8.88 range guard. v24.2.7 is the first build in this branch whose real-GPU Fixed96 correction measurements are suitable for the crossover experiment.
## v24.2.8 experiment scheduling correction
Fixed96 remains diagnostic and is not part of the production backend router. The correction execution changed from full-tile 2D dispatch with early returns to GPU compact-queue + indirect 1D dispatch. This isolates the experiment's arithmetic feasibility from an avoidable SIMD divergence/scheduling penalty. Adoption decisions must use v24.2.8-or-later timings; v24.2.7 Fixed96 timings are not representative of a sparse implementation.

136
README.md
View file

@ -1,136 +0,0 @@
# Mandelbrot Deep Zoom v24.2.8 — Fixed96 True Sparse Queue Experiment
v24.2.8は、v24.2.7で数値バグを修正したFixed96実験を**真のsparse GPU実行**へ変更した診断版です。DS guarded passが危険pixel indexをGPU queueへcompactし、1-thread finalize passがqueue件数からindirect dispatchを生成、Fixed96 Q8.88 kernelはqueueに入ったpixelだけを処理します。guard→queue→indirect Fixed96の間にCPU readbackはありません。production routerはまだ変更していません。
## 数値backend
- **Direct**: WebGPU `f32` direct iteration。
- **Deep primary**: BigInt reference orbit + WebGPU guarded rescaled `f32` perturbation。
- **Deep correction**: primaryで`FIELD_UNKNOWN`になったpixelだけ、同じreferenceを用いるdouble-single perturbationで再評価。
- **BLA**: productionでは未搭載。旧f32量子化BLAでmembership反転を再現したため再導入していません。
- **Multi-reference**: v24.2開発中に全画面モデルを評価したが、追加referenceの生成/dispatchコストに対してUNKNOWN回収が小さかったためproductionから撤去しました。
Deep primaryはroundoff上界を追跡し、escape/bounded判定を安全に受理できないpixelを`UNKNOWN`にします。double-single correctionは表示品質回復用であり、任意精度membership証明ではありません。PNG sidecarの`membershipCertified`は常に`false`です。
## Direct / Deep router
単一のzoom閾値でdeepへ切り替えません。
1. `pixel step / f32 ULP < 8`ならdirect座標分解能が不足するためdeep必須。
2. 比率が128以上ならdirectを即採用。
3. 8以上128未満のoverlap帯では、本描画前に**現在のview専用GPU probe**を実行。
4. probeではactual full-resolution座標上の複数領域をdirectと「DS補正済みdeep」で比較。
5. deep側UNKNOWN率が0.5%以下、escape/bounded分類差0を満たした場合だけ速度比較へ進む。
6. reference生成時間を含めてもdeepがdirectより15%以上高速な場合だけdeepを採用。それ以外はdirect。
過去の同深度profileだけで現在viewをdeepへ振ることはありません。profileは診断/平滑化用で、backend選択には現在viewのquality probeを要求します。
## Crossover benchmark
診断情報の **Backend比較** から、現在のGPUで z0 / 2 / 4 / 6 / 8 / 10 / 12 / 14 / 16 / 20 / 30 / 40 を自動測定できます。
速度測定は全画面を反復実行しません。現在のview座標上の代表3タイル(最大256×144)を測定して `ms/pixel` を求め、現在のcanvas全体へ換算します。品質比較は別の5領域で実施します。これによりz12以降で全画面Deepを何度も実行してGPU queueが長時間塞がる問題を避けます。reference生成または1回のGPU測定が15秒を超えた場合は明示的に中断します。
比較するbackend:
- `f32`: 現行WebGPU標準。
- `DS`: experimental whole-frame double-single Direct。**production routerには未接続**。
- `Deep warm`: BigInt referenceをcache済みとしたDeep primary + DS correction。
- `Deep cold`: verified BigInt reference構築 + Deep warm。
結果の時間欄は代表tileからの**全画面換算値**です。画面表示に加えJSON保存できます。品質のBigInt oracle掃引結果は `CROSSOVER_BENCHMARK.md` と `audit/v24-crossover-quality.json` を参照してください。CPUモデルではSeahorse trackでf32がz6付近、DS Directがz10付近からmaterial degradationを示し、DSを中間backendとして試す価値があるという結果です。
## Fixed96 Direct experiment
診断情報の **Fixed96 Direct実験** から z6 / z8 / z10 / z12 を測定できます。DS Directで `|dz/dc|` を追跡し、`1e11 / 1e12 / 1e13 / 1e14` の各risk thresholdを超えたpixelだけ、reference不要の**96-bit符号付きQ8.88固定小数点Direct**で再評価します。
v24.2.8では補正対象を`u32` pixel-index queueへcompactします。queue lengthから`dispatchWorkgroupsIndirect()`用の1D workgroup数をGPU内で生成し、Fixed96は**queued pixel数だけ**実行します。従来の「全画面dispatchして非対象laneをreturn」は廃止しました。
実験システムは各GPU sampleで次をfail-closed検証します。
- `selected == enqueued == dispatchCount == processed`
- queue overflow = 0
- indirect workgroup数 = `ceil(enqueued / 64)`
- invalid/stale queue entry = 0
- `corrected + remain == processed`
1項目でも崩れれば測定値を採用せず実験をエラー停止します。結果表には`correction`と実際の`dispatch`率を別々に表示し、`queue=ok`を確認できます。速度は代表3tileから全画面換算し、品質は5 spatial tilesでcorrected Full Deepと比較します。production routerへは未接続です。
### 単一ファイル起動
rootの `index.html` は `gpu-kernels.js` と `script.js` を内包した単一HTMLです。ZIP内のHTMLをOSが一時展開して開く場合でも、隣接JSファイルに依存しません。
## Deep correction
primary deepでUNKNOWNが残った場合、通常/精細/Strictではsparse double-single correctionを1回実行します。KNOWN pixelはshader冒頭で即returnするため、数値反復はUNKNOWNだけです。省電力モードはUNKNOWNが一定量を超えた場合のみ補正します。
`swirly-seahorses-z12`の61×39 CPU `Math.fround`モデルでは:
- primary UNKNOWN: 478 / 2379
- primary false escape: 0
- primary false bounded: 0
- DS correction回収: 478 / 478
- correction後UNKNOWN: 0
- correction false escape / false bounded: 0 / 0
escape iterationは5 pixelでBigInt oracleと差があり、最大差374 iterationでした。このためDS correctionは「見た目と有限iterationの分類回復」であり、Validated相当の保証には使用しません。
## UNKNOWN診断
内部metadataではUNKNOWN理由を分離しています。
- error-bound
- escape-uncertain
- reference-end
- rebase-gap
- range
- operation-limit
未補正UNKNOWNはinteriorの完全な黒と別色で表示され、診断欄に理由別件数が出ます。
## Export
- 512×512 reusable GPU workspace。
- deep Exportでもprimary後にDS correctionを実行。
- 2×2 AAは各4 sampleをGPUで数値計算→補正→彩色し、GPU resolve後に1回だけreadback。
- 最終画像全体の巨大Canvasは作らず、scanline bandを`CompressionStream('deflate')`へ流すstreaming PNG。
- 16384px級の片辺をtile処理可能。
- sidecarに`unresolvedSamples`を記録。
## Fallback
WebGPUそのものが利用できない場合だけ軽量JavaScript f64 fallbackを使います。deep zoomはNumber underflow等で誤画像を出さず、「このズーム深度はWebGPUが必要です」と表示して停止します。shader/pipeline failureではCPU全画面rendererへ自動fallbackしません。
## Build / test
```bash
npm test
npm run build
```
`npm test`には以下を含みます。
- WGSL reserved-word scan(9 shader。DS Direct / guarded DS / Fixed96 Direct correctionは実験専用)
- production source/tree contract
- BigInt reference Worker + P/P+64 checkpoint
- f32 direct CPU model
- view固有adaptive backend router policy
- guarded primary deep CPU model
- sparse double-single correction CPU model
- BLA disabled contract
- pixel/export geometry
- e400/e1000 exact coordinate formatting
- streaming PNG CRC/deflate/abort
- real-WebGPU acceptance harnessの静的contract
実GPU受入は `tests/webgpu-acceptance.html` をWebGPU対応ブラウザで開いて実行します。これはdirect/deep/DS/Fixed96 Direct shader compile、DS correction、Fixed96 smoke、dense既知反例、view-specific router、1×/2×2 Export readback、uncaptured WebGPU errorを検査します。
## 配布
`npm run build`で以下を生成します。
- `dist/standalone/`
- `dist/hosted/`
`dist/standalone/` は単一 `index.html`、`dist/hosted/` は `index.html`, `gpu-kernels.js`, `script.js` です。root `index.html` も単一ファイルで、ZIPから展開してそのまま開けます。

View file

@ -1,245 +0,0 @@
{
"environment_note": "Absolute timings are container/Chromium specific. Use ratios and correctness rates as the main evidence.",
"end_to_end_dynamic_base_comparison": [
{
"scene": "seahorse_z14",
"v17_base_ms": 1059.0,
"v18_base_ms": 288.0,
"speedup_x": 3.677
},
{
"scene": "seahorse_z20",
"v17_base_ms": 1226.0,
"v18_base_ms": 238.0,
"speedup_x": 5.151
},
{
"scene": "seahorse_z100",
"v17_base_ms": 983.0,
"v18_base_ms": 474.0,
"speedup_x": 2.074
},
{
"scene": "period3_z20",
"v17_base_ms": 1931.0,
"v18_base_ms": 781.0,
"speedup_x": 2.472
}
],
"v17_work_cap_correctness": {
"w": 48,
"h": 30,
"pixels": 1440,
"globalIter": 3490,
"capIter": 2870,
"capped": 1440,
"late": 1066,
"insideGlobal": 374,
"lateEscapeRateAmongCapped": 0.7402777777777778,
"elapsedMs": 6344.968723999999
},
"pilot_nearest_fill_accuracy": {
"full": [
128,
78
],
"pilot": [
56,
34
],
"fullMs": 1356.671812,
"pilotMs": 169.93951399999992,
"classMismatch": 451,
"classRate": 0.04517227564102564,
"escapeIterDiffGt8": 4364,
"diffRate": 0.437099358974359
},
"intrinsic_interior_detection": [
{
"name": "period3 wide",
"re": "-0.1225611668766536",
"im": "0.7448617666197442",
"span": 0.1,
"offMs": 766.7149390000001,
"onMs": 5.955513999999994,
"interior": 6912,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "period3 mid",
"re": "-0.1225611668766536",
"im": "0.7448617666197442",
"span": 0.03,
"offMs": 438.65803500000015,
"onMs": 2.334043000000065,
"interior": 6912,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "period2 boundary neighborhood",
"re": "-0.75",
"im": "0",
"span": 0.1,
"offMs": 364.72781799999984,
"onMs": 1.0177550000000792,
"interior": 6806,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "mixed exterior/interior",
"re": "-0.5",
"im": "0.5",
"span": 0.1,
"offMs": 282.849144,
"onMs": 16.219244000000117,
"interior": 5338,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "cardioid cusp neighborhood",
"re": "0.25",
"im": "0",
"span": 0.04,
"offMs": 329.88844100000006,
"onMs": 1.7636029999998755,
"interior": 6403,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
}
],
"reference_recenter_z14": {
"oldRefBuildMs": 29.320617,
"tests": [
{
"sx": 0.5,
"sy": 0,
"oldMs": 691.804547,
"newMs": 640.2659849999995,
"newRefBuildMs": 9.508285999999998,
"firstFrameNetSavingMs": 42.03027600000041,
"steadySavingMs": 51.53856200000041
},
{
"sx": 0.8,
"sy": 0,
"oldMs": 756.002915,
"newMs": 655.9519419999997,
"newRefBuildMs": 8.81105800000023,
"firstFrameNetSavingMs": 91.23991500000011,
"steadySavingMs": 100.05097300000034
},
{
"sx": 0,
"sy": 0.8,
"oldMs": 730.6417070000007,
"newMs": 671.6106240000008,
"newRefBuildMs": 8.391966000001048,
"firstFrameNetSavingMs": 50.639116999998805,
"steadySavingMs": 59.03108299999985
},
{
"sx": 0.8,
"sy": 0.5,
"oldMs": 787.9931480000014,
"newMs": 740.047842,
"newRefBuildMs": 7.513370000000577,
"firstFrameNetSavingMs": 40.43193600000086,
"steadySavingMs": 47.94530600000144
},
{
"sx": 1.2,
"sy": 0,
"oldMs": 705.7068249999975,
"newMs": 689.7542820000017,
"newRefBuildMs": 7.101795999999013,
"firstFrameNetSavingMs": 8.850746999996773,
"steadySavingMs": 15.952542999995785
},
{
"sx": 1.5,
"sy": 0.8,
"oldMs": 793.3721589999986,
"newMs": 733.093237000001,
"newRefBuildMs": 7.395623000000342,
"firstFrameNetSavingMs": 52.88329899999735,
"steadySavingMs": 60.27892199999769
}
]
},
"reference_recenter_z20": {
"oldRefBuildMs": 23.925890999999993,
"tests": [
{
"sx": 0.5,
"sy": 0,
"oldMs": 100.18383699999998,
"newMs": 105.04222300000004,
"newRefBuildMs": 9.637189000000006,
"firstFrameNetSavingMs": -14.49557500000006,
"steadySavingMs": -4.858386000000053
},
{
"sx": 0.8,
"sy": 0,
"oldMs": 106.89347099999986,
"newMs": 93.24754299999995,
"newRefBuildMs": 8.222010999999952,
"firstFrameNetSavingMs": 5.42391699999996,
"steadySavingMs": 13.645927999999913
},
{
"sx": 0,
"sy": 0.8,
"oldMs": 110.3922050000001,
"newMs": 98.40486799999985,
"newRefBuildMs": 7.7547400000000835,
"firstFrameNetSavingMs": 4.232597000000169,
"steadySavingMs": 11.987337000000252
},
{
"sx": 0.8,
"sy": 0.5,
"oldMs": 138.4319349999996,
"newMs": 102.46810800000003,
"newRefBuildMs": 8.225470999999743,
"firstFrameNetSavingMs": 27.73835599999984,
"steadySavingMs": 35.96382699999958
},
{
"sx": 1.2,
"sy": 0,
"oldMs": 111.11085400000002,
"newMs": 93.51345999999967,
"newRefBuildMs": 8.248458000000028,
"firstFrameNetSavingMs": 9.348936000000322,
"steadySavingMs": 17.59739400000035
},
{
"sx": 1.5,
"sy": 0.8,
"oldMs": 126.42733499999986,
"newMs": 95.50466299999971,
"newRefBuildMs": 8.381738999999925,
"firstFrameNetSavingMs": 22.540933000000223,
"steadySavingMs": 30.922672000000148
}
]
},
"code_metrics": {
"v17_script_lines": 1067,
"v17_script_bytes": 143149,
"v18_script_lines": 805,
"v18_script_bytes": 83427,
"v18_kernels_lines": 12,
"v18_kernels_bytes": 52136
}
}

View file

@ -1,82 +0,0 @@
{
"environment": "Chromium 144 headless, 780x441 viewport, container-local",
"scenes": [
{
"name": "seahorse_z14",
"bits": 288,
"first": {
"render": "789 ms / 3490 回 · BLA 54.395ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.7424271106719971
},
"base": {
"render": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.28958845138549805
},
"engine": "WASM SIMD ×2 · BLA e-28 + リベース / 288 bit",
"renderText": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z20",
"bits": 320,
"first": {
"render": "401 ms / 4509 回 · BLA 5.023ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.4155466556549072
},
"base": {
"render": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.26917600631713867
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 320 bit",
"renderText": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z100",
"bits": 576,
"first": {
"render": "482 ms / 17085 回 · BLA 0.076ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.5742058753967285
},
"base": {
"render": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.5136466026306152
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 576 bit",
"renderText": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "period3_z20",
"bits": 320,
"first": {
"render": "1420 ms / 4509 回 · BLA 0.395ms/kpx · 内部省略 100%",
"badge": "深部リベース",
"saw": true,
"wall": 1.5150198936462402
},
"base": {
"render": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
"badge": "深部リベース",
"saw": true,
"wall": 0.8927633762359619
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース · 内部早期終了 100% / 320 bit",
"renderText": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
"canvas": "780x441"
}
],
"errorCount": 0
}

View file

@ -1,82 +0,0 @@
{
"environment": "Chromium 144 headless, 780x441 viewport, container-local",
"scenes": [
{
"name": "seahorse_z14",
"bits": 288,
"first": {
"render": "789 ms / 3490 回 · BLA 54.395ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.7424271106719971
},
"base": {
"render": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.28958845138549805
},
"engine": "WASM SIMD ×2 · BLA e-28 + リベース / 288 bit",
"renderText": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z20",
"bits": 320,
"first": {
"render": "401 ms / 4509 回 · BLA 5.023ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.4155466556549072
},
"base": {
"render": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.26917600631713867
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 320 bit",
"renderText": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z100",
"bits": 576,
"first": {
"render": "482 ms / 17085 回 · BLA 0.076ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.5742058753967285
},
"base": {
"render": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.5136466026306152
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 576 bit",
"renderText": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "period3_z20",
"bits": 320,
"first": {
"render": "1420 ms / 4509 回 · BLA 0.395ms/kpx · 内部省略 100%",
"badge": "深部リベース",
"saw": true,
"wall": 1.5150198936462402
},
"base": {
"render": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
"badge": "深部リベース",
"saw": true,
"wall": 0.8927633762359619
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース · 内部早期終了 100% / 320 bit",
"renderText": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
"canvas": "780x441"
}
],
"errorCount": 0
}

View file

@ -1,168 +0,0 @@
[
{
"z": 40,
"global": 7752,
"comp": 4300,
"refBuildMs": 18.262529999999998,
"refLen": 3246,
"fullBlaMs": 5.190414000000004,
"blaBuildMs": 2.432442000000009,
"strictMs": 0,
"blaStrict": {
"valid": 41340,
"classMismatch": 0,
"classRate": 0,
"diffGt1": 0,
"diffRate": 0,
"bad": 0,
"maxDiff": 0
},
"work": {
"unresolved": 0,
"unresolvedRate": 0,
"lateEscape": 0,
"lateEscapeRate": 0,
"pilotClassMismatch": 0,
"pilotClassMismatchRate": 0,
"meanIterErr": 0,
"maxIterErr": 0
},
"safe": {
"safeRatio": 0,
"periodicMs": 0.08044799999998986,
"blackRatio": 0,
"interiorRatio": 0
},
"safeAcc": {
"skipped": 0,
"ratio": 0,
"falseEscaped": 0,
"falseRate": 0
},
"safeRun": {
"buildMs": 0.15029499999999985,
"renderMs": 1.6046879999999817,
"pixels": 41340,
"tasks": 34
},
"eps": [
{
"e": 18,
"ms": 1.3821410000000185,
"build": 0.14597599999999034
},
{
"e": 20,
"ms": 1.4494080000000054,
"build": 0.09218199999997978
},
{
"e": 23,
"ms": 1.6922059999999988,
"build": 0.09046299999999974
},
{
"e": 27,
"ms": 1.456985000000003,
"build": 0.09038200000000529
},
{
"e": 32,
"ms": 1.3537540000000092,
"build": 0.0893779999999822
}
],
"recenter": {
"oldRefLen": 3246,
"newRefLen": 3246,
"newBuildMs": 10.052744000000018,
"oldRenderMs": 1.324105999999972,
"newRenderMs": 1.4144129999999961,
"benefitMs": -0.09030700000002412,
"netFirstFrame": -10.143051000000042
}
},
{
"z": 100,
"global": 17085,
"comp": 7600,
"refBuildMs": 9.734955000000014,
"refLen": 3246,
"fullBlaMs": 1.3740439999999978,
"blaBuildMs": 0.10230300000000625,
"strictMs": 0,
"blaStrict": {
"valid": 41340,
"classMismatch": 0,
"classRate": 0,
"diffGt1": 0,
"diffRate": 0,
"bad": 0,
"maxDiff": 0
},
"work": {
"unresolved": 0,
"unresolvedRate": 0,
"lateEscape": 0,
"lateEscapeRate": 0,
"pilotClassMismatch": 0,
"pilotClassMismatchRate": 0,
"meanIterErr": 0,
"maxIterErr": 0
},
"safe": {
"safeRatio": 0,
"periodicMs": 0.06870000000000687,
"blackRatio": 0,
"interiorRatio": 0
},
"safeAcc": {
"skipped": 0,
"ratio": 0,
"falseEscaped": 0,
"falseRate": 0
},
"safeRun": {
"buildMs": 0.11969400000000974,
"renderMs": 1.3758139999999912,
"pixels": 41340,
"tasks": 34
},
"eps": [
{
"e": 18,
"ms": 1.3486680000000035,
"build": 0.08054500000000075
},
{
"e": 20,
"ms": 1.3468280000000163,
"build": 0.09072599999998943
},
{
"e": 23,
"ms": 1.3401210000000106,
"build": 0.0852779999999882
},
{
"e": 27,
"ms": 1.6213699999999847,
"build": 0.08418900000000917
},
{
"e": 32,
"ms": 1.4460180000000094,
"build": 0.08714600000001838
}
],
"recenter": {
"oldRefLen": 3246,
"newRefLen": 3246,
"newBuildMs": 8.042026000000021,
"oldRenderMs": 1.3196079999999881,
"newRenderMs": 1.3709360000000004,
"benefitMs": -0.05132800000001225,
"netFirstFrame": -8.093354000000033
}
}
]

View file

@ -1,142 +0,0 @@
seahorse-z14 iter 3490 ref 3246 build 21.53 baseline 1807.84
{ e: 23, b: 0, p: 0, int: 0 } ms 299.14 {
bla: 3043732,
ptb: 11197405,
rebase: 72197,
interior: 0,
unresolved: 0,
fail: 0
} cmp {
cls: 157,
clsRate: 0.010012755102040817,
diff: 820,
diffRate: 0.05229591836734694,
bad: 0
}
{ e: 23, b: 1600, p: 2200, int: 1 } ms 373.69 {
bla: 3043732,
ptb: 11197405,
rebase: 72197,
interior: 0,
unresolved: 0,
fail: 0
} cmp {
cls: 157,
clsRate: 0.010012755102040817,
diff: 820,
diffRate: 0.05229591836734694,
bad: 0
}
{ e: 23, b: 800, p: 1200, int: 1 } ms 398.19 {
bla: 3043729,
ptb: 11189659,
rebase: 71797,
interior: 0,
unresolved: 107,
fail: 0
} cmp {
cls: 131,
clsRate: 0.008354591836734693,
diff: 747,
diffRate: 0.04764030612244898,
bad: 107
}
{ e: 32, b: 1600, p: 2200, int: 1 } ms 785.73 {
bla: 3016859,
ptb: 26792628,
rebase: 72089,
interior: 0,
unresolved: 54,
fail: 0
} cmp {
cls: 60,
clsRate: 0.003826530612244898,
diff: 182,
diffRate: 0.011607142857142858,
bad: 54
}
{ e: 40, b: 0, p: 0, int: 1 } ms 1217.85 {
bla: 1091101,
ptb: 44427881,
rebase: 72170,
interior: 0,
unresolved: 0,
fail: 0
} cmp {
cls: 13,
clsRate: 0.0008290816326530612,
diff: 31,
diffRate: 0.0019770408163265305,
bad: 0
}
seahorse-z20 iter 4509 ref 3246 build 14.47 baseline 383.51
{ e: 23, b: 0, p: 0, int: 0 } ms 52.83 {
bla: 605601,
ptb: 2085586,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 1600, p: 2200, int: 1 } ms 60.84 {
bla: 605601,
ptb: 2085586,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 800, p: 1200, int: 1 } ms 61.15 {
bla: 605601,
ptb: 2085586,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 32, b: 1600, p: 2200, int: 1 } ms 138.45 {
bla: 1253140,
ptb: 4763652,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 40, b: 0, p: 0, int: 1 } ms 253.84 {
bla: 2443606,
ptb: 8374654,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
period2-z20 iter 4509 ref 4509 build 4.04 baseline 1552.21
{ e: 23, b: 0, p: 0, int: 0 } ms 1507.19 {
bla: 0,
ptb: 70701120,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 1600, p: 2200, int: 1 } ms 0.20 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 800, p: 1200, int: 1 } ms 0.12 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 32, b: 1600, p: 2200, int: 1 } ms 0.12 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 40, b: 0, p: 0, int: 1 } ms 0.16 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
main-z20 iter 4509 ref 4509 build 9.77 baseline 5.36
{ e: 23, b: 0, p: 0, int: 0 } ms 2.83 {
bla: 141120,
ptb: 15680,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 1600, p: 2200, int: 1 } ms 0.15 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 800, p: 1200, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 32, b: 1600, p: 2200, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 40, b: 0, p: 0, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }

View file

@ -1,132 +0,0 @@
[
{
"name": "seahorse-z14",
"results": [
{
"r": false,
"last": 693.8000000007451,
"e": "WASM SIMD ×2 · BLA e-28 + rebase · 局所補修 6",
"f": 1,
"px": 12288,
"k": 690.1000000052154,
"rep": 0.3,
"un": 0,
"int": 0,
"bla": 162.36328125,
"ptb": 1876.836181640625,
"bits": 512,
"q": 0.58
},
{
"r": false,
"last": 1999.9000000022352,
"e": "WASM SIMD ×2 · BLA e-32 + rebase · 局所補修 12",
"f": 2,
"px": 36300,
"k": 2172.9000000059605,
"rep": 0.36363636363636365,
"un": 0,
"int": 0,
"bla": 135.99895316804407,
"ptb": 2132.273305785124,
"bits": 512,
"q": 1
}
]
},
{
"name": "seahorse-z20",
"results": [
{
"r": false,
"last": 388.79999999701977,
"e": "WASM SIMD ×2 · BLA e-23 + rebase",
"f": 3,
"px": 72463,
"k": 330.9000000022352,
"rep": 0,
"un": 0,
"int": 0,
"bla": 38.68238963333011,
"ptb": 133.7588424437299,
"bits": 512,
"q": 0.58
},
{
"r": false,
"last": 1732.699999999255,
"e": "WASM SIMD ×2 · BLA e-32 + rebase",
"f": 4,
"px": 134091,
"k": 1531.5999999977648,
"rep": 0,
"un": 0,
"int": 0,
"bla": 82.81766114056872,
"ptb": 306.63208567316224,
"bits": 512,
"q": 1
}
]
},
{
"name": "period3-z20",
"results": [
{
"r": false,
"last": 174.90000000223517,
"e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%",
"f": 5,
"px": 202800,
"k": 67.5,
"rep": 0,
"un": 0,
"int": 1,
"bla": 2,
"ptb": 8,
"bits": 512,
"q": 0.58
}
]
},
{
"name": "period2-z20",
"results": [
{
"r": false,
"last": 49.19999999925494,
"e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%",
"f": 6,
"px": 202800,
"k": 4.399999998509884,
"rep": 0,
"un": 0,
"int": 1,
"bla": 0,
"ptb": 0,
"bits": 512,
"q": 0.58
}
]
},
{
"name": "cardioid-z20",
"results": [
{
"r": false,
"last": 33,
"e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%",
"f": 7,
"px": 43200,
"k": 0.5999999977648258,
"rep": 0,
"un": 0,
"int": 1,
"bla": 0,
"ptb": 0,
"bits": 512,
"q": 0.58
}
]
}
]

View file

@ -1,77 +0,0 @@
[
{
"z": 14,
"runs": [
{
"r": false,
"last": 528.9000000022352,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-28 + rebase \u00b7 \u5c40\u6240\u88dc\u4fee 1",
"f": 1,
"px": 12288,
"k": 531.7000000104308,
"pilot": 111.89999999850988,
"rep": 0.05
},
{
"r": false,
"last": 332.6000000014901,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-28 + rebase",
"f": 2,
"px": 12288,
"k": 472.99999998882413,
"pilot": 111.89999999850988,
"rep": 0
}
]
},
{
"z": 20,
"runs": [
{
"r": false,
"last": 322.1000000014901,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
"f": 3,
"px": 56444,
"k": 340.30000000447035,
"pilot": 6.699999999254942,
"rep": 0
},
{
"r": false,
"last": 216.40000000223517,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
"f": 4,
"px": 45510,
"k": 218.69999999925494,
"pilot": 6.699999999254942,
"rep": 0
}
]
},
{
"z": 100,
"runs": [
{
"r": false,
"last": 33.70000000298023,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
"f": 5,
"px": 202800,
"k": 16.599999994039536,
"pilot": 0.09999999776482582,
"rep": 0
},
{
"r": false,
"last": 46.099999997764826,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
"f": 6,
"px": 202800,
"k": 10.899999998509884,
"pilot": 0.09999999776482582,
"rep": 0
}
]
}
]

View file

@ -1,44 +0,0 @@
[
{
"z": 20,
"runs": [
{
"r": false,
"last": 1422.0999999977648,
"f": 1,
"px": 35371,
"k": 170.7000000141561,
"pilot": 10.700000002980232
},
{
"r": false,
"last": 1080.1000000014901,
"f": 2,
"px": 12288,
"k": 57.79999999701977,
"pilot": 10.700000002980232
}
]
},
{
"z": 100,
"runs": [
{
"r": false,
"last": 3249.7000000029802,
"f": 3,
"px": 202800,
"k": 17.099999982863665,
"pilot": 0.19999999925494194
},
{
"r": false,
"last": 1368.800000000745,
"f": 4,
"px": 16170,
"k": 2.2000000029802322,
"pilot": 0.19999999925494194
}
]
}
]

View file

@ -1,62 +0,0 @@
[
{
"name": "period3 wide",
"re": "-0.1225611668766536",
"im": "0.7448617666197442",
"span": 0.1,
"offMs": 766.7149390000001,
"onMs": 5.955513999999994,
"interior": 6912,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "period3 mid",
"re": "-0.1225611668766536",
"im": "0.7448617666197442",
"span": 0.03,
"offMs": 438.65803500000015,
"onMs": 2.334043000000065,
"interior": 6912,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "period2 boundary neighborhood",
"re": "-0.75",
"im": "0",
"span": 0.1,
"offMs": 364.72781799999984,
"onMs": 1.0177550000000792,
"interior": 6806,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "mixed exterior/interior",
"re": "-0.5",
"im": "0.5",
"span": 0.1,
"offMs": 282.849144,
"onMs": 16.219244000000117,
"interior": 5338,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "cardioid cusp neighborhood",
"re": "0.25",
"im": "0",
"span": 0.04,
"offMs": 329.88844100000006,
"onMs": 1.7636029999998755,
"interior": 6403,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
}
]

View file

@ -1,98 +0,0 @@
[
{
"exp": 14,
"setup": {
"bits": 768,
"z": 14,
"iter": 3490
},
"base": {
"wall": 7315.300000000745,
"lastRender": 7292.300000000745,
"w": 260,
"h": 159,
"pilotMs": 0,
"kernelMPP": 0,
"interiorSkip": 0,
"black": 0,
"bad": 0,
"engine": "WASM SIMD · main-thread rebase · skip 998",
"z": 14,
"iter": 3490,
"compute": 2870
},
"safe": null
},
{
"exp": 20,
"setup": {
"bits": 768,
"z": 20.00000000000001,
"iter": 4509
},
"base": {
"wall": 3576.60000000149,
"lastRender": 3555.39999999851,
"w": 260,
"h": 159,
"pilotMs": 0,
"kernelMPP": 0,
"interiorSkip": 0,
"black": 0,
"bad": 0,
"engine": "WASM SIMD · main-thread rebase · skip 998",
"z": 20.00000000000001,
"iter": 4509,
"compute": 3200
},
"safe": null
},
{
"exp": 40,
"setup": {
"bits": 768,
"z": 40.00000000000002,
"iter": 7752
},
"base": {
"wall": 3800.199999999255,
"lastRender": 3782.599999997765,
"w": 260,
"h": 159,
"pilotMs": 0,
"kernelMPP": 0,
"interiorSkip": 0,
"black": 0,
"bad": 0,
"engine": "WASM SIMD · main-thread rebase · skip 998",
"z": 40.00000000000002,
"iter": 7752,
"compute": 4300
},
"safe": null
},
{
"exp": 100,
"setup": {
"bits": 768,
"z": 100,
"iter": 17085
},
"base": {
"wall": 3633.7999999970198,
"lastRender": 3622,
"w": 260,
"h": 159,
"pilotMs": 0,
"kernelMPP": 0,
"interiorSkip": 0,
"black": 0,
"bad": 0,
"engine": "WASM SIMD · main-thread rebase · skip 998",
"z": 100,
"iter": 17085,
"compute": 7600
},
"safe": null
}
]

View file

@ -1,16 +0,0 @@
{
"full": [
128,
78
],
"pilot": [
56,
34
],
"fullMs": 1356.671812,
"pilotMs": 169.93951399999992,
"classMismatch": 451,
"classRate": 0.04517227564102564,
"escapeIterDiffGt8": 4364,
"diffRate": 0.437099358974359
}

View file

@ -1,59 +0,0 @@
{
"oldRefBuildMs": 29.320617,
"tests": [
{
"sx": 0.5,
"sy": 0,
"oldMs": 691.804547,
"newMs": 640.2659849999995,
"newRefBuildMs": 9.508285999999998,
"firstFrameNetSavingMs": 42.03027600000041,
"steadySavingMs": 51.53856200000041
},
{
"sx": 0.8,
"sy": 0,
"oldMs": 756.002915,
"newMs": 655.9519419999997,
"newRefBuildMs": 8.81105800000023,
"firstFrameNetSavingMs": 91.23991500000011,
"steadySavingMs": 100.05097300000034
},
{
"sx": 0,
"sy": 0.8,
"oldMs": 730.6417070000007,
"newMs": 671.6106240000008,
"newRefBuildMs": 8.391966000001048,
"firstFrameNetSavingMs": 50.639116999998805,
"steadySavingMs": 59.03108299999985
},
{
"sx": 0.8,
"sy": 0.5,
"oldMs": 787.9931480000014,
"newMs": 740.047842,
"newRefBuildMs": 7.513370000000577,
"firstFrameNetSavingMs": 40.43193600000086,
"steadySavingMs": 47.94530600000144
},
{
"sx": 1.2,
"sy": 0,
"oldMs": 705.7068249999975,
"newMs": 689.7542820000017,
"newRefBuildMs": 7.101795999999013,
"firstFrameNetSavingMs": 8.850746999996773,
"steadySavingMs": 15.952542999995785
},
{
"sx": 1.5,
"sy": 0.8,
"oldMs": 793.3721589999986,
"newMs": 733.093237000001,
"newRefBuildMs": 7.395623000000342,
"firstFrameNetSavingMs": 52.88329899999735,
"steadySavingMs": 60.27892199999769
}
]
}

View file

@ -1,59 +0,0 @@
{
"oldRefBuildMs": 23.925890999999993,
"tests": [
{
"sx": 0.5,
"sy": 0,
"oldMs": 100.18383699999998,
"newMs": 105.04222300000004,
"newRefBuildMs": 9.637189000000006,
"firstFrameNetSavingMs": -14.49557500000006,
"steadySavingMs": -4.858386000000053
},
{
"sx": 0.8,
"sy": 0,
"oldMs": 106.89347099999986,
"newMs": 93.24754299999995,
"newRefBuildMs": 8.222010999999952,
"firstFrameNetSavingMs": 5.42391699999996,
"steadySavingMs": 13.645927999999913
},
{
"sx": 0,
"sy": 0.8,
"oldMs": 110.3922050000001,
"newMs": 98.40486799999985,
"newRefBuildMs": 7.7547400000000835,
"firstFrameNetSavingMs": 4.232597000000169,
"steadySavingMs": 11.987337000000252
},
{
"sx": 0.8,
"sy": 0.5,
"oldMs": 138.4319349999996,
"newMs": 102.46810800000003,
"newRefBuildMs": 8.225470999999743,
"firstFrameNetSavingMs": 27.73835599999984,
"steadySavingMs": 35.96382699999958
},
{
"sx": 1.2,
"sy": 0,
"oldMs": 111.11085400000002,
"newMs": 93.51345999999967,
"newRefBuildMs": 8.248458000000028,
"firstFrameNetSavingMs": 9.348936000000322,
"steadySavingMs": 17.59739400000035
},
{
"sx": 1.5,
"sy": 0.8,
"oldMs": 126.42733499999986,
"newMs": 95.50466299999971,
"newRefBuildMs": 8.381738999999925,
"firstFrameNetSavingMs": 22.540933000000223,
"steadySavingMs": 30.922672000000148
}
]
}

View file

@ -1,82 +0,0 @@
{
"environment": "Chromium 144 headless, 780x441 viewport, container-local",
"scenes": [
{
"name": "seahorse_z14",
"bits": 288,
"first": {
"render": "835 ms / 3490 回 · BLA 8.034ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.9532754421234131
},
"base": {
"render": "1059 ms / 3490 回 · BLA 9.226ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.098907470703125
},
"engine": "WASM SIMD ×2 常駐Worker · BLA e-23 + リベース · スキップ 998 / 288 bit",
"renderText": "1059 ms / 3490 回 · BLA 9.226ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z20",
"bits": 320,
"first": {
"render": "1574 ms / 4509 回 · BLA 1.891ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.7402434349060059
},
"base": {
"render": "1226 ms / 4509 回 · BLA 2.126ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.3763623237609863
},
"engine": "WASM SIMD ×2 常駐Worker · BLA e-25 + リベース · スキップ 3231 / 320 bit",
"renderText": "1226 ms / 4509 回 · BLA 2.126ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z100",
"bits": 576,
"first": {
"render": "1224 ms / 17085 回 · BLA 0.104ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.4651060104370117
},
"base": {
"render": "983 ms / 17085 回 · BLA 0.097ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.1331260204315186
},
"engine": "WASM SIMD ×2 常駐Worker · BLA e-23 + リベース · スキップ 3245 / 576 bit",
"renderText": "983 ms / 17085 回 · BLA 0.097ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "period3_z20",
"bits": 320,
"first": {
"render": "1675 ms / 4509 回 · BLA 53.047ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.8480825424194336
},
"base": {
"render": "1931 ms / 4509 回 · BLA 52.519ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 2.0063276290893555
},
"engine": "WASM SIMD ×2 常駐Worker · BLA e-25 + リベース · スキップ 4508 / 320 bit",
"renderText": "1931 ms / 4509 回 · BLA 52.519ms/kpx · 内部省略 0%",
"canvas": "780x441"
}
],
"errorCount": 0
}

View file

@ -1,82 +0,0 @@
{
"environment": "Chromium 144 headless, 780x441 viewport, container-local",
"scenes": [
{
"name": "seahorse_z14",
"bits": 288,
"first": {
"render": "789 ms / 3490 回 · BLA 54.395ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.7424271106719971
},
"base": {
"render": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.28958845138549805
},
"engine": "WASM SIMD ×2 · BLA e-28 + リベース / 288 bit",
"renderText": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z20",
"bits": 320,
"first": {
"render": "401 ms / 4509 回 · BLA 5.023ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.4155466556549072
},
"base": {
"render": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.26917600631713867
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 320 bit",
"renderText": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z100",
"bits": 576,
"first": {
"render": "482 ms / 17085 回 · BLA 0.076ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.5742058753967285
},
"base": {
"render": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.5136466026306152
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 576 bit",
"renderText": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "period3_z20",
"bits": 320,
"first": {
"render": "1420 ms / 4509 回 · BLA 0.395ms/kpx · 内部省略 100%",
"badge": "深部リベース",
"saw": true,
"wall": 1.5150198936462402
},
"base": {
"render": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
"badge": "深部リベース",
"saw": true,
"wall": 0.8927633762359619
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース · 内部早期終了 100% / 320 bit",
"renderText": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
"canvas": "780x441"
}
],
"errorCount": 0
}

View file

@ -1,12 +0,0 @@
{
"w": 48,
"h": 30,
"pixels": 1440,
"globalIter": 3490,
"capIter": 2870,
"capped": 1440,
"late": 1066,
"insideGlobal": 374,
"lateEscapeRateAmongCapped": 0.7402777777777778,
"elapsedMs": 6344.968723999999
}

View file

@ -1,19 +0,0 @@
{
"values": {
"title": "Mandelbrot ∞ Zoom v18",
"engine": "WebAssembly f64 / 256 bit",
"render": "281 ms / 350 回",
"badge": "準備完了",
"canvas": "780x441",
"black": {
"n": 6035,
"b": 1358,
"uniq": 1215
},
"autoText": "❚❚ 自動",
"badge2": "自動 · 境界追従",
"render2": "53 ms / 381 回"
},
"errorCount": 0,
"errors": []
}

View file

@ -1,18 +0,0 @@
{
"format": "mandelbrot-browser-baseline-v23",
"status": "not-run",
"reason": "No in-app browser binding was available in this environment.",
"lastCheckedUtc": "2026-08-22T10:48:56.1197927Z",
"connectionAudit": {
"consecutiveGoalTurns": 3,
"auditCycle": "resumed-after-performance-fix",
"availableBrowsers": 0,
"outcome": "blocked",
"recovery": "Connect an in-app Browser, then rerun tests/browser-benchmark.html. Unrelated browser-control backends are intentionally not substituted. Node runtime-budget coverage is available in audit/v23-node-performance.json but does not replace the browser gate."
},
"runner": "tests/browser-benchmark.html?profile=<mobile|desktop|4k>&target=<hosted|standalone>",
"executionModel": "one real-DPR profile and one build target per run; iframe sizing does not emulate DPR",
"requiredTargets": ["hosted", "standalone"],
"requiredProfiles": ["mobile", "desktop", "4k"],
"acceptancePending": ["visual output", "requested viewport and actual DPR fidelity", "interaction p95", "Preview/Covered/Refined timing", "Power stops at Preview", "Standard stops at Covered without AA/continuation", "idle writes", "long tasks", "observed browser/GPU peak memory", "export download/cancel", "keyboard/focus/target-size/live-status/page-zoom accessibility"]
}

View file

@ -1,267 +0,0 @@
{
"format": "mandelbrot-browserless-audit-v23",
"generatedUtc": "2026-08-22T12:35:37.5161338Z",
"status": "pass",
"scope": "browserless-current-artifacts",
"fullAcceptance": false,
"rendererVersion": 23,
"node": {
"version": "v22.18.0",
"archive": {
"file": ".tmp-node-v22.18.0-win-x64.zip",
"sha256": "c95d8a7e1c99e669cc08c9f1176e068c1f50847c37908fcb8c35b62482366511",
"source": "https://nodejs.org/dist/v22.18.0/node-v22.18.0-win-x64.zip"
}
},
"contracts": {
"kernel": {
"status": "pass",
"modules": 4,
"compiler": "17.0.6",
"pixelContract": "sample centers at x + 0.5, y + 0.5"
},
"source": {
"status": "pass",
"rendererVersion": 23,
"wasmPayloads": 8,
"scriptBytes": 129807,
"htmlBytes": 11395
},
"document": {
"status": "pass",
"proposalContract": "revised-v23",
"scenes": 7,
"viewports": 3,
"numericPolicy": "numeric-policy-v23",
"hashedFiles": 13,
"browserRunner": "static-contract-pass",
"browserlessEvidence": "pass-with-explicit-limits"
}
},
"executableTests": {
"js-syntax.mjs": {
"status": "pass",
"rendererVersion": 23,
"lastPass": "preview",
"automaticTarget": "COVERED",
"fieldIterations": [
7,
100
],
"modeProbe": {
"targets": {
"power": "PREVIEW",
"standard": "COVERED",
"fine": "REFINED",
"validate": "VALIDATED"
},
"coldDeepPreview": [
48,
36
]
},
"referenceCheckpoints": {
"agreement": true,
"catchesMismatch": true,
"count": 17,
"bits": 320
},
"deepWorkerModuleInit": true,
"browserHarnessSyntax": true,
"sources": {
"appBytes": 129807,
"shallowWorkerCharacters": 1509,
"deepWorkerCharacters": 8788
}
},
"module-clone.mjs": {
"status": "pass",
"structuredClone": true,
"results": [
{
"name": "deep-simd.wasm",
"bytes": 4866,
"exports": [
"render_perturb_rebase_rect"
]
},
{
"name": "bla-simd.wasm",
"bytes": 4959,
"exports": [
"build_bla",
"render_bla_rect_v2"
]
},
{
"name": "color-simd.wasm",
"bytes": 1387,
"exports": [
"smooth_batch"
]
}
]
},
"precision-reference.mjs": {
"status": "pass",
"precisions": [
256,
320
],
"results": [
{
"id": "outside",
"p": 3,
"guarded": 3,
"stable": true
},
{
"id": "boundary-escape",
"p": 33,
"guarded": 33,
"stable": true
},
{
"id": "period3-center",
"p": 4000,
"guarded": 4000,
"stable": true
}
]
},
"analytic-interior.mjs": {
"status": "pass",
"checked": 14,
"precisions": [
256,
320
],
"proof": "integer cardioid/period-2 inequalities"
},
"pixel-mapping.mjs": {
"status": "pass",
"samples": 851,
"backends": [
"shallow",
"deep",
"bla"
],
"subsampleScales": [
2,
4
],
"contract": "centered-rational",
"float64UlpMax": 4,
"fixedPointRounding": "nearest-ties-away-from-zero"
},
"pixel-contract.mjs": {
"status": "pass",
"backend": "simd",
"samples": 1440,
"mismatches": 0,
"contract": "(x+0.5,y+0.5)"
},
"runtime-budget.mjs": {
"format": "mandelbrot-node-runtime-budget-v23",
"generatedUtc": "2026-08-22T12:35:33.428Z",
"node": "v22.18.0",
"status": "measured",
"acceptance": false,
"sceneCorpus": "mandelbrot-scene-corpus-v2",
"measurementNote": "Runtime characterization only; browser acceptance has separate thresholds and evidence.",
"contracts": {
"previewBudget110": true,
"modeTargets": true,
"screenBudgets": true,
"boundedContinuation": true,
"coldDeepCap": true,
"measuredDeepBudget": true,
"viewportIndependentFloor": true
},
"shallow": [
{
"id": "overview-350",
"width": 640,
"height": 400,
"iterations": 350,
"pixels": 256000,
"medianMs": 85.583,
"msPerMegapixel": 334.30859375,
"meanIterations": 78.3253515625,
"samplesMs": [
85.583,
88.88799999999998,
84.14580000000001,
84.10900000000004,
92.09339999999997
]
},
{
"id": "boundary-900",
"width": 512,
"height": 320,
"iterations": 900,
"pixels": 163840,
"medianMs": 113.91780000000006,
"msPerMegapixel": 695.2990722656253,
"meanIterations": 154.49669189453124,
"samplesMs": [
115.31709999999998,
113.91780000000006,
112.1979,
112.90809999999988,
117.25350000000003
]
}
],
"deep": {
"id": "swirly-seahorses-z12-bla-2000",
"width": 256,
"height": 144,
"iterations": 2000,
"pixels": 36864,
"blaEntries": 12,
"buildMs": 0.27119999999990796,
"medianMs": 395.4109000000003,
"msPerMegapixel": 10726.207139756953,
"unresolved": 0,
"samplesMs": [
388.1887999999999,
394.21360000000004,
413.6929,
395.4109000000003,
396.3185000000003
]
}
}
},
"runtime": {
"status": "measured-not-acceptance",
"report": "audit/v23-browserless-runtime.json",
"reason": "Runtime values are environment/load dependent and no browser paint/input/GPU threshold is asserted."
},
"sourceWasmBuild": {
"status": "prior-evidence-reused",
"manifest": {
"file": "build/wasm-v23/manifest.json",
"bytes": 4193,
"sha256": "cc388d664f642181d45a110ea5b44c47524050f8bf904ebd115e8e24c0f58a18"
},
"reason": "Verified generated manifest reused; fixed toolchain was removed after the successful gate."
},
"v22Comparison": {
"status": "not-verifiable",
"reason": "No immutable v22 artifacts, hashes, or versioned baseline are present."
},
"browserAcceptance": {
"status": "not-run",
"report": "audit/v23-browser-baseline.json",
"nonSubstitutable": [
"actual DPR and visual output",
"transform-to-paint and Preview p95",
"long tasks and observed browser/GPU memory",
"download/cancel behavior",
"keyboard/focus/page-zoom/screen-reader behavior"
]
}
}

View file

@ -1,73 +0,0 @@
{
"format": "mandelbrot-node-runtime-budget-v23",
"generatedUtc": "2026-08-22T12:35:33.428Z",
"node": "v22.18.0",
"status": "measured",
"acceptance": false,
"sceneCorpus": "mandelbrot-scene-corpus-v2",
"measurementNote": "Runtime characterization only; browser acceptance has separate thresholds and evidence.",
"contracts": {
"previewBudget110": true,
"modeTargets": true,
"screenBudgets": true,
"boundedContinuation": true,
"coldDeepCap": true,
"measuredDeepBudget": true,
"viewportIndependentFloor": true
},
"shallow": [
{
"id": "overview-350",
"width": 640,
"height": 400,
"iterations": 350,
"pixels": 256000,
"medianMs": 85.583,
"msPerMegapixel": 334.30859375,
"meanIterations": 78.3253515625,
"samplesMs": [
85.583,
88.88799999999998,
84.14580000000001,
84.10900000000004,
92.09339999999997
]
},
{
"id": "boundary-900",
"width": 512,
"height": 320,
"iterations": 900,
"pixels": 163840,
"medianMs": 113.91780000000006,
"msPerMegapixel": 695.2990722656253,
"meanIterations": 154.49669189453124,
"samplesMs": [
115.31709999999998,
113.91780000000006,
112.1979,
112.90809999999988,
117.25350000000003
]
}
],
"deep": {
"id": "swirly-seahorses-z12-bla-2000",
"width": 256,
"height": 144,
"iterations": 2000,
"pixels": 36864,
"blaEntries": 12,
"buildMs": 0.27119999999990796,
"medianMs": 395.4109000000003,
"msPerMegapixel": 10726.207139756953,
"unresolved": 0,
"samplesMs": [
388.1887999999999,
394.21360000000004,
413.6929,
395.4109000000003,
396.3185000000003
]
}
}

View file

@ -1,69 +0,0 @@
{
"format": "mandelbrot-node-runtime-budget-v23",
"generatedUtc": "2026-08-22T10:48:04.607Z",
"node": "v24.16.0",
"contracts": {
"previewBudget110": true,
"modeTargets": true,
"screenBudgets": true,
"boundedContinuation": true,
"coldDeepCap": true,
"measuredDeepBudget": true,
"viewportIndependentFloor": true
},
"shallow": [
{
"id": "overview-350",
"width": 640,
"height": 400,
"iterations": 350,
"pixels": 256000,
"medianMs": 86.21379999999999,
"msPerMegapixel": 336.77265624999995,
"meanIterations": 78.3253515625,
"samplesMs": [
83.7123,
87.21029999999999,
86.21379999999999,
87.21250000000003,
85.40730000000002
]
},
{
"id": "boundary-900",
"width": 512,
"height": 320,
"iterations": 900,
"pixels": 163840,
"medianMs": 109.8057,
"msPerMegapixel": 670.2008056640625,
"meanIterations": 154.49669189453124,
"samplesMs": [
109.8057,
110.24979999999994,
109.15139999999997,
108.90229999999997,
110.59719999999993
]
}
],
"deep": {
"id": "seahorse-bla-2000",
"width": 256,
"height": 144,
"iterations": 2000,
"pixels": 36864,
"blaEntries": 12,
"buildMs": 0.4078999999999269,
"medianMs": 722.5364,
"msPerMegapixel": 19600.054253472223,
"unresolved": 0,
"samplesMs": [
692.4027999999998,
695.0712000000003,
773.1455000000001,
722.5364,
863.2840000000006
]
}
}

View file

@ -1,109 +0,0 @@
{
"format": "mandelbrot-source-baseline-v23",
"generatedUtc": "2026-08-22T12:30:30.4538464Z",
"rendererVersion": 23,
"contract": {
"status": "pass",
"rendererVersion": 23,
"wasmPayloads": 8,
"scriptBytes": 129807,
"htmlBytes": 11395
},
"hosted": {
"firstViewFiles": [
{
"file": "dist/hosted/index.html",
"bytes": 11388,
"sha256": "e92c68d4b3c36a438675aee5fff4349c75ebd8ba8bdbc85369bfb3f55d07fd73"
},
{
"file": "dist/hosted/hosted-loader.js",
"bytes": 2338,
"sha256": "43804e07f0603d57a4c4d210cfbfb02e5fa7adc13e2dc53c1a15e05d1d74f4c1"
},
{
"file": "dist/hosted/script.js",
"bytes": 129807,
"sha256": "6ffebe51532fb8a141e9b1939e586d76b6c79db18786c06713e67ffe8d4ca78a"
},
{
"file": "dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm",
"bytes": 511,
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
}
],
"firstViewUncompressedBytes": 144044,
"deepRequestContractBeforeDeepView": 0,
"deepRequestMeasurementStatus": "not-run",
"note": "The zero is a static loader contract, not a measured request count. Transfer compression, request count, parse, compile, and runtime timings require the fixed Hosted browser benchmark."
},
"standalone": {
"files": [
{
"file": "dist/standalone/index.html",
"bytes": 11395,
"sha256": "b2017e3aac73d22d1178e2a28bb5cb1ea0035cbcae5a4ed422dbe4ff7eb951fa"
},
{
"file": "dist/standalone/script.js",
"bytes": 129807,
"sha256": "6ffebe51532fb8a141e9b1939e586d76b6c79db18786c06713e67ffe8d4ca78a"
},
{
"file": "dist/standalone/kernels.js",
"bytes": 31389,
"sha256": "e97726c09af138da92b331c376766c1128c10aa3c361b83750356824262ea1a0"
}
],
"uncompressedBytes": 172591
},
"sourceBuild": {
"files": [
{
"file": "src/shallow_kernel.c",
"bytes": 1512,
"sha256": "ec1067a7a425e180eef7bebf250730de1144e63783735c9ed22084856da096d5"
},
{
"file": "src/deep_kernel.c",
"bytes": 5076,
"sha256": "c484aa9255a7a33eb4ce4ab90c74ec45865885b9a04fd263ce230838d3ce5137"
},
{
"file": "src/bla_kernel_v18.c",
"bytes": 8744,
"sha256": "66724b9afee313899110380f427ba6abbf5340af5c9af1b715eb24b8cff901a9"
},
{
"file": "src/color_kernel.c",
"bytes": 1284,
"sha256": "a40c7bd511dba94b096bf5e075f1fb95bb9f98b5154bac712f5e2bdeb1432a09"
},
{
"file": "src/abi.json",
"bytes": 899,
"sha256": "747f6dd6a0240684e9b502ec19a90458956e2d4fc583084bfe0b3ac0e879a0da"
},
{
"file": "toolchain.lock.json",
"bytes": 381,
"sha256": "d51122cff57d4304880db660be37988403c44770e209e51fe33e5ba30c9a7f38"
}
],
"compiler": "clang 17.0.6",
"node": "v22.18.0",
"compiledInThisEnvironment": true,
"goldenStatus": "pass",
"generatedManifest": {
"file": "build/wasm-v23/manifest.json",
"bytes": 4193,
"sha256": "cc388d664f642181d45a110ea5b44c47524050f8bf904ebd115e8e24c0f58a18"
},
"reason": "Verified generated manifest reused; fixed toolchain was removed after the successful gate."
},
"browserBenchmark": {
"status": "not-run",
"report": "audit/v23-browser-baseline.json",
"reason": "No in-app browser binding was available in this environment."
}
}

View file

@ -1,9 +0,0 @@
{
"status": "pass",
"kind": "view-specific-adaptive-backend-router",
"directRequiredRatio": 8,
"directDefaultRatio": 128,
"deepSpeedMargin": 0.85,
"deepProbeUnknownMax": 0.005,
"cases": 10
}

View file

@ -1,6 +0,0 @@
{
"status": "pass",
"kind": "bla-disabled-contract",
"reason": "dense swirly regression demonstrated unsafe f32-quantized BLA classifications",
"productionBla": false
}

View file

@ -1,308 +0,0 @@
{
"status": "pass",
"kind": "guarded-production-equation-cpu-f32-model-not-real-gpu",
"thresholds": {
"balanced": 0.001,
"strict": 0.0001
},
"report": [
{
"mode": "balanced",
"id": "period2-cusp-z14",
"bits": 285,
"limit": 1200,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "balanced",
"id": "swirly-seahorses-z12",
"bits": 279,
"limit": 2000,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 49,
"known": 138,
"exactEscape": 130,
"mismatch": [
{
"x": 5,
"y": 1,
"oracle": 675,
"gpu": {
"kind": "escaped",
"n": 674,
"err": 0.06557769039826006
}
},
{
"x": 6,
"y": 1,
"oracle": 636,
"gpu": {
"kind": "escaped",
"n": 635,
"err": 0.12117791268198698
}
}
]
},
{
"mode": "balanced",
"id": "period2-cusp-z20",
"bits": 305,
"limit": 1800,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "balanced",
"id": "period2-cusp-z100",
"bits": 571,
"limit": 2400,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "balanced",
"id": "period3-interior",
"bits": 267,
"limit": 1200,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "balanced",
"id": "period2-cusp-e280",
"bits": 1171,
"limit": 2400,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "balanced",
"id": "period2-cusp-e400",
"bits": 1569,
"limit": 1600,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "period2-cusp-z14",
"bits": 285,
"limit": 1200,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "swirly-seahorses-z12",
"bits": 279,
"limit": 2000,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 49,
"known": 138,
"exactEscape": 130,
"mismatch": [
{
"x": 5,
"y": 1,
"oracle": 675,
"gpu": {
"kind": "escaped",
"n": 674,
"err": 0.06557769039826006
}
},
{
"x": 6,
"y": 1,
"oracle": 636,
"gpu": {
"kind": "escaped",
"n": 635,
"err": 0.12117791268198698
}
}
]
},
{
"mode": "strict",
"id": "period2-cusp-z20",
"bits": 305,
"limit": 1800,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "period2-cusp-z100",
"bits": 571,
"limit": 2400,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "period3-interior",
"bits": 267,
"limit": 1200,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "period2-cusp-e280",
"bits": 1171,
"limit": 2400,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "period2-cusp-e400",
"bits": 1569,
"limit": 1600,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
}
],
"denseRegression": [
{
"mode": "balanced",
"x": 25,
"y": 12,
"oracle": 2000,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 34362354130585895000
}
},
{
"mode": "strict",
"x": 25,
"y": 12,
"oracle": 2000,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 34362354130585895000
}
},
{
"mode": "balanced",
"x": 26,
"y": 15,
"oracle": 2000,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 121379814641826710000
}
},
{
"mode": "strict",
"x": 26,
"y": 15,
"oracle": 2000,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 121379814641826710000
}
},
{
"mode": "balanced",
"x": 27,
"y": 25,
"oracle": 1957,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 32570060988.51048
}
},
{
"mode": "strict",
"x": 27,
"y": 25,
"oracle": 1957,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 32570060988.51048
}
}
]
}

View file

@ -1,25 +0,0 @@
{
"status": "not-run",
"reason": "container Chromium does not expose a working WebGPU adapter / GPU process initialization fails",
"benchmark": "UI Diagnostics > Backend比較",
"depths": [
0,
2,
4,
6,
8,
10,
12,
14,
16,
20,
30,
40
],
"timings": [
"f32 numeric",
"DS direct numeric",
"Deep warm numeric",
"Deep cold = verified BigInt reference + warm numeric"
]
}

View file

@ -1,34 +0,0 @@
{
"version": "24.2.3",
"issue": "Backend comparison could appear to hang at z12 deep because the benchmark repeated corrected Deep over the entire current canvas and waited for monolithic GPU queue completion.",
"fix": {
"speedTile": [256, 144],
"speedTileCount": 3,
"timingModel": "sum representative tile GPU queue completion time, derive msPerPixel, extrapolate to current full canvas",
"qualityTileCount": 5,
"gpuTimeoutMs": 15000,
"referenceTimeoutMs": 15000,
"productionRendererChanged": false,
"benchmarkFormat": "mandelbrot-webgpu-crossover-v2"
},
"verification": {
"wgslReserved": "pass",
"sourceContract": "pass",
"indexContract": "pass",
"treeContract": "pass",
"crossoverQuality": "pass",
"crossoverContract": "pass",
"referenceWorker": "pass",
"directModel": "pass",
"backendPolicy": "pass",
"cpuNumericModel": "pass (individual run)",
"sparseCorrectionModel": "pass (individual run)",
"blaDisabledContract": "pass",
"geometryContract": "pass",
"coordinateFormat": "pass",
"pngStream": "pass",
"acceptanceContract": "pass",
"build": "pass",
"realWebGPU": "not available in this container"
}
}

View file

@ -1,766 +0,0 @@
{
"format": "mandelbrot-crossover-quality-v1",
"kind": "cpu-f32-model + BigInt P/P+64 oracle; not real GPU",
"viewport": [
1600,
900
],
"sampleGrid": [
21,
15
],
"depths": [
0,
2,
4,
6,
8,
10,
12,
14,
16,
20,
30,
40
],
"tracks": [
"cusp",
"seahorse"
],
"rows": [
{
"track": "cusp",
"z": 0,
"limit": 350,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 21,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 188
},
{
"track": "cusp",
"z": 2,
"limit": 478,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 21,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1107
},
{
"track": "cusp",
"z": 4,
"limit": 550,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 21,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 851
},
{
"track": "cusp",
"z": 6,
"limit": 611,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 761
},
{
"track": "cusp",
"z": 8,
"limit": 667,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 15
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 920
},
{
"track": "cusp",
"z": 10,
"limit": 721,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 15
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 998
},
{
"track": "cusp",
"z": 12,
"limit": 772,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 15
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1005
},
{
"track": "cusp",
"z": 14,
"limit": 821,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 15
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1099
},
{
"track": "cusp",
"z": 16,
"limit": 870,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 15
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1134
},
{
"track": "cusp",
"z": 20,
"limit": 963,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 15
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1271
},
{
"track": "cusp",
"z": 30,
"limit": 1183,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 15
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1589
},
{
"track": "cusp",
"z": 40,
"limit": 1392,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 15
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1856
},
{
"track": "seahorse",
"z": 0,
"limit": 350,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 21,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 103
},
{
"track": "seahorse",
"z": 2,
"limit": 478,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 21,
"ds": 21
},
"f32": {
"falseEscaped": 2,
"falseBounded": 0,
"escapeIterationMismatch": 2,
"uniqueCoords": 315
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 366
},
{
"track": "seahorse",
"z": 4,
"limit": 550,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 21,
"ds": 21
},
"f32": {
"falseEscaped": 0,
"falseBounded": 2,
"escapeIterationMismatch": 36,
"uniqueCoords": 315
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 2,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 1
},
"modelMs": 161
},
{
"track": "seahorse",
"z": 6,
"limit": 611,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 2,
"ds": 21
},
"f32": {
"falseEscaped": 5,
"falseBounded": 0,
"escapeIterationMismatch": 104,
"uniqueCoords": 315
},
"ds": {
"falseEscaped": 0,
"falseBounded": 1,
"escapeIterationMismatch": 2,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 230
},
{
"track": "seahorse",
"z": 8,
"limit": 667,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 4,
"falseBounded": 0,
"escapeIterationMismatch": 307,
"uniqueCoords": 8
},
"ds": {
"falseEscaped": 1,
"falseBounded": 1,
"escapeIterationMismatch": 14,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 1,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 2
},
"modelMs": 376
},
{
"track": "seahorse",
"z": 10,
"limit": 721,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 10,
"falseBounded": 0,
"escapeIterationMismatch": 305,
"uniqueCoords": 2
},
"ds": {
"falseEscaped": 6,
"falseBounded": 6,
"escapeIterationMismatch": 34,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 512
},
{
"track": "seahorse",
"z": 12,
"limit": 772,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 21
},
"f32": {
"falseEscaped": 54,
"falseBounded": 0,
"escapeIterationMismatch": 261,
"uniqueCoords": 1
},
"ds": {
"falseEscaped": 14,
"falseBounded": 13,
"escapeIterationMismatch": 103,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 3
},
"modelMs": 919
},
{
"track": "seahorse",
"z": 14,
"limit": 821,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 1
},
"f32": {
"falseEscaped": 315,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 1
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 315
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1158
},
{
"track": "seahorse",
"z": 16,
"limit": 870,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 1
},
"f32": {
"falseEscaped": 315,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 1
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 1
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1211
},
{
"track": "seahorse",
"z": 20,
"limit": 963,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 1
},
"f32": {
"falseEscaped": 315,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 1
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 1
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1374
},
{
"track": "seahorse",
"z": 30,
"limit": 1183,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 1
},
"f32": {
"falseEscaped": 315,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 1
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 1
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 1968
},
{
"track": "seahorse",
"z": 40,
"limit": 1392,
"tested": 315,
"guardMismatch": 0,
"adjacentUniqueX": {
"f32": 1,
"ds": 1
},
"f32": {
"falseEscaped": 315,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 1
},
"ds": {
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 0,
"uniqueCoords": 1
},
"deep": {
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 0
},
"modelMs": 2066
}
],
"summary": {
"firstMaterialF32DegradationDepth": 6,
"firstMaterialDsDegradationDepth": 10,
"deepIssueRows": [
{
"z": 8,
"falseEscaped": 1,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 2
}
],
"interpretation": "On the Seahorse track at a 1600x900 viewport, f32 direct becomes materially unsuitable around z6; DS direct remains a plausible bridge through roughly z8 and degrades by z10. Corrected deep is the best of the modeled backends, but one z8 false escape means it is not a membership certificate."
}
}

View file

@ -1,19 +0,0 @@
{
"version": "24.2.7-fixed96-hotfix",
"rootCause": "fx_bad_range treated the Q8.88 integer byte as redundant sign extension and rejected valid values such as +1.2",
"symptom": "correction rate approximately equaled remaining-unknown rate on the real GPU",
"fix": [
"removed invalid fx_bad_range check",
"classify |Re(z)|>2 or |Im(z)|>2 as certain escape before squaring",
"square for exact |z|^2>4 only while both components are within +/-2",
"validate Q8.88 coordinate range and pixel-step underflow before packing"
],
"regressions": [
"v24-fixed96-contract.mjs",
"v24-fixed96-limb-model.mjs",
"v24-fixed96-limb-orbit-model.mjs",
"v24-fixed96-shader-range-model.mjs",
"v24-fixed96-model.mjs"
],
"realWebGPU": "pending user rerun"
}

View file

@ -1,182 +0,0 @@
{
"format": "mandelbrot-fixed96-q88-model-v1",
"kind": "Exact BigInt model of signed Q8.88 direct + DS risk gate + BigInt P/P+64 oracle; integer GPU arithmetic should be deterministic modulo implementation bugs",
"viewport": [
1600,
900
],
"sampleGrid": [
31,
21
],
"rows": [
{
"z": 6,
"limit": 611,
"variants": [
{
"risk": 100000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 43,
"uncertainRate": 0.06605222734254992,
"wrongBefore": 4,
"wrongAfter": 0
},
{
"risk": 1000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 23,
"uncertainRate": 0.03533026113671275,
"wrongBefore": 4,
"wrongAfter": 0
},
{
"risk": 10000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 18,
"uncertainRate": 0.027649769585253458,
"wrongBefore": 4,
"wrongAfter": 0
},
{
"risk": 100000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 10,
"uncertainRate": 0.015360983102918587,
"wrongBefore": 4,
"wrongAfter": 1
}
]
},
{
"z": 8,
"limit": 667,
"variants": [
{
"risk": 100000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 202,
"uncertainRate": 0.3102918586789555,
"wrongBefore": 4,
"wrongAfter": 0
},
{
"risk": 1000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 126,
"uncertainRate": 0.1935483870967742,
"wrongBefore": 4,
"wrongAfter": 0
},
{
"risk": 10000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 72,
"uncertainRate": 0.11059907834101383,
"wrongBefore": 4,
"wrongAfter": 0
},
{
"risk": 100000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 46,
"uncertainRate": 0.0706605222734255,
"wrongBefore": 4,
"wrongAfter": 0
}
]
},
{
"z": 10,
"limit": 721,
"variants": [
{
"risk": 100000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 648,
"uncertainRate": 0.9953917050691244,
"wrongBefore": 10,
"wrongAfter": 0
},
{
"risk": 1000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 398,
"uncertainRate": 0.6113671274961597,
"wrongBefore": 10,
"wrongAfter": 0
},
{
"risk": 10000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 225,
"uncertainRate": 0.3456221198156682,
"wrongBefore": 10,
"wrongAfter": 0
},
{
"risk": 100000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 153,
"uncertainRate": 0.2350230414746544,
"wrongBefore": 10,
"wrongAfter": 0
}
]
},
{
"z": 12,
"limit": 772,
"variants": [
{
"risk": 100000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 602,
"uncertainRate": 0.9247311827956989,
"wrongBefore": 51,
"wrongAfter": 0
},
{
"risk": 1000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 600,
"uncertainRate": 0.9216589861751152,
"wrongBefore": 51,
"wrongAfter": 0
},
{
"risk": 10000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 594,
"uncertainRate": 0.9124423963133641,
"wrongBefore": 51,
"wrongAfter": 0
},
{
"risk": 100000000000000,
"tested": 651,
"guardMismatch": 0,
"uncertain": 488,
"uncertainRate": 0.7496159754224271,
"wrongBefore": 51,
"wrongAfter": 0
}
]
}
]
}

View file

@ -1,31 +0,0 @@
{
"status": "prepared",
"version": "24.2.6-fixed96-experiment",
"purpose": "DS risk gate -> sparse signed Q8.88 u32-limb direct correction -> compare with Full Deep",
"productionRouterChanged": false,
"standaloneUi": {
"rootIndexSingleFile": true,
"externalScriptTags": 0,
"button": "Fixed96 Direct実験",
"allRiskRowsVisible": true
},
"fixedPoint": {
"bits": 96,
"fractionBits": 88,
"format": "signed Q8.88",
"limbs": 3,
"limbType": "u32"
},
"cpuPreflight": {
"status": "pass",
"report": "audit/v24-fixed96-model.json",
"limbArithmetic": "tests/v24-fixed96-limb-model.mjs",
"safeCandidateAtDepths": [
6,
8,
10,
12
]
},
"realWebGPU": "pending: run tests/webgpu-acceptance.html and Diagnostics > Fixed96 Direct実験 on a WebGPU-capable browser"
}

View file

@ -1,40 +0,0 @@
{
"version": "24.2.8-fixed96-sparse-queue",
"purpose": "Make the diagnostic Fixed96 correction truly sparse on GPU and fail closed on experiment-system counter/dispatch inconsistencies.",
"productionRouterChanged": false,
"execution": {
"guard": "full-tile DS sensitivity pass; risky local pixel indices atomically compacted",
"finalize": "1 invocation writes indirect args from GPU queue length",
"correction": "1D @workgroup_size(64), dispatchWorkgroupsIndirect, one queued pixel per invocation",
"cpuReadbackBetweenPasses": false
},
"integrityGate": [
"selected == enqueued == dispatchCount == processed",
"overflow == 0",
"indirectWorkgroups == ceil(enqueued/64)",
"invalidIndex == 0",
"staleEntry == 0",
"corrected + remain == processed"
],
"tests": {
"fixed96Contract": "pass",
"queueIntegrityModel": "pass (7 cases)",
"wgslReserved": "pass (10 shaders)",
"fixed96LimbMultiply": "pass (20000 cases)",
"fixed96LimbOrbit": "pass",
"fixed96RangeRegression": "pass",
"singleFileUiContract": "pass",
"sourceContract": "pass",
"acceptanceContract": "pass",
"realWebGPU": "pending external run",
"samplingConsistencyGate": "pass (speed queue rate vs quality correction rate)"
},
"sourceSha256": {
"script.js": "0104d437471b9ad1c3a9bc69206e1562b6841846e0e2343cde2c9ce5632d0a4d",
"gpu-kernels.js": "364e2babe5e986dc02d17c3e8897891e34d73498cc4c84d93c7f09cc2b6f3bd6",
"index.external.html": "23527ec2708d162bfe40c285952e170ce783cfafb63f37a5c7b5c7ecbfdec80c",
"tests/v24-fixed96-contract.mjs": "a85f569764a5623720614c79503398e5993846fa28d0a8aec45979e0edc225ce",
"tests/v24-fixed96-queue-model.mjs": "e29f84189314a9a15ae3e08da85f5bcf2d990ec9c6ab2e9c731ae4639d8f3d22",
"tests/webgpu-acceptance.js": "e5580f7cfc402aa5b533169cda4a088d3f10e0086002c508bf989b1ffff19cb5"
}
}

View file

@ -1,63 +0,0 @@
{
"format": "mandelbrot-v24.2.4-hybrid-preparation-v1",
"createdAt": "2026-08-23T11:36:39Z",
"shaderVersion": "24.2.4-hybrid-experiment",
"productionRouterUnchanged": true,
"hybridDepths": [
6,
8,
10,
12
],
"riskLimits": [
1000000000000.0,
10000000000000.0,
100000000000000.0
],
"cpuPriorRisk1e13": {
"z6": {
"correctionRate": 0.027649769585253458,
"caughtClassErrors": "4/4"
},
"z8": {
"correctionRate": 0.11059907834101383,
"caughtClassErrors": "4/4"
},
"z10": {
"correctionRate": 0.3456221198156682,
"caughtClassErrors": "10/10"
},
"z12": {
"correctionRate": 0.9124423963133641,
"caughtClassErrors": "51/51"
}
},
"tests": {
"wgslReserved": "pass",
"hybridContract": "pass",
"hybridRiskModel": "pass",
"crossoverContract": "pass",
"sourceContract": "pass",
"treeContract": "pass",
"referenceWorker": "pass",
"directModel": "pass",
"backendPolicy": "pass",
"cpuDeepModel": "pass",
"sparseCorrection": "pass",
"blaDisabled": "pass",
"geometry": "pass",
"coordinateFormat": "pass",
"pngStreaming": "pass",
"acceptanceContract": "pass"
},
"realWebGPU": "not-run-in-container; run Hybrid実験 on target GPU",
"hashes": {
"source/index.html": "39427d275732e5bbce8d06253ce5f3e0a07e173081fc230cd34292050ec29827",
"source/gpu-kernels.js": "2773c39e416c52a7919aad0146cd95cf6fc108847ba3718db9253986a4f489e5",
"source/script.js": "d354ff6ce0937c616a02d34ed0accebe5c8c5640cc6ac98720d66f8d784359be",
"standalone/index.html": "39427d275732e5bbce8d06253ce5f3e0a07e173081fc230cd34292050ec29827",
"standalone/gpu-kernels.js": "2773c39e416c52a7919aad0146cd95cf6fc108847ba3718db9253986a4f489e5",
"standalone/script.js": "d354ff6ce0937c616a02d34ed0accebe5c8c5640cc6ac98720d66f8d784359be"
},
"sourceStandaloneMatch": true
}

View file

@ -1,56 +0,0 @@
{
"format": "mandelbrot-hybrid-risk-model-v1",
"kind": "CPU Math.fround DS + BigInt P/P+64 oracle; not real GPU",
"viewport": [
1600,
900
],
"sampleGrid": [
31,
21
],
"riskLimit": 10000000000000,
"rows": [
{
"z": 6,
"tested": 651,
"guardMismatch": 0,
"wrong": 4,
"uncertain": 18,
"caught": 4,
"uncertainRate": 0.027649769585253458,
"catchRate": 1
},
{
"z": 8,
"tested": 651,
"guardMismatch": 0,
"wrong": 4,
"uncertain": 72,
"caught": 4,
"uncertainRate": 0.11059907834101383,
"catchRate": 1
},
{
"z": 10,
"tested": 651,
"guardMismatch": 0,
"wrong": 10,
"uncertain": 225,
"caught": 10,
"uncertainRate": 0.3456221198156682,
"catchRate": 1
},
{
"z": 12,
"tested": 651,
"guardMismatch": 0,
"wrong": 51,
"uncertain": 594,
"caught": 51,
"uncertainRate": 0.9124423963133641,
"catchRate": 1
}
],
"interpretation": "At riskLimit=1e13 the derivative guard catches every observed DS classification error at z6/z8/z10 while sending about 3%/11%/35% of samples to Deep correction. At z12 it sends over 75%, which is a deliberate signal that Full Deep should replace the hybrid."
}

View file

@ -1,29 +0,0 @@
{
"format": "mandelbrot-v24.2.5-hybrid-ui-hotfix-v1",
"createdAt": "2026-08-23T11:54:13Z",
"rootCause": "Toolbar Hybrid experiment button opened the dialog only; actual measurement required a second click on the dialog start button.",
"fix": [
"Toolbar click opens the dialog and auto-starts on the next animation frame.",
"Dialog primary button is a rerun action.",
"Concurrent Backend benchmark state is surfaced explicitly instead of silently doing nothing.",
"Hybrid running state is surfaced explicitly.",
"Static contract tests assert toolbar auto-start and rerun wiring."
],
"tests": {
"syntax": "pass",
"hybridContract": "pass",
"sourceContract": "pass",
"wgslReserved": "pass",
"cpuNumeric": "pass",
"sparseCorrection": "pass",
"build": "pass"
},
"realWebGPU": "not-run-in-container; UI execution contract fixed independently of GPU availability",
"sha256": {
"script.js": "b02a47d6b6b8ea7c95e85ee26e69f877ef5c8c9cffb34e68ca37121c2fa652ac",
"gpu-kernels.js": "6e00107df84dd6e5427774ffca2cfdf6c7bfae0989fd69054b11f500a97e048e",
"index.html": "d12a742455fa2febf3639beddad063a3217901923b65927aeb1c21b224c00f3a",
"package.json": "93662efdf99f04b4bb2497f6f9512ad3c617fef7209843469f8a50ed5d6a6786",
"tests/v24-hybrid-contract.mjs": "18cb1e4b5fd9656285b021de6bea02ca767c7531cb9ee57b3cde3c38f3e887c0"
}
}

View file

@ -1,43 +0,0 @@
{
"status": "v24.2.8-fixed96-sparse-queue-real-webgpu-pending",
"reason": "container browser does not expose a usable WebGPU adapter / GPU process initialization fails",
"requiredExternalRuns": [
"tests/webgpu-acceptance.html",
"Diagnostics > Fixed96 Direct実験"
],
"experimentDepths": [
6,
8,
10,
12
],
"riskLimits": [
100000000000.0,
1000000000000.0,
10000000000000.0,
100000000000000.0
],
"outputColumns": [
"z",
"risk",
"fixed96 est",
"deep cold",
"speedup",
"correction",
"speed q",
"dispatch",
"remain",
"Δclass",
"queue",
"status"
],
"queueAcceptance": [
"selected == enqueued == dispatchCount == processed",
"overflow == 0",
"indirectWorkgroups == ceil(enqueued/64)",
"invalidIndex == 0",
"staleEntry == 0",
"corrected + remain == processed"
],
"cpuQualityReport": "audit/v24-fixed96-model.json"
}

View file

@ -1,246 +0,0 @@
{
"format": "mandelbrot-v24.2.2-crossover-release-manifest",
"generatedAt": "2026-08-23T10:22:38.264933+00:00",
"files": [
{
"path": "AUDIT_V24.md",
"bytes": 3906,
"sha256": "0ac5444e8a798e183312cf4a9c4dbe08133bccf471feac13d5e6ed06c91e5133"
},
{
"path": "CROSSOVER_BENCHMARK.md",
"bytes": 3766,
"sha256": "5f72a5a73cc73b2308e14b4a100bfd546f7067433e24db8b2b33b809f535c629"
},
{
"path": "IMPLEMENTATION_REPORT.md",
"bytes": 2869,
"sha256": "983e5c8b217b957612f7bbabdda04c7df8da16ba28941c16a4a4b5f8e85e7cc5"
},
{
"path": "MIGRATION_V24.md",
"bytes": 4710,
"sha256": "b4d55a30f38ba4c56e0aace0e69f00714878f5098f88d5c9a4f9e76546b24c16"
},
{
"path": "README.md",
"bytes": 6040,
"sha256": "c651c78374c5288cd2d5739d9cd166d38840db6069d0b80b262888d333ce5c24"
},
{
"path": "audit/legacy-v23/final-browser-benchmark.json",
"bytes": 2686,
"sha256": "bca2e141c101722f8a077d43346c2c28b5d8d7c99e70415730031065c8ab5a1c"
},
{
"path": "audit/v24-backend-policy.json",
"bytes": 205,
"sha256": "d34d894ab9d062439e7cfe5074e47ab295defa74044ed3e2619ef0289759b2fe"
},
{
"path": "audit/v24-bla-status.json",
"bytes": 177,
"sha256": "84587eb2183b1e6a2837f5018e74f32bc968c536d64cee7cf5fed55f7d3b21df"
},
{
"path": "audit/v24-cpu-numeric-model.json",
"bytes": 6217,
"sha256": "8c8f7320ae29f367d4eea111eeb658a861dd0e001e5fc2653857c24b3baf71c1"
},
{
"path": "audit/v24-crossover-gpu-status.json",
"bytes": 441,
"sha256": "dbd94c4b034c2ae521c5f591dfd745c40c9cf9d17bc08dbc00f4017216949f90"
},
{
"path": "audit/v24-crossover-quality.json",
"bytes": 16247,
"sha256": "063deef5079d077d35d5cf349186405edcc0f17c5812f38051735a455de3ea20"
},
{
"path": "audit/v24-real-webgpu-status.json",
"bytes": 601,
"sha256": "ec5171b4f059f4f810b64db1ba82d83979571001f17f564919322c69e59ba721"
},
{
"path": "audit/v24-release-verification.json",
"bytes": 944,
"sha256": "b54797dfdc9e99c3e981ac39b45ef92281af5da5bb89d0ef21b837b41e4c4463"
},
{
"path": "audit/v24-sparse-correction-model.json",
"bytes": 600,
"sha256": "0202491ac101543055d9a70c11ab3ed7bf7d253617875271c9d88c7ccd1f2acb"
},
{
"path": "audit/v24-test-summary.json",
"bytes": 1665,
"sha256": "b7afe3205c814318d2d7646bbb235b2e4c8411ecedf746e72924cb20cfcc562b"
},
{
"path": "dist/hosted/_headers",
"bytes": 68,
"sha256": "3d11be43c945209eb7e4503e384ac3007c42ee4022cfc23fc005f6217bda8d1b"
},
{
"path": "dist/hosted/gpu-kernels.js",
"bytes": 22425,
"sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329"
},
{
"path": "dist/hosted/index.html",
"bytes": 13092,
"sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
},
{
"path": "dist/hosted/script.js",
"bytes": 85737,
"sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2"
},
{
"path": "dist/standalone/gpu-kernels.js",
"bytes": 22425,
"sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329"
},
{
"path": "dist/standalone/index.html",
"bytes": 13092,
"sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
},
{
"path": "dist/standalone/script.js",
"bytes": 85737,
"sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2"
},
{
"path": "gpu-kernels.js",
"bytes": 22425,
"sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329"
},
{
"path": "index.html",
"bytes": 13092,
"sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
},
{
"path": "package.json",
"bytes": 199,
"sha256": "7c8ccf08afdaef9dc91c9041a5f9b9e1d730a1a9ba237f11d46ec293dc9690ab"
},
{
"path": "script.js",
"bytes": 85737,
"sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2"
},
{
"path": "scripts/build.mjs",
"bytes": 657,
"sha256": "84706582dd01d6de28cbc0fcdba9758c3f6f72a3d5693b74736109b35819997f"
},
{
"path": "scripts/test-all.mjs",
"bytes": 934,
"sha256": "c3ec078b2591cba429eef90cc4f58a7c55e97756c083ed7b2d9437342ad450b4"
},
{
"path": "tests/legacy-v23/numeric-policy-v23.json",
"bytes": 787,
"sha256": "7afe223d27715a79e34f8c9ea11993f1c37e75bc5d49a6b3284f412d8171a00d"
},
{
"path": "tests/scenes.json",
"bytes": 3225,
"sha256": "a92c5535c54106ce18823443244f115bf5a2a3d42abfc4ee3573f38e8234e553"
},
{
"path": "tests/v24-acceptance-contract.mjs",
"bytes": 1035,
"sha256": "114d90b92f89b8b27f6ba35b904464908ec0e1e6393adb02ade8a8dfe5a7bbf0"
},
{
"path": "tests/v24-backend-policy.mjs",
"bytes": 1192,
"sha256": "81cffe6276ed660210646067502ab9edcce7d0fc608048a98c06633760714b5a"
},
{
"path": "tests/v24-bla-model.mjs",
"bytes": 677,
"sha256": "04f22c659b61635792b9662b718ec9ae6140189a3daf553ac580b841ee48bd6b"
},
{
"path": "tests/v24-coordinate-format.mjs",
"bytes": 1253,
"sha256": "a993f04f12fceb4d38c16206636f7951d8bc4afad63a57491e0164bb3575ca3a"
},
{
"path": "tests/v24-cpu-numeric-model.mjs",
"bytes": 8452,
"sha256": "2ee7a2ef16357db9b537da4534611ea59c80d7279980cf8957adf069093f7fd6"
},
{
"path": "tests/v24-crossover-contract.mjs",
"bytes": 1460,
"sha256": "1b11958712e27308074123dea0376e067e4cb5dd29cf28a68d87bcf42b7e06ff"
},
{
"path": "tests/v24-crossover-quality.mjs",
"bytes": 13215,
"sha256": "8ab9ca4da65aee229d0b5d6bdf4a068923a337ed75263514678d4b71a8273e41"
},
{
"path": "tests/v24-direct-model.mjs",
"bytes": 2452,
"sha256": "9c9ba9cfeaf5e352059a639ba1014ae7a3b0382309a634ee128bc99e62ee3dc5"
},
{
"path": "tests/v24-geometry-contract.mjs",
"bytes": 1177,
"sha256": "2e5e6fb87f4b7130eff8fff72fd776ef8b0d0e70e91845136a3b18cbf6c86701"
},
{
"path": "tests/v24-index-contract.mjs",
"bytes": 504,
"sha256": "90f52bb91f12db47da1ebf39a6770cb58d5c4e2809f4d6aa2fdb1263d77cee57"
},
{
"path": "tests/v24-png-stream-model.mjs",
"bytes": 3411,
"sha256": "ca4dee6252bf6545310ba52c1433e82e83886d04cdb1feb20c7ab08045f431a7"
},
{
"path": "tests/v24-reference-worker.mjs",
"bytes": 2140,
"sha256": "4d637c549cdda2c28666f5bfe2925c5742103c44b752c337cadbe9bc9df6431d"
},
{
"path": "tests/v24-source-contract.mjs",
"bytes": 4239,
"sha256": "349c6d344f5f9325cb811cbb9f63017cb2d27139e7557be829f7c85f13bb39d4"
},
{
"path": "tests/v24-sparse-correction-model.mjs",
"bytes": 9810,
"sha256": "eb72f0aa062bcf8bf915c048ba89e58a7ad5ab338ea559cfc467ba260255c241"
},
{
"path": "tests/v24-tree-contract.mjs",
"bytes": 691,
"sha256": "933c8c5035806584ee3ffc7645b14aa60f262920528ebb8e5e47666023174770"
},
{
"path": "tests/v24-wgsl-reserved.mjs",
"bytes": 2260,
"sha256": "98c153ac038852d3402f36ee18ce1f8263de1a016240b4363f01d84a31f0f0ab"
},
{
"path": "tests/webgpu-acceptance.html",
"bytes": 405,
"sha256": "89b03a08f248b048eead508fc3599057a8cf5afc1d9f846de206a69272dd95bd"
},
{
"path": "tests/webgpu-acceptance.js",
"bytes": 6884,
"sha256": "e1ef5b21906b4da0fb7154f1e3e555c36267d8a5a7a1bd7ed0474eee147bfd13"
}
]
}

View file

@ -1,25 +0,0 @@
{
"format": "mandelbrot-v24.2.2-crossover-release-verification",
"generatedAt": "2026-08-23T10:22:38.262457+00:00",
"version": "24.2.2",
"sourceHashes": {
"script.js": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2",
"gpu-kernels.js": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329",
"index.html": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
},
"standaloneMatchesSource": true,
"hostedMatchesSource": true,
"qualitySweep": "audit/v24-crossover-quality.json",
"gpuTiming": "pending external WebGPU adapter; use Diagnostics > Backend比較",
"gates": {
"wgslReserved": "pass",
"sourceContract": "pass",
"indexContract": "pass",
"crossoverContract": "pass",
"crossoverQuality": "pass",
"cpuNumericModel": "pass individually",
"sparseCorrectionModel": "pass individually",
"build": "pass",
"realWebGPU": "pending"
}
}

View file

@ -1,26 +0,0 @@
{
"status": "pass",
"kind": "sparse-double-single-correction-cpu-model-not-real-gpu",
"scene": "swirly-seahorses-z12",
"grid": [
61,
39
],
"limit": 2000,
"referenceLength": 2000,
"guardMismatch": 0,
"primary": {
"unknown": 478,
"falseEscaped": 0,
"falseBounded": 0
},
"correction": {
"corrected": 478,
"remaining": 0,
"falseEscaped": 0,
"falseBounded": 0,
"escapeIterationMismatch": 5,
"maxEscapeIterationDifference": 374
},
"note": "DS correction is a visual-quality recovery pass; escape iteration equality is not certified."
}

View file

@ -1,48 +0,0 @@
{
"status": "v24.2.2-static-cpu-build-pass-real-webgpu-timing-pending",
"rendererVersion": 24,
"shaderVersion": "24.2.2-benchmark",
"packageVersion": "24.2.2",
"generatedAt": "2026-08-23T10:22:11.466258+00:00",
"production": {
"legacyDeepAssets": 0,
"bigIntPixelRenderer": false,
"bla": false,
"multiReference": false,
"sparseDoubleSingleCorrection": true,
"viewSpecificBackendProbe": true,
"correctedDeepExport": true,
"experimentalWholeFrameDsDirect": "benchmark-only"
},
"crossoverQuality": {
"firstMaterialF32DegradationDepth": 6,
"firstMaterialDsDegradationDepth": 10,
"deepIssueRows": [
{
"z": 8,
"falseEscaped": 1,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 2
}
],
"interpretation": "On the Seahorse track at a 1600x900 viewport, f32 direct becomes materially unsuitable around z6; DS direct remains a plausible bridge through roughly z8 and degrades by z10. Corrected deep is the best of the modeled backends, but one z8 false escape means it is not a membership certificate."
},
"sparseCorrection": {
"primaryUnknown": 478,
"corrected": 478,
"remaining": 0,
"falseEscaped": 0,
"falseBounded": 0
},
"build": {
"status": "pass",
"sourceDistHashMatch": true
},
"realWebGPU": {
"status": "not-run-in-container",
"benchmark": "Diagnostics > Backend比較",
"hardwareTimingRequired": true
},
"note": "Heavy CPU model gates were also run individually. The aggregate npm test wrapper can exceed the execution harness time budget even though its constituent gates pass."
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,85 +0,0 @@
{
"format": "mandelbrot-source-wasm-manifest-v23",
"toolchain": {
"format": "mandelbrot-wasm-toolchain-v1",
"compiler": "clang",
"version": "17.0.6",
"target": "wasm32-unknown-unknown",
"commonFlags": [
"-O3",
"-nostdlib",
"-ffreestanding",
"-fno-builtin",
"-Wl,--no-entry",
"-Wl,--export-memory",
"-Wl,--strip-all"
],
"scalarFlags": [
"-mno-simd128",
"-fno-vectorize",
"-fno-slp-vectorize"
],
"simdFlags": [
"-msimd128"
]
},
"abi": "src/abi.json",
"payloads": [
{
"symbol": "WASM_SIMD_B64",
"file": "wasm-simd.wasm",
"bytes": 511,
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b",
"source": "src/shallow_kernel.c"
},
{
"symbol": "WASM_SCALAR_B64",
"file": "wasm-scalar.wasm",
"bytes": 511,
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b",
"source": "src/shallow_kernel.c"
},
{
"symbol": "DEEP_SIMD_B64",
"file": "deep-simd.wasm",
"bytes": 4866,
"sha256": "d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214",
"source": "src/deep_kernel.c"
},
{
"symbol": "DEEP_SCALAR_B64",
"file": "deep-scalar.wasm",
"bytes": 5246,
"sha256": "d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2",
"source": "src/deep_kernel.c"
},
{
"symbol": "BLA_SIMD_B64",
"file": "bla-simd.wasm",
"bytes": 4959,
"sha256": "f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e",
"source": "src/bla_kernel_v18.c"
},
{
"symbol": "BLA_SCALAR_B64",
"file": "bla-scalar.wasm",
"bytes": 4593,
"sha256": "4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f",
"source": "src/bla_kernel_v18.c"
},
{
"symbol": "COLOR_SIMD_B64",
"file": "color-simd.wasm",
"bytes": 1387,
"sha256": "8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03",
"source": "src/color_kernel.c"
},
{
"symbol": "COLOR_SCALAR_B64",
"file": "color-scalar.wasm",
"bytes": 632,
"sha256": "aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90",
"source": "src/color_kernel.c"
}
]
}

Binary file not shown.

Binary file not shown.

11
dist/_headers vendored
View file

@ -1,11 +0,0 @@
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
X-Content-Type-Options: nosniff
/wasm/*
Content-Type: application/wasm
Cache-Control: public, max-age=31536000, immutable
/hosted/*
Cache-Control: no-cache

View file

@ -1,3 +0,0 @@
/*
X-Content-Type-Options: nosniff
Referrer-Policy: no-referrer

View file

@ -1,598 +0,0 @@
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x000fffffu;
const REASON_SHIFT:u32=20u;
const REASON_MASK:u32=0x0ff00000u;
const REASON_NONE:u32=0u;
const REASON_ERROR_BOUND:u32=1u;
const REASON_ESCAPE_UNCERTAIN:u32=2u;
const REASON_REFERENCE_END:u32=3u;
const REASON_REBASE_GAP:u32=4u;
const REASON_RANGE:u32=5u;
const REASON_OPERATION_LIMIT:u32=6u;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
fn pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Benchmark candidate: whole-frame double-single direct iteration.
// This intentionally has no CPU reference-build cost. It is not selected by
// the production router until the crossover benchmark justifies it.
const DIRECT_DS_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b; let e=b-(q-a); return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah;
let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh;
var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b);
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);
let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var n=0u;
loop{
if(n>=p.maxIter){break;}
z=cds_add(cds_mul(z,z),c); n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
// can be re-evaluated by the existing deep double-single correction pass.
// This shader is benchmark-only and is not selected by the production router.
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
struct HybridStats{
remaining:atomic<u32>, _a:atomic<u32>, _b:atomic<u32>, _c:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);}
fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;
let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh;
var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
fn ds_cmp(a:DS,b:DS)->i32{if(a.h<b.h){return -1;}if(a.h>b.h){return 1;}if(a.l<b.l){return -1;}if(a.l>b.l){return 1;}return 0;}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn mark_sensitive(out:u32,n:u32){
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;atomicAdd(&hybridStats.remaining,1u);
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var deriv=vec2<f32>(0.0);var n=0u;
loop{
if(n>=p.maxIter){break;}
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
z=cds_add(cds_mul(z,z),c);n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
}
`;
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct UnresolvedHead{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
return;
}
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=1.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=refs[m];
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
// perturbation. It is a visual-quality pass, not a membership certificate.
const DEEP_CORRECT_WGSL=COMMON+String.raw`
const CORRECTION_MARK:u32=128u;
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
fn count_remaining(out:u32){
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
}
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
}
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b;
let e=b-(q-a);
return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b;
let bb=q-a;
let e=(a-(q-bb))+(b-bb);
return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a;
let ah=ca-(ca-a);
let al=a-ah;
let cb=4097.0*b;
let bh=cb-(cb-b);
let bl=b-bh;
var e=ah*bh-q;
e=e+ah*bl;
e=e+al*bh;
e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h);
return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l;
e=e+a.l*b.h;
e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b);
return ds_quick(q.h,q.l+a.l*b);
}
fn ds_pow2(a:DS,e:i32)->DS{
if(e < -126){return DS(0.0,0.0);}
if(e > 126){return DS(8.507059e37,0.0);}
return DS(ldexp(a.h,e),ldexp(a.l,e));
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
loop{
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
if(m>p.refLen){count_remaining(out);return;}
let rp=refs[m];
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
let delta=cds_pow2(w,scaleExp);
let z=cds_add(r,delta);
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
return;
}
let dmag=cds_mag2(delta);
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
continue;
}
if(m>=p.refLen){count_remaining(out);return;}
let linear=cds_scale(cds_mul(r,w),2.0);
let sq=cds_pow2(cds_mul(w,w),scaleExp);
w=cds_add(cds_add(linear,sq),d);
m+=1u; n+=1u; operations+=1u;
let mm=max(cds_maxabs(w),cds_maxabs(d));
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
const COLOR_WGSL=String.raw`
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
if(p.edgeAA!=0u){
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
let x=i32(gid.x); let y=i32(gid.y);
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
`;
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.2.6-hybrid-ui-cacheproof',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
});
})();

View file

@ -1,792 +0,0 @@
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x000fffffu;
const REASON_SHIFT:u32=20u;
const REASON_MASK:u32=0x0ff00000u;
const REASON_NONE:u32=0u;
const REASON_ERROR_BOUND:u32=1u;
const REASON_ESCAPE_UNCERTAIN:u32=2u;
const REASON_REFERENCE_END:u32=3u;
const REASON_REBASE_GAP:u32=4u;
const REASON_RANGE:u32=5u;
const REASON_OPERATION_LIMIT:u32=6u;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
fn pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Benchmark candidate: whole-frame double-single direct iteration.
// This intentionally has no CPU reference-build cost. It is not selected by
// the production router until the crossover benchmark justifies it.
const DIRECT_DS_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b; let e=b-(q-a); return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah;
let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh;
var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b);
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);
let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var n=0u;
loop{
if(n>=p.maxIter){break;}
z=cds_add(cds_mul(z,z),c); n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
// can be re-evaluated by the existing deep double-single correction pass.
// This shader is benchmark-only and is not selected by the production router.
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
struct HybridStats{
remaining:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
@group(0) @binding(4) var<storage,read_write> riskQueue:array<u32>;
fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);}
fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;
let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh;
var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
fn ds_cmp(a:DS,b:DS)->i32{if(a.h<b.h){return -1;}if(a.h>b.h){return 1;}if(a.l<b.l){return -1;}if(a.l>b.l){return 1;}return 0;}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn mark_sensitive(out:u32,n:u32){
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;
let slot=atomicAdd(&hybridStats.remaining,1u);
let capacity=p.tileW*p.tileH;
if(slot<capacity){
riskQueue[slot]=out;
atomicAdd(&hybridStats.enqueued,1u);
}else{
atomicAdd(&hybridStats.overflow,1u);
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var deriv=vec2<f32>(0.0);var n=0u;
loop{
if(n>=p.maxIter){break;}
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
z=cds_add(cds_mul(z,z),c);n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
}
`;
// Converts the GPU-written risk queue length into indirect dispatch arguments.
// The correction pass therefore launches one lane per enqueued risky pixel,
// instead of one lane per screen pixel.
const DIRECT_FIXED96_QUEUE_ARGS_WGSL=COMMON+String.raw`
struct HybridStats{
remaining:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 };
@group(0) @binding(0) var<storage,read_write> hybridStats:HybridStats;
@group(0) @binding(1) var<storage,read_write> indirectArgs:IndirectArgs;
@compute @workgroup_size(1)
fn main(){
let n=atomicLoad(&hybridStats.enqueued);
atomicStore(&hybridStats.dispatchCount,n);
indirectArgs.x=(n+63u)/64u;
indirectArgs.y=1u;
indirectArgs.z=1u;
indirectArgs._pad=0u;
}
`;
// Diagnostic sparse fixed-point direct correction. It does not consume a
// deep reference. The guarded DS pass marks uncertain pixels with reason 7;
// this shader re-evaluates only those pixels using signed Q8.88 arithmetic
// implemented entirely with u32 limbs. Integer arithmetic avoids the
// reassociation/fusion sensitivity of float-expansion correction kernels.
const DIRECT_FIXED96_CORRECT_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, maxIter:u32, _flags:u32,
baseReLo:u32, baseReMid:u32, baseReHi:u32,
baseImLo:u32, baseImMid:u32, baseImHi:u32,
stepLo:u32, stepMid:u32, stepHi:u32,
_pad0:u32, _pad1:u32, _pad2:u32,
};
struct FX96{ lo:u32, mid:u32, hi:u32 };
struct FixedStats{
remaining:atomic<u32>, invalidIndex:atomic<u32>, processed:atomic<u32>, staleEntry:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
struct HybridStats{
remaining:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(3) var<storage,read_write> fixedStats:FixedStats;
@group(0) @binding(4) var<storage,read_write> hybridStats:HybridStats;
@group(0) @binding(5) var<storage,read> riskQueue:array<u32>;
fn fx_add(a:FX96,b:FX96)->FX96{
let lo=a.lo+b.lo; let c0=select(0u,1u,lo<a.lo);
let m0=a.mid+b.mid; let c1=select(0u,1u,m0<a.mid);
let mid=m0+c0; let c2=select(0u,1u,mid<m0);
return FX96(lo,mid,a.hi+b.hi+c1+c2);
}
fn fx_neg(a:FX96)->FX96{
let lo=(~a.lo)+1u; let c0=select(0u,1u,lo==0u);
let mid=(~a.mid)+c0; let c1=select(0u,1u,c0!=0u && mid==0u);
return FX96(lo,mid,(~a.hi)+c1);
}
fn fx_sub(a:FX96,b:FX96)->FX96{return fx_add(a,fx_neg(b));}
fn fx_negative(a:FX96)->bool{return (a.hi&0x80000000u)!=0u;}
fn fx_abs(a:FX96)->FX96{if(fx_negative(a)){return fx_neg(a);}return a;}
fn mul32(a:u32,b:u32)->vec2<u32>{
let a0=a&0xffffu; let a1=a>>16u; let b0=b&0xffffu; let b1=b>>16u;
let p0=a0*b0; let p1=a0*b1; let p2=a1*b0; let p3=a1*b1;
let middle=(p0>>16u)+(p1&0xffffu)+(p2&0xffffu);
let lo=(p0&0xffffu)|(middle<<16u);
let hi=p3+(p1>>16u)+(p2>>16u)+(middle>>16u);
return vec2<u32>(lo,hi);
}
fn add_at(a:ptr<function,array<u32,6>>,idx0:u32,value0:u32){
var idx=idx0; var value=value0;
loop{
if(value==0u || idx>=6u){break;}
let old=(*a)[idx]; let sum=old+value; (*a)[idx]=sum;
value=select(0u,1u,sum<old); idx+=1u;
}
}
fn add_product(a:ptr<function,array<u32,6>>,idx:u32,x:u32,y:u32){
let q=mul32(x,y); add_at(a,idx,q.x); add_at(a,idx+1u,q.y);
}
fn fx_mul(a0:FX96,b0:FX96)->FX96{
let neg=fx_negative(a0)!=fx_negative(b0); let a=fx_abs(a0); let b=fx_abs(b0);
var w=array<u32,6>(0u,0u,0u,0u,0u,0u);
add_product(&w,0u,a.lo,b.lo); add_product(&w,1u,a.lo,b.mid); add_product(&w,2u,a.lo,b.hi);
add_product(&w,1u,a.mid,b.lo); add_product(&w,2u,a.mid,b.mid);add_product(&w,3u,a.mid,b.hi);
add_product(&w,2u,a.hi,b.lo); add_product(&w,3u,a.hi,b.mid); add_product(&w,4u,a.hi,b.hi);
// Q8.88 * Q8.88 -> Q16.176. Shift right by 88 with one-bit rounding.
var r=FX96((w[2]>>24u)|(w[3]<<8u),(w[3]>>24u)|(w[4]<<8u),(w[4]>>24u)|(w[5]<<8u));
if(((w[2]>>23u)&1u)!=0u){r=fx_add(r,FX96(1u,0u,0u));}
if(neg){return fx_neg(r);}return r;
}
fn fx_mul_small(a0:FX96,k:u32)->FX96{
let neg=fx_negative(a0); let a=fx_abs(a0);
let q0=mul32(a.lo,k); let q1=mul32(a.mid,k); let q2=mul32(a.hi,k);
let mid=q1.x+q0.y; let cm=select(0u,1u,mid<q1.x);
let carry=q1.y+cm; let hi=q2.x+carry;
let r=FX96(q0.x,mid,hi); if(neg){return fx_neg(r);}return r;
}
fn fx_cmp_unsigned(a:FX96,b:FX96)->i32{
if(a.hi<b.hi){return -1;} if(a.hi>b.hi){return 1;}
if(a.mid<b.mid){return -1;} if(a.mid>b.mid){return 1;}
if(a.lo<b.lo){return -1;} if(a.lo>b.lo){return 1;} return 0;
}
fn fx_to_f32_positive(a:FX96)->f32{
return f32(a.hi)*5.960464477539063e-8 + f32(a.mid)*1.3877787807814457e-17 + f32(a.lo)*3.2311742677852644e-27;
}
fn fx_to_f32(a:FX96)->f32{
if(fx_negative(a)){return -fx_to_f32_positive(fx_neg(a));}
return fx_to_f32_positive(a);
}
fn write_escape(out:u32,n:u32,zr:FX96,zi:FX96){
// Once either component exceeds 2 in magnitude, escape is mathematically
// certain. Use f32 only for the smooth-color estimate; membership remains
// decided by the integer fixed-point state. This avoids squaring a large
// escaped value and therefore avoids Q8.88 wraparound.
let zrf=fx_to_f32(zr); let zif=fx_to_f32(zi);
let mag=max(4.0000005,zrf*zrf+zif*zif);
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,mag);
atomicAdd(&fixedStats.corrected,1u);
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let qi=gid.x;
let queued=atomicLoad(&hybridStats.dispatchCount);
if(qi>=queued){return;}
atomicAdd(&fixedStats.processed,1u);
let out=riskQueue[qi];
let capacity=p.tileW*p.tileH;
if(out>=capacity){
atomicAdd(&fixedStats.invalidIndex,1u);
atomicAdd(&fixedStats.remaining,1u);
return;
}
let old=fieldMeta[out];
let cls=(old>>28u)&3u; let reason=(old&REASON_MASK)>>REASON_SHIFT;
if(cls!=FIELD_UNKNOWN || reason!=7u){
atomicAdd(&fixedStats.staleEntry,1u);
atomicAdd(&fixedStats.remaining,1u);
return;
}
let px=out%p.tileW; let py=out/p.tileW;
let baseRe=FX96(p.baseReLo,p.baseReMid,p.baseReHi);
let baseIm=FX96(p.baseImLo,p.baseImMid,p.baseImHi);
let step=FX96(p.stepLo,p.stepMid,p.stepHi);
let cr=fx_add(baseRe,fx_mul_small(step,px));
let ci=fx_sub(baseIm,fx_mul_small(step,py));
var zr=FX96(0u,0u,0u); var zi=FX96(0u,0u,0u); var n=0u;
let TWO=FX96(0u,0u,0x02000000u);
let FOUR=FX96(0u,0u,0x04000000u);
loop{
if(n>=p.maxIter){break;}
// The previous state is inside the bailout disk, so these products are
// bounded and cannot overflow Q8.88.
let zr2=fx_mul(zr,zr); let zi2=fx_mul(zi,zi); let zri=fx_mul(zr,zi);
let nextR=fx_add(fx_sub(zr2,zi2),cr);
let nextI=fx_add(fx_add(zri,zri),ci);
zr=nextR; zi=nextI; n+=1u;
// Do not mistake the Q8.88 integer byte for sign-extension. Values such
// as +1.2 are valid. The old range guard rejected them and left virtually
// every risky escaping pixel UNKNOWN.
if(fx_cmp_unsigned(fx_abs(zr),TWO)>0 || fx_cmp_unsigned(fx_abs(zi),TWO)>0){
write_escape(out,n,zr,zi); return;
}
let rr=fx_mul(zr,zr); let ii=fx_mul(zi,zi); let mag=fx_add(rr,ii);
if(fx_cmp_unsigned(mag,FOUR)>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,fx_to_f32_positive(mag)));
atomicAdd(&fixedStats.corrected,1u); return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
atomicAdd(&fixedStats.corrected,1u);
}
`;
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct UnresolvedHead{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
return;
}
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=1.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=refs[m];
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
// perturbation. It is a visual-quality pass, not a membership certificate.
const DEEP_CORRECT_WGSL=COMMON+String.raw`
const CORRECTION_MARK:u32=128u;
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
fn count_remaining(out:u32){
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
}
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
}
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b;
let e=b-(q-a);
return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b;
let bb=q-a;
let e=(a-(q-bb))+(b-bb);
return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a;
let ah=ca-(ca-a);
let al=a-ah;
let cb=4097.0*b;
let bh=cb-(cb-b);
let bl=b-bh;
var e=ah*bh-q;
e=e+ah*bl;
e=e+al*bh;
e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h);
return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l;
e=e+a.l*b.h;
e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b);
return ds_quick(q.h,q.l+a.l*b);
}
fn ds_pow2(a:DS,e:i32)->DS{
if(e < -126){return DS(0.0,0.0);}
if(e > 126){return DS(8.507059e37,0.0);}
return DS(ldexp(a.h,e),ldexp(a.l,e));
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
loop{
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
if(m>p.refLen){count_remaining(out);return;}
let rp=refs[m];
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
let delta=cds_pow2(w,scaleExp);
let z=cds_add(r,delta);
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
return;
}
let dmag=cds_mag2(delta);
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
continue;
}
if(m>=p.refLen){count_remaining(out);return;}
let linear=cds_scale(cds_mul(r,w),2.0);
let sq=cds_pow2(cds_mul(w,w),scaleExp);
w=cds_add(cds_add(linear,sq),d);
m+=1u; n+=1u; operations+=1u;
let mm=max(cds_maxabs(w),cds_maxabs(d));
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
const COLOR_WGSL=String.raw`
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
if(p.edgeAA!=0u){
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
let x=i32(gid.x); let y=i32(gid.y);
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
`;
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.2.8-fixed96-sparse-queue',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DIRECT_FIXED96_QUEUE_ARGS_WGSL,DIRECT_FIXED96_CORRECT_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
});
})();

View file

@ -1,36 +0,0 @@
const wasmRoot = new URL('../wasm/', import.meta.url);
async function compileAsset(name) {
const url = new URL(name, wasmRoot);
const response = await fetch(url, { cache: 'force-cache' });
if (!response.ok) throw new Error(`WASM fetch failed: ${name} (${response.status})`);
if (WebAssembly.compileStreaming) {
try { return await WebAssembly.compileStreaming(Promise.resolve(response.clone())); }
catch { /* A proxy may have supplied the wrong MIME type; use bytes below. */ }
}
return WebAssembly.compile(await response.arrayBuffer());
}
let shallowModule, shallowSimd = true;
try { shallowModule = await compileAsset('wasm-simd.d19b26c04e1b2f59.wasm'); }
catch { shallowModule = await compileAsset('wasm-scalar.d19b26c04e1b2f59.wasm'); shallowSimd = false; }
const asset = name => new URL(name, wasmRoot).href;
globalThis.MANDEL_KERNELS = Object.freeze({
// The compiled module is structured-cloned to the shallow Worker. No Base64
// conversion or second compile is needed in the hosted build.
WASM_SIMD_B64: shallowModule,
WASM_SCALAR_B64: shallowModule,
DEEP_SIMD_B64: asset('deep-simd.d49385f06e4a8f55.wasm'),
DEEP_SCALAR_B64: asset('deep-scalar.d133bbecc8f6dbdf.wasm'),
BLA_SIMD_B64: asset('bla-simd.f80f136e9fb676ce.wasm'),
BLA_SCALAR_B64: asset('bla-scalar.4f06913487044823.wasm'),
COLOR_SIMD_B64: asset('color-simd.8cf83374ed0c680b.wasm'),
COLOR_SCALAR_B64: asset('color-scalar.aa2914ec1acacaa2.wasm')
});
globalThis.MANDEL_KERNEL_META = Object.freeze({"WASM_SIMD_B64":"d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b","WASM_SCALAR_B64":"d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b","DEEP_SIMD_B64":"d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214","DEEP_SCALAR_B64":"d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2","BLA_SIMD_B64":"f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e","BLA_SCALAR_B64":"4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f","COLOR_SIMD_B64":"8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03","COLOR_SCALAR_B64":"aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90"});
globalThis.MANDEL_HOSTED_SHALLOW_SIMD = shallowSimd;
const app = document.createElement('script');
app.src = './script.js';
document.body.appendChild(app);

View file

@ -1,93 +0,0 @@
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#050813">
<title>Mandelbrot Deep Zoom v24.2.8 Fixed96 Sparse Queue Experiment</title>
<style>
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}#palette{color:#111;background:#f4f5f8}#palette option{color:#111;background:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.exact-grid{display:grid;grid-template-columns:54px 1fr;gap:6px;margin-top:8px}.exact-grid input{min-width:0;width:100%;min-height:44px;border:1px solid var(--line);border-radius:8px;background:#070c19;color:var(--text);padding:6px;font:11px ui-monospace,monospace}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.diagnostics{margin-top:8px;color:var(--muted);font:10.5px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.exact-grid input{user-select:text;-webkit-user-select:text}
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}.benchmark-output{width:100%;min-height:180px;margin-top:10px;border:1px solid var(--line);border-radius:9px;background:#070c19;color:var(--text);padding:8px;font:10.5px/1.45 ui-monospace,monospace;resize:vertical}.benchmark-note{font-size:11px;line-height:1.5;color:var(--muted);margin-top:8px}
#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord">…</span></div><div class="row"><span class="muted">倍率</span> <span id="zoom">…</span> <span class="muted">表示幅</span> <span id="span">…</span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render">…</span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大">+</button><button id="zout" aria-label="中心を縮小">−</button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0.001" max="0.05" step="0.0005" value="0.008"><output id="cycleO" for="cycle">0.0080</output></div>
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
<details><summary>詳細設定・正確な座標</summary>
<div class="line"><label for="iters">基準反復</label><input id="iters" type="range" min="100" max="2500" step="25" value="350"><output id="itersO" for="iters">350</output></div>
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> GPU境界平滑化</label></div>
<div class="exact-grid"><label for="coordReInput">実部</label><input id="coordReInput"><label for="coordImInput">虚部</label><input id="coordImInput"><label for="coordSpanInput">表示幅</label><input id="coordSpanInput"></div>
<div class="mini-actions"><button id="coordApply">座標を適用</button><button id="coordCopy">正確値をコピー</button><button id="undoView">戻す</button><button id="redoView">進む</button></div>
</details>
<details><summary>診断情報</summary><div class="diagnostics"><div id="diagEngine">engine: …</div><div id="diagNumeric">numeric: …</div><div id="diagMemory">memory: …</div></div><div class="mini-actions"><button id="backendBench" type="button">Backend比較</button><button id="sparseBench" type="button">Fixed96 Direct実験</button></div></details>
</div>
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
<div class="hint">ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。</div>
</div>
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI−</button>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="exportDialog" aria-labelledby="exportTitle">
<h2 id="exportTitle">高解像度 PNG 出力</h2>
<div class="export-grid">
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
<label for="exportWidth">幅(px)</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×(高速)</option><option value="2">2×2 AA</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="strict">保守的 (Strict)</option></select>
</div>
<progress id="exportProgress" max="1" value="0" hidden></progress>
<div id="exportStatus" role="status" aria-live="polite"></div>
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
</dialog>
<dialog id="benchmarkDialog" aria-labelledby="benchmarkTitle">
<h2 id="benchmarkTitle">標準 / DS / 深部 比較</h2>
<div class="export-grid">
<label for="benchmarkTrack">測定地点</label><select id="benchmarkTrack"><option value="seahorse">Seahorse boundary</option><option value="cusp">Period-2 cusp</option></select>
<label for="benchmarkSamples">反復測定</label><select id="benchmarkSamples"><option value="2">2回(速い)</option><option value="3" selected>3回</option><option value="5">5回(安定)</option></select>
</div>
<div class="benchmark-note">z0,2,4,6,8,10,12,14,16,20,30,40を測定します。速度は現在のview上の代表3タイルを測って全画面相当に換算し、Deep coldにはBigInt reference構築を加算します。1測定が15秒を超えた場合は停止してエラー表示します。</div>
<progress id="benchmarkProgress" max="12" value="0" hidden></progress>
<div id="benchmarkStatus" role="status" aria-live="polite"></div>
<textarea id="benchmarkOutput" class="benchmark-output" readonly placeholder="結果はここに表示されます"></textarea>
<div class="export-actions"><button id="benchmarkCancel" type="button">閉じる</button><button id="benchmarkSave" type="button" disabled>JSON保存</button><button id="benchmarkStart" class="primary" type="button">測定開始</button></div>
</dialog>
<dialog id="sparseDialog" aria-labelledby="sparseTitle">
<h2 id="sparseTitle">DS + Sparse Fixed96 Direct 実験</h2>
<div class="export-grid">
<label for="sparseTrack">測定地点</label><select id="sparseTrack"><option value="seahorse">Seahorse boundary</option><option value="cusp">Period-2 cusp</option></select>
<label for="sparseSamples">反復測定</label><select id="sparseSamples"><option value="2">2回(速い)</option><option value="3" selected>3回</option><option value="5">5回(安定)</option></select>
</div>
<div class="benchmark-note">z6 / z8 / z10 / z12で、DS Directに感度ガード |dz/dc| を追加し、危険pixelだけreference不要の96-bit符号付きQ8.88整数Directで再計算します。risk=1e11 / 1e12 / 1e13 / 1e14を全候補表示し、失敗時も correction / remain / Δclass / time を隠しません。production routerは変更しません。</div>
<progress id="sparseProgress" max="4" value="0" hidden></progress>
<div id="sparseStatus" role="status" aria-live="polite"></div>
<textarea id="sparseOutput" class="benchmark-output" readonly placeholder="結果はここに表示されます"></textarea>
<div class="export-actions"><button id="sparseCancel" type="button">閉じる</button><button id="sparseSave" type="button" disabled>JSON保存</button><button id="sparseStart" class="primary" type="button">Fixed96実験開始</button></div>
</dialog>
<script src="gpu-kernels.js"></script>
<script src="script.js"></script>
</body>
</html>

View file

@ -1,454 +0,0 @@
(()=>{'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,BUILD_VERSION='24.2.6',INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
const DIRECT_REQUIRED_RATIO=8,DIRECT_DEFAULT_RATIO=128,DEEP_SPEED_MARGIN=.85,DEEP_PROBE_UNKNOWN_MAX=.005,NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,CORRECTION_MARK=128;
const BACKEND_PROBE_SPEED_W=224,BACKEND_PROBE_SPEED_H=160,BACKEND_PROBE_QUALITY_W=72,BACKEND_PROBE_QUALITY_H=54;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,backendProbes:0};
const backendProfiles=new Map(),backendViewProbes=new Map(),backendProbePending=new Map();
// ── exact fixed-point view state ─────────────────────────────────────────
function one(bits=state.bits){return 1n<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
function directMetrics(snap=snapshot(),width=Math.max(1,canvas.width)){const spanLog=log2FixedAt(snap.span,snap.bits),stepLog=spanLog-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp))),eligible=Number.isFinite(ratio)&&ratio>=DIRECT_REQUIRED_RATIO&&spanLog>-145;return{spanLog,stepLog,ratio,eligible,overlap:eligible&&ratio<DIRECT_DEFAULT_RATIO}}
function zoomExpFor(snap){return Math.max(0,Math.log10(3.4)-log2FixedAt(snap.span,snap.bits)/Math.log2(10))}
function backendProfileKey(snap,iter){const z=Math.floor(zoomExpFor(snap)*2)/2,ib=iter<600?0:iter<1500?1:iter<5000?2:3;return z.toFixed(1)+':'+ib}
function backendViewKey(snap,iter,width,height){return[snap.bits,snap.re,snap.im,snap.span,iter,width,height].join(':')}
function chooseBackend(snap=snapshot(),width=Math.max(1,canvas.width),iter=maxIter(),height=Math.max(1,canvas.height)){const m=directMetrics(snap,width),key=backendProfileKey(snap,iter),viewKey=backendViewKey(snap,iter,width,height),speed=backendProfiles.get(key),viewProbe=backendViewProbes.get(viewKey),pixels=Math.max(1,width*Math.max(1,height)),profile=speed?{...speed,predictedDirectMs:speed.directMsPerPixel*pixels,predictedDeepMs:speed.referenceBuildMs+speed.deepMsPerPixel*pixels}:null;let backend='deep',reason='direct-coordinate-limit';if(m.eligible){if(m.ratio>=DIRECT_DEFAULT_RATIO){backend='direct';reason='direct-wide-margin'}else if(!viewProbe){backend='probe';reason='overlap-view-probe-required'}else if(!viewProbe.qualityPass){backend='deep';reason='direct-quality-rejected'}else if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*DEEP_SPEED_MARGIN){backend='deep';reason='deep-view-probe-faster'}else{backend='direct';reason='direct-view-probe-preferred'}}return{backend,deep:backend==='deep',probe:backend==='probe',reason,key,viewKey,metrics:m,profile,viewProbe:viewProbe||null}}
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width),height=Math.max(1,canvas.height)){const d=chooseBackend(snap,width,maxIter(),height);return d.deep||d.probe}
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
// ── iteration / quality policy ───────────────────────────────────────────
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
'use strict';
const MAX_REF=150001,MAX_LEVELS=20;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class ReferenceService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter,fresh=false){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.2.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
}
const refs=new ReferenceService();
// ── WebGPU renderer ──────────────────────────────────────────────────────
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
class WebGpuRenderer{
constructor(adapter,device){
this.adapter=adapter;this.device=device;
const ai=adapter.info||{};
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,sdm,shm,xm,zm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('direct-ds-benchmark',G.DIRECT_DS_WGSL),this.module('direct-ds-guarded-experiment',G.DIRECT_DS_GUARDED_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
this.dsDirect=this.device.createComputePipeline({layout:'auto',compute:{module:sdm,entryPoint:'main'}});
this.dsGuarded=this.device.createComputePipeline({layout:'auto',compute:{module:shm,entryPoint:'main'}});
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}});
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
webgpuCanvasClaimed=true;this.configure();
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
ensureFrame(w,h){
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
}
ensureExportWorkspace(){
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
}
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5){const cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),crh=Math.fround(cr),cih=Math.fround(ci),sph=Math.fround(sp),invW=1/Math.max(1,fullW),invh=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,crh,true);d.setFloat32(36,Math.fround(cr-crh),true);d.setFloat32(40,cih,true);d.setFloat32(44,Math.fround(ci-cih),true);d.setFloat32(48,sph,true);d.setFloat32(52,Math.fround(sp-sph),true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,invh,true);d.setFloat32(68,Math.fround(invW-invh),true)})}
dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sx=.5,sy=.5){const a=this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx,sy),d=new DataView(a);d.setFloat32(72,Math.fround(riskLimit),true);return a}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDsDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsDirect.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsDirect);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDsGuardedNumeric(encoder,{pbuf,meta,smooth,stats,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsGuarded.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:stats}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsGuarded);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
}
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
async readUnresolved(){return(await this.readUnresolvedStats()).total}
async correctUnknownFrame(snap,iter,token){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,f.w*.5,f.h*.5,1,f.w,0));const e=d.createCommandEncoder({label:'sparse-deep-correction'}),bg=d.createBindGroup({layout:this.correct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),p=e.beginComputePass();p.setPipeline(this.correct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
async benchmarkDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap));const e=d.createCommandEncoder({label:'ds-direct-benchmark'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;[meta,fieldSmooth,pbuf].forEach(destroy);return{gpuMs}}
async renderTileMetaDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,n*4,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const e=d.createCommandEncoder({label:'ds-direct-meta'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});e.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,fieldSmooth,pbuf,read].forEach(destroy);return out}
async benchmarkHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13}){
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
read=buf(d,2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit));
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,0,fullW*.5,fullH*.5,1,w,0));
const e=d.createCommandEncoder({label:'ds-hybrid-benchmark'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
e.copyBufferToBuffer(dsStats,0,read,0,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,UNRESOLVED_BYTES,UNRESOLVED_BYTES);
const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;
await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),a=new Uint32Array(raw.slice(0,UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(UNRESOLVED_BYTES,2*UNRESOLVED_BYTES));read.unmap();
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);
return{gpuMs,uncertain:a[0]||0,remaining:b[0]||0,corrected:b[7]||0};
}
async renderTileMetaHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13,sampleX=.5,sampleY=.5}){
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
read=buf(d,n*4+2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sampleX,sampleY));
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,0,fullW*.5,fullH*.5,1,w,0));
const e=d.createCommandEncoder({label:'ds-hybrid-meta'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
e.copyBufferToBuffer(meta,0,read,0,n*4);e.copyBufferToBuffer(dsStats,0,read,n*4,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,n*4+UNRESOLVED_BYTES,UNRESOLVED_BYTES);
d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),a=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(n*4+UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES));read.unmap();
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);out.uncertain=a[0]||0;out.remaining=b[0]||0;out.corrected=b[7]||0;return out;
}
async benchmarkNumeric({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,forceStrict=false,correctUnknown=false}){await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const e=d.createCommandEncoder({label:'backend-benchmark'});if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,forceStrict?1:0));this.encodeDeepNumeric(e,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){e.clearBuffer(unresolved);this.encodeCorrectionNumeric(e,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap));this.encodeDirectNumeric(e,{pbuf,meta,smooth,w,h})}e.copyBufferToBuffer(unresolved,0,read,0,UNRESOLVED_BYTES);const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;await read.mapAsync(GPUMapMode.READ);const a=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return{gpuMs,unknown:a[0]||0,corrected:a[7]||0}}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
}
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes,UNRESOLVED_BYTES/4),unresolvedCount=stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
async renderTileRGBA2x({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount=0,corrected=0;for(let si=0;si<4;si++){const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4);unresolvedCount+=stats[0]||0;corrected+=stats[7]||0}ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext()}
}
function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true}
function backendQualityTiles(fullW,fullH){const w=Math.min(BACKEND_PROBE_QUALITY_W,fullW),h=Math.min(BACKEND_PROBE_QUALITY_H,fullH),x1=Math.max(0,fullW-w),y1=Math.max(0,fullH-h),xc=Math.max(0,Math.floor((fullW-w)/2)),yc=Math.max(0,Math.floor((fullH-h)/2));return[[0,0],[x1,0],[0,y1],[x1,y1],[xc,yc]].map(([tileX,tileY])=>({tileX,tileY,w,h}))}
async function performBackendProbe(snap,iter,decision,token){if(!renderer||!decision.metrics.overlap||token!==state.token)return null;const fullW=canvas.width,fullH=canvas.height,sw=Math.min(BACKEND_PROBE_SPEED_W,fullW),sh=Math.min(BACKEND_PROBE_SPEED_H,fullH),sx=Math.max(0,Math.floor((fullW-sw)/2)),sy=Math.max(0,Math.floor((fullH-sh)/2)),speedPixels=sw*sh,fullPixels=Math.max(1,fullW*fullH),direct=await renderer.benchmarkNumeric({snap,iter,deep:false,deepContext:null,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh});if(token!==state.token)return null;const rb=performance.now(),ctx=await refs.request(snap,iter),buildWall=performance.now()-rb;if(token!==state.token||ctx.checkpointMismatch)return null;const deepProbe=await renderer.benchmarkNumeric({snap,iter,deep:true,deepContext:ctx,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh,correctUnknown:true});if(token!==state.token)return null;let classDisagreement=0,deepUnknown=0,knownCompared=0,qualityPixels=0,corrected=0;for(const q of backendQualityTiles(fullW,fullH)){const directMeta=await renderer.renderTileMeta({snap,iter,deep:false,deepContext:null,fullW,fullH,...q});if(token!==state.token)return null;const deepMeta=await renderer.renderTileMeta({snap,iter,deep:true,deepContext:ctx,fullW,fullH,...q,correctUnknown:true});if(token!==state.token)return null;qualityPixels+=q.w*q.h;corrected+=deepMeta.corrected||0;for(let i=0;i<deepMeta.length;i++){const dc=(deepMeta[i]>>>28)&3;if(dc===FIELD_UNKNOWN){deepUnknown++;continue}const rc=(directMeta[i]>>>28)&3;knownCompared++;if((dc===FIELD_ESCAPED)!==(rc===FIELD_ESCAPED))classDisagreement++}}const buildMs=ctx.buildMs||buildWall,directMsPerPixel=direct.gpuMs/Math.max(1,speedPixels),deepMsPerPixel=deepProbe.gpuMs/Math.max(1,speedPixels),sample={directMsPerPixel,deepMsPerPixel,referenceBuildMs:buildMs},prev=backendProfiles.get(decision.key),a=prev?.samples?0.3:1,profile={samples:(prev?.samples||0)+1,directMsPerPixel:(prev?.directMsPerPixel||sample.directMsPerPixel)*(1-a)+sample.directMsPerPixel*a,deepMsPerPixel:(prev?.deepMsPerPixel||sample.deepMsPerPixel)*(1-a)+sample.deepMsPerPixel*a,referenceBuildMs:(prev?.referenceBuildMs||sample.referenceBuildMs)*(1-a)+sample.referenceBuildMs*a};backendProfiles.set(decision.key,profile);const deepUnknownRate=deepUnknown/Math.max(1,qualityPixels),qualityPass=classDisagreement===0&&deepUnknownRate<=DEEP_PROBE_UNKNOWN_MAX,viewProbe={qualityPass,deepUnknownRate,classDisagreement,knownCompared,qualityPixels,corrected,predictedDirectMs:directMsPerPixel*fullPixels,predictedDeepMs:buildMs+deepMsPerPixel*fullPixels,ratio:decision.metrics.ratio,zoom:zoomExpFor(snap),iter,speedProbe:{w:sw,h:sh,tileX:sx,tileY:sy},qualityTiles:backendQualityTiles(fullW,fullH)};backendViewProbes.set(decision.viewKey,viewProbe);runtime.backendProbes++;return viewProbe}
function ensureBackendProbe(snap,iter,decision,token){if(!decision.metrics.overlap)return Promise.resolve(null);const hit=backendViewProbes.get(decision.viewKey);if(hit)return Promise.resolve(hit);const pending=backendProbePending.get(decision.viewKey);if(pending)return pending;const task=performBackendProbe(snap,iter,decision,token).catch(e=>{if(token===state.token)console.warn('backend probe failed',e);return null}).finally(()=>backendProbePending.delete(decision.viewKey));backendProbePending.set(decision.viewKey,task);return task}
// ── backend crossover benchmark (diagnostic only) ───────────────────────
const CROSSOVER_TRACKS={
seahorse:{id:'seahorse',label:'Seahorse boundary',re:'-0.7453983606667815',im:'0.1125046349959942'},
cusp:{id:'cusp',label:'Period-2 cusp',re:'-0.75',im:'0'}
};
const CROSSOVER_DEPTHS=[0,2,4,6,8,10,12,14,16,20,30,40];
const benchmarkJob={active:false,cancelled:false,last:null};
function fixedAtDecimal(text,bits){let s=String(text).trim(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const parts=s.toLowerCase().split('e'),mant=parts[0],exp=parts[1]?parseInt(parts[1],10):0,ab=mant.split('.'),ii=ab[0]||'0',ff=ab[1]||'';let digits=(ii+ff).replace(/^0+(?=\d)/,'')||'0',places=ff.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),scale=1n<<BigInt(bits),v=(BigInt(digits)*scale+den/2n)/den;return neg?-v:v}
function crossoverSnap(track,z){const span='3.4e-'+z,bits=Math.max(320,decimalRequiredBits(track.re)+64,decimalRequiredBits(track.im)+64,decimalRequiredBits(span)+96);return{bits,re:fixedAtDecimal(track.re,bits),im:fixedAtDecimal(track.im,bits),span:fixedAtDecimal(span,bits)}}
function iterForZoom(z,base=350){return Math.max(base,base+Math.floor(70*Math.sqrt(Math.max(0,z))+15*Math.max(0,z)))}
function median(values){const a=[...values].sort((x,y)=>x-y),n=a.length;return n?a.length%2?a[(n-1)>>1]:(a[n/2-1]+a[n/2])/2:NaN}
function compareMeta(reference,candidate){let referenceUnknown=0,candidateUnknown=0,classMismatch=0,escapedCompared=0,iterMismatch=0,maxIterDelta=0,sumIterDelta=0;const n=Math.min(reference.length,candidate.length);for(let i=0;i<n;i++){const rm=reference[i]>>>0,cm=candidate[i]>>>0,rc=(rm>>>28)&3,cc=(cm>>>28)&3;if(rc===FIELD_UNKNOWN){referenceUnknown++;continue}if(cc===FIELD_UNKNOWN){candidateUnknown++;continue}const re=rc===FIELD_ESCAPED,ce=cc===FIELD_ESCAPED;if(re!==ce){classMismatch++;continue}if(re){escapedCompared++;const d=Math.abs((rm&ITER_MASK)-(cm&ITER_MASK));if(d){iterMismatch++;sumIterDelta+=d;if(d>maxIterDelta)maxIterDelta=d}}}return{pixels:n,referenceUnknown,candidateUnknown,classMismatch,classMismatchRate:classMismatch/Math.max(1,n-referenceUnknown),escapedCompared,escapeIterationMismatch:iterMismatch,escapeIterationMismatchRate:iterMismatch/Math.max(1,escapedCompared),meanEscapeIterationDelta:iterMismatch?sumIterDelta/iterMismatch:0,maxEscapeIterationDelta:maxIterDelta}}
function crossoverQualityTiles(fullW,fullH,w,h){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[0,0],[Math.max(0,fullW-w),0],[0,Math.max(0,fullH-h)],[Math.max(0,fullW-w),Math.max(0,fullH-h)],[Math.max(0,Math.floor((fullW-w)/2)),Math.max(0,Math.floor((fullH-h)/2))]];return pts.map(([tileX,tileY])=>({tileX,tileY,w,h}))}
function aggregateQuality(list){const out={pixels:0,referenceUnknown:0,candidateUnknown:0,classMismatch:0,escapedCompared:0,escapeIterationMismatch:0,sumEscapeIterationDelta:0,maxEscapeIterationDelta:0};for(const q of list){out.pixels+=q.pixels;out.referenceUnknown+=q.referenceUnknown;out.candidateUnknown+=q.candidateUnknown;out.classMismatch+=q.classMismatch;out.escapedCompared+=q.escapedCompared;out.escapeIterationMismatch+=q.escapeIterationMismatch;out.sumEscapeIterationDelta+=q.meanEscapeIterationDelta*q.escapeIterationMismatch;out.maxEscapeIterationDelta=Math.max(out.maxEscapeIterationDelta,q.maxEscapeIterationDelta)}out.classMismatchRate=out.classMismatch/Math.max(1,out.pixels-out.referenceUnknown);out.escapeIterationMismatchRate=out.escapeIterationMismatch/Math.max(1,out.escapedCompared);out.meanEscapeIterationDelta=out.escapeIterationMismatch?out.sumEscapeIterationDelta/out.escapeIterationMismatch:0;delete out.sumEscapeIterationDelta;return out}
const CROSSOVER_SPEED_W=256,CROSSOVER_SPEED_H=144,CROSSOVER_GPU_TIMEOUT_MS=15000,CROSSOVER_REF_TIMEOUT_MS=15000;
function crossoverSpeedTiles(fullW,fullH,w=CROSSOVER_SPEED_W,h=CROSSOVER_SPEED_H){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[.5,.5],[.28,.36],[.72,.64]],seen=new Set(),out=[];for(const [fx,fy] of pts){const tileX=Math.max(0,Math.min(fullW-w,Math.round(fullW*fx-w/2))),tileY=Math.max(0,Math.min(fullH-h,Math.round(fullH*fy-h/2))),k=tileX+':'+tileY;if(!seen.has(k)){seen.add(k);out.push({tileX,tileY,w,h})}}return out}
function benchmarkTimeout(promise,ms,label,onTimeout){let timer;const timeout=new Promise((_,reject)=>{timer=setTimeout(()=>{try{onTimeout?.()}catch{}reject(new Error(label+' timed out after '+Math.round(ms/1000)+'s'))},ms)});return Promise.race([promise,timeout]).finally(()=>clearTimeout(timer))}
async function benchmarkTileBatch(fn,tiles){let gpuMs=0,pixels=0;for(const q of tiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h}return{gpuMs,pixels}}
async function timedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[];for(let i=0;i<samples;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'sample',index:i+1,total:samples});values.push((await fn()).gpuMs)}return{medianMs:median(values),samplesMs:values}}
function extrapolateTiming(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels),fullMedianMs=msPerPixel*fullPixels;return{...timing,speedPixels,msPerPixel,fullMedianMs}}
async function runCrossoverBenchmark({track='seahorse',depths=CROSSOVER_DEPTHS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
if(benchmarkJob.active)throw new Error('benchmark already running');benchmarkJob.active=true;benchmarkJob.cancelled=false;
try{
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown benchmark track');
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
for(let di=0;di<depths.length;di++){
if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z),metrics=directMetrics(snap,fullW);
onProgress?.({index:di,total:depths.length,z,phase:'reference'});
const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'z'+z+' reference',()=>refs.cancelPending('benchmark reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
const timed=(name,fn)=>timedMedian(()=>gpuCall(benchmarkTileBatch(fn,speedTiles),'z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
const tileCommon=q=>({snap,iter,fullW,fullH,...q});
onProgress?.({index:di,total:depths.length,z,phase:'f32'});
const f32=extrapolateTiming(await timed('f32',q=>r.benchmarkNumeric({...tileCommon(q),deep:false,deepContext:null})),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'ds'});
const ds=extrapolateTiming(await timed('ds',q=>r.benchmarkDsDirect(tileCommon(q))),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'deep'});
const deep=extrapolateTiming(await timed('deep',q=>r.benchmarkNumeric({...tileCommon(q),deep:true,deepContext:ctx,correctUnknown:true})),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'quality'});
const f32Parts=[],dsParts=[],qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH);let deepUnknownCount=0,qualityPixels=0,deepCorrected=0;
for(const q of qTiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const qCommon={snap,iter,fullW,fullH,...q},deepMeta=await gpuCall(r.renderTileMeta({...qCommon,deep:true,deepContext:ctx,correctUnknown:true}),'z'+z+' quality deep'),f32Meta=await gpuCall(r.renderTileMeta({...qCommon,deep:false,deepContext:null}),'z'+z+' quality f32'),dsMeta=await gpuCall(r.renderTileMetaDsDirect(qCommon),'z'+z+' quality ds');qualityPixels+=q.w*q.h;deepUnknownCount+=deepMeta.unresolved||0;deepCorrected+=deepMeta.corrected||0;f32Parts.push(compareMeta(deepMeta,f32Meta));dsParts.push(compareMeta(deepMeta,dsMeta))}
const f32Quality=aggregateQuality(f32Parts),dsQuality=aggregateQuality(dsParts),deepUnknown=deepUnknownCount/Math.max(1,qualityPixels),referenceBuildMs=ctx.buildMs||referenceWallMs;
const row={track:tr.id,z,span:'3.4e-'+z,iter,resolution:[fullW,fullH],speedTiles,speedPixels,qualityTile:[qualityW,qualityH],qualityTiles:qTiles,directRatio:metrics.ratio,
f32:{...f32,quality:f32Quality},ds:{...ds,quality:dsQuality},deep:{...deep,referenceBuildMs,referenceWallMs,coldMedianMs:referenceBuildMs+deep.fullMedianMs,warmMedianMs:deep.fullMedianMs,unknownRate:deepUnknown,corrected:deepCorrected}};
const qualityLimit=.01;
row.assessment={f32VisuallySafe:f32Quality.classMismatchRate<=qualityLimit&&metrics.ratio>=1,dsVisuallySafe:dsQuality.classMismatchRate<=qualityLimit,deepVisuallySafe:deepUnknown<=.001,
fastestWarm:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.warmMedianMs]].sort((a,b)=>a[1]-b[1])[0][0],
fastestCold:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.coldMedianMs]].sort((a,b)=>a[1]-b[1])[0][0]};
rows.push(row);onProgress?.({index:di+1,total:depths.length,z,phase:'done',row});
}
const result={format:'mandelbrot-webgpu-crossover-v2',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Speed timing uses three bounded representative tiles and extrapolates ms/pixel to the current full resolution, preventing long full-frame Deep benchmark dispatches. Deep cold adds verified BigInt reference construction. Quality remains measured on five spatial tiles. Any single benchmark GPU batch or reference build exceeding 15 s aborts with an explicit error instead of waiting indefinitely.'};
benchmarkJob.last=result;return result;
}finally{benchmarkJob.active=false}
}
// ── DS + sparse Deep hybrid experiment (diagnostic only) ───────────────
const HYBRID_DEPTHS=[6,8,10,12],HYBRID_RISK_LIMITS=[1e12,1e13,1e14];
const hybridJob={active:false,cancelled:false,last:null};
async function hybridTileBatch(fn,tiles){let gpuMs=0,pixels=0,uncertain=0,remaining=0,corrected=0;for(const q of tiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h;uncertain+=r.uncertain||0;remaining+=r.remaining||0;corrected+=r.corrected||0}return{gpuMs,pixels,uncertain,remaining,corrected}}
async function hybridTimedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[],stats=[];for(let i=0;i<samples;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'sample',index:i+1,total:samples});const r=await fn();values.push(r.gpuMs);stats.push(r)}return{medianMs:median(values),samplesMs:values,sampleStats:stats}}
function hybridExtrapolate(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels);return{...timing,speedPixels,msPerPixel,fullMedianMs:msPerPixel*fullPixels}}
async function runHybridExperiment({track='seahorse',depths=HYBRID_DEPTHS,riskLimits=HYBRID_RISK_LIMITS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
if(hybridJob.active||benchmarkJob.active)throw new Error('another benchmark is running');hybridJob.active=true;hybridJob.cancelled=false;
try{
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown experiment track');
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
for(let di=0;di<depths.length;di++){
if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z);
onProgress?.({index:di,total:depths.length,z,phase:'reference'});const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'hybrid z'+z+' reference',()=>refs.cancelPending('hybrid reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
const timed=(name,fn)=>hybridTimedMedian(()=>gpuCall(hybridTileBatch(fn,speedTiles),'hybrid z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
const common=q=>({snap,iter,fullW,fullH,...q});
const ds=hybridExtrapolate(await timed('DS baseline',async q=>{const x=await r.benchmarkDsDirect(common(q));return{...x}}),speedPixels,fullPixels);
const deep=hybridExtrapolate(await timed('Full Deep',async q=>{const x=await r.benchmarkNumeric({...common(q),deep:true,deepContext:ctx,correctUnknown:true});return{...x}}),speedPixels,fullPixels);
const qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH),deepMetas=[];let deepUnknown=0;
onProgress?.({index:di,total:depths.length,z,phase:'quality reference'});
for(const q of qTiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const m=await gpuCall(r.renderTileMeta({...common(q),deep:true,deepContext:ctx,correctUnknown:true}),'hybrid z'+z+' quality deep');deepUnknown+=m.unresolved||0;deepMetas.push(m)}
const variants=[];
for(const riskLimit of riskLimits){
onProgress?.({index:di,total:depths.length,z,phase:'risk '+riskLimit.toExponential(0)});
const timing=hybridExtrapolate(await timed('Hybrid '+riskLimit.toExponential(0),q=>r.benchmarkHybrid({...common(q),deepContext:ctx,riskLimit})),speedPixels,fullPixels);
const parts=[];let uncertain=0,remaining=0,corrected=0,qualityPixels=0;
for(let qi=0;qi<qTiles.length;qi++){const q=qTiles[qi],hm=await gpuCall(r.renderTileMetaHybrid({...common(q),deepContext:ctx,riskLimit}),'hybrid z'+z+' quality '+riskLimit);qualityPixels+=q.w*q.h;uncertain+=hm.uncertain||0;remaining+=hm.remaining||0;corrected+=hm.corrected||0;parts.push(compareMeta(deepMetas[qi],hm))}
const quality=aggregateQuality(parts),referenceBuildMs=ctx.buildMs||referenceWallMs,coldMedianMs=referenceBuildMs+timing.fullMedianMs,uncertainRate=uncertain/Math.max(1,qualityPixels),remainingRate=remaining/Math.max(1,qualityPixels),safe=quality.classMismatch===0&&remaining===0;
variants.push({riskLimit,...timing,referenceBuildMs,coldMedianMs,quality,qualityPixels,uncertain,uncertainRate,remaining,remainingRate,corrected,safe,speedupVsDeepCold:(referenceBuildMs+deep.fullMedianMs)/Math.max(1e-9,coldMedianMs)});
}
const referenceBuildMs=ctx.buildMs||referenceWallMs,deepCold=referenceBuildMs+deep.fullMedianMs,viable=variants.filter(v=>v.safe).sort((a,b)=>a.coldMedianMs-b.coldMedianMs),best=viable[0]||null;
rows.push({track:tr.id,z,iter,resolution:[fullW,fullH],referenceBuildMs,ds:{...ds},deep:{...deep,coldMedianMs:deepCold,unknownRate:deepUnknown/Math.max(1,qTiles.reduce((n,q)=>n+q.w*q.h,0))},variants,bestRiskLimit:best?.riskLimit??null,bestHybridColdMs:best?.coldMedianMs??null,recommendation:best&&best.coldMedianMs<deepCold*.9&&best.uncertainRate<.6?'hybrid-candidate':'full-deep'});
onProgress?.({index:di+1,total:depths.length,z,phase:'done'});
}
const result={format:'mandelbrot-ds-sparse-deep-hybrid-v1',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],riskLimits:[...riskLimits],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Diagnostic only. DS guarded direct tracks dz/dc in f32 and marks a pixel UNKNOWN when |dz/dc| exceeds riskLimit. Existing deep double-single correction re-evaluates only those UNKNOWN pixels. Full Deep+correction is the GPU quality reference. Cold timing adds the verified BigInt reference build to both Hybrid and Full Deep. Production routing is unchanged.'};
hybridJob.last=result;return result;
}finally{hybridJob.active=false}
}
// ── GPU startup / rendering orchestration ────────────────────────────────
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
async function renderFrame(){
const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height);
state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats();
try{
const r=renderer||await initRenderer();if(token!==state.token)return;
if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}
if(decision.probe){state.lastEngine='WebGPU · direct/deep view probe';state.drawState='COVERING';updateStats();await ensureBackendProbe(snap,iter,decision,token);if(token!==state.token)return;decision=chooseBackend(snap,canvas.width,iter,canvas.height);if(decision.probe)decision={...decision,backend:'deep',deep:true,probe:false,reason:'overlap-probe-failed-safe-deep'};state.backendDecision=decision;updateStats()}
const deep=decision.deep;let ctx=null;
if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded perturbation'}else state.lastEngine='WebGPU · f32 direct';
const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;
const firstPaint=performance.now()-t0;
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,backend:decision.backend};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=firstPaint;state.rendering=deep;state.unresolved=0;
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();
if(!deep){state.rendering=false;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();return}
let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats();
const n=canvas.width*canvas.height;if(shouldApplySparseCorrection(state.unresolved,n)){state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats();const corrected=await r.correctUnknownFrame(snap,iter,token);if(!corrected||token!==state.token)return;state.correctionPasses=1;stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;const painted=await r.recolor(token);if(!painted||token!==state.token)return;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats()}
if(token!==state.token)return;state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · guarded perturbation'+(state.correctionPasses?' + DS correction':'');const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();
}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
}
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback(深部非対応)';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
const exportJob={active:false,cancelled:false};
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
class StreamingPng{
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
async rows(filteredRows){await this.writer.write(filteredRows)}
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
}
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
async function runExport(){
if(exportJob.active)return;
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),strict=$('#exportPrecision').value==='strict';
let ctx=null;
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
let done=0,unresolvedSamples=0;
try{
for(let y=0;y<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
function benchmarkSummary(result){const lines=['z | f32 est ms | DS est ms | deep warm est | deep cold est | f32 Δclass | DS Δclass | direct ratio | fastest cold','--|------------|-----------|---------------|---------------|------------|-----------|--------------|-------------'];for(const r of result.rows)lines.push([String(r.z).padStart(2),r.f32.fullMedianMs.toFixed(1),r.ds.fullMedianMs.toFixed(1),r.deep.warmMedianMs.toFixed(1),r.deep.coldMedianMs.toFixed(1),(100*r.f32.quality.classMismatchRate).toFixed(2)+'%',(100*r.ds.quality.classMismatchRate).toFixed(2)+'%',Number.isFinite(r.directRatio)?r.directRatio.toExponential(2):'n/a',r.assessment.fastestCold].join(' | '));return lines.join('\n')}
$('#backendBench').onclick=()=>{const d=$('#benchmarkDialog');$('#benchmarkStatus').textContent='';$('#benchmarkProgress').hidden=true;$('#benchmarkOutput').value=benchmarkJob.last?benchmarkSummary(benchmarkJob.last):'';$('#benchmarkSave').disabled=!benchmarkJob.last;d.showModal?d.showModal():d.setAttribute('open','')};
$('#benchmarkStart').onclick=async()=>{if(benchmarkJob.active)return;cancelRender();state.dirty=false;const start=$('#benchmarkStart'),progress=$('#benchmarkProgress'),status=$('#benchmarkStatus');start.disabled=true;$('#benchmarkSave').disabled=true;progress.hidden=false;progress.max=CROSSOVER_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#benchmarkOutput').value='';try{const result=await runCrossoverBenchmark({track:$('#benchmarkTrack').value,samples:Math.max(1,Number($('#benchmarkSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});progress.value=progress.max;status.textContent='測定完了 · '+result.rows.length+' depths';$('#benchmarkOutput').value=benchmarkSummary(result);$('#benchmarkSave').disabled=false}catch(e){status.textContent=String(e&&e.message||e)}finally{start.disabled=false;markDirty(false)}};
$('#benchmarkCancel').onclick=()=>{if(benchmarkJob.active){benchmarkJob.cancelled=true;refs.cancelPending('benchmark cancelled');$('#benchmarkStatus').textContent='中止しています…'}else $('#benchmarkDialog').close()};
$('#benchmarkSave').onclick=()=>{if(!benchmarkJob.last)return;downloadBlob(new Blob([JSON.stringify(benchmarkJob.last,null,2)],{type:'application/json'}),'mandelbrot-crossover-'+Date.now()+'.json')};
function hybridSummary(result){const lines=['z | best risk | hybrid cold est | deep cold est | speedup | correction | remain | Δclass | recommendation','--|-----------|-----------------|---------------|---------|------------|--------|--------|---------------'];for(const r of result.rows){const best=r.bestRiskLimit==null?null:r.variants.find(v=>v.riskLimit===r.bestRiskLimit),hy=best?best.coldMedianMs:NaN;lines.push([String(r.z).padStart(2),best?best.riskLimit.toExponential(0):'none',best?hy.toFixed(1):'n/a',r.deep.coldMedianMs.toFixed(1),best?(r.deep.coldMedianMs/hy).toFixed(2)+'×':'n/a',best?(100*best.uncertainRate).toFixed(1)+'%':'n/a',best?(100*best.remainingRate).toFixed(2)+'%':'n/a',best?(100*best.quality.classMismatchRate).toFixed(2)+'%':'n/a',r.recommendation].join(' | '))}return lines.join('\n')}
function openHybridDialog(){const d=$('#hybridDialog');if(!d.open){try{if(typeof d.showModal==='function')d.showModal();else d.setAttribute('open','')}catch{d.setAttribute('open','')}}return d}
async function startHybridExperimentFromUi(){
const d=openHybridDialog(),start=$('#hybridStart'),toolbar=$('#hybridBench'),progress=$('#hybridProgress'),status=$('#hybridStatus');status.textContent='Hybrid実験を起動中…';toolbar.setAttribute('aria-busy','true');
if(hybridJob.active){status.textContent='Hybrid実験を実行中です';toolbar.removeAttribute('aria-busy');return}
if(benchmarkJob.active){status.textContent='Backend比較を終了または中止してから実行してください';toolbar.removeAttribute('aria-busy');return}
cancelRender();state.dirty=false;start.disabled=true;toolbar.disabled=true;start.textContent='実行中…';$('#hybridSave').disabled=true;progress.hidden=false;progress.max=HYBRID_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#hybridOutput').value='';
try{
const result=await runHybridExperiment({track:$('#hybridTrack').value,samples:Math.max(1,Number($('#hybridSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});
progress.value=progress.max;status.textContent='実験完了 · '+result.rows.length+' depths';$('#hybridOutput').value=hybridSummary(result);$('#hybridSave').disabled=false;start.textContent='再実行';
}catch(e){status.textContent=String(e&&e.message||e);start.textContent='再実行'}
finally{start.disabled=false;toolbar.disabled=false;toolbar.removeAttribute('aria-busy');markDirty(false)}
}
$('#hybridBench').onclick=()=>{toast('Hybrid実験を開始');void startHybridExperimentFromUi()};
$('#hybridStart').onclick=startHybridExperimentFromUi;
$('#hybridCancel').onclick=()=>{if(hybridJob.active){hybridJob.cancelled=true;refs.cancelPending('hybrid experiment cancelled');$('#hybridStatus').textContent='中止しています…'}else $('#hybridDialog').close()};
$('#hybridSave').onclick=()=>{if(!hybridJob.last)return;downloadBlob(new Blob([JSON.stringify(hybridJob.last,null,2)],{type:'application/json'}),'mandelbrot-hybrid-'+Date.now()+'.json')};
// ── diagnostics ──────────────────────────────────────────────────────────
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend(snapshot(),Math.max(1,canvas.width),maxIter(),Math.max(1,canvas.height));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.probe?'WebGPU 判定中':decision.deep?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'深部補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.correctedPixels)status+=' · 回収 '+state.correctedPixels;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx,ratio=decision.metrics&&Number.isFinite(decision.metrics.ratio)?decision.metrics.ratio:0,prof=decision.profile,rs=state.unknownReasons,reasonText=rs?Object.entries(rs).filter(([,v])=>v).map(([k,v])=>k+':'+v).join(' '):'';$('#diagEngine').textContent='engine: '+state.lastEngine+' | route '+decision.backend+' ('+decision.reason+') | ratio '+(ratio?ratio.toExponential(2):'n/a')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'')+(reasonText?' | UNKNOWN '+reasonText:'')+(decision.viewProbe?' | view probe direct '+decision.viewProbe.predictedDirectMs.toFixed(0)+'ms / deep '+decision.viewProbe.predictedDeepMs.toFixed(0)+'ms / u '+(100*decision.viewProbe.deepUnknownRate).toFixed(2)+'% / Δclass '+decision.viewProbe.classDisagreement:'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height+' | speed profiles '+backendProfiles.size+' / view probes '+backendViewProbes.size}
globalThis.__MANDEL_TEST__={
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w,h):!!forceDeep;let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
async runCrossoverBenchmark(options={}){return runCrossoverBenchmark(options)},
async runHybridExperiment(options={}){return runHybridExperiment(options)},
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
};
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,buildVersion:BUILD_VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:(state.backendDecision||chooseBackend()).deep,backendDecision:state.backendDecision,backendProfiles:Array.from(backendProfiles.entries()),backendViewProbes:Array.from(backendViewProbes.entries()),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,unknownReasons:state.unknownReasons,correctionPasses:state.correctionPasses,correctedPixels:state.correctedPixels,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
})();

444
dist/hosted/script.js vendored
View file

@ -1,444 +0,0 @@
(()=>{'use strict';
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
const $=s=>document.querySelector(s),canvas=$('#view');
const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
const DIRECT_REQUIRED_RATIO=8,DIRECT_DEFAULT_RATIO=128,DEEP_SPEED_MARGIN=.85,DEEP_PROBE_UNKNOWN_MAX=.005,NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,FIXED96_INDIRECT_BYTES=16,FIXED96_WORKGROUP_SIZE=64,CORRECTION_MARK=128;
const BACKEND_PROBE_SPEED_W=224,BACKEND_PROBE_SPEED_H=160,BACKEND_PROBE_QUALITY_W=72,BACKEND_PROBE_QUALITY_H=54;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,backendProbes:0};
const backendProfiles=new Map(),backendViewProbes=new Map(),backendProbePending=new Map();
// ── exact fixed-point view state ─────────────────────────────────────────
function one(bits=state.bits){return 1n<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
function f32ToFixed(x,bits){
x=Math.fround(x);if(!Number.isFinite(x))throw new Error('non-finite f32 split component');if(x===0)return 0n;
const ab=new ArrayBuffer(4),dv=new DataView(ab);dv.setFloat32(0,x,false);const u=dv.getUint32(0,false),neg=(u>>>31)!==0,eb=(u>>>23)&255,frac=u&0x7fffff,m=BigInt(eb===0?frac:(0x800000|frac)),e=(eb===0?-149:eb-127-23),sh=bits+e;
let q=sh>=0?m<<BigInt(sh):roundDivSigned(m,1n<<BigInt(-sh));return neg?-q:q;
}
function splitFixedF32(v,bits,count=3){const out=[];let r=v;for(let i=0;i<count;i++){const x=Math.fround(fixedNum(r,bits));out.push(x);if(x===0)continue;r-=f32ToFixed(x,bits)}return out}
const FIXED96_FRAC_BITS=88,FIXED96_WIDTH_BITS=96n,FIXED96_MOD=1n<<FIXED96_WIDTH_BITS,FIXED96_MASK32=0xffffffffn,FIXED96_MIN=-(1n<<95n),FIXED96_MAX=(1n<<95n)-1n;
function q88FromRational(numer,denom,bits){let n=BigInt(numer),d=BigInt(denom),sh=FIXED96_FRAC_BITS-bits;if(sh>=0)n<<=BigInt(sh);else d<<=BigInt(-sh);return roundDivSigned(n,d)}
function q88Words(v){let x=BigInt(v);if(x<FIXED96_MIN||x>FIXED96_MAX)throw new Error('Fixed96 Q8.88 coordinate overflow');if(x<0)x+=FIXED96_MOD;return[Number(x&FIXED96_MASK32),Number((x>>32n)&FIXED96_MASK32),Number((x>>64n)&FIXED96_MASK32)]}
function fixed96TileData(snap,fullW,fullH,tileX,tileY,sampleX=.5,sampleY=.5){const sx4=Math.round(sampleX*4),sy4=Math.round(sampleY*4);if(Math.abs(sampleX*4-sx4)>1e-6||Math.abs(sampleY*4-sy4)>1e-6)throw new Error('fixed96 experiment supports quarter-pixel sample offsets only');const den=4n*BigInt(Math.max(1,fullW)),ox4=4n*BigInt(tileX)+BigInt(sx4)-2n*BigInt(fullW),oy4=2n*BigInt(fullH)-4n*BigInt(tileY)-BigInt(sy4),reNum=snap.re*den+snap.span*ox4,imNum=snap.im*den+snap.span*oy4,baseRe=q88FromRational(reNum,den,snap.bits),baseIm=q88FromRational(imNum,den,snap.bits),step=q88FromRational(snap.span,BigInt(Math.max(1,fullW)),snap.bits);if(step===0n&&snap.span!==0n)throw new Error('Fixed96 Q8.88 pixel step underflow');return{baseRe:q88Words(baseRe),baseIm:q88Words(baseIm),step:q88Words(step)}}
function fixed96QueueIntegrity(guard,fixed,args,capacity){const selected=guard[0]||0,overflow=guard[1]||0,enqueued=guard[2]||0,dispatchCount=guard[3]||0,remaining=fixed[0]||0,invalidIndex=fixed[1]||0,processed=fixed[2]||0,staleEntry=fixed[3]||0,corrected=fixed[7]||0,workgroups=args[0]||0,expectedWorkgroups=Math.ceil(enqueued/FIXED96_WORKGROUP_SIZE),issues=[];if(selected>capacity)issues.push('selected>capacity');if(overflow!==0)issues.push('queue-overflow');if(enqueued!==selected)issues.push('enqueued!=selected');if(dispatchCount!==enqueued)issues.push('dispatch-count!=enqueued');if(workgroups!==expectedWorkgroups||(args[1]||0)!==1||(args[2]||0)!==1)issues.push('indirect-args');if(processed!==enqueued)issues.push('processed!=enqueued');if(invalidIndex!==0)issues.push('invalid-index');if(staleEntry!==0)issues.push('stale-entry');if(corrected+remaining!==processed)issues.push('corrected+remaining!=processed');return{ok:issues.length===0,issues,selected,overflow,enqueued,dispatchCount,workgroups,expectedWorkgroups,processed,remaining,invalidIndex,staleEntry,corrected,capacity}}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
function directMetrics(snap=snapshot(),width=Math.max(1,canvas.width)){const spanLog=log2FixedAt(snap.span,snap.bits),stepLog=spanLog-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp))),eligible=Number.isFinite(ratio)&&ratio>=DIRECT_REQUIRED_RATIO&&spanLog>-145;return{spanLog,stepLog,ratio,eligible,overlap:eligible&&ratio<DIRECT_DEFAULT_RATIO}}
function zoomExpFor(snap){return Math.max(0,Math.log10(3.4)-log2FixedAt(snap.span,snap.bits)/Math.log2(10))}
function backendProfileKey(snap,iter){const z=Math.floor(zoomExpFor(snap)*2)/2,ib=iter<600?0:iter<1500?1:iter<5000?2:3;return z.toFixed(1)+':'+ib}
function backendViewKey(snap,iter,width,height){return[snap.bits,snap.re,snap.im,snap.span,iter,width,height].join(':')}
function chooseBackend(snap=snapshot(),width=Math.max(1,canvas.width),iter=maxIter(),height=Math.max(1,canvas.height)){const m=directMetrics(snap,width),key=backendProfileKey(snap,iter),viewKey=backendViewKey(snap,iter,width,height),speed=backendProfiles.get(key),viewProbe=backendViewProbes.get(viewKey),pixels=Math.max(1,width*Math.max(1,height)),profile=speed?{...speed,predictedDirectMs:speed.directMsPerPixel*pixels,predictedDeepMs:speed.referenceBuildMs+speed.deepMsPerPixel*pixels}:null;let backend='deep',reason='direct-coordinate-limit';if(m.eligible){if(m.ratio>=DIRECT_DEFAULT_RATIO){backend='direct';reason='direct-wide-margin'}else if(!viewProbe){backend='probe';reason='overlap-view-probe-required'}else if(!viewProbe.qualityPass){backend='deep';reason='direct-quality-rejected'}else if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*DEEP_SPEED_MARGIN){backend='deep';reason='deep-view-probe-faster'}else{backend='direct';reason='direct-view-probe-preferred'}}return{backend,deep:backend==='deep',probe:backend==='probe',reason,key,viewKey,metrics:m,profile,viewProbe:viewProbe||null}}
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width),height=Math.max(1,canvas.height)){const d=chooseBackend(snap,width,maxIter(),height);return d.deep||d.probe}
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
// ── iteration / quality policy ───────────────────────────────────────────
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
'use strict';
const MAX_REF=150001,MAX_LEVELS=20;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class ReferenceService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter,fresh=false){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.2.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
}
const refs=new ReferenceService();
// ── WebGPU renderer ──────────────────────────────────────────────────────
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
class WebGpuRenderer{
constructor(adapter,device){
this.adapter=adapter;this.device=device;
const ai=adapter.info||{};
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,sdm,shm,qm,fm,xm,zm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('direct-ds-benchmark',G.DIRECT_DS_WGSL),this.module('direct-ds-guarded-experiment',G.DIRECT_DS_GUARDED_WGSL),this.module('direct-fixed96-queue-args-experiment',G.DIRECT_FIXED96_QUEUE_ARGS_WGSL),this.module('direct-fixed96-correction-experiment',G.DIRECT_FIXED96_CORRECT_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
this.dsDirect=this.device.createComputePipeline({layout:'auto',compute:{module:sdm,entryPoint:'main'}});
this.dsGuarded=this.device.createComputePipeline({layout:'auto',compute:{module:shm,entryPoint:'main'}});
this.fixed96QueueArgs=this.device.createComputePipeline({layout:'auto',compute:{module:qm,entryPoint:'main'}});
this.fixed96Correct=this.device.createComputePipeline({layout:'auto',compute:{module:fm,entryPoint:'main'}});
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}});
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
webgpuCanvasClaimed=true;this.configure();
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
ensureFrame(w,h){
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
}
ensureExportWorkspace(){
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
}
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5){const cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),crh=Math.fround(cr),cih=Math.fround(ci),sph=Math.fround(sp),invW=1/Math.max(1,fullW),invh=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,crh,true);d.setFloat32(36,Math.fround(cr-crh),true);d.setFloat32(40,cih,true);d.setFloat32(44,Math.fround(ci-cih),true);d.setFloat32(48,sph,true);d.setFloat32(52,Math.fround(sp-sph),true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,invh,true);d.setFloat32(68,Math.fround(invW-invh),true)})}
dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sx=.5,sy=.5){const a=this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx,sy),d=new DataView(a);d.setFloat32(72,Math.fround(riskLimit),true);return a}
fixed96Params(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5){const q=fixed96TileData(snap,fullW,fullH,tileX,tileY,sx,sy);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,iter,0,...q.baseRe,...q.baseIm,...q.step,0,0,0].forEach((v,i)=>d.setUint32(i*4,v>>>0,true))})}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDsDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsDirect.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsDirect);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDsGuardedNumeric(encoder,{pbuf,meta,smooth,stats,queue,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsGuarded.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:stats}},{binding:4,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsGuarded);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeFixed96QueueArgs(encoder,{guardStats,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.fixed96QueueArgs.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:guardStats}},{binding:1,resource:{buffer:indirect}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fixed96QueueArgs);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end()}
encodeFixed96CorrectionNumeric(encoder,{pbuf,meta,smooth,stats,guardStats,queue,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.fixed96Correct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:stats}},{binding:4,resource:{buffer:guardStats}},{binding:5,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fixed96Correct);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()}
encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
}
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
async readUnresolved(){return(await this.readUnresolvedStats()).total}
async correctUnknownFrame(snap,iter,token){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,f.w*.5,f.h*.5,1,f.w,0));const e=d.createCommandEncoder({label:'sparse-deep-correction'}),bg=d.createBindGroup({layout:this.correct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),p=e.beginComputePass();p.setPipeline(this.correct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
async benchmarkDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap));const e=d.createCommandEncoder({label:'ds-direct-benchmark'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;[meta,fieldSmooth,pbuf].forEach(destroy);return{gpuMs}}
async renderTileMetaDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,n*4,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const e=d.createCommandEncoder({label:'ds-direct-meta'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});e.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,fieldSmooth,pbuf,read].forEach(destroy);return out}
async benchmarkFixed96Direct({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13}){
await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,
meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
guardStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),correctStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
riskQueue=buf(d,n*4,B.STORAGE|B.COPY_DST,'fixed96-risk-queue'),indirect=buf(d,FIXED96_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'fixed96-indirect'),
guardPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),correctPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
read=buf(d,2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES,B.COPY_DST|B.MAP_READ);
d.queue.writeBuffer(guardStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(correctStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(indirect,0,new Uint32Array(FIXED96_INDIRECT_BYTES/4));
d.queue.writeBuffer(guardPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit));d.queue.writeBuffer(correctPbuf,0,this.fixed96Params(w,h,fullW,fullH,tileX,tileY,iter,snap));
const e=d.createCommandEncoder({label:'ds-fixed96-queued-benchmark'});this.encodeDsGuardedNumeric(e,{pbuf:guardPbuf,meta,smooth:fieldSmooth,stats:guardStats,queue:riskQueue,w,h});this.encodeFixed96QueueArgs(e,{guardStats,indirect});this.encodeFixed96CorrectionNumeric(e,{pbuf:correctPbuf,meta,smooth:fieldSmooth,stats:correctStats,guardStats,queue:riskQueue,indirect});
e.copyBufferToBuffer(guardStats,0,read,0,UNRESOLVED_BYTES);e.copyBufferToBuffer(correctStats,0,read,UNRESOLVED_BYTES,UNRESOLVED_BYTES);e.copyBufferToBuffer(indirect,0,read,2*UNRESOLVED_BYTES,FIXED96_INDIRECT_BYTES);
const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;
await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),a=new Uint32Array(raw.slice(0,UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(UNRESOLVED_BYTES,2*UNRESOLVED_BYTES)),args=new Uint32Array(raw.slice(2*UNRESOLVED_BYTES,2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES));read.unmap();
const integrity=fixed96QueueIntegrity(a,b,args,n);[meta,fieldSmooth,guardStats,correctStats,riskQueue,indirect,guardPbuf,correctPbuf,read].forEach(destroy);if(!integrity.ok)throw new Error('Fixed96 queue integrity failure: '+integrity.issues.join(', '));return{gpuMs,uncertain:integrity.selected,remaining:integrity.remaining,corrected:integrity.corrected,queue:integrity};
}
async renderTileMetaFixed96Direct({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13,sampleX=.5,sampleY=.5}){
await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,
meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
guardStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),correctStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
riskQueue=buf(d,n*4,B.STORAGE|B.COPY_DST,'fixed96-risk-queue-meta'),indirect=buf(d,FIXED96_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'fixed96-indirect-meta'),
guardPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),correctPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
read=buf(d,n*4+2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES,B.COPY_DST|B.MAP_READ);
d.queue.writeBuffer(guardStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(correctStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(indirect,0,new Uint32Array(FIXED96_INDIRECT_BYTES/4));
d.queue.writeBuffer(guardPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sampleX,sampleY));d.queue.writeBuffer(correctPbuf,0,this.fixed96Params(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));
const e=d.createCommandEncoder({label:'ds-fixed96-queued-meta'});this.encodeDsGuardedNumeric(e,{pbuf:guardPbuf,meta,smooth:fieldSmooth,stats:guardStats,queue:riskQueue,w,h});this.encodeFixed96QueueArgs(e,{guardStats,indirect});this.encodeFixed96CorrectionNumeric(e,{pbuf:correctPbuf,meta,smooth:fieldSmooth,stats:correctStats,guardStats,queue:riskQueue,indirect});
e.copyBufferToBuffer(meta,0,read,0,n*4);e.copyBufferToBuffer(guardStats,0,read,n*4,UNRESOLVED_BYTES);e.copyBufferToBuffer(correctStats,0,read,n*4+UNRESOLVED_BYTES,UNRESOLVED_BYTES);e.copyBufferToBuffer(indirect,0,read,n*4+2*UNRESOLVED_BYTES,FIXED96_INDIRECT_BYTES);
d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),a=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(n*4+UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES)),args=new Uint32Array(raw.slice(n*4+2*UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES));read.unmap();
const integrity=fixed96QueueIntegrity(a,b,args,n);[meta,fieldSmooth,guardStats,correctStats,riskQueue,indirect,guardPbuf,correctPbuf,read].forEach(destroy);if(!integrity.ok)throw new Error('Fixed96 queue integrity failure: '+integrity.issues.join(', '));out.uncertain=integrity.selected;out.remaining=integrity.remaining;out.corrected=integrity.corrected;out.queue=integrity;return out;
}
async benchmarkNumeric({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,forceStrict=false,correctUnknown=false}){await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const e=d.createCommandEncoder({label:'backend-benchmark'});if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,forceStrict?1:0));this.encodeDeepNumeric(e,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){e.clearBuffer(unresolved);this.encodeCorrectionNumeric(e,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap));this.encodeDirectNumeric(e,{pbuf,meta,smooth,w,h})}e.copyBufferToBuffer(unresolved,0,read,0,UNRESOLVED_BYTES);const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;await read.mapAsync(GPUMapMode.READ);const a=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return{gpuMs,unknown:a[0]||0,corrected:a[7]||0}}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
}
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes,UNRESOLVED_BYTES/4),unresolvedCount=stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
async renderTileRGBA2x({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount=0,corrected=0;for(let si=0;si<4;si++){const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4);unresolvedCount+=stats[0]||0;corrected+=stats[7]||0}ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext()}
}
function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true}
function backendQualityTiles(fullW,fullH){const w=Math.min(BACKEND_PROBE_QUALITY_W,fullW),h=Math.min(BACKEND_PROBE_QUALITY_H,fullH),x1=Math.max(0,fullW-w),y1=Math.max(0,fullH-h),xc=Math.max(0,Math.floor((fullW-w)/2)),yc=Math.max(0,Math.floor((fullH-h)/2));return[[0,0],[x1,0],[0,y1],[x1,y1],[xc,yc]].map(([tileX,tileY])=>({tileX,tileY,w,h}))}
async function performBackendProbe(snap,iter,decision,token){if(!renderer||!decision.metrics.overlap||token!==state.token)return null;const fullW=canvas.width,fullH=canvas.height,sw=Math.min(BACKEND_PROBE_SPEED_W,fullW),sh=Math.min(BACKEND_PROBE_SPEED_H,fullH),sx=Math.max(0,Math.floor((fullW-sw)/2)),sy=Math.max(0,Math.floor((fullH-sh)/2)),speedPixels=sw*sh,fullPixels=Math.max(1,fullW*fullH),direct=await renderer.benchmarkNumeric({snap,iter,deep:false,deepContext:null,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh});if(token!==state.token)return null;const rb=performance.now(),ctx=await refs.request(snap,iter),buildWall=performance.now()-rb;if(token!==state.token||ctx.checkpointMismatch)return null;const deepProbe=await renderer.benchmarkNumeric({snap,iter,deep:true,deepContext:ctx,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh,correctUnknown:true});if(token!==state.token)return null;let classDisagreement=0,deepUnknown=0,knownCompared=0,qualityPixels=0,corrected=0;for(const q of backendQualityTiles(fullW,fullH)){const directMeta=await renderer.renderTileMeta({snap,iter,deep:false,deepContext:null,fullW,fullH,...q});if(token!==state.token)return null;const deepMeta=await renderer.renderTileMeta({snap,iter,deep:true,deepContext:ctx,fullW,fullH,...q,correctUnknown:true});if(token!==state.token)return null;qualityPixels+=q.w*q.h;corrected+=deepMeta.corrected||0;for(let i=0;i<deepMeta.length;i++){const dc=(deepMeta[i]>>>28)&3;if(dc===FIELD_UNKNOWN){deepUnknown++;continue}const rc=(directMeta[i]>>>28)&3;knownCompared++;if((dc===FIELD_ESCAPED)!==(rc===FIELD_ESCAPED))classDisagreement++}}const buildMs=ctx.buildMs||buildWall,directMsPerPixel=direct.gpuMs/Math.max(1,speedPixels),deepMsPerPixel=deepProbe.gpuMs/Math.max(1,speedPixels),sample={directMsPerPixel,deepMsPerPixel,referenceBuildMs:buildMs},prev=backendProfiles.get(decision.key),a=prev?.samples?0.3:1,profile={samples:(prev?.samples||0)+1,directMsPerPixel:(prev?.directMsPerPixel||sample.directMsPerPixel)*(1-a)+sample.directMsPerPixel*a,deepMsPerPixel:(prev?.deepMsPerPixel||sample.deepMsPerPixel)*(1-a)+sample.deepMsPerPixel*a,referenceBuildMs:(prev?.referenceBuildMs||sample.referenceBuildMs)*(1-a)+sample.referenceBuildMs*a};backendProfiles.set(decision.key,profile);const deepUnknownRate=deepUnknown/Math.max(1,qualityPixels),qualityPass=classDisagreement===0&&deepUnknownRate<=DEEP_PROBE_UNKNOWN_MAX,viewProbe={qualityPass,deepUnknownRate,classDisagreement,knownCompared,qualityPixels,corrected,predictedDirectMs:directMsPerPixel*fullPixels,predictedDeepMs:buildMs+deepMsPerPixel*fullPixels,ratio:decision.metrics.ratio,zoom:zoomExpFor(snap),iter,speedProbe:{w:sw,h:sh,tileX:sx,tileY:sy},qualityTiles:backendQualityTiles(fullW,fullH)};backendViewProbes.set(decision.viewKey,viewProbe);runtime.backendProbes++;return viewProbe}
function ensureBackendProbe(snap,iter,decision,token){if(!decision.metrics.overlap)return Promise.resolve(null);const hit=backendViewProbes.get(decision.viewKey);if(hit)return Promise.resolve(hit);const pending=backendProbePending.get(decision.viewKey);if(pending)return pending;const task=performBackendProbe(snap,iter,decision,token).catch(e=>{if(token===state.token)console.warn('backend probe failed',e);return null}).finally(()=>backendProbePending.delete(decision.viewKey));backendProbePending.set(decision.viewKey,task);return task}
// ── backend crossover benchmark (diagnostic only) ───────────────────────
const CROSSOVER_TRACKS={
seahorse:{id:'seahorse',label:'Seahorse boundary',re:'-0.7453983606667815',im:'0.1125046349959942'},
cusp:{id:'cusp',label:'Period-2 cusp',re:'-0.75',im:'0'}
};
const CROSSOVER_DEPTHS=[0,2,4,6,8,10,12,14,16,20,30,40];
const benchmarkJob={active:false,cancelled:false,last:null};
function fixedAtDecimal(text,bits){let s=String(text).trim(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const parts=s.toLowerCase().split('e'),mant=parts[0],exp=parts[1]?parseInt(parts[1],10):0,ab=mant.split('.'),ii=ab[0]||'0',ff=ab[1]||'';let digits=(ii+ff).replace(/^0+(?=\d)/,'')||'0',places=ff.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),scale=1n<<BigInt(bits),v=(BigInt(digits)*scale+den/2n)/den;return neg?-v:v}
function crossoverSnap(track,z){const span='3.4e-'+z,bits=Math.max(320,decimalRequiredBits(track.re)+64,decimalRequiredBits(track.im)+64,decimalRequiredBits(span)+96);return{bits,re:fixedAtDecimal(track.re,bits),im:fixedAtDecimal(track.im,bits),span:fixedAtDecimal(span,bits)}}
function iterForZoom(z,base=350){return Math.max(base,base+Math.floor(70*Math.sqrt(Math.max(0,z))+15*Math.max(0,z)))}
function median(values){const a=[...values].sort((x,y)=>x-y),n=a.length;return n?a.length%2?a[(n-1)>>1]:(a[n/2-1]+a[n/2])/2:NaN}
function compareMeta(reference,candidate){let referenceUnknown=0,candidateUnknown=0,classMismatch=0,escapedCompared=0,iterMismatch=0,maxIterDelta=0,sumIterDelta=0;const n=Math.min(reference.length,candidate.length);for(let i=0;i<n;i++){const rm=reference[i]>>>0,cm=candidate[i]>>>0,rc=(rm>>>28)&3,cc=(cm>>>28)&3;if(rc===FIELD_UNKNOWN){referenceUnknown++;continue}if(cc===FIELD_UNKNOWN){candidateUnknown++;continue}const re=rc===FIELD_ESCAPED,ce=cc===FIELD_ESCAPED;if(re!==ce){classMismatch++;continue}if(re){escapedCompared++;const d=Math.abs((rm&ITER_MASK)-(cm&ITER_MASK));if(d){iterMismatch++;sumIterDelta+=d;if(d>maxIterDelta)maxIterDelta=d}}}return{pixels:n,referenceUnknown,candidateUnknown,classMismatch,classMismatchRate:classMismatch/Math.max(1,n-referenceUnknown),escapedCompared,escapeIterationMismatch:iterMismatch,escapeIterationMismatchRate:iterMismatch/Math.max(1,escapedCompared),meanEscapeIterationDelta:iterMismatch?sumIterDelta/iterMismatch:0,maxEscapeIterationDelta:maxIterDelta}}
function crossoverQualityTiles(fullW,fullH,w,h){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[0,0],[Math.max(0,fullW-w),0],[0,Math.max(0,fullH-h)],[Math.max(0,fullW-w),Math.max(0,fullH-h)],[Math.max(0,Math.floor((fullW-w)/2)),Math.max(0,Math.floor((fullH-h)/2))]];return pts.map(([tileX,tileY])=>({tileX,tileY,w,h}))}
function aggregateQuality(list){const out={pixels:0,referenceUnknown:0,candidateUnknown:0,classMismatch:0,escapedCompared:0,escapeIterationMismatch:0,sumEscapeIterationDelta:0,maxEscapeIterationDelta:0};for(const q of list){out.pixels+=q.pixels;out.referenceUnknown+=q.referenceUnknown;out.candidateUnknown+=q.candidateUnknown;out.classMismatch+=q.classMismatch;out.escapedCompared+=q.escapedCompared;out.escapeIterationMismatch+=q.escapeIterationMismatch;out.sumEscapeIterationDelta+=q.meanEscapeIterationDelta*q.escapeIterationMismatch;out.maxEscapeIterationDelta=Math.max(out.maxEscapeIterationDelta,q.maxEscapeIterationDelta)}out.classMismatchRate=out.classMismatch/Math.max(1,out.pixels-out.referenceUnknown);out.escapeIterationMismatchRate=out.escapeIterationMismatch/Math.max(1,out.escapedCompared);out.meanEscapeIterationDelta=out.escapeIterationMismatch?out.sumEscapeIterationDelta/out.escapeIterationMismatch:0;delete out.sumEscapeIterationDelta;return out}
const CROSSOVER_SPEED_W=256,CROSSOVER_SPEED_H=144,CROSSOVER_GPU_TIMEOUT_MS=15000,CROSSOVER_REF_TIMEOUT_MS=15000;
function crossoverSpeedTiles(fullW,fullH,w=CROSSOVER_SPEED_W,h=CROSSOVER_SPEED_H){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[.5,.5],[.28,.36],[.72,.64]],seen=new Set(),out=[];for(const [fx,fy] of pts){const tileX=Math.max(0,Math.min(fullW-w,Math.round(fullW*fx-w/2))),tileY=Math.max(0,Math.min(fullH-h,Math.round(fullH*fy-h/2))),k=tileX+':'+tileY;if(!seen.has(k)){seen.add(k);out.push({tileX,tileY,w,h})}}return out}
function benchmarkTimeout(promise,ms,label,onTimeout){let timer;const timeout=new Promise((_,reject)=>{timer=setTimeout(()=>{try{onTimeout?.()}catch{}reject(new Error(label+' timed out after '+Math.round(ms/1000)+'s'))},ms)});return Promise.race([promise,timeout]).finally(()=>clearTimeout(timer))}
async function benchmarkTileBatch(fn,tiles){let gpuMs=0,pixels=0;for(const q of tiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h}return{gpuMs,pixels}}
async function timedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[];for(let i=0;i<samples;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'sample',index:i+1,total:samples});values.push((await fn()).gpuMs)}return{medianMs:median(values),samplesMs:values}}
function extrapolateTiming(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels),fullMedianMs=msPerPixel*fullPixels;return{...timing,speedPixels,msPerPixel,fullMedianMs}}
async function runCrossoverBenchmark({track='seahorse',depths=CROSSOVER_DEPTHS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
if(benchmarkJob.active)throw new Error('benchmark already running');benchmarkJob.active=true;benchmarkJob.cancelled=false;
try{
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown benchmark track');
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
for(let di=0;di<depths.length;di++){
if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z),metrics=directMetrics(snap,fullW);
onProgress?.({index:di,total:depths.length,z,phase:'reference'});
const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'z'+z+' reference',()=>refs.cancelPending('benchmark reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
const timed=(name,fn)=>timedMedian(()=>gpuCall(benchmarkTileBatch(fn,speedTiles),'z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
const tileCommon=q=>({snap,iter,fullW,fullH,...q});
onProgress?.({index:di,total:depths.length,z,phase:'f32'});
const f32=extrapolateTiming(await timed('f32',q=>r.benchmarkNumeric({...tileCommon(q),deep:false,deepContext:null})),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'ds'});
const ds=extrapolateTiming(await timed('ds',q=>r.benchmarkDsDirect(tileCommon(q))),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'deep'});
const deep=extrapolateTiming(await timed('deep',q=>r.benchmarkNumeric({...tileCommon(q),deep:true,deepContext:ctx,correctUnknown:true})),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'quality'});
const f32Parts=[],dsParts=[],qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH);let deepUnknownCount=0,qualityPixels=0,deepCorrected=0;
for(const q of qTiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const qCommon={snap,iter,fullW,fullH,...q},deepMeta=await gpuCall(r.renderTileMeta({...qCommon,deep:true,deepContext:ctx,correctUnknown:true}),'z'+z+' quality deep'),f32Meta=await gpuCall(r.renderTileMeta({...qCommon,deep:false,deepContext:null}),'z'+z+' quality f32'),dsMeta=await gpuCall(r.renderTileMetaDsDirect(qCommon),'z'+z+' quality ds');qualityPixels+=q.w*q.h;deepUnknownCount+=deepMeta.unresolved||0;deepCorrected+=deepMeta.corrected||0;f32Parts.push(compareMeta(deepMeta,f32Meta));dsParts.push(compareMeta(deepMeta,dsMeta))}
const f32Quality=aggregateQuality(f32Parts),dsQuality=aggregateQuality(dsParts),deepUnknown=deepUnknownCount/Math.max(1,qualityPixels),referenceBuildMs=ctx.buildMs||referenceWallMs;
const row={track:tr.id,z,span:'3.4e-'+z,iter,resolution:[fullW,fullH],speedTiles,speedPixels,qualityTile:[qualityW,qualityH],qualityTiles:qTiles,directRatio:metrics.ratio,
f32:{...f32,quality:f32Quality},ds:{...ds,quality:dsQuality},deep:{...deep,referenceBuildMs,referenceWallMs,coldMedianMs:referenceBuildMs+deep.fullMedianMs,warmMedianMs:deep.fullMedianMs,unknownRate:deepUnknown,corrected:deepCorrected}};
const qualityLimit=.01;
row.assessment={f32VisuallySafe:f32Quality.classMismatchRate<=qualityLimit&&metrics.ratio>=1,dsVisuallySafe:dsQuality.classMismatchRate<=qualityLimit,deepVisuallySafe:deepUnknown<=.001,
fastestWarm:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.warmMedianMs]].sort((a,b)=>a[1]-b[1])[0][0],
fastestCold:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.coldMedianMs]].sort((a,b)=>a[1]-b[1])[0][0]};
rows.push(row);onProgress?.({index:di+1,total:depths.length,z,phase:'done',row});
}
const result={format:'mandelbrot-webgpu-crossover-v2',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Speed timing uses three bounded representative tiles and extrapolates ms/pixel to the current full resolution, preventing long full-frame Deep benchmark dispatches. Deep cold adds verified BigInt reference construction. Quality remains measured on five spatial tiles. Any single benchmark GPU batch or reference build exceeding 15 s aborts with an explicit error instead of waiting indefinitely.'};
benchmarkJob.last=result;return result;
}finally{benchmarkJob.active=false}
}
// ── DS + sparse integer fixed-point direct experiment (diagnostic only) ──
const FIXED96_DEPTHS=[6,8,10,12],FIXED96_RISK_LIMITS=[1e11,1e12,1e13,1e14];
const fixed96Job={active:false,cancelled:false,last:null};
async function fixed96TileBatch(fn,tiles){let gpuMs=0,pixels=0,uncertain=0,remaining=0,corrected=0,enqueued=0,processed=0,workgroups=0;for(const q of tiles){if(fixed96Job.cancelled)throw new Error('fixed96 experiment cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h;uncertain+=r.uncertain||0;remaining+=r.remaining||0;corrected+=r.corrected||0;if(r.queue){enqueued+=r.queue.enqueued||0;processed+=r.queue.processed||0;workgroups+=r.queue.workgroups||0}}return{gpuMs,pixels,uncertain,remaining,corrected,enqueued,processed,workgroups}}
async function fixed96TimedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(fixed96Job.cancelled)throw new Error('fixed96 experiment cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[],stats=[];for(let i=0;i<samples;i++){if(fixed96Job.cancelled)throw new Error('fixed96 experiment cancelled');onStep?.({kind:'sample',index:i+1,total:samples});const r=await fn();values.push(r.gpuMs);stats.push(r)}return{medianMs:median(values),samplesMs:values,sampleStats:stats}}
function fixed96Extrapolate(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels);return{...timing,speedPixels,msPerPixel,fullMedianMs:msPerPixel*fullPixels}}
async function runFixed96Experiment({track='seahorse',depths=FIXED96_DEPTHS,riskLimits=FIXED96_RISK_LIMITS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
if(fixed96Job.active||benchmarkJob.active)throw new Error('another benchmark is running');fixed96Job.active=true;fixed96Job.cancelled=false;
try{
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown experiment track');
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
for(let di=0;di<depths.length;di++){
if(fixed96Job.cancelled)throw new Error('fixed96 experiment cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z),common=q=>({snap,iter,fullW,fullH,...q});
const timed=(name,fn)=>fixed96TimedMedian(()=>gpuCall(fixed96TileBatch(fn,speedTiles),'fixed96 z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
onProgress?.({index:di,total:depths.length,z,phase:'DS baseline'});const ds=fixed96Extrapolate(await timed('DS baseline',async q=>r.benchmarkDsDirect(common(q))),speedPixels,fullPixels);
const variants=[];
for(const riskLimit of riskLimits){onProgress?.({index:di,total:depths.length,z,phase:'Fixed96 '+riskLimit.toExponential(0)});const timing=fixed96Extrapolate(await timed('Fixed96 '+riskLimit.toExponential(0),q=>r.benchmarkFixed96Direct({...common(q),riskLimit})),speedPixels,fullPixels),speedQueueRate=median(timing.sampleStats.map(x=>(x.enqueued||0)/Math.max(1,x.pixels||speedPixels))),speedProcessedRate=median(timing.sampleStats.map(x=>(x.processed||0)/Math.max(1,x.pixels||speedPixels)));variants.push({riskLimit,...timing,speedQueueRate,speedProcessedRate});}
onProgress?.({index:di,total:depths.length,z,phase:'Deep reference'});const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'fixed96 z'+z+' reference',()=>refs.cancelPending('fixed96 reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
const deep=fixed96Extrapolate(await timed('Full Deep',q=>r.benchmarkNumeric({...common(q),deep:true,deepContext:ctx,correctUnknown:true})),speedPixels,fullPixels),referenceBuildMs=ctx.buildMs||referenceWallMs,deepCold=referenceBuildMs+deep.fullMedianMs;
const qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH),deepMetas=[];let deepUnknown=0;onProgress?.({index:di,total:depths.length,z,phase:'quality reference'});
for(const q of qTiles){if(fixed96Job.cancelled)throw new Error('fixed96 experiment cancelled');const m=await gpuCall(r.renderTileMeta({...common(q),deep:true,deepContext:ctx,correctUnknown:true}),'fixed96 z'+z+' quality deep');deepUnknown+=m.unresolved||0;deepMetas.push(m)}
for(const v of variants){const parts=[];let uncertain=0,remaining=0,corrected=0,enqueued=0,processed=0,workgroups=0,qualityPixels=0;for(let qi=0;qi<qTiles.length;qi++){const q=qTiles[qi],fm=await gpuCall(r.renderTileMetaFixed96Direct({...common(q),riskLimit:v.riskLimit}),'fixed96 z'+z+' quality '+v.riskLimit);qualityPixels+=q.w*q.h;uncertain+=fm.uncertain||0;remaining+=fm.remaining||0;corrected+=fm.corrected||0;enqueued+=fm.queue?.enqueued||0;processed+=fm.queue?.processed||0;workgroups+=fm.queue?.workgroups||0;parts.push(compareMeta(deepMetas[qi],fm))}const quality=aggregateQuality(parts),uncertainRate=uncertain/Math.max(1,qualityPixels),remainingRate=remaining/Math.max(1,qualityPixels),dispatchRate=processed/Math.max(1,qualityPixels),samplingDelta=Math.abs((v.speedQueueRate||0)-uncertainRate),timingRepresentative=samplingDelta<=Math.max(.03,uncertainRate*.5),safe=quality.classMismatch===0&&remaining===0,failureReason=quality.classMismatch!==0?'class-mismatch':remaining!==0?'remaining-unknown':!timingRepresentative?'sampling-mismatch':null;Object.assign(v,{quality,qualityPixels,uncertain,uncertainRate,remaining,remainingRate,corrected,enqueued,processed,workgroups,dispatchRate,queueIntegrity:true,samplingDelta,timingRepresentative,safe,failureReason,speedupVsDeepCold:deepCold/Math.max(1e-9,v.fullMedianMs)})}
const viable=variants.filter(v=>v.safe&&v.timingRepresentative).sort((a,b)=>a.fullMedianMs-b.fullMedianMs),best=viable[0]||null,recommendation=best&&best.fullMedianMs<deepCold*.9&&best.uncertainRate<.75?'fixed96-candidate':'full-deep';
rows.push({track:tr.id,z,iter,resolution:[fullW,fullH],ds,deep:{...deep,referenceBuildMs,coldMedianMs:deepCold,unknownRate:deepUnknown/Math.max(1,qTiles.reduce((n,q)=>n+q.w*q.h,0))},variants,bestRiskLimit:best?.riskLimit??null,bestFixed96Ms:best?.fullMedianMs??null,recommendation});onProgress?.({index:di+1,total:depths.length,z,phase:'done'});
}
const result={format:'mandelbrot-ds-sparse-fixed96-queue-v2',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],riskLimits:[...riskLimits],fixedPoint:{bits:96,fractionBits:88,format:'signed Q8.88 u32 limbs'},warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Diagnostic only. Guarded DS Direct compacts sensitivity-risk pixels into a GPU queue. Fixed96 consumes only that queue through dispatchWorkgroupsIndirect; queue integrity is validated for every timing and quality sample. Every risk candidate is reported even when it fails quality gates. Full Deep+correction is the quality baseline. Production routing is unchanged.'};fixed96Job.last=result;return result;
}finally{fixed96Job.active=false}
}
// ── GPU startup / rendering orchestration ────────────────────────────────
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
async function renderFrame(){
const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height);
state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats();
try{
const r=renderer||await initRenderer();if(token!==state.token)return;
if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}
if(decision.probe){state.lastEngine='WebGPU · direct/deep view probe';state.drawState='COVERING';updateStats();await ensureBackendProbe(snap,iter,decision,token);if(token!==state.token)return;decision=chooseBackend(snap,canvas.width,iter,canvas.height);if(decision.probe)decision={...decision,backend:'deep',deep:true,probe:false,reason:'overlap-probe-failed-safe-deep'};state.backendDecision=decision;updateStats()}
const deep=decision.deep;let ctx=null;
if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded perturbation'}else state.lastEngine='WebGPU · f32 direct';
const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;
const firstPaint=performance.now()-t0;
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,backend:decision.backend};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=firstPaint;state.rendering=deep;state.unresolved=0;
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();
if(!deep){state.rendering=false;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();return}
let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats();
const n=canvas.width*canvas.height;if(shouldApplySparseCorrection(state.unresolved,n)){state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats();const corrected=await r.correctUnknownFrame(snap,iter,token);if(!corrected||token!==state.token)return;state.correctionPasses=1;stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;const painted=await r.recolor(token);if(!painted||token!==state.token)return;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats()}
if(token!==state.token)return;state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · guarded perturbation'+(state.correctionPasses?' + DS correction':'');const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();
}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
}
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback(深部非対応)';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
const exportJob={active:false,cancelled:false};
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
class StreamingPng{
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
async rows(filteredRows){await this.writer.write(filteredRows)}
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
}
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
async function runExport(){
if(exportJob.active)return;
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),strict=$('#exportPrecision').value==='strict';
let ctx=null;
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
let done=0,unresolvedSamples=0;
try{
for(let y=0;y<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
function benchmarkSummary(result){const lines=['z | f32 est ms | DS est ms | deep warm est | deep cold est | f32 Δclass | DS Δclass | direct ratio | fastest cold','--|------------|-----------|---------------|---------------|------------|-----------|--------------|-------------'];for(const r of result.rows)lines.push([String(r.z).padStart(2),r.f32.fullMedianMs.toFixed(1),r.ds.fullMedianMs.toFixed(1),r.deep.warmMedianMs.toFixed(1),r.deep.coldMedianMs.toFixed(1),(100*r.f32.quality.classMismatchRate).toFixed(2)+'%',(100*r.ds.quality.classMismatchRate).toFixed(2)+'%',Number.isFinite(r.directRatio)?r.directRatio.toExponential(2):'n/a',r.assessment.fastestCold].join(' | '));return lines.join('\n')}
$('#backendBench').onclick=()=>{const d=$('#benchmarkDialog');$('#benchmarkStatus').textContent='';$('#benchmarkProgress').hidden=true;$('#benchmarkOutput').value=benchmarkJob.last?benchmarkSummary(benchmarkJob.last):'';$('#benchmarkSave').disabled=!benchmarkJob.last;d.showModal?d.showModal():d.setAttribute('open','')};
$('#benchmarkStart').onclick=async()=>{if(benchmarkJob.active)return;cancelRender();state.dirty=false;const start=$('#benchmarkStart'),progress=$('#benchmarkProgress'),status=$('#benchmarkStatus');start.disabled=true;$('#benchmarkSave').disabled=true;progress.hidden=false;progress.max=CROSSOVER_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#benchmarkOutput').value='';try{const result=await runCrossoverBenchmark({track:$('#benchmarkTrack').value,samples:Math.max(1,Number($('#benchmarkSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});progress.value=progress.max;status.textContent='測定完了 · '+result.rows.length+' depths';$('#benchmarkOutput').value=benchmarkSummary(result);$('#benchmarkSave').disabled=false}catch(e){status.textContent=String(e&&e.message||e)}finally{start.disabled=false;markDirty(false)}};
$('#benchmarkCancel').onclick=()=>{if(benchmarkJob.active){benchmarkJob.cancelled=true;refs.cancelPending('benchmark cancelled');$('#benchmarkStatus').textContent='中止しています…'}else $('#benchmarkDialog').close()};
$('#benchmarkSave').onclick=()=>{if(!benchmarkJob.last)return;downloadBlob(new Blob([JSON.stringify(benchmarkJob.last,null,2)],{type:'application/json'}),'mandelbrot-crossover-'+Date.now()+'.json')};
function fixed96Summary(result){const lines=['z | risk | fixed96 est | deep cold | speedup | correction | speed q | dispatch | remain | Δclass | queue | status','--|------|-------------|-----------|---------|------------|---------|----------|--------|--------|-------|-------'];for(const r of result.rows){for(const v of r.variants){const status=v.failureReason||(v.safe&&v.timingRepresentative?(v.fullMedianMs<r.deep.coldMedianMs*.9&&v.uncertainRate<.75?'candidate':'safe-but-slower'):'rejected'),queue=v.queueIntegrity===false?'ERROR':Math.abs((v.dispatchRate||0)-(v.uncertainRate||0))>1e-9?'MISMATCH':'ok';lines.push([String(r.z).padStart(2),v.riskLimit.toExponential(0),v.fullMedianMs.toFixed(1),r.deep.coldMedianMs.toFixed(1),(r.deep.coldMedianMs/Math.max(1e-9,v.fullMedianMs)).toFixed(2)+'×',(100*v.uncertainRate).toFixed(1)+'%',(100*(v.speedQueueRate||0)).toFixed(1)+'%',(100*(v.dispatchRate||0)).toFixed(1)+'%',(100*v.remainingRate).toFixed(2)+'%',(100*v.quality.classMismatchRate).toFixed(2)+'%',queue,status].join(' | '))}}return lines.join('\n')}
$('#sparseBench').onclick=()=>{const d=$('#sparseDialog');$('#sparseStatus').textContent='';$('#sparseProgress').hidden=true;$('#sparseOutput').value=fixed96Job.last?fixed96Summary(fixed96Job.last):'';$('#sparseSave').disabled=!fixed96Job.last;d.showModal?d.showModal():d.setAttribute('open','')};
$('#sparseStart').onclick=async()=>{if(fixed96Job.active)return;cancelRender();state.dirty=false;const start=$('#sparseStart'),progress=$('#sparseProgress'),status=$('#sparseStatus');start.disabled=true;$('#sparseSave').disabled=true;progress.hidden=false;progress.max=FIXED96_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#sparseOutput').value='';try{const result=await runFixed96Experiment({track:$('#sparseTrack').value,samples:Math.max(1,Number($('#sparseSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});progress.value=progress.max;status.textContent='実験完了 · '+result.rows.length+' depths';$('#sparseOutput').value=fixed96Summary(result);$('#sparseSave').disabled=false}catch(e){status.textContent=String(e&&e.message||e)}finally{start.disabled=false;markDirty(false)}};
$('#sparseCancel').onclick=()=>{if(fixed96Job.active){fixed96Job.cancelled=true;refs.cancelPending('fixed96 experiment cancelled');$('#sparseStatus').textContent='中止しています…'}else $('#sparseDialog').close()};
$('#sparseSave').onclick=()=>{if(!fixed96Job.last)return;downloadBlob(new Blob([JSON.stringify(fixed96Job.last,null,2)],{type:'application/json'}),'mandelbrot-fixed96-'+Date.now()+'.json')};
// ── diagnostics ──────────────────────────────────────────────────────────
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend(snapshot(),Math.max(1,canvas.width),maxIter(),Math.max(1,canvas.height));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.probe?'WebGPU 判定中':decision.deep?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'深部補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.correctedPixels)status+=' · 回収 '+state.correctedPixels;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx,ratio=decision.metrics&&Number.isFinite(decision.metrics.ratio)?decision.metrics.ratio:0,prof=decision.profile,rs=state.unknownReasons,reasonText=rs?Object.entries(rs).filter(([,v])=>v).map(([k,v])=>k+':'+v).join(' '):'';$('#diagEngine').textContent='engine: '+state.lastEngine+' | route '+decision.backend+' ('+decision.reason+') | ratio '+(ratio?ratio.toExponential(2):'n/a')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'')+(reasonText?' | UNKNOWN '+reasonText:'')+(decision.viewProbe?' | view probe direct '+decision.viewProbe.predictedDirectMs.toFixed(0)+'ms / deep '+decision.viewProbe.predictedDeepMs.toFixed(0)+'ms / u '+(100*decision.viewProbe.deepUnknownRate).toFixed(2)+'% / Δclass '+decision.viewProbe.classDisagreement:'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height+' | speed profiles '+backendProfiles.size+' / view probes '+backendViewProbes.size}
globalThis.__MANDEL_TEST__={
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w,h):!!forceDeep;let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
async runCrossoverBenchmark(options={}){return runCrossoverBenchmark(options)},
async runFixed96Experiment(options={}){return runFixed96Experiment(options)},
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
};
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:(state.backendDecision||chooseBackend()).deep,backendDecision:state.backendDecision,backendProfiles:Array.from(backendProfiles.entries()),backendViewProbes:Array.from(backendViewProbes.entries()),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,unknownReasons:state.unknownReasons,correctionPasses:state.correctionPasses,correctedPixels:state.correctedPixels,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
})();

View file

@ -1,598 +0,0 @@
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x000fffffu;
const REASON_SHIFT:u32=20u;
const REASON_MASK:u32=0x0ff00000u;
const REASON_NONE:u32=0u;
const REASON_ERROR_BOUND:u32=1u;
const REASON_ESCAPE_UNCERTAIN:u32=2u;
const REASON_REFERENCE_END:u32=3u;
const REASON_REBASE_GAP:u32=4u;
const REASON_RANGE:u32=5u;
const REASON_OPERATION_LIMIT:u32=6u;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
fn pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Benchmark candidate: whole-frame double-single direct iteration.
// This intentionally has no CPU reference-build cost. It is not selected by
// the production router until the crossover benchmark justifies it.
const DIRECT_DS_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b; let e=b-(q-a); return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah;
let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh;
var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b);
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);
let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var n=0u;
loop{
if(n>=p.maxIter){break;}
z=cds_add(cds_mul(z,z),c); n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
// can be re-evaluated by the existing deep double-single correction pass.
// This shader is benchmark-only and is not selected by the production router.
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
struct HybridStats{
remaining:atomic<u32>, _a:atomic<u32>, _b:atomic<u32>, _c:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);}
fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;
let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh;
var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
fn ds_cmp(a:DS,b:DS)->i32{if(a.h<b.h){return -1;}if(a.h>b.h){return 1;}if(a.l<b.l){return -1;}if(a.l>b.l){return 1;}return 0;}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn mark_sensitive(out:u32,n:u32){
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;atomicAdd(&hybridStats.remaining,1u);
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var deriv=vec2<f32>(0.0);var n=0u;
loop{
if(n>=p.maxIter){break;}
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
z=cds_add(cds_mul(z,z),c);n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
}
`;
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct UnresolvedHead{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
return;
}
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=1.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=refs[m];
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
// perturbation. It is a visual-quality pass, not a membership certificate.
const DEEP_CORRECT_WGSL=COMMON+String.raw`
const CORRECTION_MARK:u32=128u;
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
fn count_remaining(out:u32){
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
}
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
}
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b;
let e=b-(q-a);
return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b;
let bb=q-a;
let e=(a-(q-bb))+(b-bb);
return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a;
let ah=ca-(ca-a);
let al=a-ah;
let cb=4097.0*b;
let bh=cb-(cb-b);
let bl=b-bh;
var e=ah*bh-q;
e=e+ah*bl;
e=e+al*bh;
e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h);
return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l;
e=e+a.l*b.h;
e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b);
return ds_quick(q.h,q.l+a.l*b);
}
fn ds_pow2(a:DS,e:i32)->DS{
if(e < -126){return DS(0.0,0.0);}
if(e > 126){return DS(8.507059e37,0.0);}
return DS(ldexp(a.h,e),ldexp(a.l,e));
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
loop{
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
if(m>p.refLen){count_remaining(out);return;}
let rp=refs[m];
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
let delta=cds_pow2(w,scaleExp);
let z=cds_add(r,delta);
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
return;
}
let dmag=cds_mag2(delta);
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
continue;
}
if(m>=p.refLen){count_remaining(out);return;}
let linear=cds_scale(cds_mul(r,w),2.0);
let sq=cds_pow2(cds_mul(w,w),scaleExp);
w=cds_add(cds_add(linear,sq),d);
m+=1u; n+=1u; operations+=1u;
let mm=max(cds_maxabs(w),cds_maxabs(d));
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
const COLOR_WGSL=String.raw`
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
if(p.edgeAA!=0u){
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
let x=i32(gid.x); let y=i32(gid.y);
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
`;
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.2.6-hybrid-ui-cacheproof',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
});
})();

View file

@ -1,598 +0,0 @@
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x000fffffu;
const REASON_SHIFT:u32=20u;
const REASON_MASK:u32=0x0ff00000u;
const REASON_NONE:u32=0u;
const REASON_ERROR_BOUND:u32=1u;
const REASON_ESCAPE_UNCERTAIN:u32=2u;
const REASON_REFERENCE_END:u32=3u;
const REASON_REBASE_GAP:u32=4u;
const REASON_RANGE:u32=5u;
const REASON_OPERATION_LIMIT:u32=6u;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
fn pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Benchmark candidate: whole-frame double-single direct iteration.
// This intentionally has no CPU reference-build cost. It is not selected by
// the production router until the crossover benchmark justifies it.
const DIRECT_DS_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b; let e=b-(q-a); return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah;
let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh;
var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b);
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);
let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var n=0u;
loop{
if(n>=p.maxIter){break;}
z=cds_add(cds_mul(z,z),c); n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
// can be re-evaluated by the existing deep double-single correction pass.
// This shader is benchmark-only and is not selected by the production router.
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
struct HybridStats{
remaining:atomic<u32>, _a:atomic<u32>, _b:atomic<u32>, _c:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);}
fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;
let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh;
var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
fn ds_cmp(a:DS,b:DS)->i32{if(a.h<b.h){return -1;}if(a.h>b.h){return 1;}if(a.l<b.l){return -1;}if(a.l>b.l){return 1;}return 0;}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn mark_sensitive(out:u32,n:u32){
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;atomicAdd(&hybridStats.remaining,1u);
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var deriv=vec2<f32>(0.0);var n=0u;
loop{
if(n>=p.maxIter){break;}
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
z=cds_add(cds_mul(z,z),c);n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
}
`;
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct UnresolvedHead{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
return;
}
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=1.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=refs[m];
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
// perturbation. It is a visual-quality pass, not a membership certificate.
const DEEP_CORRECT_WGSL=COMMON+String.raw`
const CORRECTION_MARK:u32=128u;
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
fn count_remaining(out:u32){
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
}
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
}
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b;
let e=b-(q-a);
return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b;
let bb=q-a;
let e=(a-(q-bb))+(b-bb);
return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a;
let ah=ca-(ca-a);
let al=a-ah;
let cb=4097.0*b;
let bh=cb-(cb-b);
let bl=b-bh;
var e=ah*bh-q;
e=e+ah*bl;
e=e+al*bh;
e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h);
return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l;
e=e+a.l*b.h;
e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b);
return ds_quick(q.h,q.l+a.l*b);
}
fn ds_pow2(a:DS,e:i32)->DS{
if(e < -126){return DS(0.0,0.0);}
if(e > 126){return DS(8.507059e37,0.0);}
return DS(ldexp(a.h,e),ldexp(a.l,e));
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
loop{
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
if(m>p.refLen){count_remaining(out);return;}
let rp=refs[m];
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
let delta=cds_pow2(w,scaleExp);
let z=cds_add(r,delta);
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
return;
}
let dmag=cds_mag2(delta);
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
continue;
}
if(m>=p.refLen){count_remaining(out);return;}
let linear=cds_scale(cds_mul(r,w),2.0);
let sq=cds_pow2(cds_mul(w,w),scaleExp);
w=cds_add(cds_add(linear,sq),d);
m+=1u; n+=1u; operations+=1u;
let mm=max(cds_maxabs(w),cds_maxabs(d));
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
const COLOR_WGSL=String.raw`
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
if(p.edgeAA!=0u){
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
let x=i32(gid.x); let y=i32(gid.y);
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
`;
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.2.6-hybrid-ui-cacheproof',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
});
})();

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,454 +0,0 @@
(()=>{'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,BUILD_VERSION='24.2.6',INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
const DIRECT_REQUIRED_RATIO=8,DIRECT_DEFAULT_RATIO=128,DEEP_SPEED_MARGIN=.85,DEEP_PROBE_UNKNOWN_MAX=.005,NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,CORRECTION_MARK=128;
const BACKEND_PROBE_SPEED_W=224,BACKEND_PROBE_SPEED_H=160,BACKEND_PROBE_QUALITY_W=72,BACKEND_PROBE_QUALITY_H=54;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,backendProbes:0};
const backendProfiles=new Map(),backendViewProbes=new Map(),backendProbePending=new Map();
// ── exact fixed-point view state ─────────────────────────────────────────
function one(bits=state.bits){return 1n<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
function directMetrics(snap=snapshot(),width=Math.max(1,canvas.width)){const spanLog=log2FixedAt(snap.span,snap.bits),stepLog=spanLog-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp))),eligible=Number.isFinite(ratio)&&ratio>=DIRECT_REQUIRED_RATIO&&spanLog>-145;return{spanLog,stepLog,ratio,eligible,overlap:eligible&&ratio<DIRECT_DEFAULT_RATIO}}
function zoomExpFor(snap){return Math.max(0,Math.log10(3.4)-log2FixedAt(snap.span,snap.bits)/Math.log2(10))}
function backendProfileKey(snap,iter){const z=Math.floor(zoomExpFor(snap)*2)/2,ib=iter<600?0:iter<1500?1:iter<5000?2:3;return z.toFixed(1)+':'+ib}
function backendViewKey(snap,iter,width,height){return[snap.bits,snap.re,snap.im,snap.span,iter,width,height].join(':')}
function chooseBackend(snap=snapshot(),width=Math.max(1,canvas.width),iter=maxIter(),height=Math.max(1,canvas.height)){const m=directMetrics(snap,width),key=backendProfileKey(snap,iter),viewKey=backendViewKey(snap,iter,width,height),speed=backendProfiles.get(key),viewProbe=backendViewProbes.get(viewKey),pixels=Math.max(1,width*Math.max(1,height)),profile=speed?{...speed,predictedDirectMs:speed.directMsPerPixel*pixels,predictedDeepMs:speed.referenceBuildMs+speed.deepMsPerPixel*pixels}:null;let backend='deep',reason='direct-coordinate-limit';if(m.eligible){if(m.ratio>=DIRECT_DEFAULT_RATIO){backend='direct';reason='direct-wide-margin'}else if(!viewProbe){backend='probe';reason='overlap-view-probe-required'}else if(!viewProbe.qualityPass){backend='deep';reason='direct-quality-rejected'}else if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*DEEP_SPEED_MARGIN){backend='deep';reason='deep-view-probe-faster'}else{backend='direct';reason='direct-view-probe-preferred'}}return{backend,deep:backend==='deep',probe:backend==='probe',reason,key,viewKey,metrics:m,profile,viewProbe:viewProbe||null}}
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width),height=Math.max(1,canvas.height)){const d=chooseBackend(snap,width,maxIter(),height);return d.deep||d.probe}
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
// ── iteration / quality policy ───────────────────────────────────────────
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
'use strict';
const MAX_REF=150001,MAX_LEVELS=20;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class ReferenceService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter,fresh=false){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.2.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
}
const refs=new ReferenceService();
// ── WebGPU renderer ──────────────────────────────────────────────────────
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
class WebGpuRenderer{
constructor(adapter,device){
this.adapter=adapter;this.device=device;
const ai=adapter.info||{};
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,sdm,shm,xm,zm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('direct-ds-benchmark',G.DIRECT_DS_WGSL),this.module('direct-ds-guarded-experiment',G.DIRECT_DS_GUARDED_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
this.dsDirect=this.device.createComputePipeline({layout:'auto',compute:{module:sdm,entryPoint:'main'}});
this.dsGuarded=this.device.createComputePipeline({layout:'auto',compute:{module:shm,entryPoint:'main'}});
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}});
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
webgpuCanvasClaimed=true;this.configure();
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
ensureFrame(w,h){
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
}
ensureExportWorkspace(){
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
}
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5){const cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),crh=Math.fround(cr),cih=Math.fround(ci),sph=Math.fround(sp),invW=1/Math.max(1,fullW),invh=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,crh,true);d.setFloat32(36,Math.fround(cr-crh),true);d.setFloat32(40,cih,true);d.setFloat32(44,Math.fround(ci-cih),true);d.setFloat32(48,sph,true);d.setFloat32(52,Math.fround(sp-sph),true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,invh,true);d.setFloat32(68,Math.fround(invW-invh),true)})}
dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sx=.5,sy=.5){const a=this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx,sy),d=new DataView(a);d.setFloat32(72,Math.fround(riskLimit),true);return a}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDsDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsDirect.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsDirect);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDsGuardedNumeric(encoder,{pbuf,meta,smooth,stats,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsGuarded.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:stats}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsGuarded);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
}
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
async readUnresolved(){return(await this.readUnresolvedStats()).total}
async correctUnknownFrame(snap,iter,token){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,f.w*.5,f.h*.5,1,f.w,0));const e=d.createCommandEncoder({label:'sparse-deep-correction'}),bg=d.createBindGroup({layout:this.correct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),p=e.beginComputePass();p.setPipeline(this.correct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
async benchmarkDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap));const e=d.createCommandEncoder({label:'ds-direct-benchmark'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;[meta,fieldSmooth,pbuf].forEach(destroy);return{gpuMs}}
async renderTileMetaDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,n*4,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const e=d.createCommandEncoder({label:'ds-direct-meta'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});e.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,fieldSmooth,pbuf,read].forEach(destroy);return out}
async benchmarkHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13}){
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
read=buf(d,2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit));
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,0,fullW*.5,fullH*.5,1,w,0));
const e=d.createCommandEncoder({label:'ds-hybrid-benchmark'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
e.copyBufferToBuffer(dsStats,0,read,0,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,UNRESOLVED_BYTES,UNRESOLVED_BYTES);
const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;
await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),a=new Uint32Array(raw.slice(0,UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(UNRESOLVED_BYTES,2*UNRESOLVED_BYTES));read.unmap();
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);
return{gpuMs,uncertain:a[0]||0,remaining:b[0]||0,corrected:b[7]||0};
}
async renderTileMetaHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13,sampleX=.5,sampleY=.5}){
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
read=buf(d,n*4+2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sampleX,sampleY));
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,0,fullW*.5,fullH*.5,1,w,0));
const e=d.createCommandEncoder({label:'ds-hybrid-meta'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
e.copyBufferToBuffer(meta,0,read,0,n*4);e.copyBufferToBuffer(dsStats,0,read,n*4,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,n*4+UNRESOLVED_BYTES,UNRESOLVED_BYTES);
d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),a=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(n*4+UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES));read.unmap();
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);out.uncertain=a[0]||0;out.remaining=b[0]||0;out.corrected=b[7]||0;return out;
}
async benchmarkNumeric({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,forceStrict=false,correctUnknown=false}){await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const e=d.createCommandEncoder({label:'backend-benchmark'});if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,forceStrict?1:0));this.encodeDeepNumeric(e,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){e.clearBuffer(unresolved);this.encodeCorrectionNumeric(e,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap));this.encodeDirectNumeric(e,{pbuf,meta,smooth,w,h})}e.copyBufferToBuffer(unresolved,0,read,0,UNRESOLVED_BYTES);const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;await read.mapAsync(GPUMapMode.READ);const a=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return{gpuMs,unknown:a[0]||0,corrected:a[7]||0}}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
}
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes,UNRESOLVED_BYTES/4),unresolvedCount=stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
async renderTileRGBA2x({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount=0,corrected=0;for(let si=0;si<4;si++){const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4);unresolvedCount+=stats[0]||0;corrected+=stats[7]||0}ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext()}
}
function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true}
function backendQualityTiles(fullW,fullH){const w=Math.min(BACKEND_PROBE_QUALITY_W,fullW),h=Math.min(BACKEND_PROBE_QUALITY_H,fullH),x1=Math.max(0,fullW-w),y1=Math.max(0,fullH-h),xc=Math.max(0,Math.floor((fullW-w)/2)),yc=Math.max(0,Math.floor((fullH-h)/2));return[[0,0],[x1,0],[0,y1],[x1,y1],[xc,yc]].map(([tileX,tileY])=>({tileX,tileY,w,h}))}
async function performBackendProbe(snap,iter,decision,token){if(!renderer||!decision.metrics.overlap||token!==state.token)return null;const fullW=canvas.width,fullH=canvas.height,sw=Math.min(BACKEND_PROBE_SPEED_W,fullW),sh=Math.min(BACKEND_PROBE_SPEED_H,fullH),sx=Math.max(0,Math.floor((fullW-sw)/2)),sy=Math.max(0,Math.floor((fullH-sh)/2)),speedPixels=sw*sh,fullPixels=Math.max(1,fullW*fullH),direct=await renderer.benchmarkNumeric({snap,iter,deep:false,deepContext:null,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh});if(token!==state.token)return null;const rb=performance.now(),ctx=await refs.request(snap,iter),buildWall=performance.now()-rb;if(token!==state.token||ctx.checkpointMismatch)return null;const deepProbe=await renderer.benchmarkNumeric({snap,iter,deep:true,deepContext:ctx,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh,correctUnknown:true});if(token!==state.token)return null;let classDisagreement=0,deepUnknown=0,knownCompared=0,qualityPixels=0,corrected=0;for(const q of backendQualityTiles(fullW,fullH)){const directMeta=await renderer.renderTileMeta({snap,iter,deep:false,deepContext:null,fullW,fullH,...q});if(token!==state.token)return null;const deepMeta=await renderer.renderTileMeta({snap,iter,deep:true,deepContext:ctx,fullW,fullH,...q,correctUnknown:true});if(token!==state.token)return null;qualityPixels+=q.w*q.h;corrected+=deepMeta.corrected||0;for(let i=0;i<deepMeta.length;i++){const dc=(deepMeta[i]>>>28)&3;if(dc===FIELD_UNKNOWN){deepUnknown++;continue}const rc=(directMeta[i]>>>28)&3;knownCompared++;if((dc===FIELD_ESCAPED)!==(rc===FIELD_ESCAPED))classDisagreement++}}const buildMs=ctx.buildMs||buildWall,directMsPerPixel=direct.gpuMs/Math.max(1,speedPixels),deepMsPerPixel=deepProbe.gpuMs/Math.max(1,speedPixels),sample={directMsPerPixel,deepMsPerPixel,referenceBuildMs:buildMs},prev=backendProfiles.get(decision.key),a=prev?.samples?0.3:1,profile={samples:(prev?.samples||0)+1,directMsPerPixel:(prev?.directMsPerPixel||sample.directMsPerPixel)*(1-a)+sample.directMsPerPixel*a,deepMsPerPixel:(prev?.deepMsPerPixel||sample.deepMsPerPixel)*(1-a)+sample.deepMsPerPixel*a,referenceBuildMs:(prev?.referenceBuildMs||sample.referenceBuildMs)*(1-a)+sample.referenceBuildMs*a};backendProfiles.set(decision.key,profile);const deepUnknownRate=deepUnknown/Math.max(1,qualityPixels),qualityPass=classDisagreement===0&&deepUnknownRate<=DEEP_PROBE_UNKNOWN_MAX,viewProbe={qualityPass,deepUnknownRate,classDisagreement,knownCompared,qualityPixels,corrected,predictedDirectMs:directMsPerPixel*fullPixels,predictedDeepMs:buildMs+deepMsPerPixel*fullPixels,ratio:decision.metrics.ratio,zoom:zoomExpFor(snap),iter,speedProbe:{w:sw,h:sh,tileX:sx,tileY:sy},qualityTiles:backendQualityTiles(fullW,fullH)};backendViewProbes.set(decision.viewKey,viewProbe);runtime.backendProbes++;return viewProbe}
function ensureBackendProbe(snap,iter,decision,token){if(!decision.metrics.overlap)return Promise.resolve(null);const hit=backendViewProbes.get(decision.viewKey);if(hit)return Promise.resolve(hit);const pending=backendProbePending.get(decision.viewKey);if(pending)return pending;const task=performBackendProbe(snap,iter,decision,token).catch(e=>{if(token===state.token)console.warn('backend probe failed',e);return null}).finally(()=>backendProbePending.delete(decision.viewKey));backendProbePending.set(decision.viewKey,task);return task}
// ── backend crossover benchmark (diagnostic only) ───────────────────────
const CROSSOVER_TRACKS={
seahorse:{id:'seahorse',label:'Seahorse boundary',re:'-0.7453983606667815',im:'0.1125046349959942'},
cusp:{id:'cusp',label:'Period-2 cusp',re:'-0.75',im:'0'}
};
const CROSSOVER_DEPTHS=[0,2,4,6,8,10,12,14,16,20,30,40];
const benchmarkJob={active:false,cancelled:false,last:null};
function fixedAtDecimal(text,bits){let s=String(text).trim(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const parts=s.toLowerCase().split('e'),mant=parts[0],exp=parts[1]?parseInt(parts[1],10):0,ab=mant.split('.'),ii=ab[0]||'0',ff=ab[1]||'';let digits=(ii+ff).replace(/^0+(?=\d)/,'')||'0',places=ff.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),scale=1n<<BigInt(bits),v=(BigInt(digits)*scale+den/2n)/den;return neg?-v:v}
function crossoverSnap(track,z){const span='3.4e-'+z,bits=Math.max(320,decimalRequiredBits(track.re)+64,decimalRequiredBits(track.im)+64,decimalRequiredBits(span)+96);return{bits,re:fixedAtDecimal(track.re,bits),im:fixedAtDecimal(track.im,bits),span:fixedAtDecimal(span,bits)}}
function iterForZoom(z,base=350){return Math.max(base,base+Math.floor(70*Math.sqrt(Math.max(0,z))+15*Math.max(0,z)))}
function median(values){const a=[...values].sort((x,y)=>x-y),n=a.length;return n?a.length%2?a[(n-1)>>1]:(a[n/2-1]+a[n/2])/2:NaN}
function compareMeta(reference,candidate){let referenceUnknown=0,candidateUnknown=0,classMismatch=0,escapedCompared=0,iterMismatch=0,maxIterDelta=0,sumIterDelta=0;const n=Math.min(reference.length,candidate.length);for(let i=0;i<n;i++){const rm=reference[i]>>>0,cm=candidate[i]>>>0,rc=(rm>>>28)&3,cc=(cm>>>28)&3;if(rc===FIELD_UNKNOWN){referenceUnknown++;continue}if(cc===FIELD_UNKNOWN){candidateUnknown++;continue}const re=rc===FIELD_ESCAPED,ce=cc===FIELD_ESCAPED;if(re!==ce){classMismatch++;continue}if(re){escapedCompared++;const d=Math.abs((rm&ITER_MASK)-(cm&ITER_MASK));if(d){iterMismatch++;sumIterDelta+=d;if(d>maxIterDelta)maxIterDelta=d}}}return{pixels:n,referenceUnknown,candidateUnknown,classMismatch,classMismatchRate:classMismatch/Math.max(1,n-referenceUnknown),escapedCompared,escapeIterationMismatch:iterMismatch,escapeIterationMismatchRate:iterMismatch/Math.max(1,escapedCompared),meanEscapeIterationDelta:iterMismatch?sumIterDelta/iterMismatch:0,maxEscapeIterationDelta:maxIterDelta}}
function crossoverQualityTiles(fullW,fullH,w,h){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[0,0],[Math.max(0,fullW-w),0],[0,Math.max(0,fullH-h)],[Math.max(0,fullW-w),Math.max(0,fullH-h)],[Math.max(0,Math.floor((fullW-w)/2)),Math.max(0,Math.floor((fullH-h)/2))]];return pts.map(([tileX,tileY])=>({tileX,tileY,w,h}))}
function aggregateQuality(list){const out={pixels:0,referenceUnknown:0,candidateUnknown:0,classMismatch:0,escapedCompared:0,escapeIterationMismatch:0,sumEscapeIterationDelta:0,maxEscapeIterationDelta:0};for(const q of list){out.pixels+=q.pixels;out.referenceUnknown+=q.referenceUnknown;out.candidateUnknown+=q.candidateUnknown;out.classMismatch+=q.classMismatch;out.escapedCompared+=q.escapedCompared;out.escapeIterationMismatch+=q.escapeIterationMismatch;out.sumEscapeIterationDelta+=q.meanEscapeIterationDelta*q.escapeIterationMismatch;out.maxEscapeIterationDelta=Math.max(out.maxEscapeIterationDelta,q.maxEscapeIterationDelta)}out.classMismatchRate=out.classMismatch/Math.max(1,out.pixels-out.referenceUnknown);out.escapeIterationMismatchRate=out.escapeIterationMismatch/Math.max(1,out.escapedCompared);out.meanEscapeIterationDelta=out.escapeIterationMismatch?out.sumEscapeIterationDelta/out.escapeIterationMismatch:0;delete out.sumEscapeIterationDelta;return out}
const CROSSOVER_SPEED_W=256,CROSSOVER_SPEED_H=144,CROSSOVER_GPU_TIMEOUT_MS=15000,CROSSOVER_REF_TIMEOUT_MS=15000;
function crossoverSpeedTiles(fullW,fullH,w=CROSSOVER_SPEED_W,h=CROSSOVER_SPEED_H){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[.5,.5],[.28,.36],[.72,.64]],seen=new Set(),out=[];for(const [fx,fy] of pts){const tileX=Math.max(0,Math.min(fullW-w,Math.round(fullW*fx-w/2))),tileY=Math.max(0,Math.min(fullH-h,Math.round(fullH*fy-h/2))),k=tileX+':'+tileY;if(!seen.has(k)){seen.add(k);out.push({tileX,tileY,w,h})}}return out}
function benchmarkTimeout(promise,ms,label,onTimeout){let timer;const timeout=new Promise((_,reject)=>{timer=setTimeout(()=>{try{onTimeout?.()}catch{}reject(new Error(label+' timed out after '+Math.round(ms/1000)+'s'))},ms)});return Promise.race([promise,timeout]).finally(()=>clearTimeout(timer))}
async function benchmarkTileBatch(fn,tiles){let gpuMs=0,pixels=0;for(const q of tiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h}return{gpuMs,pixels}}
async function timedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[];for(let i=0;i<samples;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'sample',index:i+1,total:samples});values.push((await fn()).gpuMs)}return{medianMs:median(values),samplesMs:values}}
function extrapolateTiming(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels),fullMedianMs=msPerPixel*fullPixels;return{...timing,speedPixels,msPerPixel,fullMedianMs}}
async function runCrossoverBenchmark({track='seahorse',depths=CROSSOVER_DEPTHS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
if(benchmarkJob.active)throw new Error('benchmark already running');benchmarkJob.active=true;benchmarkJob.cancelled=false;
try{
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown benchmark track');
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
for(let di=0;di<depths.length;di++){
if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z),metrics=directMetrics(snap,fullW);
onProgress?.({index:di,total:depths.length,z,phase:'reference'});
const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'z'+z+' reference',()=>refs.cancelPending('benchmark reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
const timed=(name,fn)=>timedMedian(()=>gpuCall(benchmarkTileBatch(fn,speedTiles),'z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
const tileCommon=q=>({snap,iter,fullW,fullH,...q});
onProgress?.({index:di,total:depths.length,z,phase:'f32'});
const f32=extrapolateTiming(await timed('f32',q=>r.benchmarkNumeric({...tileCommon(q),deep:false,deepContext:null})),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'ds'});
const ds=extrapolateTiming(await timed('ds',q=>r.benchmarkDsDirect(tileCommon(q))),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'deep'});
const deep=extrapolateTiming(await timed('deep',q=>r.benchmarkNumeric({...tileCommon(q),deep:true,deepContext:ctx,correctUnknown:true})),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'quality'});
const f32Parts=[],dsParts=[],qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH);let deepUnknownCount=0,qualityPixels=0,deepCorrected=0;
for(const q of qTiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const qCommon={snap,iter,fullW,fullH,...q},deepMeta=await gpuCall(r.renderTileMeta({...qCommon,deep:true,deepContext:ctx,correctUnknown:true}),'z'+z+' quality deep'),f32Meta=await gpuCall(r.renderTileMeta({...qCommon,deep:false,deepContext:null}),'z'+z+' quality f32'),dsMeta=await gpuCall(r.renderTileMetaDsDirect(qCommon),'z'+z+' quality ds');qualityPixels+=q.w*q.h;deepUnknownCount+=deepMeta.unresolved||0;deepCorrected+=deepMeta.corrected||0;f32Parts.push(compareMeta(deepMeta,f32Meta));dsParts.push(compareMeta(deepMeta,dsMeta))}
const f32Quality=aggregateQuality(f32Parts),dsQuality=aggregateQuality(dsParts),deepUnknown=deepUnknownCount/Math.max(1,qualityPixels),referenceBuildMs=ctx.buildMs||referenceWallMs;
const row={track:tr.id,z,span:'3.4e-'+z,iter,resolution:[fullW,fullH],speedTiles,speedPixels,qualityTile:[qualityW,qualityH],qualityTiles:qTiles,directRatio:metrics.ratio,
f32:{...f32,quality:f32Quality},ds:{...ds,quality:dsQuality},deep:{...deep,referenceBuildMs,referenceWallMs,coldMedianMs:referenceBuildMs+deep.fullMedianMs,warmMedianMs:deep.fullMedianMs,unknownRate:deepUnknown,corrected:deepCorrected}};
const qualityLimit=.01;
row.assessment={f32VisuallySafe:f32Quality.classMismatchRate<=qualityLimit&&metrics.ratio>=1,dsVisuallySafe:dsQuality.classMismatchRate<=qualityLimit,deepVisuallySafe:deepUnknown<=.001,
fastestWarm:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.warmMedianMs]].sort((a,b)=>a[1]-b[1])[0][0],
fastestCold:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.coldMedianMs]].sort((a,b)=>a[1]-b[1])[0][0]};
rows.push(row);onProgress?.({index:di+1,total:depths.length,z,phase:'done',row});
}
const result={format:'mandelbrot-webgpu-crossover-v2',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Speed timing uses three bounded representative tiles and extrapolates ms/pixel to the current full resolution, preventing long full-frame Deep benchmark dispatches. Deep cold adds verified BigInt reference construction. Quality remains measured on five spatial tiles. Any single benchmark GPU batch or reference build exceeding 15 s aborts with an explicit error instead of waiting indefinitely.'};
benchmarkJob.last=result;return result;
}finally{benchmarkJob.active=false}
}
// ── DS + sparse Deep hybrid experiment (diagnostic only) ───────────────
const HYBRID_DEPTHS=[6,8,10,12],HYBRID_RISK_LIMITS=[1e12,1e13,1e14];
const hybridJob={active:false,cancelled:false,last:null};
async function hybridTileBatch(fn,tiles){let gpuMs=0,pixels=0,uncertain=0,remaining=0,corrected=0;for(const q of tiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h;uncertain+=r.uncertain||0;remaining+=r.remaining||0;corrected+=r.corrected||0}return{gpuMs,pixels,uncertain,remaining,corrected}}
async function hybridTimedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[],stats=[];for(let i=0;i<samples;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'sample',index:i+1,total:samples});const r=await fn();values.push(r.gpuMs);stats.push(r)}return{medianMs:median(values),samplesMs:values,sampleStats:stats}}
function hybridExtrapolate(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels);return{...timing,speedPixels,msPerPixel,fullMedianMs:msPerPixel*fullPixels}}
async function runHybridExperiment({track='seahorse',depths=HYBRID_DEPTHS,riskLimits=HYBRID_RISK_LIMITS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
if(hybridJob.active||benchmarkJob.active)throw new Error('another benchmark is running');hybridJob.active=true;hybridJob.cancelled=false;
try{
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown experiment track');
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
for(let di=0;di<depths.length;di++){
if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z);
onProgress?.({index:di,total:depths.length,z,phase:'reference'});const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'hybrid z'+z+' reference',()=>refs.cancelPending('hybrid reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
const timed=(name,fn)=>hybridTimedMedian(()=>gpuCall(hybridTileBatch(fn,speedTiles),'hybrid z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
const common=q=>({snap,iter,fullW,fullH,...q});
const ds=hybridExtrapolate(await timed('DS baseline',async q=>{const x=await r.benchmarkDsDirect(common(q));return{...x}}),speedPixels,fullPixels);
const deep=hybridExtrapolate(await timed('Full Deep',async q=>{const x=await r.benchmarkNumeric({...common(q),deep:true,deepContext:ctx,correctUnknown:true});return{...x}}),speedPixels,fullPixels);
const qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH),deepMetas=[];let deepUnknown=0;
onProgress?.({index:di,total:depths.length,z,phase:'quality reference'});
for(const q of qTiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const m=await gpuCall(r.renderTileMeta({...common(q),deep:true,deepContext:ctx,correctUnknown:true}),'hybrid z'+z+' quality deep');deepUnknown+=m.unresolved||0;deepMetas.push(m)}
const variants=[];
for(const riskLimit of riskLimits){
onProgress?.({index:di,total:depths.length,z,phase:'risk '+riskLimit.toExponential(0)});
const timing=hybridExtrapolate(await timed('Hybrid '+riskLimit.toExponential(0),q=>r.benchmarkHybrid({...common(q),deepContext:ctx,riskLimit})),speedPixels,fullPixels);
const parts=[];let uncertain=0,remaining=0,corrected=0,qualityPixels=0;
for(let qi=0;qi<qTiles.length;qi++){const q=qTiles[qi],hm=await gpuCall(r.renderTileMetaHybrid({...common(q),deepContext:ctx,riskLimit}),'hybrid z'+z+' quality '+riskLimit);qualityPixels+=q.w*q.h;uncertain+=hm.uncertain||0;remaining+=hm.remaining||0;corrected+=hm.corrected||0;parts.push(compareMeta(deepMetas[qi],hm))}
const quality=aggregateQuality(parts),referenceBuildMs=ctx.buildMs||referenceWallMs,coldMedianMs=referenceBuildMs+timing.fullMedianMs,uncertainRate=uncertain/Math.max(1,qualityPixels),remainingRate=remaining/Math.max(1,qualityPixels),safe=quality.classMismatch===0&&remaining===0;
variants.push({riskLimit,...timing,referenceBuildMs,coldMedianMs,quality,qualityPixels,uncertain,uncertainRate,remaining,remainingRate,corrected,safe,speedupVsDeepCold:(referenceBuildMs+deep.fullMedianMs)/Math.max(1e-9,coldMedianMs)});
}
const referenceBuildMs=ctx.buildMs||referenceWallMs,deepCold=referenceBuildMs+deep.fullMedianMs,viable=variants.filter(v=>v.safe).sort((a,b)=>a.coldMedianMs-b.coldMedianMs),best=viable[0]||null;
rows.push({track:tr.id,z,iter,resolution:[fullW,fullH],referenceBuildMs,ds:{...ds},deep:{...deep,coldMedianMs:deepCold,unknownRate:deepUnknown/Math.max(1,qTiles.reduce((n,q)=>n+q.w*q.h,0))},variants,bestRiskLimit:best?.riskLimit??null,bestHybridColdMs:best?.coldMedianMs??null,recommendation:best&&best.coldMedianMs<deepCold*.9&&best.uncertainRate<.6?'hybrid-candidate':'full-deep'});
onProgress?.({index:di+1,total:depths.length,z,phase:'done'});
}
const result={format:'mandelbrot-ds-sparse-deep-hybrid-v1',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],riskLimits:[...riskLimits],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Diagnostic only. DS guarded direct tracks dz/dc in f32 and marks a pixel UNKNOWN when |dz/dc| exceeds riskLimit. Existing deep double-single correction re-evaluates only those UNKNOWN pixels. Full Deep+correction is the GPU quality reference. Cold timing adds the verified BigInt reference build to both Hybrid and Full Deep. Production routing is unchanged.'};
hybridJob.last=result;return result;
}finally{hybridJob.active=false}
}
// ── GPU startup / rendering orchestration ────────────────────────────────
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
async function renderFrame(){
const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height);
state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats();
try{
const r=renderer||await initRenderer();if(token!==state.token)return;
if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}
if(decision.probe){state.lastEngine='WebGPU · direct/deep view probe';state.drawState='COVERING';updateStats();await ensureBackendProbe(snap,iter,decision,token);if(token!==state.token)return;decision=chooseBackend(snap,canvas.width,iter,canvas.height);if(decision.probe)decision={...decision,backend:'deep',deep:true,probe:false,reason:'overlap-probe-failed-safe-deep'};state.backendDecision=decision;updateStats()}
const deep=decision.deep;let ctx=null;
if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded perturbation'}else state.lastEngine='WebGPU · f32 direct';
const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;
const firstPaint=performance.now()-t0;
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,backend:decision.backend};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=firstPaint;state.rendering=deep;state.unresolved=0;
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();
if(!deep){state.rendering=false;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();return}
let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats();
const n=canvas.width*canvas.height;if(shouldApplySparseCorrection(state.unresolved,n)){state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats();const corrected=await r.correctUnknownFrame(snap,iter,token);if(!corrected||token!==state.token)return;state.correctionPasses=1;stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;const painted=await r.recolor(token);if(!painted||token!==state.token)return;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats()}
if(token!==state.token)return;state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · guarded perturbation'+(state.correctionPasses?' + DS correction':'');const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();
}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
}
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback(深部非対応)';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
const exportJob={active:false,cancelled:false};
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
class StreamingPng{
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
async rows(filteredRows){await this.writer.write(filteredRows)}
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
}
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
async function runExport(){
if(exportJob.active)return;
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),strict=$('#exportPrecision').value==='strict';
let ctx=null;
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
let done=0,unresolvedSamples=0;
try{
for(let y=0;y<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
function benchmarkSummary(result){const lines=['z | f32 est ms | DS est ms | deep warm est | deep cold est | f32 Δclass | DS Δclass | direct ratio | fastest cold','--|------------|-----------|---------------|---------------|------------|-----------|--------------|-------------'];for(const r of result.rows)lines.push([String(r.z).padStart(2),r.f32.fullMedianMs.toFixed(1),r.ds.fullMedianMs.toFixed(1),r.deep.warmMedianMs.toFixed(1),r.deep.coldMedianMs.toFixed(1),(100*r.f32.quality.classMismatchRate).toFixed(2)+'%',(100*r.ds.quality.classMismatchRate).toFixed(2)+'%',Number.isFinite(r.directRatio)?r.directRatio.toExponential(2):'n/a',r.assessment.fastestCold].join(' | '));return lines.join('\n')}
$('#backendBench').onclick=()=>{const d=$('#benchmarkDialog');$('#benchmarkStatus').textContent='';$('#benchmarkProgress').hidden=true;$('#benchmarkOutput').value=benchmarkJob.last?benchmarkSummary(benchmarkJob.last):'';$('#benchmarkSave').disabled=!benchmarkJob.last;d.showModal?d.showModal():d.setAttribute('open','')};
$('#benchmarkStart').onclick=async()=>{if(benchmarkJob.active)return;cancelRender();state.dirty=false;const start=$('#benchmarkStart'),progress=$('#benchmarkProgress'),status=$('#benchmarkStatus');start.disabled=true;$('#benchmarkSave').disabled=true;progress.hidden=false;progress.max=CROSSOVER_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#benchmarkOutput').value='';try{const result=await runCrossoverBenchmark({track:$('#benchmarkTrack').value,samples:Math.max(1,Number($('#benchmarkSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});progress.value=progress.max;status.textContent='測定完了 · '+result.rows.length+' depths';$('#benchmarkOutput').value=benchmarkSummary(result);$('#benchmarkSave').disabled=false}catch(e){status.textContent=String(e&&e.message||e)}finally{start.disabled=false;markDirty(false)}};
$('#benchmarkCancel').onclick=()=>{if(benchmarkJob.active){benchmarkJob.cancelled=true;refs.cancelPending('benchmark cancelled');$('#benchmarkStatus').textContent='中止しています…'}else $('#benchmarkDialog').close()};
$('#benchmarkSave').onclick=()=>{if(!benchmarkJob.last)return;downloadBlob(new Blob([JSON.stringify(benchmarkJob.last,null,2)],{type:'application/json'}),'mandelbrot-crossover-'+Date.now()+'.json')};
function hybridSummary(result){const lines=['z | best risk | hybrid cold est | deep cold est | speedup | correction | remain | Δclass | recommendation','--|-----------|-----------------|---------------|---------|------------|--------|--------|---------------'];for(const r of result.rows){const best=r.bestRiskLimit==null?null:r.variants.find(v=>v.riskLimit===r.bestRiskLimit),hy=best?best.coldMedianMs:NaN;lines.push([String(r.z).padStart(2),best?best.riskLimit.toExponential(0):'none',best?hy.toFixed(1):'n/a',r.deep.coldMedianMs.toFixed(1),best?(r.deep.coldMedianMs/hy).toFixed(2)+'×':'n/a',best?(100*best.uncertainRate).toFixed(1)+'%':'n/a',best?(100*best.remainingRate).toFixed(2)+'%':'n/a',best?(100*best.quality.classMismatchRate).toFixed(2)+'%':'n/a',r.recommendation].join(' | '))}return lines.join('\n')}
function openHybridDialog(){const d=$('#hybridDialog');if(!d.open){try{if(typeof d.showModal==='function')d.showModal();else d.setAttribute('open','')}catch{d.setAttribute('open','')}}return d}
async function startHybridExperimentFromUi(){
const d=openHybridDialog(),start=$('#hybridStart'),toolbar=$('#hybridBench'),progress=$('#hybridProgress'),status=$('#hybridStatus');status.textContent='Hybrid実験を起動中…';toolbar.setAttribute('aria-busy','true');
if(hybridJob.active){status.textContent='Hybrid実験を実行中です';toolbar.removeAttribute('aria-busy');return}
if(benchmarkJob.active){status.textContent='Backend比較を終了または中止してから実行してください';toolbar.removeAttribute('aria-busy');return}
cancelRender();state.dirty=false;start.disabled=true;toolbar.disabled=true;start.textContent='実行中…';$('#hybridSave').disabled=true;progress.hidden=false;progress.max=HYBRID_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#hybridOutput').value='';
try{
const result=await runHybridExperiment({track:$('#hybridTrack').value,samples:Math.max(1,Number($('#hybridSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});
progress.value=progress.max;status.textContent='実験完了 · '+result.rows.length+' depths';$('#hybridOutput').value=hybridSummary(result);$('#hybridSave').disabled=false;start.textContent='再実行';
}catch(e){status.textContent=String(e&&e.message||e);start.textContent='再実行'}
finally{start.disabled=false;toolbar.disabled=false;toolbar.removeAttribute('aria-busy');markDirty(false)}
}
$('#hybridBench').onclick=()=>{toast('Hybrid実験を開始');void startHybridExperimentFromUi()};
$('#hybridStart').onclick=startHybridExperimentFromUi;
$('#hybridCancel').onclick=()=>{if(hybridJob.active){hybridJob.cancelled=true;refs.cancelPending('hybrid experiment cancelled');$('#hybridStatus').textContent='中止しています…'}else $('#hybridDialog').close()};
$('#hybridSave').onclick=()=>{if(!hybridJob.last)return;downloadBlob(new Blob([JSON.stringify(hybridJob.last,null,2)],{type:'application/json'}),'mandelbrot-hybrid-'+Date.now()+'.json')};
// ── diagnostics ──────────────────────────────────────────────────────────
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend(snapshot(),Math.max(1,canvas.width),maxIter(),Math.max(1,canvas.height));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.probe?'WebGPU 判定中':decision.deep?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'深部補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.correctedPixels)status+=' · 回収 '+state.correctedPixels;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx,ratio=decision.metrics&&Number.isFinite(decision.metrics.ratio)?decision.metrics.ratio:0,prof=decision.profile,rs=state.unknownReasons,reasonText=rs?Object.entries(rs).filter(([,v])=>v).map(([k,v])=>k+':'+v).join(' '):'';$('#diagEngine').textContent='engine: '+state.lastEngine+' | route '+decision.backend+' ('+decision.reason+') | ratio '+(ratio?ratio.toExponential(2):'n/a')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'')+(reasonText?' | UNKNOWN '+reasonText:'')+(decision.viewProbe?' | view probe direct '+decision.viewProbe.predictedDirectMs.toFixed(0)+'ms / deep '+decision.viewProbe.predictedDeepMs.toFixed(0)+'ms / u '+(100*decision.viewProbe.deepUnknownRate).toFixed(2)+'% / Δclass '+decision.viewProbe.classDisagreement:'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height+' | speed profiles '+backendProfiles.size+' / view probes '+backendViewProbes.size}
globalThis.__MANDEL_TEST__={
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w,h):!!forceDeep;let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
async runCrossoverBenchmark(options={}){return runCrossoverBenchmark(options)},
async runHybridExperiment(options={}){return runHybridExperiment(options)},
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
};
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,buildVersion:BUILD_VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:(state.backendDecision||chooseBackend()).deep,backendDecision:state.backendDecision,backendProfiles:Array.from(backendProfiles.entries()),backendViewProbes:Array.from(backendViewProbes.entries()),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,unknownReasons:state.unknownReasons,correctionPasses:state.correctionPasses,correctedPixels:state.correctedPixels,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
})();

View file

@ -1,454 +0,0 @@
(()=>{'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,BUILD_VERSION='24.2.6',INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
const DIRECT_REQUIRED_RATIO=8,DIRECT_DEFAULT_RATIO=128,DEEP_SPEED_MARGIN=.85,DEEP_PROBE_UNKNOWN_MAX=.005,NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,CORRECTION_MARK=128;
const BACKEND_PROBE_SPEED_W=224,BACKEND_PROBE_SPEED_H=160,BACKEND_PROBE_QUALITY_W=72,BACKEND_PROBE_QUALITY_H=54;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,backendProbes:0};
const backendProfiles=new Map(),backendViewProbes=new Map(),backendProbePending=new Map();
// ── exact fixed-point view state ─────────────────────────────────────────
function one(bits=state.bits){return 1n<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
function directMetrics(snap=snapshot(),width=Math.max(1,canvas.width)){const spanLog=log2FixedAt(snap.span,snap.bits),stepLog=spanLog-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp))),eligible=Number.isFinite(ratio)&&ratio>=DIRECT_REQUIRED_RATIO&&spanLog>-145;return{spanLog,stepLog,ratio,eligible,overlap:eligible&&ratio<DIRECT_DEFAULT_RATIO}}
function zoomExpFor(snap){return Math.max(0,Math.log10(3.4)-log2FixedAt(snap.span,snap.bits)/Math.log2(10))}
function backendProfileKey(snap,iter){const z=Math.floor(zoomExpFor(snap)*2)/2,ib=iter<600?0:iter<1500?1:iter<5000?2:3;return z.toFixed(1)+':'+ib}
function backendViewKey(snap,iter,width,height){return[snap.bits,snap.re,snap.im,snap.span,iter,width,height].join(':')}
function chooseBackend(snap=snapshot(),width=Math.max(1,canvas.width),iter=maxIter(),height=Math.max(1,canvas.height)){const m=directMetrics(snap,width),key=backendProfileKey(snap,iter),viewKey=backendViewKey(snap,iter,width,height),speed=backendProfiles.get(key),viewProbe=backendViewProbes.get(viewKey),pixels=Math.max(1,width*Math.max(1,height)),profile=speed?{...speed,predictedDirectMs:speed.directMsPerPixel*pixels,predictedDeepMs:speed.referenceBuildMs+speed.deepMsPerPixel*pixels}:null;let backend='deep',reason='direct-coordinate-limit';if(m.eligible){if(m.ratio>=DIRECT_DEFAULT_RATIO){backend='direct';reason='direct-wide-margin'}else if(!viewProbe){backend='probe';reason='overlap-view-probe-required'}else if(!viewProbe.qualityPass){backend='deep';reason='direct-quality-rejected'}else if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*DEEP_SPEED_MARGIN){backend='deep';reason='deep-view-probe-faster'}else{backend='direct';reason='direct-view-probe-preferred'}}return{backend,deep:backend==='deep',probe:backend==='probe',reason,key,viewKey,metrics:m,profile,viewProbe:viewProbe||null}}
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width),height=Math.max(1,canvas.height)){const d=chooseBackend(snap,width,maxIter(),height);return d.deep||d.probe}
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
// ── iteration / quality policy ───────────────────────────────────────────
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
'use strict';
const MAX_REF=150001,MAX_LEVELS=20;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class ReferenceService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter,fresh=false){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.2.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
}
const refs=new ReferenceService();
// ── WebGPU renderer ──────────────────────────────────────────────────────
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
class WebGpuRenderer{
constructor(adapter,device){
this.adapter=adapter;this.device=device;
const ai=adapter.info||{};
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,sdm,shm,xm,zm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('direct-ds-benchmark',G.DIRECT_DS_WGSL),this.module('direct-ds-guarded-experiment',G.DIRECT_DS_GUARDED_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
this.dsDirect=this.device.createComputePipeline({layout:'auto',compute:{module:sdm,entryPoint:'main'}});
this.dsGuarded=this.device.createComputePipeline({layout:'auto',compute:{module:shm,entryPoint:'main'}});
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}});
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
webgpuCanvasClaimed=true;this.configure();
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
ensureFrame(w,h){
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
}
ensureExportWorkspace(){
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
}
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5){const cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),crh=Math.fround(cr),cih=Math.fround(ci),sph=Math.fround(sp),invW=1/Math.max(1,fullW),invh=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,crh,true);d.setFloat32(36,Math.fround(cr-crh),true);d.setFloat32(40,cih,true);d.setFloat32(44,Math.fround(ci-cih),true);d.setFloat32(48,sph,true);d.setFloat32(52,Math.fround(sp-sph),true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,invh,true);d.setFloat32(68,Math.fround(invW-invh),true)})}
dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sx=.5,sy=.5){const a=this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx,sy),d=new DataView(a);d.setFloat32(72,Math.fround(riskLimit),true);return a}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDsDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsDirect.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsDirect);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeDsGuardedNumeric(encoder,{pbuf,meta,smooth,stats,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsGuarded.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:stats}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsGuarded);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
}
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
async readUnresolved(){return(await this.readUnresolvedStats()).total}
async correctUnknownFrame(snap,iter,token){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,f.w*.5,f.h*.5,1,f.w,0));const e=d.createCommandEncoder({label:'sparse-deep-correction'}),bg=d.createBindGroup({layout:this.correct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),p=e.beginComputePass();p.setPipeline(this.correct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
async benchmarkDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap));const e=d.createCommandEncoder({label:'ds-direct-benchmark'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;[meta,fieldSmooth,pbuf].forEach(destroy);return{gpuMs}}
async renderTileMetaDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,n*4,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const e=d.createCommandEncoder({label:'ds-direct-meta'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});e.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,fieldSmooth,pbuf,read].forEach(destroy);return out}
async benchmarkHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13}){
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
read=buf(d,2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit));
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,0,fullW*.5,fullH*.5,1,w,0));
const e=d.createCommandEncoder({label:'ds-hybrid-benchmark'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
e.copyBufferToBuffer(dsStats,0,read,0,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,UNRESOLVED_BYTES,UNRESOLVED_BYTES);
const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;
await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),a=new Uint32Array(raw.slice(0,UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(UNRESOLVED_BYTES,2*UNRESOLVED_BYTES));read.unmap();
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);
return{gpuMs,uncertain:a[0]||0,remaining:b[0]||0,corrected:b[7]||0};
}
async renderTileMetaHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13,sampleX=.5,sampleY=.5}){
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
read=buf(d,n*4+2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sampleX,sampleY));
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,0,fullW*.5,fullH*.5,1,w,0));
const e=d.createCommandEncoder({label:'ds-hybrid-meta'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
e.copyBufferToBuffer(meta,0,read,0,n*4);e.copyBufferToBuffer(dsStats,0,read,n*4,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,n*4+UNRESOLVED_BYTES,UNRESOLVED_BYTES);
d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),a=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(n*4+UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES));read.unmap();
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);out.uncertain=a[0]||0;out.remaining=b[0]||0;out.corrected=b[7]||0;return out;
}
async benchmarkNumeric({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,forceStrict=false,correctUnknown=false}){await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const e=d.createCommandEncoder({label:'backend-benchmark'});if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,forceStrict?1:0));this.encodeDeepNumeric(e,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){e.clearBuffer(unresolved);this.encodeCorrectionNumeric(e,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap));this.encodeDirectNumeric(e,{pbuf,meta,smooth,w,h})}e.copyBufferToBuffer(unresolved,0,read,0,UNRESOLVED_BYTES);const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;await read.mapAsync(GPUMapMode.READ);const a=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return{gpuMs,unknown:a[0]||0,corrected:a[7]||0}}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
}
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes,UNRESOLVED_BYTES/4),unresolvedCount=stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
async renderTileRGBA2x({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount=0,corrected=0;for(let si=0;si<4;si++){const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4);unresolvedCount+=stats[0]||0;corrected+=stats[7]||0}ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext()}
}
function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true}
function backendQualityTiles(fullW,fullH){const w=Math.min(BACKEND_PROBE_QUALITY_W,fullW),h=Math.min(BACKEND_PROBE_QUALITY_H,fullH),x1=Math.max(0,fullW-w),y1=Math.max(0,fullH-h),xc=Math.max(0,Math.floor((fullW-w)/2)),yc=Math.max(0,Math.floor((fullH-h)/2));return[[0,0],[x1,0],[0,y1],[x1,y1],[xc,yc]].map(([tileX,tileY])=>({tileX,tileY,w,h}))}
async function performBackendProbe(snap,iter,decision,token){if(!renderer||!decision.metrics.overlap||token!==state.token)return null;const fullW=canvas.width,fullH=canvas.height,sw=Math.min(BACKEND_PROBE_SPEED_W,fullW),sh=Math.min(BACKEND_PROBE_SPEED_H,fullH),sx=Math.max(0,Math.floor((fullW-sw)/2)),sy=Math.max(0,Math.floor((fullH-sh)/2)),speedPixels=sw*sh,fullPixels=Math.max(1,fullW*fullH),direct=await renderer.benchmarkNumeric({snap,iter,deep:false,deepContext:null,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh});if(token!==state.token)return null;const rb=performance.now(),ctx=await refs.request(snap,iter),buildWall=performance.now()-rb;if(token!==state.token||ctx.checkpointMismatch)return null;const deepProbe=await renderer.benchmarkNumeric({snap,iter,deep:true,deepContext:ctx,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh,correctUnknown:true});if(token!==state.token)return null;let classDisagreement=0,deepUnknown=0,knownCompared=0,qualityPixels=0,corrected=0;for(const q of backendQualityTiles(fullW,fullH)){const directMeta=await renderer.renderTileMeta({snap,iter,deep:false,deepContext:null,fullW,fullH,...q});if(token!==state.token)return null;const deepMeta=await renderer.renderTileMeta({snap,iter,deep:true,deepContext:ctx,fullW,fullH,...q,correctUnknown:true});if(token!==state.token)return null;qualityPixels+=q.w*q.h;corrected+=deepMeta.corrected||0;for(let i=0;i<deepMeta.length;i++){const dc=(deepMeta[i]>>>28)&3;if(dc===FIELD_UNKNOWN){deepUnknown++;continue}const rc=(directMeta[i]>>>28)&3;knownCompared++;if((dc===FIELD_ESCAPED)!==(rc===FIELD_ESCAPED))classDisagreement++}}const buildMs=ctx.buildMs||buildWall,directMsPerPixel=direct.gpuMs/Math.max(1,speedPixels),deepMsPerPixel=deepProbe.gpuMs/Math.max(1,speedPixels),sample={directMsPerPixel,deepMsPerPixel,referenceBuildMs:buildMs},prev=backendProfiles.get(decision.key),a=prev?.samples?0.3:1,profile={samples:(prev?.samples||0)+1,directMsPerPixel:(prev?.directMsPerPixel||sample.directMsPerPixel)*(1-a)+sample.directMsPerPixel*a,deepMsPerPixel:(prev?.deepMsPerPixel||sample.deepMsPerPixel)*(1-a)+sample.deepMsPerPixel*a,referenceBuildMs:(prev?.referenceBuildMs||sample.referenceBuildMs)*(1-a)+sample.referenceBuildMs*a};backendProfiles.set(decision.key,profile);const deepUnknownRate=deepUnknown/Math.max(1,qualityPixels),qualityPass=classDisagreement===0&&deepUnknownRate<=DEEP_PROBE_UNKNOWN_MAX,viewProbe={qualityPass,deepUnknownRate,classDisagreement,knownCompared,qualityPixels,corrected,predictedDirectMs:directMsPerPixel*fullPixels,predictedDeepMs:buildMs+deepMsPerPixel*fullPixels,ratio:decision.metrics.ratio,zoom:zoomExpFor(snap),iter,speedProbe:{w:sw,h:sh,tileX:sx,tileY:sy},qualityTiles:backendQualityTiles(fullW,fullH)};backendViewProbes.set(decision.viewKey,viewProbe);runtime.backendProbes++;return viewProbe}
function ensureBackendProbe(snap,iter,decision,token){if(!decision.metrics.overlap)return Promise.resolve(null);const hit=backendViewProbes.get(decision.viewKey);if(hit)return Promise.resolve(hit);const pending=backendProbePending.get(decision.viewKey);if(pending)return pending;const task=performBackendProbe(snap,iter,decision,token).catch(e=>{if(token===state.token)console.warn('backend probe failed',e);return null}).finally(()=>backendProbePending.delete(decision.viewKey));backendProbePending.set(decision.viewKey,task);return task}
// ── backend crossover benchmark (diagnostic only) ───────────────────────
const CROSSOVER_TRACKS={
seahorse:{id:'seahorse',label:'Seahorse boundary',re:'-0.7453983606667815',im:'0.1125046349959942'},
cusp:{id:'cusp',label:'Period-2 cusp',re:'-0.75',im:'0'}
};
const CROSSOVER_DEPTHS=[0,2,4,6,8,10,12,14,16,20,30,40];
const benchmarkJob={active:false,cancelled:false,last:null};
function fixedAtDecimal(text,bits){let s=String(text).trim(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const parts=s.toLowerCase().split('e'),mant=parts[0],exp=parts[1]?parseInt(parts[1],10):0,ab=mant.split('.'),ii=ab[0]||'0',ff=ab[1]||'';let digits=(ii+ff).replace(/^0+(?=\d)/,'')||'0',places=ff.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),scale=1n<<BigInt(bits),v=(BigInt(digits)*scale+den/2n)/den;return neg?-v:v}
function crossoverSnap(track,z){const span='3.4e-'+z,bits=Math.max(320,decimalRequiredBits(track.re)+64,decimalRequiredBits(track.im)+64,decimalRequiredBits(span)+96);return{bits,re:fixedAtDecimal(track.re,bits),im:fixedAtDecimal(track.im,bits),span:fixedAtDecimal(span,bits)}}
function iterForZoom(z,base=350){return Math.max(base,base+Math.floor(70*Math.sqrt(Math.max(0,z))+15*Math.max(0,z)))}
function median(values){const a=[...values].sort((x,y)=>x-y),n=a.length;return n?a.length%2?a[(n-1)>>1]:(a[n/2-1]+a[n/2])/2:NaN}
function compareMeta(reference,candidate){let referenceUnknown=0,candidateUnknown=0,classMismatch=0,escapedCompared=0,iterMismatch=0,maxIterDelta=0,sumIterDelta=0;const n=Math.min(reference.length,candidate.length);for(let i=0;i<n;i++){const rm=reference[i]>>>0,cm=candidate[i]>>>0,rc=(rm>>>28)&3,cc=(cm>>>28)&3;if(rc===FIELD_UNKNOWN){referenceUnknown++;continue}if(cc===FIELD_UNKNOWN){candidateUnknown++;continue}const re=rc===FIELD_ESCAPED,ce=cc===FIELD_ESCAPED;if(re!==ce){classMismatch++;continue}if(re){escapedCompared++;const d=Math.abs((rm&ITER_MASK)-(cm&ITER_MASK));if(d){iterMismatch++;sumIterDelta+=d;if(d>maxIterDelta)maxIterDelta=d}}}return{pixels:n,referenceUnknown,candidateUnknown,classMismatch,classMismatchRate:classMismatch/Math.max(1,n-referenceUnknown),escapedCompared,escapeIterationMismatch:iterMismatch,escapeIterationMismatchRate:iterMismatch/Math.max(1,escapedCompared),meanEscapeIterationDelta:iterMismatch?sumIterDelta/iterMismatch:0,maxEscapeIterationDelta:maxIterDelta}}
function crossoverQualityTiles(fullW,fullH,w,h){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[0,0],[Math.max(0,fullW-w),0],[0,Math.max(0,fullH-h)],[Math.max(0,fullW-w),Math.max(0,fullH-h)],[Math.max(0,Math.floor((fullW-w)/2)),Math.max(0,Math.floor((fullH-h)/2))]];return pts.map(([tileX,tileY])=>({tileX,tileY,w,h}))}
function aggregateQuality(list){const out={pixels:0,referenceUnknown:0,candidateUnknown:0,classMismatch:0,escapedCompared:0,escapeIterationMismatch:0,sumEscapeIterationDelta:0,maxEscapeIterationDelta:0};for(const q of list){out.pixels+=q.pixels;out.referenceUnknown+=q.referenceUnknown;out.candidateUnknown+=q.candidateUnknown;out.classMismatch+=q.classMismatch;out.escapedCompared+=q.escapedCompared;out.escapeIterationMismatch+=q.escapeIterationMismatch;out.sumEscapeIterationDelta+=q.meanEscapeIterationDelta*q.escapeIterationMismatch;out.maxEscapeIterationDelta=Math.max(out.maxEscapeIterationDelta,q.maxEscapeIterationDelta)}out.classMismatchRate=out.classMismatch/Math.max(1,out.pixels-out.referenceUnknown);out.escapeIterationMismatchRate=out.escapeIterationMismatch/Math.max(1,out.escapedCompared);out.meanEscapeIterationDelta=out.escapeIterationMismatch?out.sumEscapeIterationDelta/out.escapeIterationMismatch:0;delete out.sumEscapeIterationDelta;return out}
const CROSSOVER_SPEED_W=256,CROSSOVER_SPEED_H=144,CROSSOVER_GPU_TIMEOUT_MS=15000,CROSSOVER_REF_TIMEOUT_MS=15000;
function crossoverSpeedTiles(fullW,fullH,w=CROSSOVER_SPEED_W,h=CROSSOVER_SPEED_H){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[.5,.5],[.28,.36],[.72,.64]],seen=new Set(),out=[];for(const [fx,fy] of pts){const tileX=Math.max(0,Math.min(fullW-w,Math.round(fullW*fx-w/2))),tileY=Math.max(0,Math.min(fullH-h,Math.round(fullH*fy-h/2))),k=tileX+':'+tileY;if(!seen.has(k)){seen.add(k);out.push({tileX,tileY,w,h})}}return out}
function benchmarkTimeout(promise,ms,label,onTimeout){let timer;const timeout=new Promise((_,reject)=>{timer=setTimeout(()=>{try{onTimeout?.()}catch{}reject(new Error(label+' timed out after '+Math.round(ms/1000)+'s'))},ms)});return Promise.race([promise,timeout]).finally(()=>clearTimeout(timer))}
async function benchmarkTileBatch(fn,tiles){let gpuMs=0,pixels=0;for(const q of tiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h}return{gpuMs,pixels}}
async function timedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[];for(let i=0;i<samples;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'sample',index:i+1,total:samples});values.push((await fn()).gpuMs)}return{medianMs:median(values),samplesMs:values}}
function extrapolateTiming(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels),fullMedianMs=msPerPixel*fullPixels;return{...timing,speedPixels,msPerPixel,fullMedianMs}}
async function runCrossoverBenchmark({track='seahorse',depths=CROSSOVER_DEPTHS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
if(benchmarkJob.active)throw new Error('benchmark already running');benchmarkJob.active=true;benchmarkJob.cancelled=false;
try{
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown benchmark track');
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
for(let di=0;di<depths.length;di++){
if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z),metrics=directMetrics(snap,fullW);
onProgress?.({index:di,total:depths.length,z,phase:'reference'});
const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'z'+z+' reference',()=>refs.cancelPending('benchmark reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
const timed=(name,fn)=>timedMedian(()=>gpuCall(benchmarkTileBatch(fn,speedTiles),'z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
const tileCommon=q=>({snap,iter,fullW,fullH,...q});
onProgress?.({index:di,total:depths.length,z,phase:'f32'});
const f32=extrapolateTiming(await timed('f32',q=>r.benchmarkNumeric({...tileCommon(q),deep:false,deepContext:null})),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'ds'});
const ds=extrapolateTiming(await timed('ds',q=>r.benchmarkDsDirect(tileCommon(q))),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'deep'});
const deep=extrapolateTiming(await timed('deep',q=>r.benchmarkNumeric({...tileCommon(q),deep:true,deepContext:ctx,correctUnknown:true})),speedPixels,fullPixels);
onProgress?.({index:di,total:depths.length,z,phase:'quality'});
const f32Parts=[],dsParts=[],qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH);let deepUnknownCount=0,qualityPixels=0,deepCorrected=0;
for(const q of qTiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const qCommon={snap,iter,fullW,fullH,...q},deepMeta=await gpuCall(r.renderTileMeta({...qCommon,deep:true,deepContext:ctx,correctUnknown:true}),'z'+z+' quality deep'),f32Meta=await gpuCall(r.renderTileMeta({...qCommon,deep:false,deepContext:null}),'z'+z+' quality f32'),dsMeta=await gpuCall(r.renderTileMetaDsDirect(qCommon),'z'+z+' quality ds');qualityPixels+=q.w*q.h;deepUnknownCount+=deepMeta.unresolved||0;deepCorrected+=deepMeta.corrected||0;f32Parts.push(compareMeta(deepMeta,f32Meta));dsParts.push(compareMeta(deepMeta,dsMeta))}
const f32Quality=aggregateQuality(f32Parts),dsQuality=aggregateQuality(dsParts),deepUnknown=deepUnknownCount/Math.max(1,qualityPixels),referenceBuildMs=ctx.buildMs||referenceWallMs;
const row={track:tr.id,z,span:'3.4e-'+z,iter,resolution:[fullW,fullH],speedTiles,speedPixels,qualityTile:[qualityW,qualityH],qualityTiles:qTiles,directRatio:metrics.ratio,
f32:{...f32,quality:f32Quality},ds:{...ds,quality:dsQuality},deep:{...deep,referenceBuildMs,referenceWallMs,coldMedianMs:referenceBuildMs+deep.fullMedianMs,warmMedianMs:deep.fullMedianMs,unknownRate:deepUnknown,corrected:deepCorrected}};
const qualityLimit=.01;
row.assessment={f32VisuallySafe:f32Quality.classMismatchRate<=qualityLimit&&metrics.ratio>=1,dsVisuallySafe:dsQuality.classMismatchRate<=qualityLimit,deepVisuallySafe:deepUnknown<=.001,
fastestWarm:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.warmMedianMs]].sort((a,b)=>a[1]-b[1])[0][0],
fastestCold:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.coldMedianMs]].sort((a,b)=>a[1]-b[1])[0][0]};
rows.push(row);onProgress?.({index:di+1,total:depths.length,z,phase:'done',row});
}
const result={format:'mandelbrot-webgpu-crossover-v2',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Speed timing uses three bounded representative tiles and extrapolates ms/pixel to the current full resolution, preventing long full-frame Deep benchmark dispatches. Deep cold adds verified BigInt reference construction. Quality remains measured on five spatial tiles. Any single benchmark GPU batch or reference build exceeding 15 s aborts with an explicit error instead of waiting indefinitely.'};
benchmarkJob.last=result;return result;
}finally{benchmarkJob.active=false}
}
// ── DS + sparse Deep hybrid experiment (diagnostic only) ───────────────
const HYBRID_DEPTHS=[6,8,10,12],HYBRID_RISK_LIMITS=[1e12,1e13,1e14];
const hybridJob={active:false,cancelled:false,last:null};
async function hybridTileBatch(fn,tiles){let gpuMs=0,pixels=0,uncertain=0,remaining=0,corrected=0;for(const q of tiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h;uncertain+=r.uncertain||0;remaining+=r.remaining||0;corrected+=r.corrected||0}return{gpuMs,pixels,uncertain,remaining,corrected}}
async function hybridTimedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[],stats=[];for(let i=0;i<samples;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'sample',index:i+1,total:samples});const r=await fn();values.push(r.gpuMs);stats.push(r)}return{medianMs:median(values),samplesMs:values,sampleStats:stats}}
function hybridExtrapolate(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels);return{...timing,speedPixels,msPerPixel,fullMedianMs:msPerPixel*fullPixels}}
async function runHybridExperiment({track='seahorse',depths=HYBRID_DEPTHS,riskLimits=HYBRID_RISK_LIMITS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
if(hybridJob.active||benchmarkJob.active)throw new Error('another benchmark is running');hybridJob.active=true;hybridJob.cancelled=false;
try{
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown experiment track');
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
for(let di=0;di<depths.length;di++){
if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z);
onProgress?.({index:di,total:depths.length,z,phase:'reference'});const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'hybrid z'+z+' reference',()=>refs.cancelPending('hybrid reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
const timed=(name,fn)=>hybridTimedMedian(()=>gpuCall(hybridTileBatch(fn,speedTiles),'hybrid z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
const common=q=>({snap,iter,fullW,fullH,...q});
const ds=hybridExtrapolate(await timed('DS baseline',async q=>{const x=await r.benchmarkDsDirect(common(q));return{...x}}),speedPixels,fullPixels);
const deep=hybridExtrapolate(await timed('Full Deep',async q=>{const x=await r.benchmarkNumeric({...common(q),deep:true,deepContext:ctx,correctUnknown:true});return{...x}}),speedPixels,fullPixels);
const qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH),deepMetas=[];let deepUnknown=0;
onProgress?.({index:di,total:depths.length,z,phase:'quality reference'});
for(const q of qTiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const m=await gpuCall(r.renderTileMeta({...common(q),deep:true,deepContext:ctx,correctUnknown:true}),'hybrid z'+z+' quality deep');deepUnknown+=m.unresolved||0;deepMetas.push(m)}
const variants=[];
for(const riskLimit of riskLimits){
onProgress?.({index:di,total:depths.length,z,phase:'risk '+riskLimit.toExponential(0)});
const timing=hybridExtrapolate(await timed('Hybrid '+riskLimit.toExponential(0),q=>r.benchmarkHybrid({...common(q),deepContext:ctx,riskLimit})),speedPixels,fullPixels);
const parts=[];let uncertain=0,remaining=0,corrected=0,qualityPixels=0;
for(let qi=0;qi<qTiles.length;qi++){const q=qTiles[qi],hm=await gpuCall(r.renderTileMetaHybrid({...common(q),deepContext:ctx,riskLimit}),'hybrid z'+z+' quality '+riskLimit);qualityPixels+=q.w*q.h;uncertain+=hm.uncertain||0;remaining+=hm.remaining||0;corrected+=hm.corrected||0;parts.push(compareMeta(deepMetas[qi],hm))}
const quality=aggregateQuality(parts),referenceBuildMs=ctx.buildMs||referenceWallMs,coldMedianMs=referenceBuildMs+timing.fullMedianMs,uncertainRate=uncertain/Math.max(1,qualityPixels),remainingRate=remaining/Math.max(1,qualityPixels),safe=quality.classMismatch===0&&remaining===0;
variants.push({riskLimit,...timing,referenceBuildMs,coldMedianMs,quality,qualityPixels,uncertain,uncertainRate,remaining,remainingRate,corrected,safe,speedupVsDeepCold:(referenceBuildMs+deep.fullMedianMs)/Math.max(1e-9,coldMedianMs)});
}
const referenceBuildMs=ctx.buildMs||referenceWallMs,deepCold=referenceBuildMs+deep.fullMedianMs,viable=variants.filter(v=>v.safe).sort((a,b)=>a.coldMedianMs-b.coldMedianMs),best=viable[0]||null;
rows.push({track:tr.id,z,iter,resolution:[fullW,fullH],referenceBuildMs,ds:{...ds},deep:{...deep,coldMedianMs:deepCold,unknownRate:deepUnknown/Math.max(1,qTiles.reduce((n,q)=>n+q.w*q.h,0))},variants,bestRiskLimit:best?.riskLimit??null,bestHybridColdMs:best?.coldMedianMs??null,recommendation:best&&best.coldMedianMs<deepCold*.9&&best.uncertainRate<.6?'hybrid-candidate':'full-deep'});
onProgress?.({index:di+1,total:depths.length,z,phase:'done'});
}
const result={format:'mandelbrot-ds-sparse-deep-hybrid-v1',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],riskLimits:[...riskLimits],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Diagnostic only. DS guarded direct tracks dz/dc in f32 and marks a pixel UNKNOWN when |dz/dc| exceeds riskLimit. Existing deep double-single correction re-evaluates only those UNKNOWN pixels. Full Deep+correction is the GPU quality reference. Cold timing adds the verified BigInt reference build to both Hybrid and Full Deep. Production routing is unchanged.'};
hybridJob.last=result;return result;
}finally{hybridJob.active=false}
}
// ── GPU startup / rendering orchestration ────────────────────────────────
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
async function renderFrame(){
const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height);
state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats();
try{
const r=renderer||await initRenderer();if(token!==state.token)return;
if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}
if(decision.probe){state.lastEngine='WebGPU · direct/deep view probe';state.drawState='COVERING';updateStats();await ensureBackendProbe(snap,iter,decision,token);if(token!==state.token)return;decision=chooseBackend(snap,canvas.width,iter,canvas.height);if(decision.probe)decision={...decision,backend:'deep',deep:true,probe:false,reason:'overlap-probe-failed-safe-deep'};state.backendDecision=decision;updateStats()}
const deep=decision.deep;let ctx=null;
if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded perturbation'}else state.lastEngine='WebGPU · f32 direct';
const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;
const firstPaint=performance.now()-t0;
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,backend:decision.backend};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=firstPaint;state.rendering=deep;state.unresolved=0;
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();
if(!deep){state.rendering=false;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();return}
let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats();
const n=canvas.width*canvas.height;if(shouldApplySparseCorrection(state.unresolved,n)){state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats();const corrected=await r.correctUnknownFrame(snap,iter,token);if(!corrected||token!==state.token)return;state.correctionPasses=1;stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;const painted=await r.recolor(token);if(!painted||token!==state.token)return;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats()}
if(token!==state.token)return;state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · guarded perturbation'+(state.correctionPasses?' + DS correction':'');const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();
}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
}
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback(深部非対応)';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
const exportJob={active:false,cancelled:false};
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
class StreamingPng{
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
async rows(filteredRows){await this.writer.write(filteredRows)}
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
}
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
async function runExport(){
if(exportJob.active)return;
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),strict=$('#exportPrecision').value==='strict';
let ctx=null;
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
let done=0,unresolvedSamples=0;
try{
for(let y=0;y<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
function benchmarkSummary(result){const lines=['z | f32 est ms | DS est ms | deep warm est | deep cold est | f32 Δclass | DS Δclass | direct ratio | fastest cold','--|------------|-----------|---------------|---------------|------------|-----------|--------------|-------------'];for(const r of result.rows)lines.push([String(r.z).padStart(2),r.f32.fullMedianMs.toFixed(1),r.ds.fullMedianMs.toFixed(1),r.deep.warmMedianMs.toFixed(1),r.deep.coldMedianMs.toFixed(1),(100*r.f32.quality.classMismatchRate).toFixed(2)+'%',(100*r.ds.quality.classMismatchRate).toFixed(2)+'%',Number.isFinite(r.directRatio)?r.directRatio.toExponential(2):'n/a',r.assessment.fastestCold].join(' | '));return lines.join('\n')}
$('#backendBench').onclick=()=>{const d=$('#benchmarkDialog');$('#benchmarkStatus').textContent='';$('#benchmarkProgress').hidden=true;$('#benchmarkOutput').value=benchmarkJob.last?benchmarkSummary(benchmarkJob.last):'';$('#benchmarkSave').disabled=!benchmarkJob.last;d.showModal?d.showModal():d.setAttribute('open','')};
$('#benchmarkStart').onclick=async()=>{if(benchmarkJob.active)return;cancelRender();state.dirty=false;const start=$('#benchmarkStart'),progress=$('#benchmarkProgress'),status=$('#benchmarkStatus');start.disabled=true;$('#benchmarkSave').disabled=true;progress.hidden=false;progress.max=CROSSOVER_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#benchmarkOutput').value='';try{const result=await runCrossoverBenchmark({track:$('#benchmarkTrack').value,samples:Math.max(1,Number($('#benchmarkSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});progress.value=progress.max;status.textContent='測定完了 · '+result.rows.length+' depths';$('#benchmarkOutput').value=benchmarkSummary(result);$('#benchmarkSave').disabled=false}catch(e){status.textContent=String(e&&e.message||e)}finally{start.disabled=false;markDirty(false)}};
$('#benchmarkCancel').onclick=()=>{if(benchmarkJob.active){benchmarkJob.cancelled=true;refs.cancelPending('benchmark cancelled');$('#benchmarkStatus').textContent='中止しています…'}else $('#benchmarkDialog').close()};
$('#benchmarkSave').onclick=()=>{if(!benchmarkJob.last)return;downloadBlob(new Blob([JSON.stringify(benchmarkJob.last,null,2)],{type:'application/json'}),'mandelbrot-crossover-'+Date.now()+'.json')};
function hybridSummary(result){const lines=['z | best risk | hybrid cold est | deep cold est | speedup | correction | remain | Δclass | recommendation','--|-----------|-----------------|---------------|---------|------------|--------|--------|---------------'];for(const r of result.rows){const best=r.bestRiskLimit==null?null:r.variants.find(v=>v.riskLimit===r.bestRiskLimit),hy=best?best.coldMedianMs:NaN;lines.push([String(r.z).padStart(2),best?best.riskLimit.toExponential(0):'none',best?hy.toFixed(1):'n/a',r.deep.coldMedianMs.toFixed(1),best?(r.deep.coldMedianMs/hy).toFixed(2)+'×':'n/a',best?(100*best.uncertainRate).toFixed(1)+'%':'n/a',best?(100*best.remainingRate).toFixed(2)+'%':'n/a',best?(100*best.quality.classMismatchRate).toFixed(2)+'%':'n/a',r.recommendation].join(' | '))}return lines.join('\n')}
function openHybridDialog(){const d=$('#hybridDialog');if(!d.open){try{if(typeof d.showModal==='function')d.showModal();else d.setAttribute('open','')}catch{d.setAttribute('open','')}}return d}
async function startHybridExperimentFromUi(){
const d=openHybridDialog(),start=$('#hybridStart'),toolbar=$('#hybridBench'),progress=$('#hybridProgress'),status=$('#hybridStatus');status.textContent='Hybrid実験を起動中…';toolbar.setAttribute('aria-busy','true');
if(hybridJob.active){status.textContent='Hybrid実験を実行中です';toolbar.removeAttribute('aria-busy');return}
if(benchmarkJob.active){status.textContent='Backend比較を終了または中止してから実行してください';toolbar.removeAttribute('aria-busy');return}
cancelRender();state.dirty=false;start.disabled=true;toolbar.disabled=true;start.textContent='実行中…';$('#hybridSave').disabled=true;progress.hidden=false;progress.max=HYBRID_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#hybridOutput').value='';
try{
const result=await runHybridExperiment({track:$('#hybridTrack').value,samples:Math.max(1,Number($('#hybridSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});
progress.value=progress.max;status.textContent='実験完了 · '+result.rows.length+' depths';$('#hybridOutput').value=hybridSummary(result);$('#hybridSave').disabled=false;start.textContent='再実行';
}catch(e){status.textContent=String(e&&e.message||e);start.textContent='再実行'}
finally{start.disabled=false;toolbar.disabled=false;toolbar.removeAttribute('aria-busy');markDirty(false)}
}
$('#hybridBench').onclick=()=>{toast('Hybrid実験を開始');void startHybridExperimentFromUi()};
$('#hybridStart').onclick=startHybridExperimentFromUi;
$('#hybridCancel').onclick=()=>{if(hybridJob.active){hybridJob.cancelled=true;refs.cancelPending('hybrid experiment cancelled');$('#hybridStatus').textContent='中止しています…'}else $('#hybridDialog').close()};
$('#hybridSave').onclick=()=>{if(!hybridJob.last)return;downloadBlob(new Blob([JSON.stringify(hybridJob.last,null,2)],{type:'application/json'}),'mandelbrot-hybrid-'+Date.now()+'.json')};
// ── diagnostics ──────────────────────────────────────────────────────────
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend(snapshot(),Math.max(1,canvas.width),maxIter(),Math.max(1,canvas.height));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.probe?'WebGPU 判定中':decision.deep?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'深部補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.correctedPixels)status+=' · 回収 '+state.correctedPixels;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx,ratio=decision.metrics&&Number.isFinite(decision.metrics.ratio)?decision.metrics.ratio:0,prof=decision.profile,rs=state.unknownReasons,reasonText=rs?Object.entries(rs).filter(([,v])=>v).map(([k,v])=>k+':'+v).join(' '):'';$('#diagEngine').textContent='engine: '+state.lastEngine+' | route '+decision.backend+' ('+decision.reason+') | ratio '+(ratio?ratio.toExponential(2):'n/a')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'')+(reasonText?' | UNKNOWN '+reasonText:'')+(decision.viewProbe?' | view probe direct '+decision.viewProbe.predictedDirectMs.toFixed(0)+'ms / deep '+decision.viewProbe.predictedDeepMs.toFixed(0)+'ms / u '+(100*decision.viewProbe.deepUnknownRate).toFixed(2)+'% / Δclass '+decision.viewProbe.classDisagreement:'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height+' | speed profiles '+backendProfiles.size+' / view probes '+backendViewProbes.size}
globalThis.__MANDEL_TEST__={
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w,h):!!forceDeep;let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
async runCrossoverBenchmark(options={}){return runCrossoverBenchmark(options)},
async runHybridExperiment(options={}){return runHybridExperiment(options)},
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
};
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,buildVersion:BUILD_VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:(state.backendDecision||chooseBackend()).deep,backendDecision:state.backendDecision,backendProfiles:Array.from(backendProfiles.entries()),backendViewProbes:Array.from(backendViewProbes.entries()),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,unknownReasons:state.unknownReasons,correctionPasses:state.correctionPasses,correctedPixels:state.correctedPixels,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
})();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,55 +0,0 @@
{
"format": "mandelbrot-wasm-manifest-v1",
"generatedUtc": "2026-08-22T12:30:26.3193366Z",
"input": "kernels.js",
"payloads": [
{
"symbol": "WASM_SIMD_B64",
"file": "wasm-simd.wasm",
"bytes": 511,
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
},
{
"symbol": "WASM_SCALAR_B64",
"file": "wasm-scalar.wasm",
"bytes": 511,
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
},
{
"symbol": "DEEP_SIMD_B64",
"file": "deep-simd.wasm",
"bytes": 4866,
"sha256": "d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214"
},
{
"symbol": "DEEP_SCALAR_B64",
"file": "deep-scalar.wasm",
"bytes": 5246,
"sha256": "d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2"
},
{
"symbol": "BLA_SIMD_B64",
"file": "bla-simd.wasm",
"bytes": 4959,
"sha256": "f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e"
},
{
"symbol": "BLA_SCALAR_B64",
"file": "bla-scalar.wasm",
"bytes": 4593,
"sha256": "4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f"
},
{
"symbol": "COLOR_SIMD_B64",
"file": "color-simd.wasm",
"bytes": 1387,
"sha256": "8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03"
},
{
"symbol": "COLOR_SCALAR_B64",
"file": "color-scalar.wasm",
"bytes": 632,
"sha256": "aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90"
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

579
docs/OPTIMIZATION_PLAN.md Normal file
View file

@ -0,0 +1,579 @@
# マンデルブロ集合ビューアー最適化計画
対象baseline: `index.html` v24.2.55 (`24.2.55-no-black-hole-async-recovery`)
実装版: `index.html` v24.3.0 (`24.3.0-optimized-unknown-pipeline`)
作成日: 2026-08-25
## 本番昇格状況(2026-08-25)
現時点では本番未昇格である。`PIXEL_FRONTIER_PRODUCTION=false` を維持し、候補経路は `?frontier-candidate=1` または自動ゲート実行時だけ有効にする。未検証の active-list engine を Accurate の初期全面描画には使わず、初期描画は bounded strip の既存 Deep kernel、active-list は operation-limit 画素の継続計算だけに限定する。
| 条件 | 状態 | 根拠 / 残作業 |
| --- | --- | --- |
| JavaScript 構文、worker 構文、kernel hash | 合格 | `tests/optimization_regression.mjs` |
| 1 dispatch の上限 4,000,000 pixel-iterations | 静的合格 | 初期 Deep、sparse DS、frontier の各契約を回帰テストで固定 |
| Accurate 初期描画が active-list production path を使わない | 静的合格 | `computeFrame()` は bounded `computePerturbFrameTiled()` を使用 |
| UNKNOWN を色で隠さず対象画素を計算する | 静的合格 | operation-limit 全 index を continuation queue に投入。色置換や倍率しきい値は不使用 |
| 早すぎる安定判定の防止 | 候補実装済み | 画素幅から連続的に求めた最低反復数へ到達する前は、2 round 安定でも終了しない |
| exact escape iteration / class mismatch = 0 | 未確認 | 単一ブラウザーセッションで corpus と高精度 oracle を実行する |
| final numeric UNKNOWN = 0 | 未確認 | reset、seahorse、real-axis、報告座標で確認する |
| WGSL error / uncaptured error / device loss = 0 | 未確認 | Windows D3D12 実機を含む最終ブラウザー確認が必要 |
| p50 / p95 性能基準 | 未確認 | 同一セッション A/B を採取し、特に 10^14 付近の初回表示を確認する |
| 通常操作で黒領域が出現・消失しない | 未確認 | 同一近傍の pan / zoom / round-trip hash を確認する |
`pixelFrontierIterationFloor()` は zoom 倍率で描画方式を切り替えない。`span / canvasWidth` が表す1画素のスケールから最低反復予算を連続的に算出する。これは「少し拡大しただけで未計算領域を黒として確定する」問題を抑えるための下限であり、数学的な集合所属証明ではない。最終的な本番昇格には上表の実機ゲート通過を必須とする。
本番判定は `tests/production_gate.mjs` に固定した。候補段階では `node tests/production_gate.mjs <artifact> preflight`、本番フラグを有効化した後は同じコーパスを再実行して `node tests/production_gate.mjs <artifact> production` を通す。artifact は同一adapterで採取したbaseline/candidateの対応、異なる2 adapter以上、全12 correctness case、全6 performance caseを各10計測run、3 interaction caseを含まなければ合格しない。10^5と10^8はFAST/Accurateの両方、10^14は800×600でFAST p50 1000 ms・p95 1500 ms未満、Accurate p50 1500 ms・p95 2000 ms未満を追加の絶対条件とする。
## 追加修正: 10^14付近の速度とWindows GPU停止
実機ログで、Accurate Deepの長い単一dispatchがWindowsのGPU監視時間を超え、`DXGI_ERROR_DEVICE_HUNG`を発生させていたことを確認した。`powerPreference`の警告は原因ではない。
- Accurate DeepのActive-list本番化は、正常版との分類経路差になるため撤回する。正確モードはv24.2.55と同じDeep perturbation式へ戻し、1 stripを最大4,000,000 pixel-iterationsへ縮小することでTDRを回避する。
- Deep/Direct-DS補修はより重い演算なので、1 dispatch相当を最大4,000,000 pixel-iterationsへ制限する。疎Deep補修がこの上限を超える場合も補修を省略せず、2次元タイルのbounded scanですべての数値UNKNOWNを処理する。
- state/二重queueは最大64 MiBのband workspaceとして再利用し、adapterのbuffer/binding上限を超えない。band間で出力座標を保持するためqueue indexはband-local、meta/smooth indexはframe-globalとする。
- device loss直後のadapter再作成を1.2秒遅延し、D3D12復旧前の連続した`Device failed at creation`を避ける。
- FASTの最終フレームから無条件Deep補修を外す。数値UNKNOWNが実際に残った場合だけ局所reference補修と画素精度BigInt補修を同期実行し、数値UNKNOWNが0になるまで新しいframeを公開しない。通常FASTはDeepを待たず、未計算frameも表示しない。
- 履歴のない深部FASTでは、短いreferenceのpreviewがfinalへ再利用できず二重計算になっていたため、最終品質を1回だけ生成する。既存のstable historyがある操作では従来どおり再投影を即時previewとして使う。
- 倍率による描画方式の固定閾値は設けない。必要精度を `span / canvasWidth`(複素平面上の1画素幅)から算出し、参照軌道は画素精度+40 guard bit、任意精度補修は同じ精度と+32 bitの二重計算が一致した場合だけ採用する。画素より細かい座標情報は計算しない。数値UNKNOWNは件数上限で打ち切らず、bounded chunkですべて補修してから完成扱いにする。色で未計算を隠す処理や10^100切替は使用しない。
### 根本解決: pixel-convergent frontier
黒領域の膨張には、座標精度不足とは別に「反復上限まで脱出しなかった点を、そのまま集合内部と同じ黒で表示する」問題がある。倍率ごとの固定反復数や固定切替では解決しないため、最終形は次の画素収束方式とする。
1. 初回passは現在のadaptive反復数まで全画素を計算する。
2. `operation-limit` 画素のうち、8近傍に `escaped` がある画素だけをGPU上でfrontier queueへcompactする。黒領域の奥を再計算しない。
3. frontier画素は保存したperturbation stateからbounded chunkで継続し、新たに脱出した画素だけを更新する。その近傍を次のfrontierへ追加する。
4. analytic cardioid/bulbまたは検証済み周期性で内部と証明できた画素はfrontierから除外する。単に反復上限へ達しただけの画素は「証明済み内部」へ昇格しない。
5. 変化した境界のHausdorff距離が1画素未満になり、数値UNKNOWNが0になった時だけframeを完成・history commitする。これにより画素以下の境界情報は追跡しない。
6. 1 dispatchは既存のpixel-iteration予算内に分割し、操作が入ればgeneration tokenで即時中止する。同一viewの反復延長ではreference/state/queueを再利用し、iteration 0からやり直さない。
現在の実装では、この前提となる画素幅基準precision、全残存数値UNKNOWNのbounded補修、未完成frameのpublish禁止までを本番経路へ入れた。frontier continuationは既存のshadow active-listを、実機zero-tolerance corpusとWindows TDR試験に通した後で本番へ昇格する。これが反復上限由来の過大な黒を、全画面高反復の速度低下なしに除く本命経路である。
- adaptive反復数の64単位上方丸め、BLA既定OFF、同一viewだけのreference再利用は維持する。したがって操作順による黒領域の揺れを抑える決定性は維持し、反復数・誤差guard・分類式は削減しない。
## 実装状況(2026-08-25)
この計画のうち、数値品質を維持したまま静的・モデル検証できる項目をv24.3.0へ実装した。
- generationごとにprimary/final/numeric-quiescent/history-commit、submit/sync差分、UNKNOWN数を最大64件記録し、テストhookへprimary/final/numeric-complete待機とmeta/smooth SHA-256取得を追加する。
- Accurate seedを最大24反復で終了し、未採用画素をreason 1〜5のUNKNOWNとしてDSへ渡す。
- DSと高精度・高速参照workerで更新後平方を次反復へ引き継ぐ。
- Direct、Accurate、FAST、Deep、BLAで共通のUNKNOWN post-statsを使い、理由0、1〜5、6を同じ規則で集計する。
- Deep補正をbucket、prefix、scatter、indirect dispatchの疎queueへ接続し、queue不変条件が崩れた時だけbounded full scanへ戻す。
- Accurate seed後はUNKNOWN indexだけをcompactし、時間予算に合わせたDirect-DS queue sliceで処理する。selected/enqueued/dispatch/processedまたは範囲不変条件が崩れた場合だけ従来の全画面guard走査へ戻す。
- failure mapをGPU上で空間タイル集計し、全画面metadata readbackを局所補修ループから除去する。局所referenceは3 workerで並列準備する。
- failure密度によるFAST→Deep直接昇格、同一viewの検証済みreference再利用、final reference prefetch、BLA table cacheを追加する。近傍viewへのreference流用は黒領域の操作順依存を生じ得るため撤回した。
- previewの確定済みescape・解析的内点を同一referenceのfinalで再利用し、UNKNOWNだけを再計算する。
- GPU先行投入を24 ms(直近入力)/40 ms(idle)の予測時間予算で制限する。
- 初期pipelineをDirect、color、present、UNKNOWN stats、実軸対称copyへ縮小し、Accurate、FAST拡張、Deep、補正、AAを遅延コンパイルする。
- 最後に残ったreason 1〜5だけをview精度+128 bitと+192 bitで再計算し、両者のescape反復とf32 smoothが一致した結果だけを採用する。
- 中心虚部が厳密に0の通常画面では、共役対称性により上半分だけを計算して下半分へmetadata/smoothを複製する。
- BLAはoperation削減率だけでなく、直近baseline、table build、probeを含む推定net時間が5%以上改善する場合だけ有効化する。
- BLAの不安全分岐は0反復目restartではなく最後の正確checkpointからFAST recurrenceを再開する。
- exportはadapter limitに応じた256/512 px tileと2〜3個のstaging workspaceを使い、GPU/readbackを重ねる。PNGのrow順は維持し、UI yieldは24 ms経過時だけ行う。
- UNKNOWN/queue statsはframe単位の3-buffer readback ringへ移し、安定buffer/textureのbind groupをcacheする。
- stable historyは全画面copyを廃止し、3枚のcolor textureをfront/back/history/spareとして役割回転する。
- Direct/Deepのactive-list continuation、indirect compaction、FAST/Deepの証明付き内点判定候補をshadow-onlyで追加した。Deep continuationも品質回帰の切り分けを優先して本番採用を撤回した。内点候補は外向き丸めのf32区間演算を使い、候補indexだけを画素精度+40/+72 bit oracleで確認する。
- 操作ごとに黒領域が揺れる問題への安定化として、adaptive反復数を品質を下げない64反復単位の上方丸めにし、近傍view間の参照軌道流用を廃止した。FASTで数値UNKNOWNが発生した場合は、同一viewの局所referenceと画素精度BigInt補修を完了してから表示し、未完成frameをpresent/commitしない。
`tests/optimization_regression.mjs` は、2本のscript構文、36 string/kernel hash、旧新BigInt軌道2,000例、旧新DS軌道600例、Direct active chunk分割1,000例、任意精度worker構文を検査する。この環境ではブラウザ接続とWebGPU adapterを利用できなかったため、実GPU WGSL compile、画面hash、KPI実測は未実施である。
`tests/OPTIMIZATION_BENCHMARK.json` のBigInt worker modelでは、240点、700反復、192〜320 bit、7 runの中央値が201.5509 msから115.3558 msになり、42.77%短縮した。この値は参照軌道の平方再利用だけを測るもので、ブラウザや実GPUのend-to-end改善率ではない。
BLA replayコードは保持しているが、実測時間で数値backendが揺れることを防ぐためproduction既定値をOFFに戻し、明示的なテストopt-inだけを許可する。export ring、readback/bind cache、history role rotationはproduction経路へ接続済みである。Active-listと証明付き内点判定は`productionEligible:false`のshadow-onlyへ戻し、本番Deepは正常版の式を小さなstripで実行する。
### 要件別の実装証拠
| 計画項目 | 現在の実装 | 検証証拠 |
| --- | --- | --- |
| P0-1 seed打切り | `DIRECT_ACCURATE_SEED_WGSL` が24反復でreason 2へ移行 | kernel hash、source invariant |
| P0-2 平方再利用 | Direct DS、Deep/Fast reference worker、独立verifyで更新後平方を保持 | BigInt 2,000例、DS 600例の旧新全state一致 |
| P0-3 計測 | generation別primary/final/numeric-quiescent/history、submit/sync、UNKNOWN理由を記録 | `waitForPrimary/Final/NumericComplete`、`fieldHashes`、`gpuDiagnostics` |
| P1-1 疎queue | Deep bucket/prefix/scatter/indirectとAccurate Direct-DS compact queue | selected/enqueued/dispatch/processed不変条件、異常時full scan |
| P1-2 補修routing | failure密度によるFAST→Deep、GPU reason tile map、局所reference | source invariant、reason別post-stats |
| P1-3 実体cache | reference prefix/checkpoint、BLA table、同一pending requestを再利用 | key/coalescing/cancel source invariant |
| P1-4 preview再利用 | 同一view/referenceのknown pixelを保持しUNKNOWNだけfinal再計算 | `unknownOnly` とreference key guard |
| P1-5 時間予算 | 24/40 ms予測budget、adaptive strip/burst、queued DS slice | scheduler source invariant |
| P2-1 failure map | GPU tile集計、3 workerのlocal reference並列準備 | recovery経路に全画面meta readbackなし |
| P2-2 BLA replay | 最後の正確anchorからbaseline recurrenceを再開。production既定OFF、明示opt-inのみ | 7 fallback分岐、zero restartなし、kernel hash |
| P2-3 固定費 | lazy pipeline、3-buffer stats readback、bind cache、history role rotation | 1,000回のtexture role model、source invariant |
| P2-4 export | 256/512 tile、2〜3 staging slot、ordered `allSettled`、24 ms yield | export構造回帰、SS1/SS2 smoke hook |
| P2-5 任意精度 | reason 1〜5を画素精度+40/+72 bitで評価して一致時のみpatch。全残存画素をbounded chunkで処理 | worker構文、受理条件source invariant |
| P3-1 active-list | Direct/Deep state、二重queue、indirect compaction、preview→final resume | Direct chunk 1,000例、GPU zero-tolerance shadow hook |
| P3-2 証明付きskip | 厳密実軸対称copy、外向きf32区間によるcardioid/bulb候補 | 画素精度+40/+72 bit oracle shadow、`productionEligible:false` |
`tests/optimization_webgpu_corpus.mjs` は上表の実GPU証拠を収集するdriverである。7 correctness case、4 performance scenario、SS1/SS2 exportに加え、近接2 viewと同一view再訪からなるinteraction stability sequenceを定義する。WGSL error、uncaptured error、device loss、coverage UNKNOWN、最終numeric UNKNOWN、history commit、field hash、Direct/Deep active mismatch、内点oracle mismatchをzero-toleranceで検査し、同一view再訪時のmeta/smooth hash一致も必須とする。numeric recoveryが完了しないcaseも即失敗とする。
## 結論
品質を落とさずに改善できる可能性が高い順序は、次のとおりである。これはprofile前の期待順位であり、Milestone 0のbaseline実測で入れ替える。
1. Accurate の f32 seed を24反復で終了させる。現在は「24未満の脱出だけを採用」するにもかかわらず、未採用ピクセルを `maxIter` または脱出まで計算し、その後DSで0反復目から再計算している。
2. Accurate DSとBigInt参照軌道の反復内で重複計算している更新後の平方を次反復へ引き継ぐ。
3. すでに存在する Deep UNKNOWN のbucket/queue/indirect補正を本番経路へ接続し、全画面DS補正走査をやめる。
4. previewで確定した脱出・解析的内点と、同一prefixの参照軌道をfinalへ引き継ぎ、同じピクセルを0反復目から二度計算しない。
5. 参照軌道とBLA tableを実際の参照点でキャッシュする。同一viewの反復数増加では再利用または延長できるが、近接pan/zoomへの流用は同一view再訪hash gateを通過するまで行わない。
6. 数値未確定の位置特定を全画面GPU→CPU readbackからGPU側tile集計へ移し、reason別にDS、軌道延長、局所referenceを選ぶ。
7. GPUへ先行投入する仕事を「strip本数」ではなく予測GPU時間で制限し、キャンセル済みビューの無駄計算を抑える。
8. 長期的には、一定反復ごとのactive-pixel compactionへ移行し、preview継続、分岐発散削減、細粒度キャンセルを同時に実現する。
最初の3項目は、最終的に実行する数値式を変えず、明白な重複または空振り計算だけを減らせる。特に1項目は小さな変更で高い効果が見込まれるため最初に検証するが、実効果は解析的内点、24反復未満の脱出、DS対象ピクセルの比率に依存する。
この文書は実装監査に基づく提案であり、ここに記載した性能効果はまだ実GPUで測定した結果ではない。既存の386.9 ms削減値もworker modelであり、canvasを含む実端末フレーム時間ではない。
## 目標と非目標
目標は次の3点である。
- 最終フレームに残る数値未確定ピクセルを最小化する。
- 入力から新規画像、最終品質、数値補修完了までの時間と、PNG生成時間を短縮する。
- v24.2.55の解像度、`maxIter`、サンプリング位置、色、数値安全弁を維持し、既知ピクセルをUNKNOWNへ退行させない。
速度のためにDPR、反復数、AA sample数、誤差判定の安全余裕を下げることは対象外とする。また、本ビューアーのexport metadataは現在も `membershipCertified:false` であるため、本計画の「品質不変」は数学的な集合所属証明ではなく、現行baselineとの同等性と高精度oracleに対する非退行を意味する。
## 用語と計測単位
コード上には「計算放棄」という正式な状態名がない。曖昧な集計を避けるため、本計画では次のように分ける。
| 区分 | metadata reason | 本計画での扱い |
| --- | ---: | --- |
| 未書込・coverage hole | 0 | バグ検知指標。最終値は必ず0 |
| 数値未確定 | 1〜5 | 「計算放棄ピクセル」の主指標 |
| 反復上限到達 | 6 | 数値故障ではない。失敗率へ加算しない |
| キャンセル済みビューの計算 | metadata外 | GPUへ投入済みだがpublishされない無駄仕事として別集計 |
| 重複計算 | metadata外 | preview→final、seed→DS、primary→recoveryで同じピクセルを再反復した仕事 |
reason 1〜5は `error-bound`、`escape-uncertain`、`reference-end`、`rebase-gap`、`range` であり、現行の `numericalFailureCount()` と同じ定義である(`index.html:2023-2025`)。reason 6は `maxIter` まで脱出しなかったピクセルで、COLOR passでは不透明な黒になる。数値未確定とは区別する。
ピクセル数だけでは、1反復で失敗したピクセルと12000反復後に捨てたピクセルを区別できない。主な性能指標には必ず「pixel-iterationsまたは実際のkernel steps」も併記する。
## 現行パイプライン
```text
入力
├─ 操作中: stable historyを再投影
└─ 操作確定
├─ backend選択
│ ├─ FAST direct f32 → COLOR → PRESENT
│ ├─ FAST perturbation / optional BLA
│ │ └─ coverage repair + COLOR → stats readback
│ │ └─ background recovery予約 → 一次PRESENT
│ │ └─ 60 ms後にlocal refs
│ │ └─ 残存時full Deep + DS + local refs → recolor
│ ├─ Accurate direct: f32 seed → guarded DS → COLOR → PRESENT
│ └─ Accurate Deep: perturbation → stats readback
│ └─ 条件成立時full-screen guarded DS → local refs
│ └─ COLOR → PRESENT
└─ intended contract: 数値未確定が0の時だけstable historyへcommit
```
品質面では、未完成frontをhistoryへ昇格させず、通常のpan/zoomでは透明な数値未確定部分を直前のstable frameで補うことが意図された契約である。ただし現行Accurate hybridには未確定を0件と仮定する例外があり、後述の共通post-statsで修正する。最適化後は全backendでこの契約を満たす。
## 実装監査で確認した主要ボトルネック
### 1. Accurate seedが「安価なseed」になっていない
`DIRECT_ACCURATE_SEED_WGSL` は脱出反復が24未満のピクセルだけを採用するが、loop自体は `maxIter` まで継続する(`index.html:191-200`)。その後、UNKNOWNピクセルはDSで反復0から再計算される(`index.html:1944-1964`)。
したがって、解析的に証明できない内点は f32で `maxIter` 回、さらにDSで `maxIter` 回計算される。遅く脱出する境界ピクセルも、f32の脱出まで計算した後にDSで再計算される。既存fuzz契約は「f32 escape iteration < 24」のみを採用するため、seedは24反復に達した時点でUNKNOWNとして終了しても最終DS結果は変わらない(`tests/ACCURATE_SEED_FUZZ.json`)。
### 2. 疎なDS補正用GPU queueが存在するが未使用
8 bucketのhistogram、prefix、scatter、indirect dispatch、およびqueued DS kernelはすでに実装されている(`index.html:416-800`)。pipelineとencoder helperも起動時に作成される(`index.html:1820-1827`, `1841-1842`, `1863-1867`)。
一方、本番の `correctUnknownFrame()` は全画面をrow stripでdispatchし、各strip後に同期する(`index.html:1994-2000`)。shader内で既知ピクセルはearly returnするが、全ピクセル分のinvocation、buffer read、分岐とsubmit/waitは残る。「sparse correction」という名前の判定も、実際には全画面走査を呼んでいる。
### 3. previewとfinalが同じ仕事を繰り返す
historyがないFAST viewではpreviewとfinalの2段階を使う(`index.html:1445-1453`)。しかしfinalではmeta/smoothをclearし、全ピクセルを0反復目から再計算する(`index.html:1897-1939`)。参照軌道cache keyにも `iter` が入るため、previewとfinalで軌道も再構築される(`index.html:1490`, `1513`)。
previewで既に脱出したピクセルと解析的に証明された内点は、`maxIter` を増やしても結果が変わらない。同じ参照軌道prefixと同じ数値式を使える場合、これらをfinalで再計算する必要はない。
### 4. 局所補修が全画面readbackと直列処理を繰り返す
`recoverNumericalUnknownTiles()` は各roundでmeta全体をGPUからCPUへ読み戻し、main threadで全画素を走査してtileを作る。最後にも再読込する(`index.html:1986`, `2031-2034`)。その後は、1 tileごとにreference build、GPU correction、stats readbackを順番に待つ。
また「広域failureならfull Deepが安い」とする `shouldPromoteFastToDeep()` が定義されているが、呼ばれていない(`index.html:2026-2028`)。実際のFAST background recoveryはfailure密度にかかわらずlocal recoveryから開始し、残ればfull Deepで画面全体を上書きする(`index.html:2050-2073`)。広域failureでは最初のlocal処理が無駄になる。
### 5. 参照軌道とBLA tableの再利用範囲が狭い
Deep/FAST referenceの再利用は同じ `iter` と完全に同じ `span` を要求する(`index.html:2089-2090`)。しかし参照軌道そのものは参照点 `c_ref` に依存し、表示spanには依存しない。参照点が新viewの有効範囲にあり、精度と長さが足りれば、既存の誤差guardを保ったままzoom後にも利用できる。
BLA tableも参照軌道から作るが、build keyはview座標・寸法込みであり、近接viewでも再構築される(`index.html:1598`, `1674-1679`, `2110-2114`)。さらに `BlaShadowService.cancelPending()` はPromiseを破棄するだけでworkerをterminateしない。同期的な旧viewのtable build中は、新viewの要求がworker queueで待たされる(`index.html:1594-1600`)。
### 6. キャンセルはpublishを止めるが、投入済みGPU仕事は止めない
token checkにより古い結果のpublishは防いでいるが、WebGPU queueへsubmit済みのcommand bufferはキャンセルできない。現行はDeep 2本、FAST 4本、BLA 8本をまとめて投入してから待つ(`index.html:1897-1925`)。目標strip時間はDeep 18 ms、FAST 22 msであるため、BLAでは理論上かなり長い旧view仕事がqueueに残り得る。
`cancelRender()` はJS状態とworker要求を止めるが、GPU queueをpreemptしない(`index.html:2094`)。新viewのcommandは旧viewの仕事の後ろへ並ぶため、無駄計算と入力後latencyの両方に影響する。
### 7. 初期化、bind group、history copy、exportに固定費がある
- 初期化では初期FAST表示に不要なAccurate、Deep、queue、AA pipelineまで一括compileする(`index.html:1811-1834`)。
- stripごとに同じresource構成のbind groupを作り直す(`index.html:1857-1868`)。
- stable history更新はfront texture全体をcopyする(`index.html:1982`)。
- exportは単一readback workspaceでtileを完全直列処理し、各row band後に必ず `requestAnimationFrame` を待つ(`index.html:2276-2289`)。
これらは数値品質に触れずに削減できる固定費である。
### 8. backend間でUNKNOWN集計が一致していない
Directはreason 6をmetadataへ書くがstatsを集計しない。より重要なのは、Accurate hybridのDS kernelが `REASON_RANGE` を出し得る一方、orchestrationは完了後に `state.unresolved=0` と無条件設定する点である(`index.html:248`, `2131-2133`)。この場合、数値未確定を見逃してincomplete frameをhistoryへcommitする可能性がある。
全backendの最終color前に共通post-statsを通し、reason 0、reason 1〜5、reason 6をmetadata実体から集計する。これは最適化効果を測る前提であると同時に、品質契約の穴を塞ぐ変更でもある。
## 期待優先順位付き最適化案
| 期待順位 | 案 | 数値未確定 | 生成時間への期待 | キャンセル無駄 | 品質リスク |
| ---: | --- | --- | --- | --- | --- |
| P0 | Accurate seedを24反復で終了 | 同じ | 大(view依存) | 削減 | 低 |
| P0 | DS/BigIntの更新後平方を再利用 | 同じ | 中〜大 | 削減 | 低 |
| P0 | 計測契約とbaselineを固定 | 判定可能にする | 判定可能にする | 判定可能にする | 低 |
| P1 | UNKNOWN統計をpost-pass化 | 同じ | interior-heavyで削減 | 削減 | 低 |
| P1 | 既存の疎queueでDS補正 | 同じか減少 | 大 | 削減 | 低〜中 |
| P1 | failure密度・reason別routing | 減少 | 大 | 削減 | 低 |
| P1 | reference/BLA cache・延長・同一view再利用 | 減少 | 大 | 削減 | 低〜中 |
| P1 | preview確定結果をfinalで再利用 | 同じ | 大 | 削減 | 低〜中 |
| P1 | in-flight GPU時間を動的制限 | 同じ | 操作時に改善 | 大きく削減 | 低 |
| P2 | GPU failure mapと並列local refs | 減少 | 大 | 削減 | 中 |
| P2 | BLA checkpoint replayとnet-time gate | 同じ | BLA viewで改善 | 削減 | 中 |
| P2 | pipeline/export/resource固定費削減 | 同じ | cold load/export改善 | 小幅削減 | 低 |
| P2 | 残存UNKNOWNだけ任意精度fallback | 最終値を0へ | 少数時のみ有利 | 同じ | 中 |
| P3 | active-pixel continuation/compaction | 同じか減少 | hard viewで最大 | 大きく削減 | 高 |
| P3 | 証明付き内点判定・対称性 | 同じ | 対象viewで改善 | 削減 | 中〜高 |
### P0-1. Accurate seedを `lateThreshold` で打ち切る
`DIRECT_ACCURATE_SEED_WGSL` の各pixelは次のどちらかだけを行う。
- cardioid/period-2 bulbを解析的に証明できれば確定する。
- 最大24回だけf32反復し、24未満で安全に脱出した場合だけ確定する。それ以外は直ちにUNKNOWNとする。
DS passは現在と同じくUNKNOWNを0反復目から計算する。このため最終meta/smoothは現行と同じで、seed側の24回を超える仕事だけが消える。打切りmetadataは後段queueが選べるreason 1〜5(例えば `REASON_ESCAPE_UNCERTAIN`)にし、reason 6やzero sentinelとは区別する。まず単独feature flagで実装し、既存60,000 fuzz、full-frame meta/smooth hash、strip-order testを通す。
### P0-2. DSとBigInt軌道で更新後の平方を再利用する
`DIRECT_DS_CORRECT_WGSL` は、各反復の更新前に `zr²`、`zi²`、`zr*zi` を計算し、更新直後の脱出判定で新しい `zr²` と `zi²` を再計算する。次のloop先頭では同じ新しい平方をもう一度計算する(`index.html:240-246`)。更新後の2平方をstateとして次反復へ渡せば、DS乗算は概ね5回/反復から3回/反復になり、個々の `ds_mul` の入力と演算順は変わらない。
Deep/Fast reference workerの `orbit()`、`orbitEscape()`、`verify()` にも、座標更新用と直後のmag用で同じBigInt平方を二度作る箇所がある(`index.html:1479-1483`, `1503-1506`)。丸め済みの更新後平方を次反復へ持ち越す。各変更は旧/新の軌道配列、脱出反復、meta/smoothをbit比較する。
### P0-3. 先に計測を直す
現行の `setView()` は `dirty || rendering` が終わるまでしか待たず、`refinePending` とbackground numeric recoveryを待たない(`index.html:2303`)。`lastRender` だけを比較すると、preview完了と最終補修完了を取り違える。
テストhookへ次を追加する。
- `waitForPrimary()`、`waitForFinal()`、`waitForNumericComplete()`。
- generationごとのphase時刻: reference、BLA build/probe、primary、coverage+color、stats readback、DS、local refs、full Deep、recolor、history commit。
- reason 0、reason 1〜5の内訳、reason 6をprimary/DS/local/finalの各時点で保存。
- 全backendを共通post-statsへ通し、Accurate hybridで `unresolved=0` を仮定しない。
- kernel steps: direct、perturbation、DS、BLA、replay、およびキャンセルされたgenerationのsteps。
- submit、sync wait、readback回数・bytes・時間、reference/BLA cache hit。
- adapter/browser/実ピクセル数/effective DPR/実効 `maxIter`。
`timestamp-query` が利用可能なadapterではGPU timestampを使い、利用できない場合はwall clockと明記する。累積runtime counterではなく、各generationの差分を記録する。
### P1-1. Deep/Accurate補正を既存の疎queueへ接続する
推奨フローは次のとおりである。
1. primary Deepはpixelごとのglobal atomicを避けるpost-stats variantを使う。
2. histogram passでreason 1〜5だけを8個の開始反復bucketへ集計する。
3. prefixとscatterでUNKNOWN index queueを作る。
4. `dispatchWorkgroupsIndirect` でqueued `correct_pixel()` だけを実行する。
5. queue overflow、invalid index、stale entryが1件でもあれば現行full-scanへfallbackする。
histogram/prefix/scatter基盤はAccurate f32 seed後のDSにも使う。ただし既存queued kernelはDeep referenceを読むperturbation版 `correct_pixel()` であり、絶対座標を使う `DIRECT_DS_CORRECT_WGSL` へそのまま接続してはならない。Direct-DSの数値式をそのまま持つ別のqueued kernelを作る。seedで確定したピクセルはqueueに入らず、DS invocation自体を発生させない。bucket順により、同じ程度の反復で終わるlaneが隣接し、SIMD divergenceも減る。
queue構築は全画面を軽く走査するため、UNKNOWNが非常に多い場合はfull-scanの方が速い可能性がある。adapter別の実測から密度crossoverを決め、queue/full-scanを選ぶ。各backendで元kernelと同じper-pixel arithmeticとoutput semanticsを保ち、出力差を許容しない。
同時に、primary hot loopからpixel単位のglobal atomic統計を外す。現在はFAST/Deep/BLAのUNKNOWN 1 pixelにつき最大2回、同じcounterへatomic addする(`index.html:268-270`, `339-347`, `1335-1339`)。特にreason 6が多いinterior-heavy viewでは、長い反復の最後に競合が集中する。Deepにはatomicを除いた `DEEP_PERTURB_POSTSTATS_WGSL` とworkgroup集約histogramが既にあるため、まずこれを接続し、FAST/BLAにも同型のpost-pass集計を用意する。meta/smoothは変更しない。
### P1-2. failure密度とreasonで補修経路を選ぶ
一次statsを得た直後に次のroutingを行う。
- failureが `max(60000, 8% of pixels)` 以上なら、既存 `shouldPromoteFastToDeep()` を使ってlocal-firstを省き、full Deepへ直行する。
- `error-bound` / `escape-uncertain` はまずqueued DSを使う。
- `reference-end` は「cache prefixが要求iterより短い」場合と「reference自身が脱出して `refLen=escape` になった」場合を分ける。前者だけ同一軌道を延長し、後者は原則として非脱出またはより長く安定する新referenceを選ぶ。脱出後の軌道を無条件に延長しない。
- `range` / `rebase-gap` は失敗領域に近いlocal referenceを優先する。
- reason 0は数値補修へ混ぜず、coverage契約だけで処理する。
各経路で `recovered / attempted` とmsを記録し、近接viewでは前回の成功経路をpriorとして使う。ただし結果が残る場合は現在と同じDeep/DS/local fallbackを最後まで保持する。
### P1-3. 参照軌道とBLA tableを実体ベースで再利用する
reference cache keyを「要求view」から次の実体へ変更する。
- 実際に選択された `referenceRe/referenceIm`。
- precision bits。
- orbitの最長計算済み長さ。
- kernel format/version。
短い `iter` 要求にはprefixを返し、長い要求にはworker内に保持したBigInt状態から延長する。preview→finalで同じ軌道を作り直さない。
参照点選択でも二重計算を避ける。現在は `orbitEscape()` で候補を評価した後、選ばれた非中心候補を `orbit()` でもう一度0から計算する。候補評価時の最良軌道を保持すれば、選択軌道の再走査を省ける。
一方、Deepの独立 `verify()` は通常modeでも外さない。新規orbitは従来どおり検証し、検証済みimmutable orbitをcacheから再利用する時だけ検証結果も再利用する。第一pass中のhashだけへ置き換える案は独立guardの弱体化になるため、別実装の同等以上の検証器を用意できるまで採用しない。
再利用判定では `newSpan === oldSpan` を必須にせず、参照点が新view内または安全margin内にあること、精度が十分なこと、orbit長が足りることを確認する。Deepの誤差bound、reference-end、checkpoint検証は残す。guardが不利と判断した場合だけ新しい参照点を作る。
BLA tableは `reference buffer/hash + refLen + safety + table format version` でcacheする。viewごとに必要なのはtable再構築ではなく、pixel分布に対する採用probeだけである。旧view buildのキャンセルは、ReferenceServiceと同様のworker terminate/recreate、またはchunk化した協調キャンセルで実際のCPU仕事も止める。
### P1-4. previewの確定結果をfinalへ引き継ぐ
最初の段階ではorbit stateを保存せず、次だけを再利用する。
- `FIELD_ESCAPED`: finalの `maxIter` を増やしても最初の脱出反復とsmooth値は変わらない。
- `FIELD_INTERIOR_PROVEN`: classとsmoothは変わらないが、packed iteration部はpreviewの `maxIter` になっている。final前の軽量patch passで `pack_meta(finalIter, FIELD_INTERIOR_PROVEN)` へ更新してから再利用する。
- previewのreason 6: finalで再計算が必要。
- 数値未確定reason 1〜5: finalまたは補修で再計算が必要。
Directはfinal kernelへ `unknownOnly` を加えるだけで実現できる。perturbationでは、previewとfinalが同じ最終長referenceのprefix、同じ参照点、同じpixel latticeを使う場合だけ既知pixelを再利用する。条件が1つでも違えば現行full recomputeへ戻す。
次段階では、未脱出pixelの `z/w/d/scaleExp/reference index/error` をcheckpointとして保存し、preview末尾からfinalを再開する。ただし浮動小数点演算順を変える可能性があるため、これはP3のactive-list方式と同じ厳しい品質gateを通す。
### P1-5. GPU先行投入量を時間予算で制限する
現在の固定 `burstLimit` を、直近stripのGPU時間EMAから求める。
```text
burstLimit = clamp(1, maxBurst, floor(maxInFlightMs / predictedStripMs))
```
初期案として、直近に入力があった時は `maxInFlightMs=24`、十分idleなら40程度から測定を始める。値は品質ではなくsubmit/waitとキャンセルlatencyのtrade-offなので、adapter別に自動調整する。
Directの全画面1 dispatchと高反復exportも同じ時間上限でstrip化する。perturbationだけを細分化しても、Directが長時間queueを占有すれば新viewは待たされる。
追加で、top-to-bottom固定順ではなく、focus座標と画面中心に近いmacro-tileから処理する。キャンセルされた場合でも、ユーザーが見ている領域に使える仕事の割合が増える。部分表示を行う場合はgeneration stamp付きwork textureを使い、旧generationを絶対にpublishしない。
### P2-1. failure mapをGPUで作り、local refsを並列化する
全meta readbackの代わりに、GPUで固定macro-tileごとの次の小さなsummaryを作る。
- reason別件数。
- `sumX/sumY`、min/max座標。
- UNKNOWN index rangeまたはcompact queue offset。
CPUへ戻すのは数KBのsummaryだけにする。必要なlocal referenceは2〜4 workerのpoolで並列構築し、完成したreferenceごとに1 tileずつ待たず、複数queueをまとめてGPUへsubmitする。補正対象も長方形全体ではなく、そのreferenceへ割り当てたUNKNOWN indexだけにする。
近接pan/zoomでは前frameのfailure mapを再投影してcoarse probeの優先候補に使える。ただし最終判定は新frameのguard結果で行う。
### P2-2. BLA fallbackを0反復目restartからcheckpoint replayへ変える
production candidateのBLAは近似chainが不安全になるとbaselineを0反復目から再実行する(`index.html:1320-1323`)。一方、検証用shadow kernelには最後のexact anchorからreplayする実装がある(`index.html:1130-1232`)。このcheckpoint/replayをproductionへ移すと、fallback pixelでBLA以前の仕事を捨てずに済む。
BLA採用条件も、現在の `fallback <= 12.5%` と `operation reduction >= 50%` だけでなく、table build、upload、probe、replayを含む予測net msで決める。cache miss時の固定費が節約時間を上回るviewではbaseline FASTを選ぶ。
品質gateではclassとescape iterationだけでなくsmooth値と最終RGBAも比較する。approximationが不確かなpixelは必ず既存FAST recurrenceへreplay/fallbackし、BLA由来の新しいUNKNOWNを許さない。
### P2-3. 初期化・resource・historyの固定費を減らす
- cold startではPRESENT、Direct、COLORだけを先にcompileし、FAST extended、Accurate、Deep、queue、AA、診断pipelineは必要時またはidle時にwarm upする。
- pipeline/frame/contextごとにbind groupとtexture viewをcacheする。
- 小さなstats readback bufferを毎回create/destroyせずringで再利用する。
- front/back/historyを3 textureのrole rotationにし、history commit時の全texture copyをなくす。
これらはshader結果を変えないため、bit-identical testを要求する。
### P2-4. exportをGPU・readback・PNG圧縮のpipelineにする
現在の単一workspaceを2〜3個のringへ変え、tile N+1のGPU計算、tile Nのmap/copy、前bandのCompressionStream書込を重ねる。PNGのrow順は維持する。
各row band後の無条件 `requestAnimationFrame` は、経過時間に応じたyieldへ変える。操作応答性を守る必要がある時だけyieldし、headless/非操作exportでは不要な1 frame待ちを入れない。tile sizeも512固定ではなく、adapter memory limitと目標in-flight時間から選ぶ。
2x AAは4 sampleを維持する。サンプル削減は、pixel footprint全体が内点であることを証明できる場合以外は行わない。
### P2-5. 最後に残ったUNKNOWNだけを任意精度で解決する
local referenceにはmode別の `maxRefs` 上限があり、予算を使い切るとreason 1〜5が残り得る(`index.html:2029-2034`)。final Accurate/strict exportでは、残存indexだけをCPU workerの固定小数点または疎なGPU limb演算で `maxIter` まで直接評価し、escapedまたは各backendの現行maxIter metadataへpatchする。Deep/Fastはreason 6、Accurate Direct DSは `FIELD_INTERIOR_LIKELY` という違いを保持する。これにより、画面やtile全体を再計算せずに最終numeric UNKNOWNを0へ近づけられる。
これは集合所属の無限反復証明ではない。`maxIter` まで未脱出なら、起点backendの現行分類・metadata policyを変えない。対象が多い場合は先に別reference/full Deepへ戻し、疎な最終手段としてだけ使う。
### P3-1. active-pixel continuationへ移行する
最終的な高反復view向けには、1 pixelを1 dispatchで `maxIter` まで回す方式を次へ置き換える。
1. 64〜256反復のchunkだけ実行する。
2. 脱出・証明済み内点を除き、active pixel indexと継続stateをcompactする。
3. 次chunkはactive queueだけを実行する。
4. preview時点のqueue/stateをfinalへそのまま継続する。
これにより、早期脱出pixelを後続chunkから除外し、workgroup divergenceを減らし、chunk境界でキャンセルできる。Directでは `z`、Deepでは `w/d/scaleExp/reference index/error` が必要になる。二重queueの論理使用量はactive数に比例するが、通常はworst caseとして全 `P` pixel分のbuffer確保が必要であり、メモリ量もKPIと採用条件に含める。
最大の注意点は再開前後で演算順、rescale位置、誤差伝播を変えないことである。baselineとbit-identicalにならない場合は高精度oracleに対する誤差非増加を証明し、既存pathをfallbackとして残す。
### P3-2. 証明できる場合だけ計算を省く
- Deep/FASTでも `c_ref + dc` をdouble-singleまたは区間で再構成し、main cardioid/period-2 bulbの片側安全判定を追加する。
- center imaginary=0かつsample latticeが厳密に鏡映対称なviewでは、実軸対称性を使って片側のmeta/smoothをcopyする。
- 周期検出やtile一括fillは、有限精度のepsilon一致だけで内点扱いしない。区間または収縮証明が成立した時だけ確定する。
素朴なperiodicity checking、境界だけを調べるrectangle fill、色差だけによるadaptive AAは小さい構造を消す可能性があるため採用しない。
## 品質契約
最適化を2種類に分ける。
### 構造だけを変える最適化
seedの早期終了、平方再利用、queue化、post-stats、bind group再利用、export pipelineなど、最終数値式と採用経路を変えない案は次を必須とする。
- `fieldMeta`、`fieldSmooth`、最終RGBAがbaselineとbit-identical。
- known→UNKNOWNが0。
- final reason 0が0。
- 数値未確定を含むframeのhistory commitが0。
- stale generationのpublishが0。
### 数値アルゴリズムを変える最適化
BLA、continuation、証明付き内点判定に加え、選択referenceや補修経路が変わるcache/routingは次を必須とする。同じreference実体をそのまま再利用するだけなら構造変更としてbit比較する。
- baselineで既知のpixelをUNKNOWNへ戻さない。
- baselineとcandidateの双方が既知なら、classはbaselineおよび高精度oracleと一致する。
- baseline UNKNOWNをcandidateがescapedまたはreason 6へ解決する差分は改善として許可し、raw classification mismatchへ加算しない。
- exact同値を主張する経路ではfirst escape iteration mismatchが0。
- BLA derived UNKNOWNとinternal failureが0。
- smooth/RGBAのoracle誤差がbaseline以下。単なる画像平均ではなく最大誤差と差分pixel数を保存する。
- final numeric UNKNOWNは各caseでbaseline以下。baselineが0ならcandidateも0。
- 異常時は同一viewでbaselineへ自動fallbackする。
既存Accurate hybrid記録にはreset viewで2/1200のescape iteration mismatchがあるため、少なくともこれを悪化させず、目標は0とする。SwiftShaderの時間値はcorrectness用の相対参考であり、実GPU性能gateには使わない。
## KPI
画素数を `P = width * height`、reason別件数を `U_r` とする。
- `coverage_hole_ppm = 1e6 * U_0 / P`。hard targetはfinalで0。
- `numeric_unknown_ppm = 1e6 * sum(U_1..U_5) / P`。
- `operation_limit_ppm = 1e6 * U_6 / P`。情報指標であり失敗率ではない。
- `recovery_rate = (U_primary - U_final) / max(1, U_primary)`。
- `recovery_cost_ms_per_1k = (T_recovery_quiescent - T_primary) / recovered_kpixels`。
- `cancelled_work_ratio = cancelled_kernel_steps / all_kernel_steps`。
- `duplicate_work_ratio = repeated_kernel_steps / all_kernel_steps`。
latencyは最低でも次の5点を分ける。
- `T_feedback`: inputから再投影presentまで。
- `T_primary`: 同一viewの最初の新規frameまで。
- `T_final`: finalIter frameのpresentまで。
- `T_recovery_quiescent`: background補修が成功、予算終了、またはfallback完了で停止するまで。数値未確定が残る場合も記録する。
- `T_history_commit`: 数値未確定0のstable frameがhistoryへcommitされるまで。未確定が残って到達しないrunは、打切り時刻でcensored dataとして扱う。
p50、p95、maxを保存し、reference build、BLA build/probe、primary、DS、local refs、readback、color、export compressionの内訳も残す。
## ベンチマーク行列
### correctness corpus
- reset: center `(-0.5, 0)`, span `3.4`。
- seahorse-low: `(-0.743643887037151, 0.13182590420533)`, span `0.1`。
- cusp-low: `(0.25, 0)`, span `0.85`。
- classic Seahorse deep: span `3.4e-13`, 2900 iterations。
- BLAが有利なviewと、BLA probeがOFFになるSeahorse/Misiurewicz hard view。
- `directPixelRatio` の4直前/直後、32直前/直後。
- reason 1〜5をそれぞれ誘発する固定seed。
- 実軸対称、interior-heavy、boundary-heavy、reference-end-heavyの各view。
### performance corpus
- 320×220 / 750 iter: 既存Accurate比較の継続用。
- 654×690 / 2900 iter: 既存BLA prep scenarioとの接続用。
- 1920×1080、および対象端末で可能なら4K。
- 900 / 1500 / 3000 / 6000 / 12000 iter。
- cold load、warm same view、same-span pan、small zoom、distant jump。
- FAST/Accurate、BLA forced on/off、連続pan/zoomのcancel storm。
- export SS1/SS2、balanced/strict。
`processMode` はpixel budgetとiteration係数を同時に変えるため、異なるmodeのraw msを直接比較しない。必ず実ピクセル数、DPR、iterを記録する。
実機は低電力integrated、現行integrated、discrete GPUを最低構成とし、SwiftShaderはcorrectnessだけに使う。各caseはwarmup後にbaseline/candidateを同一sessionで交互に10〜20回実行し、medianとp95を比較する。
## 合格基準
### hard quality/safety gate
- 全caseでfinal reason 0 = 0。
- known→UNKNOWN、stale publish、不完全history commit = 0。
- baseline既知pixelのknown→UNKNOWN = 0。双方既知pixelのclass mismatch = 0。
- baseline UNKNOWNの解決は許可し、その結果が高精度oracleと矛盾しない。
- exact経路のescape iteration mismatch = 0。
- final numeric UNKNOWNは全caseでbaseline以下。strict/validate golden corpusの目標は0。
- reference checkpoint mismatch、strip/full mismatch、WGSL error、uncaptured error、device loss = 0。
- operation-limitは別集計し、数値未確定削減として数えない。
### performance gateの初期値
以下は実測後に調整する導入基準であり、達成値ではない。
- 対象hard viewの `T_primary` と `T_recovery_quiescent` のp50を20%以上、p95を15%以上短縮。`T_history_commit` 到達runも別集計で非退行とする。
- 非対象viewでp50 +5%、p95 +10%を超える退行を出さない。
- failure corpusのprimary numeric UNKNOWN中央値を50%以上、finalを90%以上削減し、各caseでは非増加。
- BLA enabled viewはbuild+probe込みでbaselineより10%以上速い。BLA rejected/offの追加費用は5%または8 ms以下。
- 1 burstのp95はFAST 25 ms以下、Deep 22 ms以下、max 50 ms以下を初期目標とする。
- readback bytes、readback回数、correction sync waitsはbaseline以下。
## 実装ロードマップ
### Milestone 0: 観測とbaseline固定
1. generation単位のmetricsと3種類のUNKNOWN集計を追加する。
2. `waitForFinal()` と `waitForNumericComplete()` をテストhookへ追加する。
3. meta/smooth/RGBAのgolden artifactと操作traceを作る。
4. 実GPUでcold/warm/cancel/export baselineを採る。
### Milestone 1: 小変更で重複仕事を除く
1. Accurate seedを24反復で終了する。
2. Accurate DSとreference workerで更新後平方を再利用する。
3. 全backendのUNKNOWNを共通post-statsで集計し、Deep/FAST/BLA primaryのpixel単位atomicを外す。
4. `shouldPromoteFastToDeep()` を実経路へ接続する。
5. reference/BLA cache keyを実体ベースへ変更し、旧BLA buildを実際にキャンセルする。
6. stats readback bufferとbind groupを再利用する。
各変更を単独flagでA/Bし、bit-identicalと性能gateを確認する。
### Milestone 2: 疎queueとpreview再利用
1. Deep補正を既存bucket/indirect queueへ接続する。
2. 同じcompaction基盤と新しいDirect-DS queued kernelをAccurate seed後へ接続する。
3. previewのknown pixelと最終長reference prefixをfinalへ引き継ぐ。
4. in-flight時間budgetを導入する。
queue異常時は現行full-scan、reuse条件不成立時は現行full recomputeへ戻す。
### Milestone 3: recoveryとBLA/exportのpipeline化
1. GPU failure summaryとUNKNOWN index queueを実装する。
2. local reference worker poolとbatched correctionを実装する。
3. production BLAへcheckpoint replayとnet-time gateを移す。
4. export staging ringとtime-based yieldを実装する。
5. strict/export向けの疎な任意精度最終fallbackを実装する。
### Milestone 4: active-list engine
chunk continuation、compaction、証明付き内点判定、厳密対称性をshadow-onlyで導入する。golden full-frame比較、複数adapter canary、kill switchを通過したbackendだけを標準へ昇格する。
## 展開とrollback
- 新経路はすべてfeature flag既定OFFから開始する。
- 数値kernel/BLA変更は最初にshadow-onlyでbaselineと同時計算する。
- adapter/browser bucket単位で10%→25%→50%→100%へ段階展開する。
- hard quality gateはzero toleranceとし、1件でも違反したbucketは即baselineへ戻す。
- default化後も旧path、kill switch、kernel hashを最低1 release残す。
- mismatchまたは新しいUNKNOWN reasonを起こしたviewは座標、span、iter、adapter情報と共にgolden corpusへ追加する。
## 採用しない近道
次は生成時間を短く見せられるが、品質不変の条件に反するため採用しない。
- adaptive `maxIter`、DPR、export解像度、AA sample数を下げる。
- error boundやBLA safetyを緩め、失敗を内点または脱出扱いする。
- reason 6を「計算放棄」と数え、黒のまま補修済みと見なす。
- 再投影画像の色が近いという理由だけで、新viewの数値metaを再利用する。
- 未証明のperiodicity、rectangle border fill、色差ベースAAで内部sampleを省く。
- BLA/近似経路からbaseline fallbackとgeneration tokenを外す。
この順序なら、まず明白な二重計算と未使用の疎処理を取り除き、その後に参照選択とGPU schedulingを改善できる。画質を守る責務は各最適化のfallbackと自動比較に持たせ、速度と数値未確定率のどちらか一方だけを改善したように見せない。

31
docs/README.md Normal file
View file

@ -0,0 +1,31 @@
# Mandelbrot WebGPU v24.2.55
## 目的
v24.2.54で確認された、FAST深部描画の矩形状/穴状の黒欠損と、数値補修を最終表示前に待つことによるレイテンシ増大を修正する。
## 黒穴の原因
COLOR passでは数値UNKNOWNを透明としていたが、PRESENT passが履歴補完を `scale <= 1.035 / offset <= 0.022` に制限していた。そのため通常のズームでも履歴補完が無効になり、透明UNKNOWNがcanvasの黒背景へ落ちた。
さらに、その不完全front textureを完成historyとしてcommitする経路があった。局所reference補修が予算内で全UNKNOWNを回収できない場合、タイル境界に沿った黒欠損が完成画像へ固定され得た。
## v24.2.55の契約
1. 数値UNKNOWNを含むfrontはstable historyへ昇格しない。
2. 通常のパン/ズーム範囲では、数値UNKNOWNを直前のstable frameから再投影して埋める。
3. FASTの数値補修は一次画像のpublishをブロックせず、同一viewのbackground taskとして継続する。
4. background補修が完了して数値UNKNOWNが0になった時点で初めてhistoryを更新する。
5. tiled FAST開始時にmeta/smoothをzero sentinelへ初期化し、最終color前にreason=0の未書込pixelだけbaseline FASTで再計算する。
`REASON_OPERATION_LIMIT` は数値故障ではなく、maxIterまで未脱出だったpixelなので従来どおり黒表示する。今回の矩形状欠損対策は主に数値UNKNOWN/未書込pixelを対象とする。
## FASTレイテンシ
production BLAのCPU側64点shadow計算を廃止した。BLA workerはtable構築だけを行い、採用判定は既存の64点GPU probeへ一本化した。
classic Seahorse Valley相当 (654x690, span 3.4e-13, 2900 iter) のNode/V8 workerモデル:
- 旧 CPU validate 64点: 約446.9 ms
- 新 table build only: 約60.0 ms
- 同期prep削減: 約386.9 ms
BLAが不利なviewでは本体FAST計算時間そのものは残る。今回の変更では、数値補修Deep/multi-referenceを一次表示のクリティカルパスから外すことで、黒穴を出さずに操作から画像表示までの待ち時間を短縮する。
## 数値コア
v24.2.54から変更したproduction WGSLはFAST_PERTURB_WGSLのみ。通常 `unknownOnly=0` の計算式・perturbation recurrenceは変更していない。追加分はcoverage repair用のearly guardである。

75
docs/VALIDATION_REPORT.md Normal file
View file

@ -0,0 +1,75 @@
# Validation Report — v24.2.55
## 1. JavaScript / bundle syntax
- kernel bundle: PASS
- application bundle: PASS
## 2. production kernel差分
v24.2.54とのSHA-256比較。
変更:
- `version`
- `FAST_PERTURB_WGSL`
不変:
- Direct
- Accurate seed / DS direct
- Deep perturbation
- Deep correction
- COLOR
- PRESENT shader本体
- BLA candidate / production frame kernels
FAST_PERTURBの通常recurrenceは不変。`unknownOnly=2` 時だけzero-sentinel pixelを再計算するearly guardを追加。
## 3. coverage contract
PASS:
- tiled numeric開始時にmeta/smoothをzero clear
- color前にbaseline FAST coverage repair
- coverage repairはreason=0 UNKNOWNだけをiteration
- 既計算pixelはearly return
これにより、strip/tile未書込が発生してもrectangular zero-metadata holeをそのままpublishしない。
## 4. temporal fill / history
PASS:
- history利用範囲をstable reprojectionと同じ `2.25 / 0.45` へ統一
- numeric recovery pending/running時はstable historyへcommitしない
- recolor経路も `numericFrameComplete()` を満たさない限りhistoryへcommitしない
## 5. background numeric recovery
PASS (static/control-flow):
- FAST primary終了後に数値failureがあれば60ms後にbackground recoveryを予約
- local multi-referenceを先行
- 残存時だけfull Deep + DS + local recovery
- view tokenが変われば結果をpublishしない
- recovery中のrecolorはqueueへ退避
## 6. BLA production prep
classic Seahorse Valley相当のworker-model実測:
- 旧64-point CPU validation: 446.9 ms
- 新build-only: 60.0 ms
- 同期prep削減: 386.9 ms
BLA table buildそのものはほぼ同時間 (58.4 vs 59.4 ms)。削減分は描画前CPU shadow sample計算。
## 7. BLA safety regression
既存289-point Seahorse test、safety=1/64:
- classification mismatch: 0
- first escape mismatch: 0
productionではCPU per-view validationを外したが、64-point GPU probeとBLA pixel fallback契約は維持。
## 8. browser smoke
Chromiumでlocalhostからロード:
- page JavaScript error: 0
- kernelVersion: `24.2.55-no-black-hole-async-recovery`
このコンテナでは当該起動条件でWebGPU adapterを取得できなかったため、canvasを含む実端末フレーム時間は未測定。
## 9. release layout
- executable HTML: `index.html` の1本のみ
- `index.baseline-*.html`: なし
## 残る性能課題
BLA probeがOFFになるSeahorse/Misiurewicz型viewではbaseline FASTのpixel-iteration量が依然支配的。v24.2.55はこのケースのCPU prepと同期補修待ちを削るが、primary perturbation本体を別アルゴリズムで高速化する変更ではない。

View file

@ -1,598 +0,0 @@
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x000fffffu;
const REASON_SHIFT:u32=20u;
const REASON_MASK:u32=0x0ff00000u;
const REASON_NONE:u32=0u;
const REASON_ERROR_BOUND:u32=1u;
const REASON_ESCAPE_UNCERTAIN:u32=2u;
const REASON_REFERENCE_END:u32=3u;
const REASON_REBASE_GAP:u32=4u;
const REASON_RANGE:u32=5u;
const REASON_OPERATION_LIMIT:u32=6u;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
fn pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Benchmark candidate: whole-frame double-single direct iteration.
// This intentionally has no CPU reference-build cost. It is not selected by
// the production router until the crossover benchmark justifies it.
const DIRECT_DS_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b; let e=b-(q-a); return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah;
let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh;
var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b);
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);
let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var n=0u;
loop{
if(n>=p.maxIter){break;}
z=cds_add(cds_mul(z,z),c); n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
// can be re-evaluated by the existing deep double-single correction pass.
// This shader is benchmark-only and is not selected by the production router.
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
struct HybridStats{
remaining:atomic<u32>, _a:atomic<u32>, _b:atomic<u32>, _c:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);}
fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;
let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh;
var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
fn ds_cmp(a:DS,b:DS)->i32{if(a.h<b.h){return -1;}if(a.h>b.h){return 1;}if(a.l<b.l){return -1;}if(a.l>b.l){return 1;}return 0;}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn mark_sensitive(out:u32,n:u32){
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;atomicAdd(&hybridStats.remaining,1u);
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var deriv=vec2<f32>(0.0);var n=0u;
loop{
if(n>=p.maxIter){break;}
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
z=cds_add(cds_mul(z,z),c);n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
}
`;
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct UnresolvedHead{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
return;
}
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=1.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=refs[m];
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
// perturbation. It is a visual-quality pass, not a membership certificate.
const DEEP_CORRECT_WGSL=COMMON+String.raw`
const CORRECTION_MARK:u32=128u;
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
fn count_remaining(out:u32){
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
}
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
}
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b;
let e=b-(q-a);
return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b;
let bb=q-a;
let e=(a-(q-bb))+(b-bb);
return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a;
let ah=ca-(ca-a);
let al=a-ah;
let cb=4097.0*b;
let bh=cb-(cb-b);
let bl=b-bh;
var e=ah*bh-q;
e=e+ah*bl;
e=e+al*bh;
e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h);
return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l;
e=e+a.l*b.h;
e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b);
return ds_quick(q.h,q.l+a.l*b);
}
fn ds_pow2(a:DS,e:i32)->DS{
if(e < -126){return DS(0.0,0.0);}
if(e > 126){return DS(8.507059e37,0.0);}
return DS(ldexp(a.h,e),ldexp(a.l,e));
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
loop{
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
if(m>p.refLen){count_remaining(out);return;}
let rp=refs[m];
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
let delta=cds_pow2(w,scaleExp);
let z=cds_add(r,delta);
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
return;
}
let dmag=cds_mag2(delta);
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
continue;
}
if(m>=p.refLen){count_remaining(out);return;}
let linear=cds_scale(cds_mul(r,w),2.0);
let sq=cds_pow2(cds_mul(w,w),scaleExp);
w=cds_add(cds_add(linear,sq),d);
m+=1u; n+=1u; operations+=1u;
let mm=max(cds_maxabs(w),cds_maxabs(d));
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
const COLOR_WGSL=String.raw`
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
if(p.edgeAA!=0u){
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
let x=i32(gid.x); let y=i32(gid.y);
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
`;
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.2.6-hybrid-ui-cacheproof',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
});
})();

View file

@ -1,792 +0,0 @@
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x000fffffu;
const REASON_SHIFT:u32=20u;
const REASON_MASK:u32=0x0ff00000u;
const REASON_NONE:u32=0u;
const REASON_ERROR_BOUND:u32=1u;
const REASON_ESCAPE_UNCERTAIN:u32=2u;
const REASON_REFERENCE_END:u32=3u;
const REASON_REBASE_GAP:u32=4u;
const REASON_RANGE:u32=5u;
const REASON_OPERATION_LIMIT:u32=6u;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
fn pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Benchmark candidate: whole-frame double-single direct iteration.
// This intentionally has no CPU reference-build cost. It is not selected by
// the production router until the crossover benchmark justifies it.
const DIRECT_DS_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b; let e=b-(q-a); return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah;
let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh;
var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b);
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);
let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var n=0u;
loop{
if(n>=p.maxIter){break;}
z=cds_add(cds_mul(z,z),c); n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
// can be re-evaluated by the existing deep double-single correction pass.
// This shader is benchmark-only and is not selected by the production router.
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
struct HybridStats{
remaining:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
@group(0) @binding(4) var<storage,read_write> riskQueue:array<u32>;
fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);}
fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;
let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh;
var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
fn ds_cmp(a:DS,b:DS)->i32{if(a.h<b.h){return -1;}if(a.h>b.h){return 1;}if(a.l<b.l){return -1;}if(a.l>b.l){return 1;}return 0;}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii);
}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn mark_sensitive(out:u32,n:u32){
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;
let slot=atomicAdd(&hybridStats.remaining,1u);
let capacity=p.tileW*p.tileH;
if(slot<capacity){
riskQueue[slot]=out;
atomicAdd(&hybridStats.enqueued,1u);
}else{
atomicAdd(&hybridStats.overflow,1u);
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
var deriv=vec2<f32>(0.0);var n=0u;
loop{
if(n>=p.maxIter){break;}
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
z=cds_add(cds_mul(z,z),c);n+=1u;
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
}
`;
// Converts the GPU-written risk queue length into indirect dispatch arguments.
// The correction pass therefore launches one lane per enqueued risky pixel,
// instead of one lane per screen pixel.
const DIRECT_FIXED96_QUEUE_ARGS_WGSL=COMMON+String.raw`
struct HybridStats{
remaining:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 };
@group(0) @binding(0) var<storage,read_write> hybridStats:HybridStats;
@group(0) @binding(1) var<storage,read_write> indirectArgs:IndirectArgs;
@compute @workgroup_size(1)
fn main(){
let n=atomicLoad(&hybridStats.enqueued);
atomicStore(&hybridStats.dispatchCount,n);
indirectArgs.x=(n+63u)/64u;
indirectArgs.y=1u;
indirectArgs.z=1u;
indirectArgs._pad=0u;
}
`;
// Diagnostic sparse fixed-point direct correction. It does not consume a
// deep reference. The guarded DS pass marks uncertain pixels with reason 7;
// this shader re-evaluates only those pixels using signed Q8.88 arithmetic
// implemented entirely with u32 limbs. Integer arithmetic avoids the
// reassociation/fusion sensitivity of float-expansion correction kernels.
const DIRECT_FIXED96_CORRECT_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, maxIter:u32, _flags:u32,
baseReLo:u32, baseReMid:u32, baseReHi:u32,
baseImLo:u32, baseImMid:u32, baseImHi:u32,
stepLo:u32, stepMid:u32, stepHi:u32,
_pad0:u32, _pad1:u32, _pad2:u32,
};
struct FX96{ lo:u32, mid:u32, hi:u32 };
struct FixedStats{
remaining:atomic<u32>, invalidIndex:atomic<u32>, processed:atomic<u32>, staleEntry:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
struct HybridStats{
remaining:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(3) var<storage,read_write> fixedStats:FixedStats;
@group(0) @binding(4) var<storage,read_write> hybridStats:HybridStats;
@group(0) @binding(5) var<storage,read> riskQueue:array<u32>;
fn fx_add(a:FX96,b:FX96)->FX96{
let lo=a.lo+b.lo; let c0=select(0u,1u,lo<a.lo);
let m0=a.mid+b.mid; let c1=select(0u,1u,m0<a.mid);
let mid=m0+c0; let c2=select(0u,1u,mid<m0);
return FX96(lo,mid,a.hi+b.hi+c1+c2);
}
fn fx_neg(a:FX96)->FX96{
let lo=(~a.lo)+1u; let c0=select(0u,1u,lo==0u);
let mid=(~a.mid)+c0; let c1=select(0u,1u,c0!=0u && mid==0u);
return FX96(lo,mid,(~a.hi)+c1);
}
fn fx_sub(a:FX96,b:FX96)->FX96{return fx_add(a,fx_neg(b));}
fn fx_negative(a:FX96)->bool{return (a.hi&0x80000000u)!=0u;}
fn fx_abs(a:FX96)->FX96{if(fx_negative(a)){return fx_neg(a);}return a;}
fn mul32(a:u32,b:u32)->vec2<u32>{
let a0=a&0xffffu; let a1=a>>16u; let b0=b&0xffffu; let b1=b>>16u;
let p0=a0*b0; let p1=a0*b1; let p2=a1*b0; let p3=a1*b1;
let middle=(p0>>16u)+(p1&0xffffu)+(p2&0xffffu);
let lo=(p0&0xffffu)|(middle<<16u);
let hi=p3+(p1>>16u)+(p2>>16u)+(middle>>16u);
return vec2<u32>(lo,hi);
}
fn add_at(a:ptr<function,array<u32,6>>,idx0:u32,value0:u32){
var idx=idx0; var value=value0;
loop{
if(value==0u || idx>=6u){break;}
let old=(*a)[idx]; let sum=old+value; (*a)[idx]=sum;
value=select(0u,1u,sum<old); idx+=1u;
}
}
fn add_product(a:ptr<function,array<u32,6>>,idx:u32,x:u32,y:u32){
let q=mul32(x,y); add_at(a,idx,q.x); add_at(a,idx+1u,q.y);
}
fn fx_mul(a0:FX96,b0:FX96)->FX96{
let neg=fx_negative(a0)!=fx_negative(b0); let a=fx_abs(a0); let b=fx_abs(b0);
var w=array<u32,6>(0u,0u,0u,0u,0u,0u);
add_product(&w,0u,a.lo,b.lo); add_product(&w,1u,a.lo,b.mid); add_product(&w,2u,a.lo,b.hi);
add_product(&w,1u,a.mid,b.lo); add_product(&w,2u,a.mid,b.mid);add_product(&w,3u,a.mid,b.hi);
add_product(&w,2u,a.hi,b.lo); add_product(&w,3u,a.hi,b.mid); add_product(&w,4u,a.hi,b.hi);
// Q8.88 * Q8.88 -> Q16.176. Shift right by 88 with one-bit rounding.
var r=FX96((w[2]>>24u)|(w[3]<<8u),(w[3]>>24u)|(w[4]<<8u),(w[4]>>24u)|(w[5]<<8u));
if(((w[2]>>23u)&1u)!=0u){r=fx_add(r,FX96(1u,0u,0u));}
if(neg){return fx_neg(r);}return r;
}
fn fx_mul_small(a0:FX96,k:u32)->FX96{
let neg=fx_negative(a0); let a=fx_abs(a0);
let q0=mul32(a.lo,k); let q1=mul32(a.mid,k); let q2=mul32(a.hi,k);
let mid=q1.x+q0.y; let cm=select(0u,1u,mid<q1.x);
let carry=q1.y+cm; let hi=q2.x+carry;
let r=FX96(q0.x,mid,hi); if(neg){return fx_neg(r);}return r;
}
fn fx_cmp_unsigned(a:FX96,b:FX96)->i32{
if(a.hi<b.hi){return -1;} if(a.hi>b.hi){return 1;}
if(a.mid<b.mid){return -1;} if(a.mid>b.mid){return 1;}
if(a.lo<b.lo){return -1;} if(a.lo>b.lo){return 1;} return 0;
}
fn fx_to_f32_positive(a:FX96)->f32{
return f32(a.hi)*5.960464477539063e-8 + f32(a.mid)*1.3877787807814457e-17 + f32(a.lo)*3.2311742677852644e-27;
}
fn fx_to_f32(a:FX96)->f32{
if(fx_negative(a)){return -fx_to_f32_positive(fx_neg(a));}
return fx_to_f32_positive(a);
}
fn write_escape(out:u32,n:u32,zr:FX96,zi:FX96){
// Once either component exceeds 2 in magnitude, escape is mathematically
// certain. Use f32 only for the smooth-color estimate; membership remains
// decided by the integer fixed-point state. This avoids squaring a large
// escaped value and therefore avoids Q8.88 wraparound.
let zrf=fx_to_f32(zr); let zif=fx_to_f32(zi);
let mag=max(4.0000005,zrf*zrf+zif*zif);
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,mag);
atomicAdd(&fixedStats.corrected,1u);
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let qi=gid.x;
let queued=atomicLoad(&hybridStats.dispatchCount);
if(qi>=queued){return;}
atomicAdd(&fixedStats.processed,1u);
let out=riskQueue[qi];
let capacity=p.tileW*p.tileH;
if(out>=capacity){
atomicAdd(&fixedStats.invalidIndex,1u);
atomicAdd(&fixedStats.remaining,1u);
return;
}
let old=fieldMeta[out];
let cls=(old>>28u)&3u; let reason=(old&REASON_MASK)>>REASON_SHIFT;
if(cls!=FIELD_UNKNOWN || reason!=7u){
atomicAdd(&fixedStats.staleEntry,1u);
atomicAdd(&fixedStats.remaining,1u);
return;
}
let px=out%p.tileW; let py=out/p.tileW;
let baseRe=FX96(p.baseReLo,p.baseReMid,p.baseReHi);
let baseIm=FX96(p.baseImLo,p.baseImMid,p.baseImHi);
let step=FX96(p.stepLo,p.stepMid,p.stepHi);
let cr=fx_add(baseRe,fx_mul_small(step,px));
let ci=fx_sub(baseIm,fx_mul_small(step,py));
var zr=FX96(0u,0u,0u); var zi=FX96(0u,0u,0u); var n=0u;
let TWO=FX96(0u,0u,0x02000000u);
let FOUR=FX96(0u,0u,0x04000000u);
loop{
if(n>=p.maxIter){break;}
// The previous state is inside the bailout disk, so these products are
// bounded and cannot overflow Q8.88.
let zr2=fx_mul(zr,zr); let zi2=fx_mul(zi,zi); let zri=fx_mul(zr,zi);
let nextR=fx_add(fx_sub(zr2,zi2),cr);
let nextI=fx_add(fx_add(zri,zri),ci);
zr=nextR; zi=nextI; n+=1u;
// Do not mistake the Q8.88 integer byte for sign-extension. Values such
// as +1.2 are valid. The old range guard rejected them and left virtually
// every risky escaping pixel UNKNOWN.
if(fx_cmp_unsigned(fx_abs(zr),TWO)>0 || fx_cmp_unsigned(fx_abs(zi),TWO)>0){
write_escape(out,n,zr,zi); return;
}
let rr=fx_mul(zr,zr); let ii=fx_mul(zi,zi); let mag=fx_add(rr,ii);
if(fx_cmp_unsigned(mag,FOUR)>0){
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
fieldSmooth[out]=smooth_escape(n,max(4.0000005,fx_to_f32_positive(mag)));
atomicAdd(&fixedStats.corrected,1u); return;
}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
atomicAdd(&fixedStats.corrected,1u);
}
`;
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct UnresolvedHead{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32,reason:u32){
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-p.refPixelX)/f32(p.fullW);
let dy=(p.refPixelY-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
return;
}
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=1.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
let r=refs[m];
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
// perturbation. It is a visual-quality pass, not a membership certificate.
const DEEP_CORRECT_WGSL=COMMON+String.raw`
const CORRECTION_MARK:u32=128u;
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct CorrectionStats{
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
};
struct DS{ h:f32, l:f32 };
struct CDS{ r:DS, i:DS };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
fn count_remaining(out:u32){
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
atomicAdd(&stats.remaining,1u);
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
}
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
}
fn ds_quick(a:f32,b:f32)->DS{
let q=a+b;
let e=b-(q-a);
return DS(q,e);
}
fn ds_sum(a:f32,b:f32)->DS{
let q=a+b;
let bb=q-a;
let e=(a-(q-bb))+(b-bb);
return DS(q,e);
}
fn ds_prod(a:f32,b:f32)->DS{
let q=a*b;
let ca=4097.0*a;
let ah=ca-(ca-a);
let al=a-ah;
let cb=4097.0*b;
let bh=cb-(cb-b);
let bl=b-bh;
var e=ah*bh-q;
e=e+ah*bl;
e=e+al*bh;
e=e+al*bl;
return DS(q,e);
}
fn ds_add(a:DS,b:DS)->DS{
let q=ds_sum(a.h,b.h);
return ds_quick(q.h,q.l+(a.l+b.l));
}
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
fn ds_mul(a:DS,b:DS)->DS{
let q=ds_prod(a.h,b.h);
var e=q.l+a.h*b.l;
e=e+a.l*b.h;
e=e+a.l*b.l;
return ds_quick(q.h,e);
}
fn ds_scale(a:DS,b:f32)->DS{
let q=ds_prod(a.h,b);
return ds_quick(q.h,q.l+a.l*b);
}
fn ds_pow2(a:DS,e:i32)->DS{
if(e < -126){return DS(0.0,0.0);}
if(e > 126){return DS(8.507059e37,0.0);}
return DS(ldexp(a.h,e),ldexp(a.l,e));
}
fn ds_cmp(a:DS,b:DS)->i32{
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
}
fn ds_value(a:DS)->f32{return a.h+a.l;}
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
fn cds_mul(a:CDS,b:CDS)->CDS{
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
return CDS(rr,ii);
}
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
fn correct_pixel(out:u32,gx:f32,gy:f32){
let invW=DS(p.invFullWHi,p.invFullWLo);
let sm=DS(p.spanMantHi,p.spanMantLo);
let ox=gx-p.refPixelX;
let oy=p.refPixelY-gy;
let dx=ds_scale(invW,ox);
let dy=ds_scale(invW,oy);
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
var d=d0;
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
loop{
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
if(m>p.refLen){count_remaining(out);return;}
let rp=refs[m];
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
let delta=cds_pow2(w,scaleExp);
let z=cds_add(r,delta);
let mag=cds_mag2(z);
if(ds_cmp(mag,DS(4.0,0.0))>0){
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
return;
}
let dmag=cds_mag2(delta);
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
continue;
}
if(m>=p.refLen){count_remaining(out);return;}
let linear=cds_scale(cds_mul(r,w),2.0);
let sq=cds_pow2(cds_mul(w,w),scaleExp);
w=cds_add(cds_add(linear,sq),d);
m+=1u; n+=1u; operations+=1u;
let mm=max(cds_maxabs(w),cds_maxabs(d));
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=p.outputBase+gid.y*p.outputStride+gid.x;
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
correct_pixel(out,gx,gy);
}
`;
const COLOR_WGSL=String.raw`
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
if(p.edgeAA!=0u){
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
let x=i32(gid.x); let y=i32(gid.y);
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
`;
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.2.8-fixed96-sparse-queue',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DIRECT_FIXED96_QUEUE_ARGS_WGSL,DIRECT_FIXED96_CORRECT_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
});
})();

View file

@ -1,11 +0,0 @@
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
X-Content-Type-Options: nosniff
/wasm/*
Content-Type: application/wasm
Cache-Control: public, max-age=31536000, immutable
/hosted/*
Cache-Control: no-cache

View file

@ -1,36 +0,0 @@
const wasmRoot = new URL('../wasm/', import.meta.url);
async function compileAsset(name) {
const url = new URL(name, wasmRoot);
const response = await fetch(url, { cache: 'force-cache' });
if (!response.ok) throw new Error(`WASM fetch failed: ${name} (${response.status})`);
if (WebAssembly.compileStreaming) {
try { return await WebAssembly.compileStreaming(Promise.resolve(response.clone())); }
catch { /* A proxy may have supplied the wrong MIME type; use bytes below. */ }
}
return WebAssembly.compile(await response.arrayBuffer());
}
let shallowModule, shallowSimd = true;
try { shallowModule = await compileAsset('wasm-simd.wasm'); }
catch { shallowModule = await compileAsset('wasm-scalar.wasm'); shallowSimd = false; }
const asset = name => new URL(name, wasmRoot).href;
globalThis.MANDEL_KERNELS = Object.freeze({
// The compiled module is structured-cloned to the shallow Worker. No Base64
// conversion or second compile is needed in the hosted build.
WASM_SIMD_B64: shallowModule,
WASM_SCALAR_B64: shallowModule,
DEEP_SIMD_B64: asset('deep-simd.wasm'),
DEEP_SCALAR_B64: asset('deep-scalar.wasm'),
BLA_SIMD_B64: asset('bla-simd.wasm'),
BLA_SCALAR_B64: asset('bla-scalar.wasm'),
COLOR_SIMD_B64: asset('color-simd.wasm'),
COLOR_SCALAR_B64: asset('color-scalar.wasm')
});
globalThis.MANDEL_KERNEL_META = Object.freeze(/*__KERNEL_META__*/{});
globalThis.MANDEL_HOSTED_SHALLOW_SIMD = shallowSimd;
const app = document.createElement('script');
app.src = './script.js';
document.body.appendChild(app);

View file

@ -1,93 +0,0 @@
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#050813">
<title>Mandelbrot Deep Zoom v24.2.8 Fixed96 Sparse Queue Experiment</title>
<style>
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}#palette{color:#111;background:#f4f5f8}#palette option{color:#111;background:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.exact-grid{display:grid;grid-template-columns:54px 1fr;gap:6px;margin-top:8px}.exact-grid input{min-width:0;width:100%;min-height:44px;border:1px solid var(--line);border-radius:8px;background:#070c19;color:var(--text);padding:6px;font:11px ui-monospace,monospace}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.diagnostics{margin-top:8px;color:var(--muted);font:10.5px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.exact-grid input{user-select:text;-webkit-user-select:text}
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}.benchmark-output{width:100%;min-height:180px;margin-top:10px;border:1px solid var(--line);border-radius:9px;background:#070c19;color:var(--text);padding:8px;font:10.5px/1.45 ui-monospace,monospace;resize:vertical}.benchmark-note{font-size:11px;line-height:1.5;color:var(--muted);margin-top:8px}
#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord">…</span></div><div class="row"><span class="muted">倍率</span> <span id="zoom">…</span> <span class="muted">表示幅</span> <span id="span">…</span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render">…</span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大">+</button><button id="zout" aria-label="中心を縮小">−</button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0.001" max="0.05" step="0.0005" value="0.008"><output id="cycleO" for="cycle">0.0080</output></div>
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
<details><summary>詳細設定・正確な座標</summary>
<div class="line"><label for="iters">基準反復</label><input id="iters" type="range" min="100" max="2500" step="25" value="350"><output id="itersO" for="iters">350</output></div>
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> GPU境界平滑化</label></div>
<div class="exact-grid"><label for="coordReInput">実部</label><input id="coordReInput"><label for="coordImInput">虚部</label><input id="coordImInput"><label for="coordSpanInput">表示幅</label><input id="coordSpanInput"></div>
<div class="mini-actions"><button id="coordApply">座標を適用</button><button id="coordCopy">正確値をコピー</button><button id="undoView">戻す</button><button id="redoView">進む</button></div>
</details>
<details><summary>診断情報</summary><div class="diagnostics"><div id="diagEngine">engine: …</div><div id="diagNumeric">numeric: …</div><div id="diagMemory">memory: …</div></div><div class="mini-actions"><button id="backendBench" type="button">Backend比較</button><button id="sparseBench" type="button">Fixed96 Direct実験</button></div></details>
</div>
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
<div class="hint">ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。</div>
</div>
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI−</button>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="exportDialog" aria-labelledby="exportTitle">
<h2 id="exportTitle">高解像度 PNG 出力</h2>
<div class="export-grid">
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
<label for="exportWidth">幅(px)</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×(高速)</option><option value="2">2×2 AA</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="strict">保守的 (Strict)</option></select>
</div>
<progress id="exportProgress" max="1" value="0" hidden></progress>
<div id="exportStatus" role="status" aria-live="polite"></div>
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
</dialog>
<dialog id="benchmarkDialog" aria-labelledby="benchmarkTitle">
<h2 id="benchmarkTitle">標準 / DS / 深部 比較</h2>
<div class="export-grid">
<label for="benchmarkTrack">測定地点</label><select id="benchmarkTrack"><option value="seahorse">Seahorse boundary</option><option value="cusp">Period-2 cusp</option></select>
<label for="benchmarkSamples">反復測定</label><select id="benchmarkSamples"><option value="2">2回(速い)</option><option value="3" selected>3回</option><option value="5">5回(安定)</option></select>
</div>
<div class="benchmark-note">z0,2,4,6,8,10,12,14,16,20,30,40を測定します。速度は現在のview上の代表3タイルを測って全画面相当に換算し、Deep coldにはBigInt reference構築を加算します。1測定が15秒を超えた場合は停止してエラー表示します。</div>
<progress id="benchmarkProgress" max="12" value="0" hidden></progress>
<div id="benchmarkStatus" role="status" aria-live="polite"></div>
<textarea id="benchmarkOutput" class="benchmark-output" readonly placeholder="結果はここに表示されます"></textarea>
<div class="export-actions"><button id="benchmarkCancel" type="button">閉じる</button><button id="benchmarkSave" type="button" disabled>JSON保存</button><button id="benchmarkStart" class="primary" type="button">測定開始</button></div>
</dialog>
<dialog id="sparseDialog" aria-labelledby="sparseTitle">
<h2 id="sparseTitle">DS + Sparse Fixed96 Direct 実験</h2>
<div class="export-grid">
<label for="sparseTrack">測定地点</label><select id="sparseTrack"><option value="seahorse">Seahorse boundary</option><option value="cusp">Period-2 cusp</option></select>
<label for="sparseSamples">反復測定</label><select id="sparseSamples"><option value="2">2回(速い)</option><option value="3" selected>3回</option><option value="5">5回(安定)</option></select>
</div>
<div class="benchmark-note">z6 / z8 / z10 / z12で、DS Directに感度ガード |dz/dc| を追加し、危険pixelだけreference不要の96-bit符号付きQ8.88整数Directで再計算します。risk=1e11 / 1e12 / 1e13 / 1e14を全候補表示し、失敗時も correction / remain / Δclass / time を隠しません。production routerは変更しません。</div>
<progress id="sparseProgress" max="4" value="0" hidden></progress>
<div id="sparseStatus" role="status" aria-live="polite"></div>
<textarea id="sparseOutput" class="benchmark-output" readonly placeholder="結果はここに表示されます"></textarea>
<div class="export-actions"><button id="sparseCancel" type="button">閉じる</button><button id="sparseSave" type="button" disabled>JSON保存</button><button id="sparseStart" class="primary" type="button">Fixed96実験開始</button></div>
</dialog>
<script src="gpu-kernels.js"></script>
<script src="script.js"></script>
</body>
</html>

2785
index.html

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more