This commit is contained in:
33333-33333 2026-08-23 23:27:38 +09:00
commit 8a2b1f6d4b
64 changed files with 10760 additions and 757 deletions

View file

@ -1,95 +1,182 @@
# v24.1.3 completion audit
# v24.2.3 crossover-benchmark audit
## 判定
**v24.1.2で実ブラウザからWGSL reserved-word parse errorが報告されたため、従来のrelease判定を撤回。v24.1.3でshader命名とfallback負荷を修正し、静的・CPU数値モデル・build gateを再実行してPASS。実WebGPU adapter acceptanceはこの環境では引き続き未実行。**
静的contract、Node数値モデル、build/reproducibility gateはPASS。実WebGPU adapterでのv24.2.3 compile/dispatchはこのコンテナではadapterを取得できないため未実行であり、`tests/webgpu-acceptance.html`を外部実機gateとして残す。
実GPU未実行を「PASS」とは扱いません。
## 主な修正
## 修正済みの重大事項
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反転の既知反例があるため引き続き未搭載。
1. f32量子化BLAによるmembership反転
- production BLAを完全除去。
2. BLAなしperturbationのfalse bounded反例
- roundoff error boundを導入し、既知反例を`UNKNOWN`へ退避。
3. recolor/render token race
- `recolorPending`方式へ変更。
4. redundant Strict retry
- 1-pass policyへ整理し、大画面queue/indirect dispatchを削除。
5. Exportの4 readback/tile
- 4 sampleを1 command streamで処理しGPU resolve後1 readback。
6. Exportのtile resource churn
- 512² reusable GPU workspaceへ変更。
7. 16K巨大Canvas
- streaming PNGへ変更。
8. deep JS fallback誤描画
- deep fallbackを明示拒否。
9. WGSL parse error (`meta` / `smooth` reserved words)
- storage変数を`fieldMeta` / `fieldSmooth`へ改名。WGSL予約語を全shaderで走査する回帰テストを追加。
10. WebGPU shader/pipeline failure時のCPU暴走
- shader/pipelineエラーではJavaScript full-frame fallbackを起動せず停止。WebGPU自体がない場合だけ軽量fallbackを使用。
11. 画面描画負荷
- standard screen budgetをdesktop約4M→1.5M、小型端末約2M→0.75Mへ低減。power約0.5M、fine最大約3M、Strict最大約2M。
12. WebGPU canvas context claim
- pipeline成功後へ遅延。
13. e340以深の座標表示0化
- fixed-point exact decimal formatterの340桁capを撤去。
## CPU numeric model results
## CPU numeric regression
### Primary guarded perturbation
`guarded-production-equation-cpu-f32-model-not-real-gpu`:
canonical 17×11 corpus:
- period2-cusp-z14: false escaped 0 / false bounded 0 / UNKNOWN 0
- period2-cusp-z20: 0 / 0 / 0
- period2-cusp-z100: 0 / 0 / 0
- period3-interior: 0 / 0 / 0
- period2-cusp-e280: 0 / 0 / 0
- period2-cusp-e400: 0 / 0 / 0
- swirly-seahorses-z12: false escaped 0 / false bounded 0 / UNKNOWN 71 of 187
- 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
BalancedとStrictの両方で上記membership gateを満たします。swirlyの未確定率は既知の制約で、誤分類より保守退避を優先しています
Dense known regression pixels `(25,12)`, `(26,15)`, `(27,25)`はprimaryでは誤分類せずUNKNOWNへ退避。
Shallow f32 modelは693 sampleでfalse escaped 0 / false bounded 0。escape iteration count差は2 sampleあり、membership certificationを名乗らない理由の一つです。
### Sparse double-single correction
## BLA status
`swirly-seahorses-z12`, 61×39, 2000 iter:
production sourceにBLA builder/evaluator/node/levelはありません。再導入にはf32係数量子化を含む誤差上界付き受入条件が必要です。
- 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
## Export audit
DS passはfinite-iteration visual classification recoveryとして有効だが、escape iteration equalityは保証しない。
- tile max 512²
- reusable GPU workspace
- 2×2 AA: one readback/tile
- unresolved counterをRGBA readback末尾へ同梱
- PNG signature / IHDR / CRC / multiple IDAT / inflate scanlineをNode modelで検証
- abort path settles without unhandled rejection
## Router contract
- direct coordinate floor: ratio 8
- no-probe direct margin: ratio 128
- overlap: 8128
- 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`は以下を実adapterで検証します。
`tests/webgpu-acceptance.html`は以下を要求する
- shader compilation
- standard / Strict deep dispatch
- BigInt reference guard
- sampled membership
- dense known regressions
- Export 1× / 2×2 readback
- uncaptured validation errors
- 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
stable scenesはsampled UNKNOWNを許容しません。swirlyは25 sample中12以上の確定を要求します。
この環境では`navigator.gpu`/adapterを実利用できないため、上記hardware gateのみ未判定
この作業環境のChromiumは`navigator.gpu`を公開しないため、このgateは未実行です。`audit/v24-real-webgpu-status.json`に環境結果を記録します。
## v24.1.3 hotfix verification
## v24.2.3 crossover追加監査
- WGSL 16.2 reserved-word scan: PASS (5 shader modules)
- `meta` / `smooth` WGSL identifier occurrences: 0
- source contract: PASS (28 checks)
- `npm test`: PASS
- clean `npm run build`: PASS
- source ↔ standalone/hosted hashes: identical
- shader/pipeline init failure: CPU full-frame fallback is not entered and failed init is not retried every render
- normal desktop screen budget: ~1.5M px (v24.1.2 ~4M px)
- no-WebGPU/no-adapter fallback budget: ~0.25M px
- 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比較で取得する。
実WebGPU adapter上のv24.1.3 compile/dispatchだけは、このコンテナではadapterを得られないため未実行です。v24.1.2で報告されたreserved-word parse failureそのものは、仕様予約語の除去とreserved-token回帰gateによってsource上修正済みです。
## 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.

62
CROSSOVER_BENCHMARK.md Normal file
View file

@ -0,0 +1,62 @@
# 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** になる。

70
FIXED96_EXPERIMENT.md Normal file
View file

@ -0,0 +1,70 @@
# 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 z6z12 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.

69
HYBRID_EXPERIMENT.md Normal file
View file

@ -0,0 +1,69 @@
# 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,92 +1,118 @@
# Mandelbrot Deep Zoom v23 実装報告
# v24.2.3 crossover benchmark implementation report
## 結論
## ファイルの役割
`IMPROVEMENT_PROPOSAL.md`のv22向け設計判断をrenderer v23へ反映し、ローカル数値検証、WASM再現build、HostedStandalone成果物の生成まで実施した。提案は履歴文書であり、修正版のcoverageExport受入契約を現行基準とする。実ブラウザでのみ測定できる性能・表示・操作・download検証は、今回の環境にin-app browser bindingがないため未実行である。したがって、実装と確認済みの非ブラウザgate、未確認のbrowser acceptanceを分けて扱う。
- `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からの設計変更と、廃案を含む理由。
## 実装済み
## 今回作成/変更した理由
- 常時RAFを廃止し、invalidate時だけ動くevent-driven schedulerへ変更した。
- wheelpinchpan中は再投影だけを行い、settle後に計算を1回開始する。
- Preview時間予算を110msへ下げ、cold deep Previewは幅48px以下から開始する。以後は実測MPPでPreview寸法を決める。
- 標準deepのCoveredは実測MPPから約1.4秒のpixel budgetを算出する。未計測・再投影のみの場合は保守的MPPを使い、DPR下限は固定比率ではなく長辺64pxとするため4Kでも予算が効く。精細・精度優先ではこの時間適応を行わない。
- 処理モードの自動到達点を固定し、省電力はPreview、標準はCovered、精細はRefined、精度優先はValidatedで停止する。標準では境界AAと未確定追加反復を既定で実行しない。
- 精細の未確定追加反復は全未確定pixelではなくescape境界候補に限定し、1 viewあたりdeep 384点shallow 4,096点を上限にする。
- `Reprojected → Preview → Covered → Refined → Validated検証未完了`を分離した。
- Coveredはeffective DPRの表示Canvas全域をpixel中心`(x + 0.5, y + 0.5)`で計算する。
- escape field、分類、confidenceと彩色を分離し、palette変更時の反復再計算を廃止した。
- fieldへ各sampleの実escape iterationを保持し、再着色・未確定継続・Export監査で近似値ではなく同じ数値結果を使う。
- adaptive 2×4× AA、linear-light resolve、byte-budget detail cacheを実装した。
- 深部Worker、deep/BLA/color WASM、reference資産を遅延生成し、Worker数を実測とmemory budgetで増減する。
- deep/BLA/colorはmainで各1回だけcompileし、compile済み`WebAssembly.Module`をWorkerへstructured cloneする。Worker sourceから埋込payload、palette LUT、RGBA生成の重複を除き、Workerは数値fieldだけを返す。
- 通常jobから学習したstrip時間とWorker数をkernel version・端末帯域keyで永続化し、30日TTL、schema version、storage拒否fallbackを付けて次回起動で再利用する。
- fixed-point direct、perturbation、rebase、BLA、局所guarded directを共通tile schedulerへ接続した。
- deep切替を固定zoom閾値ではなくpixel stepとULPの比、hysteresisで決める。
- deep切替とguard bitsへ直近orbitのglitch未確定補修率を加え、精度昇格時は低精度で作った基準軌道を破棄して再構築する。
- 高精度directを約6msでyieldする非同期処理へ変更し、取消可能にした。
- f64解析・収縮判定は`INTERIOR_LIKELY`のまま保持し、ValidatedではPP+64双方の固定小数点整数不等式で主カージオイド周期2球を証明できたsampleだけを`INTERIOR_PROVEN`へ昇格する。
- 精度優先のdeep描画では、基準軌道もPP+64で独立再計算して疎なcheckpointとescape位置を照合し、不一致時はglobal reference全体を32 bit昇格して作り直す。
- 1×2×4×custom、1×2×2 AA、BalancedValidated、進捗、取消に対応する独立tile exportを実装した。
- Balanced Exportは未確定数をmetadataへ記録して完了可能とし、Validated Exportは未確定sampleが残る場合にfinal PNGを出さず明示失敗する。
- Export sidecarへViewSpec、precisioniteration policy、未確定sample数、backend、全kernel SHA-256、色空間、encoder、paletteを記録する。
- version付きURL、戻る進む、座標・spanの正確値入力copy、UndoRedoを実装した。
- 初回UI、一般statusと診断status、mobile compact status、keyboard操作、live region、visible focus、44px target、reduced motiontransparencyを実装した。
- Hosted版はcontent-hashed外部WASMとstreaming compile、Standalone版は3ファイル直開きを維持した。
- cacheは件数ではなくmobile 96 MiBdesktop 192 MiBのlogical byte ledgerで管理する。
深部primaryのUNKNOWNを黒として残すことが閲覧品質を下げていたため、追加referenceではなく同じreference上のdouble-single correctionへ変更した。またdirect利用範囲を固定閾値だけで狭めず、actual view上のqualityとGPU速度を測ってdeep採用を限定するためrouterを変更した。
## 再現buildと配布
## 機能検査
- `src/shallow_kernel.c``src/deep_kernel.c``src/bla_kernel_v18.c``src/color_kernel.c``src/abi.json`を配置した。
- `toolchain.lock.json`でclang 17.0.6、target、SIMDscalar flagsを固定した。
- clang 17.0.6とNode.js v22.18.0で8 WASMを再buildした。tool配布archiveのURLSHA-256はrepositoryに保持されていないため、再監査可能な証拠範囲はversionflags、source hash、生成manifest、golden結果までとする。
- shallow、deep、BLA、colorのgoldenとSIMDscalar同値gateに合格後、payloadと`kernels.js`を昇格した。
- `scripts/build-kernels.ps1`は同一payloadだけを自動deduplicateし、異なるCOLOR variantsも正しく保持する。
- `scripts/build-hosted.ps1`は現manifest以外の古いcontent-hashed WASMを削除する。
`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
- Hosted: `dist/hosted/``dist/wasm/`
- Standalone: `dist/standalone/`
- 再現build: `build/wasm-v23/`
## 性能設計
現在の非圧縮基準値は、Hosted first view 144,044 bytes、Standalone 172,591 bytesである。詳細なfile hashは`audit/v23-source-baseline.json`に記録した。v22成果物が残っていないため、v22の転送bytescompile timeに対するnon-regressionは判定不能であり、このv23値を今後の比較起点とする。
- 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固定で再利用。
## 合格済みgate
## 既知の制約
- source contract: pass
- kernel sourceABI contract: pass
- fixed-point precision 256320 bit: pass
- 基準軌道PP+64 checkpoint一致と故意の不一致検出: pass
- analytic interior integer proof 256320 bit: pass
- pixel mapping 851 samples、shallowdeepBLA、2×4× tile seam: pass
- 配布shallow WASM pixel contract 1,440 samples、mismatch 0: pass
- generated-WASM shallowcolordeepBLA golden: pass
- SIMDscalar結果比較: pass
- 本体、shallow Worker、deep WorkerのJavaScript構文: pass
- mock DOM初期化とv23 diagnostics: pass
- compile済みdeepBLAcolor Moduleのstructured clone・instantiate: pass
- Node固定sceneの計測記録と処理量予算式の構造契約: pass`audit/v23-node-performance.json`。browser時間目標の達成を意味しない
- HostedStandalone buildと全payload SHA-256: pass
- double-singleはWGSLの実装差を含めmembership certificateとして扱わない。
- 実GPU acceptanceはWebGPU adapterを取得できる環境で別途実行が必要。
toolchainを利用できる環境では次で省略なしに再実行できる。
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/test-all.ps1 -RequireToolchain
```
## v24.2.3追加作業
## Browser acceptance gate
- `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の判定基準。
`tests/browser-benchmark.html`は1 runを1 profile・1 buildへ限定し、`?profile=desktop&target=hosted`のように実行する。iframe寸法によるDPR模擬を廃止し、非zero wheelのtransform-to-paint、30回以上のPreview、実際のFine→Refined、raw pixel golden、long task、Export metadata完了前download再現性、keyboard操作を自動判定する。HostedとStandaloneを別に開く。visual golden未登録、observed browser/GPU memory、visible focus、page zoom/pinch、screen-reader順序は外部gateとして残り、自動項目が通ってもfull acceptanceを返さない。今回の環境ではbrowser bindingがなかったため、`audit/v23-browser-baseline.json``not-run`のままである。
現時点ではDS Directは診断専用であり、本体のbackend選択は変更していない。実GPU速度結果なしにproductionへ昇格させないための措置。
残る実機確認:
- mobiledesktop4Kの表示画像とnative coverage
- 操作反映p95、PreviewCoveredRefined所要時間
- idle時の予約RAFtimerbackground jobCanvas・DOM write 0
- browserGPUを含むobserved peak memory
- Export寸法、完了前downloadなし、取消、PNGJSON内容
- keyboard-only、focus、44px target、live status、page zoomとcanvas pinchの操作確認
## v24.2.4 Hybrid experiment preparation
このgateが合格するまで、v23の実装完了と数値gate合格は主張できるが、固定実機に対する性能目標の達成は主張しない。
- 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.
browser非依存の現行成果物監査は`audit/v23-browserless-baseline.json`に保存した。公式SHA-256を照合したNode.js v22.18.0 portable binaryで7 executable testsが合格し、sourcedocument contract、851 pixel mapping、1,440 pixel contract、Module cloneを再確認した。deep runtimeは出典付きの混在境界sceneで特性を測るが、値は`measured-not-acceptance`である。v22比較は`not-verifiable`、source-WASM再buildは保存済みprior evidenceの再利用として明示している。
## 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,115 +1,157 @@
# v23 → v24.1.3 WebGPU migration
## v24.1.3 browser hotfix
# v23 → v24.2.3 WebGPU migration / crossover study
実WebGPUブラウザで`direct` shaderのparse時に、WGSL予約語`meta` / `smooth`をstorage変数名へ使用していることが判明しました。両識別子を`fieldMeta` / `fieldSmooth`へ改名し、WGSL 16.2 reserved-word listを全shader sourceへ機械照合するtestを追加しました。
## 目的
同時に、shader初期化失敗を通常のWebGPU非対応と同じ扱いにしてCPU f64 rendererへ落としていた挙動を廃止しました。旧挙動は数百万pixel × 数百iterationをmain-thread fallbackで実行し、shader errorの直後にUIが極端に重くなる原因でした。v24.1.3ではshader/pipeline failureは描画を停止して明示し、CPU fallbackは`navigator.gpu`がない/adapterがない場合だけ使います。screen pixel budgetも標準約1.5Mdesktopへ縮小しました
v23のCPU/WASM deep pixel rendererとWorker poolを廃止し、画素単位のmassively parallel処理をWebGPUへ集約する。CPU高精度処理はBigInt ViewStateとreference orbit生成に限定する
## 最終アーキテクチャ
## 最終構成
```text
Main thread
UI / BigInt ViewState
BigInt ViewState
├─ shallow ─────────────────────────────┐
│ │
└─ deep → Reference Worker │
BigInt orbit P+64 │
checkpoint guard P+128 │
hi/lo f32 packing │
│ │
└──────────────┐ │
▼ ▼
WebGPU numeric
f32 direct / guarded
rescaled perturbation
fieldMeta + fieldSmooth field
GPU color / AA
GPU presentation
├─ 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
```
CPU側に残る高精度処理はview座標とreference orbit生成だけで、pixel rendererではありません。
## v24.1系からの品質修正
## BLAを最終版から外した理由
v24.1のguarded f32 perturbationは誤分類を避けるためUNKNOWNへ保守退避する設計だった。境界sceneではUNKNOWNが大きく、interiorに近い暗色表示と相まって黒面積増大/黒点として見えた。
初期WebGPU版ではCPU f64でBLAを作り、GPU用f32係数へ量子化して評価していました。しかし`swirly-seahorses-z12`を61×39へ高密度化した回帰で、BLAあり経路がbounded pixelをescapedへ反転する反例を検出しました。
### Multi-reference評価
f64で導出した受入半径へf32係数量子化誤差を安全に織り込む設計が未確立だったため、閾値調整で残すのではなくproduction BLAを完全に除去しました
最初にUNKNOWN macro tileへsecondary BigInt referenceを追加する方式をモデル化した。しかし複数referenceを追加しても`swirly-seahorses-z12`全画面モデルのUNKNOWN回収率が低く、reference生成・転送・再dispatchのコストに見合わなかった。そのためproductionへは採用しない
## Deep numeric policy
### Sparse double-single correction
Deepではpixel offsetを
代わりにUNKNOWNだけをdouble-single complex perturbationで再評価する。reference orbitはprimaryと共用するため追加BigInt orbitは不要。
```text
delta = w * 2^scaleExp
```
61×39 dense model / 2000 iter:
として保持します。通常反復はf32の正規化座標で行い、値域が偏った時だけ16 bit単位でrescaleします。
- primary UNKNOWN 478
- primary membership reversal 0
- correction recovered 478
- remaining UNKNOWN 0
- correction membership reversal 0
- escape iteration mismatch 5最大374
さらにroundoff上界を追跡します。
したがってcorrectionは表示品質passとして採用し、membership certificateには使わない
- escape候補で `|z| - error > 2` の時だけescapedを確定
- max iteration到達時もabsolute errorがpolicy閾値以下の場合だけ`INTERIOR_LIKELY`
- 不確かな場合は`UNKNOWN`
## Backend router再設計
閾値:
旧設計のzoom/ULP単一閾値はdeepを早く使いすぎる一方、閾値を下げるだけではdirectの境界誤分類リスクがある。
```text
Balanced: 1e-3
Strict: 1e-4
```
v24.2.1は三領域に分ける。
Strictは同じ1-pass perturbationの受入閾値を厳しくするpolicyです。以前の「Fast → unresolved queue → Strict再実行」は、BLA撤去後には同じ式を二度計算するだけだったため削除しました。
- 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。
`swirly-seahorses-z12`の61×39 gridで旧版が誤った3 pixelをregressionへ固定しています
深度binの過去profileだけでbackendを決定しない
```text
(25,12) oracle bounded
(26,15) oracle bounded
(27,25) oracle escaped @ 1957
```
## BLAを戻さない理由
v24.1.3ではBalanced/Strictともこれらを誤分類せず`UNKNOWN`へ退避します。
## Presentation / recolor
数値field (`fieldMeta`, `fieldSmooth`) はGPU常駐です。色変更はGPU recolorだけで、Mandelbrot反復を再実行しません。
描画中にrecolor操作が入った場合はrender tokenを破壊せず`recolorPending`へ集約し、数値frame完了後に最新色を適用します。旧版にあった`rendering=true`残留レースを避けています。
f64 BLAをf32係数へ量子化した旧試作は、dense swirly回帰でbounded/escaped membership反転を起こした。量子化誤差を含む安全な受入半径が未確立のため、BLA builder/evaluator/nodeはproduction treeに存在しない。
## Export
旧巨大Canvas方式は廃止しました
v24.2.1ではscreenだけでなくExportにもdeep correctionを適用する。
- max side 16,384
- 512² tile
- reusable GPU export workspace
- 2×2 AA: 4 GPU samples → GPU linear-light resolve → 1 readback/tile
- PNG: scanline bands → deflate stream → PNG chunks
- sidecar: exact BigInt view + `unresolvedSamples`
1×:
raw 16K RGBA全体をCanvasへ保持しないため、GPU texture limit回避だけでなくCPU raw-imageメモリも抑えます。
```text
primary deep → clear stats → DS correction → color → 1 readback
```
## Fallback
2×2:
WebGPU contextを早期取得すると、pipeline初期化失敗後に同じCanvasを2Dへ切り替えられません。v24.1.3ではshader/pipeline作成を先に行い、成功後にWebGPU canvas contextを取得します。
```text
4 samples × (primary deep → DS correction → color)
→ GPU linear-light resolve
→ 1 readback / tile
```
WebGPU unavailable時はshallow f64 fallbackのみです。deepをNumberへunderflowさせて描画する経路は削除しました。
4 sampleのunresolved counterを分離し、sidecarの`unresolvedSamples`へ合計する
## 削除したv23 production資産
## 削除済み
- deep WASM renderer / Worker pool
- BLA WASM renderer
- color WASM
- BigInt direct pixel renderer
- CPU field → ImageData hot path
- Canvas detail tile cache
- Validated direct Export
- 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
BigInt direct相当はテストoracleにのみ存在します。
## 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.

194
README.md
View file

@ -1,110 +1,136 @@
# Mandelbrot Deep Zoom v24.1.3 — WebGPU
# Mandelbrot Deep Zoom v24.2.8 — Fixed96 True Sparse Queue Experiment
v23のCPU/WASM deep pixel rendererを撤去し、画素計算・field・彩色・再投影・高解像度ExportをWebGPUへ移した版です
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
- ViewState: BigInt固定小数点。深度に応じてbit数を自動拡張。
- Shallow: WebGPU `f32` direct iteration。f32座標分解能に十分な余裕があるviewだけで使用。
- Deep reference: 専用WorkerでBigInt固定小数点orbitを1本生成。referenceはview精度より64 bit高く生成し、さらに+64 bitのguard orbitでcheckpoint照合。
- Deep pixels: WebGPU `guarded rescaled f32 perturbation`。spanはmantissa + exponentへ分離し、`1e-400`級でもpixel offsetをf32 absolute値へ潰さない。
- Error handling: roundoff上界がescape/bounded判定へ影響し得るpixelは `FIELD_UNKNOWN` にする。UNKNOWNを内部点へ偽装しない。
- BLA: **productionでは無効・未搭載**。旧f32量子化BLAで境界pixelのmembership反転を再現したため、再導入していない。
- **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から撤去しました。
旧v23の`Validated direct`は削除しています。v24.1.3のStrictは誤差許容閾値を厳しくする保守的GPU policyで、任意精度direct全画素証明ではありません。PNG sidecarは常に `membershipCertified:false` です。
Deep primaryはroundoff上界を追跡し、escape/bounded判定を安全に受理できないpixelを`UNKNOWN`にします。double-single correctionは表示品質回復用であり、任意精度membership証明ではありません。PNG sidecarの`membershipCertified`は常に`false`です。
## 描画パイプライン
## Direct / Deep router
通常表示ではfieldをGPUに保持します
単一のzoom閾値でdeepへ切り替えません
```text
BigInt ViewState
└─ deep時: BigInt reference Worker
WebGPU direct / guarded perturbation
fieldMeta + fieldSmooth GPU buffers
GPU color / optional boundary smoothing
offscreen texture
GPU reprojection / canvas
```
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。
パレット・cycle・shift・HQ変更は数値fieldを再計算せずrecolorします。pan / wheel / pinch操作中は直前frameをGPUで再投影し、settle後に新しい数値frameを計算します。
過去の同深度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
高解像度PNGは最大辺16,384 pxです。
- 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`を記録。
- 512×512以下のGPU tileで計算。
- Export用GPU buffers/textures/readback bufferは固定workspaceを再利用し、tileごとの大量生成を避ける。
- 2×2 AAは4 sampleをGPUで計算し、GPUでlinear-light resolveした後、tileにつき1回だけreadback。
- 全画像Canvasを確保せず、scanline bandを`CompressionStream('deflate')`へ送りPNGを構築。
- sidecar JSONへ`unresolvedSamples`を記録。
## Fallback
`unresolvedSamples > 0`は、数値policyがそのsampleを安全に分類できなかったことを意味します。
WebGPUそのものが利用できない場合だけ軽量JavaScript f64 fallbackを使います。deep zoomはNumber underflow等で誤画像を出さず、「このズーム深度はWebGPUが必要です」と表示して停止します。shader/pipeline failureではCPU全画面rendererへ自動fallbackしません。
## WebGPU unavailable
WebGPUが利用できない場合は浅部のみJavaScript f64 fallbackを使います。deep zoomは誤画像を出さず、「このズーム深度はWebGPUが必要です」と表示します。
WebGPU自体が存在しない場合だけ軽量な浅部JavaScript fallbackを使います。**WGSL/shader/pipeline初期化エラー時はCPU全画面fallbackへ自動移行しません**。エラーを表示して停止し、shader不具合を隠したままCPUを占有しない設計です。`webgpu` canvas contextはpipeline作成成功後に取得します。high-performance adapterが得られない場合は通常のadapter requestも再試行します。
## v24.1.3 hotfix
実ブラウザで発覚したWGSL parse errorを修正しました。WGSL 16.2で予約されている`meta` / `smooth`をstorage-buffer変数名に使っていたため、`fieldMeta` / `fieldSmooth`へ変更しています。全shaderをWGSL reserved-word一覧へ照合するNode gate `tests/v24-wgsl-reserved.mjs`も追加しました。
表示負荷も見直し、標準モードのscreen pixel budgetをdesktop約1.5M / 小型端末約0.75Mへ縮小しました旧版は約4M / 2M。省電力は約0.5M、精細は最大約3M、Strictは最大約2Mです。WebGPU非対応時のCPU fallbackは約0.25M pixelに制限します。
## Standalone
`index.html`, `gpu-kernels.js`, `script.js`の3ファイルで動作します。外部WGSL fetchはありません。
```text
index.html
gpu-kernels.js
script.js
```
ブラウザが`file://`上でWebGPUを許可しない構成ではlocalhost/HTTPSで開いてください。
## Test
## Build / test
```bash
npm test
npm run build
```
`npm test`は以下を検査します。
`npm test`には以下を含みます。
- JS syntax / source contracts
- WGSL reserved-word token audit (`meta`, `smooth`, `ref`等を含む仕様予約語)
- 旧C/WASM deep assetがproduction treeに残っていないこと
- BigInt reference Worker + guard checkpoints
- shallow f32 CPU model vs BigInt oracle
- guarded perturbation CPU f32 model vs BigInt oracle
- `swirly-seahorses-z12`高密度回帰と既知3反例
- BLAがproductionから除去されていること
- tile/pixel geometry
- `1e-400` / `1e-1000` coordinate formatting
- streaming PNG structure / CRC / inflate / abort
- real-WebGPU acceptance harnessのJS syntax
- WGSL reserved-word scan9 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 acceptance
実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を検査します。
`tests/webgpu-acceptance.html`をWebGPU対応browserで開きます。これは実adapter上でshader compile / compute / readbackを行い、BigInt oracleと比較します。
## 配布
必須gate:
`npm run build`で以下を生成します。
- stable corpus scene: sampled `UNKNOWN = 0`
- swirly scene: 25 sample中12以上を確定
- `falseEscaped = 0`
- `falseBounded = 0`
- reference guard mismatch = 0
- 既知dense反例で誤分類しない
- 1× / 2×2 Export smokeの未確定sample = 0
- uncaptured WebGPU validation error = 0
- `dist/standalone/`
- `dist/hosted/`
このリポジトリを生成した実行環境では`navigator.gpu`が公開されなかったため、実adapter gateだけは未実行です。静的/CPU-model gateの代替ではありません
`dist/standalone/` は単一 `index.html``dist/hosted/``index.html`, `gpu-kernels.js`, `script.js` です。root `index.html` も単一ファイルで、ZIPから展開してそのまま開けます。

View file

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

View file

@ -27,10 +27,31 @@
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 71,
"known": 116,
"exactEscape": 110,
"mismatch": []
"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",
@ -118,10 +139,31 @@
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 71,
"known": 116,
"exactEscape": 110,
"mismatch": []
"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",
@ -199,7 +241,7 @@
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 2.1991906643574973e+21
"err": 34362354130585895000
}
},
{
@ -211,7 +253,7 @@
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 2.1991906643574973e+21
"err": 34362354130585895000
}
},
{
@ -223,7 +265,7 @@
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 7.768308137076909e+21
"err": 121379814641826710000
}
},
{
@ -235,7 +277,7 @@
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 7.768308137076909e+21
"err": 121379814641826710000
}
},
{
@ -247,7 +289,7 @@
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 2084483903264.6707
"err": 32570060988.51048
}
},
{
@ -259,7 +301,7 @@
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 2084483903264.6707
"err": 32570060988.51048
}
}
]

View file

@ -0,0 +1,25 @@
{
"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

@ -0,0 +1,34 @@
{
"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

@ -0,0 +1,766 @@
{
"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

@ -0,0 +1,19 @@
{
"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

@ -0,0 +1,182 @@
{
"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

@ -0,0 +1,31 @@
{
"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

@ -0,0 +1,40 @@
{
"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

@ -0,0 +1,63 @@
{
"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

@ -0,0 +1,56 @@
{
"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

@ -0,0 +1,29 @@
{
"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,24 +1,43 @@
{
"status": "v24.1.3-real-webgpu-rerun-pending",
"gate": "real-webgpu-compile-dispatch-numeric-acceptance",
"environment": {
"browser": "Chromium 144.0.7559.96 Debian GNU/Linux 13",
"display": "container/Xvfb",
"navigatorGpuPreviouslyObserved": false,
"latestAttempt": "GPU command-buffer/context initialization failed before acceptance completed",
"adapterAccepted": false
},
"attemptedModes": [
"headless Chromium default",
"--enable-unsafe-webgpu",
"--enable-unsafe-webgpu --use-angle=swiftshader",
"Xvfb + unsafe WebGPU"
"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実験"
],
"interpretation": "The reported real-browser v24.1.2 parse failure is fixed in source in v24.1.3. This container still cannot obtain a real WebGPU adapter, so v24.1.3 compile/dispatch must be rerun on a WebGPU-capable browser/device.",
"externalObservation": {
"version": "24.1.2",
"result": "shader-parse-failed",
"error": "WGSL reserved identifiers meta and smooth in direct shader",
"hotfix": "renamed to fieldMeta/fieldSmooth; reserved-word static gate added"
}
"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,27 +1,42 @@
{
"format": "mandelbrot-v24.1.3-release-manifest",
"generatedAt": "2026-08-23T16:17:33+09:00",
"format": "mandelbrot-v24.2.2-crossover-release-manifest",
"generatedAt": "2026-08-23T10:22:38.264933+00:00",
"files": [
{
"path": "AUDIT_V24.md",
"bytes": 4405,
"sha256": "ef09e435b24a64bdbf0872e9f182c31ab2edea5afd0c959a5681d2b9be87ddbf"
"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": 5185,
"sha256": "2a4fe1754c521901b0e1f2c99d46426055fd3dc2932da93df12ea01a15469839"
"bytes": 4710,
"sha256": "b4d55a30f38ba4c56e0aace0e69f00714878f5098f88d5c9a4f9e76546b24c16"
},
{
"path": "README.md",
"bytes": 5532,
"sha256": "5d3e7db74400db1fb0585f23b966d1eab08f3729fe5f253ef533060f65f1790d"
"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,
@ -29,23 +44,38 @@
},
{
"path": "audit/v24-cpu-numeric-model.json",
"bytes": 5391,
"sha256": "f25e830d831f828b84f327670b5be5e9afcf6118164b2d217a9eae24443f84bf"
"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": 1066,
"sha256": "9791925d9ddb890644b895d4cd659d7377d7c48dd94df86495fef04ee1325c8a"
"bytes": 601,
"sha256": "ec5171b4f059f4f810b64db1ba82d83979571001f17f564919322c69e59ba721"
},
{
"path": "audit/v24-release-verification.json",
"bytes": 290,
"sha256": "6cd0dd2b293bae482e8cbd511caa50aebe34597ee2c12b442de75fa7a937dccd"
"bytes": 944,
"sha256": "b54797dfdc9e99c3e981ac39b45ef92281af5da5bb89d0ef21b837b41e4c4463"
},
{
"path": "audit/v24-sparse-correction-model.json",
"bytes": 600,
"sha256": "0202491ac101543055d9a70c11ab3ed7bf7d253617875271c9d88c7ccd1f2acb"
},
{
"path": "audit/v24-test-summary.json",
"bytes": 3373,
"sha256": "260173d15640a9681ae049f3e1c2707c774cf2af7a4fc4d6475115e7be344e8c"
"bytes": 1665,
"sha256": "b7afe3205c814318d2d7646bbb235b2e4c8411ecedf746e72924cb20cfcc562b"
},
{
"path": "dist/hosted/_headers",
@ -54,53 +84,53 @@
},
{
"path": "dist/hosted/gpu-kernels.js",
"bytes": 12196,
"sha256": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a"
"bytes": 22425,
"sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329"
},
{
"path": "dist/hosted/index.html",
"bytes": 11416,
"sha256": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc"
"bytes": 13092,
"sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
},
{
"path": "dist/hosted/script.js",
"bytes": 58865,
"sha256": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61"
"bytes": 85737,
"sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2"
},
{
"path": "dist/standalone/gpu-kernels.js",
"bytes": 12196,
"sha256": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a"
"bytes": 22425,
"sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329"
},
{
"path": "dist/standalone/index.html",
"bytes": 11416,
"sha256": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc"
"bytes": 13092,
"sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
},
{
"path": "dist/standalone/script.js",
"bytes": 58865,
"sha256": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61"
"bytes": 85737,
"sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2"
},
{
"path": "gpu-kernels.js",
"bytes": 12196,
"sha256": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a"
"bytes": 22425,
"sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329"
},
{
"path": "index.html",
"bytes": 11416,
"sha256": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc"
"bytes": 13092,
"sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
},
{
"path": "package.json",
"bytes": 199,
"sha256": "0fa1b8155f09356fba73e94c8db6428f1367aefef40efd116ad89c89a8a8a13f"
"sha256": "7c8ccf08afdaef9dc91c9041a5f9b9e1d730a1a9ba237f11d46ec293dc9690ab"
},
{
"path": "script.js",
"bytes": 58865,
"sha256": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61"
"bytes": 85737,
"sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2"
},
{
"path": "scripts/build.mjs",
@ -109,8 +139,8 @@
},
{
"path": "scripts/test-all.mjs",
"bytes": 818,
"sha256": "1fbebd942d5c3ee94a1cbb449a29168c50b0bc8d3285cbae1c7c01eb188b29ec"
"bytes": 934,
"sha256": "c3ec078b2591cba429eef90cc4f58a7c55e97756c083ed7b2d9437342ad450b4"
},
{
"path": "tests/legacy-v23/numeric-policy-v23.json",
@ -124,8 +154,13 @@
},
{
"path": "tests/v24-acceptance-contract.mjs",
"bytes": 818,
"sha256": "4bc444c694934d8bfd4a56ab597ab9fe834867ed6bc97a36c8182f99c25e39a6"
"bytes": 1035,
"sha256": "114d90b92f89b8b27f6ba35b904464908ec0e1e6393adb02ade8a8dfe5a7bbf0"
},
{
"path": "tests/v24-backend-policy.mjs",
"bytes": 1192,
"sha256": "81cffe6276ed660210646067502ab9edcce7d0fc608048a98c06633760714b5a"
},
{
"path": "tests/v24-bla-model.mjs",
@ -139,8 +174,18 @@
},
{
"path": "tests/v24-cpu-numeric-model.mjs",
"bytes": 8197,
"sha256": "8f24ccd80bf00b5b13e09a1b31bddf6df1c17dc8d7541e04ea1e7421165a2ce0"
"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",
@ -150,12 +195,12 @@
{
"path": "tests/v24-geometry-contract.mjs",
"bytes": 1177,
"sha256": "9403a8d1efe6d0d5547bd7ebaa966578dea1ba7630f094fc117e0ae4fe395996"
"sha256": "2e5e6fb87f4b7130eff8fff72fd776ef8b0d0e70e91845136a3b18cbf6c86701"
},
{
"path": "tests/v24-index-contract.mjs",
"bytes": 461,
"sha256": "ffeeccba4b72613f061c7add2f8cdecc03a6c01e2e5a01d4f2a2cf53f94345ef"
"bytes": 504,
"sha256": "90f52bb91f12db47da1ebf39a6770cb58d5c4e2809f4d6aa2fdb1263d77cee57"
},
{
"path": "tests/v24-png-stream-model.mjs",
@ -169,8 +214,13 @@
},
{
"path": "tests/v24-source-contract.mjs",
"bytes": 3805,
"sha256": "ce3e8dc4be17f19af362087772e7a17b59d7e8e007c5e19f00153899fe0a20af"
"bytes": 4239,
"sha256": "349c6d344f5f9325cb811cbb9f63017cb2d27139e7557be829f7c85f13bb39d4"
},
{
"path": "tests/v24-sparse-correction-model.mjs",
"bytes": 9810,
"sha256": "eb72f0aa062bcf8bf915c048ba89e58a7ad5ab338ea559cfc467ba260255c241"
},
{
"path": "tests/v24-tree-contract.mjs",
@ -189,8 +239,8 @@
},
{
"path": "tests/webgpu-acceptance.js",
"bytes": 6072,
"sha256": "c38fbeca176a464bfb6d4a0685f66469065ba3392a65f0617215a85e3fccbeb0"
"bytes": 6884,
"sha256": "e1ef5b21906b4da0fb7154f1e3e555c36267d8a5a7a1bd7ed0474eee147bfd13"
}
]
}
}

View file

@ -1,12 +1,25 @@
{
"status": "pass",
"version": "24.1.3",
"generatedAt": "2026-08-23T16:17:15+09:00",
"preZipCleanCopy": {
"npmTest": "pass",
"npmBuild": "pass"
"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"
},
"wgslReservedWords": "pass",
"sourceDistHashesMatch": true,
"realWebGpu": "pending: container has no usable adapter"
}
"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

@ -0,0 +1,26 @@
{
"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,94 +1,48 @@
{
"status": "v24.1.3-static-cpu-build-pass-real-webgpu-rerun-pending",
"status": "v24.2.2-static-cpu-build-pass-real-webgpu-timing-pending",
"rendererVersion": 24,
"shaderVersion": "24.1.3",
"packageVersion": "24.1.3",
"shaderVersion": "24.2.2-benchmark",
"packageVersion": "24.2.2",
"generatedAt": "2026-08-23T10:22:11.466258+00:00",
"production": {
"legacyDeepAssets": 0,
"bigIntPixelRenderer": false,
"referenceWorker": "BigInt fixed-point reference at viewBits+64 with viewBits+128 guard checkpoints",
"gpuDeep": "guarded rescaled f32 perturbation",
"productionBla": false,
"strictRetryPass": false,
"gpuFieldColorPresentation": true,
"recolorWithoutNumericRecompute": true,
"reusableExportWorkspace": true,
"streamingPng": true,
"gpuAaResolve2x2": true,
"maxExportSide": 16384,
"membershipCertified": false
"bla": false,
"multiReference": false,
"sparseDoubleSingleCorrection": true,
"viewSpecificBackendProbe": true,
"correctedDeepExport": true,
"experimentalWholeFrameDsDirect": "benchmark-only"
},
"nodeGates": {
"status": "pass",
"directFalseEscaped": 0,
"directFalseBounded": 0,
"deepFalseEscaped": 0,
"deepFalseBounded": 0,
"deepGuardMismatch": 0,
"swirlyUnknownBalanced": 71,
"swirlyKnownBalanced": 116,
"denseKnownCounterexamples": "all conservatively UNKNOWN",
"coordinateDepths": [
"1e-400",
"1e-1000"
"crossoverQuality": {
"firstMaterialF32DegradationDepth": 6,
"firstMaterialDsDegradationDepth": 10,
"deepIssueRows": [
{
"z": 8,
"falseEscaped": 1,
"falseBounded": 0,
"unknown": 0,
"escapeIterationMismatch": 2
}
],
"pngStreamModel": "pass",
"acceptanceHarnessContract": "pass"
"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",
"standaloneFiles": [
"index.html",
"gpu-kernels.js",
"script.js"
],
"hostedFiles": [
"index.html",
"gpu-kernels.js",
"script.js",
"_headers"
]
"sourceDistHashMatch": true
},
"realWebGPU": {
"status": "rerun-pending-for-v24.1.3",
"v24.1.2ObservedFailure": "reserved-word parse error from real WebGPU browser",
"v24.1.3SourceFix": "static reserved-word gate passes",
"containerAdapterAccepted": false
"status": "not-run-in-container",
"benchmark": "Diagnostics > Backend比較",
"hardwareTimingRequired": true
},
"generatedAt": "2026-08-23T16:17:15+09:00",
"hotfix": {
"wgslReservedWords": "pass: all 5 WGSL modules contain no WGSL 16.2 reserved-word tokens",
"renamedIdentifiers": [
"meta -> fieldMeta",
"smooth -> fieldSmooth"
],
"shaderFailureFallback": "CPU full-frame fallback disabled for shader/pipeline failures",
"screenPixelBudget": {
"power": 524288,
"standardDesktop": 1572864,
"standardSmall": 786432,
"fineDesktop": 3145728,
"strictDesktop": 2097152,
"noWebGpuFallback": 262144
},
"errorBadge": "compiler/runtime errors truncated to 120 chars in status UI; full diagnostics retained"
},
"hashes": {
"index.html": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc",
"script.js": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61",
"gpu-kernels.js": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a",
"dist/standalone/index.html": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc",
"dist/standalone/script.js": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61",
"dist/standalone/gpu-kernels.js": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a",
"dist/hosted/index.html": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc",
"dist/hosted/script.js": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61",
"dist/hosted/gpu-kernels.js": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a"
},
"cleanCopyVerification": {
"directory": "v24_hotfix_verify_pre",
"npmTest": "pass",
"npmBuild": "pass",
"sourceHashesMatch": true,
"sourceDistHashesMatch": 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."
}

598
dist/hosted/gpu-kernels-v24.2.6.js vendored Normal file
View file

@ -0,0 +1,598 @@
(()=>{'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

@ -4,10 +4,19 @@ 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=0x0fffffffu;
const STATUS_UNRESOLVED:u32=0xfffffffeu;
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);
}
@ -20,7 +29,7 @@ fn pow2_safe(e:i32)->f32 {
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=64.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
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>{
@ -77,35 +86,402 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
}
`;
// 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, _numeric0:u32, _numeric1:u32, _numeric2:u32,
spanMant:f32, spanExp:i32, sampleX:f32, sampleY:f32,
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>, _p0:u32, _p1:u32, _p2:u32 };
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){
fieldMeta[out]=pack_meta(n,FIELD_UNKNOWN); fieldSmooth[out]=0.0;
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-0.5*f32(p.fullW))/f32(p.fullW);
let dy=(0.5*f32(p.fullH)-gy)/f32(p.fullW);
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.spanMant*dx,p.spanMant*dy);
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=64.0*F32_U*maxabs(d);
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
@ -113,10 +489,10 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
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);}
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);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);
@ -124,49 +500,207 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
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);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);return;}
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.spanMant*dx,p.spanMant*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=64.0*F32_U*maxabs(d);
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);return;}
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=64.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
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);return;}
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);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n);return;}
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=gid.y*p.tileW+gid.x;
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,
@ -194,10 +728,10 @@ fn current_palette(t0:f32)->vec3<f32>{
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(20,22,30)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
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&0x0fffffffu); 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 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));}
@ -208,7 +742,7 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
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&0x0fffffffu)-i32(m&0x0fffffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
@ -253,6 +787,6 @@ struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.1.3',DIRECT_F32_WGSL,DEEP_PERTURB_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
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

@ -4,7 +4,7 @@
<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.1.3 WebGPU</title>
<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}
@ -19,7 +19,7 @@ details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8p
.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}
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}}
@ -44,7 +44,7 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
<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></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>
@ -63,6 +63,30 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
<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>

454
dist/hosted/script-v24.2.6.js vendored Normal file
View file

@ -0,0 +1,454 @@
(()=>{'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();
})();

270
dist/hosted/script.js vendored
View file

@ -2,15 +2,20 @@
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;
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,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
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};
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}
@ -19,6 +24,17 @@ function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=
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)}
@ -29,7 +45,12 @@ function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.
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 deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){const stepLog=log2FixedAt(snap.span,snap.bits)-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)));return !Number.isFinite(ratio)||ratio<96||stepLog<-120}
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()}
@ -53,11 +74,11 @@ function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new
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=null;this.failed=false}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([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=d;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){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.1'].join(':');if(this.cache&&this.cache.key===key)return Promise.resolve(this.cache);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})})}
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');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();
@ -82,9 +103,14 @@ class WebGpuRenderer{
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,xm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
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'}});
@ -96,20 +122,30 @@ class WebGpuRenderer{
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,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,64,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;
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'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved'),pbufs:Array.from({length:4},(_,i)=>buf(d,64,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+16,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
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','unresolved','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
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(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){const se=spanMantExp(snap);return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,0,0,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,Math.fround(se.mant),true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true)})}
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(4));
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();}
@ -121,37 +157,183 @@ class WebGpuRenderer{
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 readUnresolved(){if(!this.frame)return 0;const d=this.device,B=GPUBufferUsage,r=buf(d,16,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,16);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const value=new Uint32Array(r.getMappedRange().slice(0))[0];r.unmap();destroy(r);return value}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=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,16,B.STORAGE|B.COPY_DST),pbuf=buf(d,64,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.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(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const 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();}
const read=buf(d,n*4,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);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 renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false}){
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.unresolved,pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.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}}]}),p=encoder.beginComputePass();p.setPipeline(this.deep);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),p=encoder.beginComputePass();p.setPipeline(this.direct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
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,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),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 unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
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}){
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.unresolved,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);d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si];if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.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(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const 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();}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();}
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;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),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 unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
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(),deep=deepNeeded(snap,canvas.width),t0=performance.now();state.rendering=true;state.dirty=false;state.drawState='COVERING';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}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 rescaled perturbation'}else state.lastEngine='WebGPU · f32 direct';const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false;state.unresolved=0;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}else r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();if(deep)r.readUnresolved().then(q=>{if(token===state.token){state.unresolved=q||0;updateStats()}}).catch(()=>{})}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)){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 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())}
@ -203,7 +385,7 @@ function exportDimensions(){const scale=Number($('#exportScale').value),aspect=c
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),strict=$('#exportPrecision').value==='strict';
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;
@ -230,17 +412,31 @@ async function runExport(){
}
$('#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));$('#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?(deepNeeded()?'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==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;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;$('#diagEngine').textContent='engine: '+state.lastEngine+' | WebGPU '+(renderer?'ready':'unavailable')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'');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}
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}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);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}))},
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);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}}
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:deepNeeded(),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,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():[]})};
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});

598
dist/standalone/gpu-kernels-v24.2.6.js vendored Normal file
View file

@ -0,0 +1,598 @@
(()=>{'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

@ -4,10 +4,19 @@ 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=0x0fffffffu;
const STATUS_UNRESOLVED:u32=0xfffffffeu;
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);
}
@ -20,7 +29,7 @@ fn pow2_safe(e:i32)->f32 {
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=64.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
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>{
@ -77,35 +86,208 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
}
`;
// 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, _numeric0:u32, _numeric1:u32, _numeric2:u32,
spanMant:f32, spanExp:i32, sampleX:f32, sampleY:f32,
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>, _p0:u32, _p1:u32, _p2:u32 };
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){
fieldMeta[out]=pack_meta(n,FIELD_UNKNOWN); fieldSmooth[out]=0.0;
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-0.5*f32(p.fullW))/f32(p.fullW);
let dy=(0.5*f32(p.fullH)-gy)/f32(p.fullW);
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.spanMant*dx,p.spanMant*dy);
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=64.0*F32_U*maxabs(d);
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
@ -113,10 +295,10 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
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);}
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);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);
@ -124,49 +306,207 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
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);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);return;}
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.spanMant*dx,p.spanMant*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=64.0*F32_U*maxabs(d);
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);return;}
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=64.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
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);return;}
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);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n);return;}
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=gid.y*p.tileW+gid.x;
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,
@ -194,10 +534,10 @@ fn current_palette(t0:f32)->vec3<f32>{
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(20,22,30)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
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&0x0fffffffu); 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 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));}
@ -208,7 +548,7 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
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&0x0fffffffu)-i32(m&0x0fffffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
@ -253,6 +593,6 @@ struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.1.3',DIRECT_F32_WGSL,DEEP_PERTURB_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
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

454
dist/standalone/script-v24.2.6.js vendored Normal file
View file

@ -0,0 +1,454 @@
(()=>{'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,16 +1,21 @@
(()=>{'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;
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,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
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};
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}
@ -29,7 +34,12 @@ function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.
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 deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){const stepLog=log2FixedAt(snap.span,snap.bits)-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)));return !Number.isFinite(ratio)||ratio<96||stepLog<-120}
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()}
@ -53,11 +63,11 @@ function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new
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=null;this.failed=false}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([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=d;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){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.1'].join(':');if(this.cache&&this.cache.key===key)return Promise.resolve(this.cache);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})})}
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');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();
@ -82,9 +92,12 @@ class WebGpuRenderer{
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,xm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
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'}});
@ -96,20 +109,27 @@ class WebGpuRenderer{
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,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,64,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;
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'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved'),pbufs:Array.from({length:4},(_,i)=>buf(d,64,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+16,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
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','unresolved','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
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(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){const se=spanMantExp(snap);return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,0,0,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,Math.fround(se.mant),true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true)})}
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(4));
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();}
@ -121,37 +141,196 @@ class WebGpuRenderer{
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 readUnresolved(){if(!this.frame)return 0;const d=this.device,B=GPUBufferUsage,r=buf(d,16,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,16);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const value=new Uint32Array(r.getMappedRange().slice(0))[0];r.unmap();destroy(r);return value}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=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,16,B.STORAGE|B.COPY_DST),pbuf=buf(d,64,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.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(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const 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();}
const read=buf(d,n*4,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);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 renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false}){
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.unresolved,pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.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}}]}),p=encoder.beginComputePass();p.setPipeline(this.deep);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),p=encoder.beginComputePass();p.setPipeline(this.direct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
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,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),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 unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
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}){
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.unresolved,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);d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si];if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.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(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const 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();}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();}
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;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),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 unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
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(),deep=deepNeeded(snap,canvas.width),t0=performance.now();state.rendering=true;state.dirty=false;state.drawState='COVERING';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}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 rescaled perturbation'}else state.lastEngine='WebGPU · f32 direct';const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false;state.unresolved=0;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}else r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();if(deep)r.readUnresolved().then(q=>{if(token===state.token){state.unresolved=q||0;updateStats()}}).catch(()=>{})}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)){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 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())}
@ -203,7 +382,7 @@ function exportDimensions(){const scale=Number($('#exportScale').value),aspect=c
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),strict=$('#exportPrecision').value==='strict';
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;
@ -230,17 +409,44 @@ async function runExport(){
}
$('#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));$('#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?(deepNeeded()?'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==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;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;$('#diagEngine').textContent='engine: '+state.lastEngine+' | WebGPU '+(renderer?'ready':'unavailable')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'');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}
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}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);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}))},
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);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}}
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,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:deepNeeded(),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,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():[]})};
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});

598
gpu-kernels-v24.2.6.js Normal file
View file

@ -0,0 +1,598 @@
(()=>{'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

@ -4,10 +4,19 @@ 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=0x0fffffffu;
const STATUS_UNRESOLVED:u32=0xfffffffeu;
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);
}
@ -20,7 +29,7 @@ fn pow2_safe(e:i32)->f32 {
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=64.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
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>{
@ -77,35 +86,402 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
}
`;
// 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, _numeric0:u32, _numeric1:u32, _numeric2:u32,
spanMant:f32, spanExp:i32, sampleX:f32, sampleY:f32,
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>, _p0:u32, _p1:u32, _p2:u32 };
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){
fieldMeta[out]=pack_meta(n,FIELD_UNKNOWN); fieldSmooth[out]=0.0;
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-0.5*f32(p.fullW))/f32(p.fullW);
let dy=(0.5*f32(p.fullH)-gy)/f32(p.fullW);
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.spanMant*dx,p.spanMant*dy);
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=64.0*F32_U*maxabs(d);
var errScaled=1.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
@ -113,10 +489,10 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
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);}
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);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);
@ -124,49 +500,207 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
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);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);return;}
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.spanMant*dx,p.spanMant*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=64.0*F32_U*maxabs(d);
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);return;}
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=64.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
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);return;}
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);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n);return;}
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=gid.y*p.tileW+gid.x;
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,
@ -194,10 +728,10 @@ fn current_palette(t0:f32)->vec3<f32>{
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(20,22,30)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
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&0x0fffffffu); 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 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));}
@ -208,7 +742,7 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
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&0x0fffffffu)-i32(m&0x0fffffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
@ -253,6 +787,6 @@ struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.1.3',DIRECT_F32_WGSL,DEEP_PERTURB_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
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
});
})();

93
index.external.html Normal file
View file

@ -0,0 +1,93 @@
<!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>

1275
index.html

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "mandelbrot-webgpu-v24",
"version": "24.1.3",
"version": "24.2.8",
"private": true,
"type": "module",
"scripts": {

454
script-v24.2.6.js Normal file
View file

@ -0,0 +1,454 @@
(()=>{'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();
})();

270
script.js
View file

@ -2,15 +2,20 @@
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;
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,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
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};
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}
@ -19,6 +24,17 @@ function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=
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)}
@ -29,7 +45,12 @@ function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.
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 deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){const stepLog=log2FixedAt(snap.span,snap.bits)-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)));return !Number.isFinite(ratio)||ratio<96||stepLog<-120}
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()}
@ -53,11 +74,11 @@ function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new
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=null;this.failed=false}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([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=d;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){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.1'].join(':');if(this.cache&&this.cache.key===key)return Promise.resolve(this.cache);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})})}
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');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();
@ -82,9 +103,14 @@ class WebGpuRenderer{
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,xm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
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'}});
@ -96,20 +122,30 @@ class WebGpuRenderer{
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,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,64,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;
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'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved'),pbufs:Array.from({length:4},(_,i)=>buf(d,64,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+16,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
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','unresolved','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
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(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){const se=spanMantExp(snap);return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,0,0,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,Math.fround(se.mant),true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true)})}
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(4));
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();}
@ -121,37 +157,183 @@ class WebGpuRenderer{
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 readUnresolved(){if(!this.frame)return 0;const d=this.device,B=GPUBufferUsage,r=buf(d,16,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,16);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const value=new Uint32Array(r.getMappedRange().slice(0))[0];r.unmap();destroy(r);return value}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=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,16,B.STORAGE|B.COPY_DST),pbuf=buf(d,64,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.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(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const 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();}
const read=buf(d,n*4,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);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 renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false}){
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.unresolved,pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.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}}]}),p=encoder.beginComputePass();p.setPipeline(this.deep);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),p=encoder.beginComputePass();p.setPipeline(this.direct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
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,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),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 unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
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}){
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.unresolved,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);d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si];if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.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(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const 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();}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();}
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;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),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 unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
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(),deep=deepNeeded(snap,canvas.width),t0=performance.now();state.rendering=true;state.dirty=false;state.drawState='COVERING';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}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 rescaled perturbation'}else state.lastEngine='WebGPU · f32 direct';const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false;state.unresolved=0;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}else r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();if(deep)r.readUnresolved().then(q=>{if(token===state.token){state.unresolved=q||0;updateStats()}}).catch(()=>{})}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)){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 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())}
@ -203,7 +385,7 @@ function exportDimensions(){const scale=Number($('#exportScale').value),aspect=c
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),strict=$('#exportPrecision').value==='strict';
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;
@ -230,17 +412,31 @@ async function runExport(){
}
$('#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));$('#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?(deepNeeded()?'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==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;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;$('#diagEngine').textContent='engine: '+state.lastEngine+' | WebGPU '+(renderer?'ready':'unavailable')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'');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}
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}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);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}))},
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);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}}
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:deepNeeded(),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,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():[]})};
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});

View file

@ -1,8 +1,23 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url),files=['index.html','gpu-kernels.js','script.js'];
const root=new URL('../',import.meta.url);
const template=await fs.readFile(new URL('../index.external.html',import.meta.url),'utf8');
const kernels=await fs.readFile(new URL('../gpu-kernels.js',import.meta.url),'utf8');
const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8');
const safe=s=>s.replace(/<\/script/gi,'<\\/script');
const standalone=template
.replace('<script src="gpu-kernels.js"></script>',`<script data-bundle="gpu-kernels">\n${safe(kernels)}\n</script>`)
.replace('<script src="script.js"></script>',`<script data-bundle="app">\n${safe(script)}\n</script>`)
.replace('<head>','<head>\n<meta name="mandelbrot-bundle" content="single-file-v24.2.8">');
await fs.writeFile(new URL('../index.html',import.meta.url),standalone);
for(const variant of ['standalone','hosted']){
const dir=new URL(`../dist/${variant}/`,import.meta.url);await fs.rm(dir,{recursive:true,force:true});await fs.mkdir(dir,{recursive:true});
for(const f of files)await fs.copyFile(new URL('../'+f,import.meta.url),new URL(f,dir));
if(variant==='standalone'){
await fs.writeFile(new URL('index.html',dir),standalone);
}else{
await fs.writeFile(new URL('index.html',dir),template);
await fs.copyFile(new URL('../gpu-kernels.js',import.meta.url),new URL('gpu-kernels.js',dir));
await fs.copyFile(new URL('../script.js',import.meta.url),new URL('script.js',dir));
await fs.writeFile(new URL('_headers',dir),`/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: no-referrer\n`);
}
}
await fs.writeFile(new URL('../dist/hosted/_headers',import.meta.url),`/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: no-referrer\n`);
console.log(JSON.stringify({status:'pass',outputs:['dist/standalone','dist/hosted'],files},null,2));
console.log(JSON.stringify({status:'pass',outputs:['index.html','dist/standalone/index.html','dist/hosted'],standalone:'single-file'},null,2));

View file

@ -1,7 +1,7 @@
import {spawnSync} from 'node:child_process';
const commands=[
['node',['--check','script.js']],['node',['--check','gpu-kernels.js']],['node',['--check','tests/webgpu-acceptance.js']],
...['v24-wgsl-reserved.mjs','v24-source-contract.mjs','v24-index-contract.mjs','v24-tree-contract.mjs','v24-reference-worker.mjs','v24-direct-model.mjs','v24-cpu-numeric-model.mjs','v24-bla-model.mjs','v24-geometry-contract.mjs','v24-coordinate-format.mjs','v24-png-stream-model.mjs','v24-acceptance-contract.mjs'].map(f=>['node',['tests/'+f]])
...['v24-wgsl-reserved.mjs','v24-source-contract.mjs','v24-index-contract.mjs','v24-standalone-ui-contract.mjs','v24-tree-contract.mjs','v24-crossover-quality.mjs','v24-crossover-contract.mjs','v24-fixed96-contract.mjs','v24-fixed96-queue-model.mjs','v24-fixed96-model.mjs','v24-fixed96-limb-model.mjs','v24-fixed96-limb-orbit-model.mjs','v24-fixed96-shader-range-model.mjs','v24-reference-worker.mjs','v24-direct-model.mjs','v24-backend-policy.mjs','v24-cpu-numeric-model.mjs','v24-sparse-correction-model.mjs','v24-bla-model.mjs','v24-geometry-contract.mjs','v24-coordinate-format.mjs','v24-png-stream-model.mjs','v24-acceptance-contract.mjs'].map(f=>['node',['tests/'+f]])
];
for(const [cmd,args] of commands){const r=spawnSync(cmd,args,{stdio:'inherit'});if(r.status!==0)process.exit(r.status??1)}
console.log(JSON.stringify({status:'pass',suite:'v24-static-and-cpu-model',realWebGPU:'run tests/webgpu-acceptance.html in a WebGPU-capable browser'},null,2));

View file

@ -1,10 +1,12 @@
import fs from 'node:fs/promises';
const s=await fs.readFile(new URL('./webgpu-acceptance.js',import.meta.url),'utf8');
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
must(/row\.known<12/.test(s),'swirly minimum-known gate missing');
must(/row\.unknown!==0/.test(s),'stable-scene UNKNOWN=0 gate missing');
must(/corrected production frame produced UNKNOWN/.test(s),'corrected screen UNKNOWN=0 gate missing');
must(/forceDeep:true,correctUnknown:true/.test(s),'forced corrected deep dense probe missing');
must(/dense DS correction left known regression pixel UNKNOWN/.test(s),'dense DS recovery gate missing');
must(/falseEscaped/.test(s)&&/falseBounded/.test(s)&&/guardMismatch/.test(s),'membership/reference gates missing');
must(/dense false escape/.test(s)&&/dense false bounded/.test(s),'dense regression gates missing');
must(/exportSmoke\.unresolved!==0/.test(s)&&/exportSmokeAA\.unresolved!==0/.test(s),'export unresolved gate missing');
must(/backend router ignored measured 15% speed margin/.test(s)&&/viewProbe/.test(s),'view-specific router acceptance gate missing');
must(/exportSmoke\.unresolved!==0/.test(s)&&/exportSmokeAA\.unresolved!==0/.test(s),'corrected export unresolved gate missing');
must(/uncapturedErrors/.test(s),'uncaptured WebGPU error gate missing');
console.log(JSON.stringify({status:'pass',checks:6,stableUnknownMax:0,swirlyKnownMin:12},null,2));
must(/runFixed96Experiment/.test(s)&&/fixed96 experiment smoke failed/.test(s)&&/queueIntegrity/.test(s)&&/dispatchRate/.test(s),'fixed96 sparse-queue real-GPU smoke gate missing');
console.log(JSON.stringify({status:'pass',checks:8,correctedUnknownMax:0,routerSpeedMargin:.15,fixed96Smoke:true},null,2));

View file

@ -0,0 +1,10 @@
const REQUIRED=8,DEFAULT=128,MARGIN=.85,UNKNOWN_MAX=.005;
function choose(ratio,viewProbe=null){const eligible=Number.isFinite(ratio)&&ratio>=REQUIRED;if(!eligible)return'deep';if(ratio>=DEFAULT)return'direct';if(!viewProbe)return'probe';if(!viewProbe.qualityPass||viewProbe.classDisagreement!==0||viewProbe.deepUnknownRate>UNKNOWN_MAX)return'deep';if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*MARGIN)return'deep';return'direct'}
const good=(d,u=0)=>({qualityPass:true,classDisagreement:0,deepUnknownRate:u,predictedDirectMs:100,predictedDeepMs:d});
const cases=[
[0.5,null,'deep'],[7.999,null,'deep'],[8,null,'probe'],[64,null,'probe'],[128,null,'direct'],
[32,good(80),'deep'],[32,good(86),'direct'],[32,good(60,.01),'deep'],
[32,{...good(60),qualityPass:false},'deep'],[32,{...good(60),classDisagreement:1},'deep']
];
for(const [ratio,p,want] of cases){const got=choose(ratio,p);if(got!==want)throw new Error(JSON.stringify({ratio,p,want,got}))}
console.log(JSON.stringify({status:'pass',kind:'view-specific-adaptive-backend-router',directRequiredRatio:REQUIRED,directDefaultRatio:DEFAULT,deepSpeedMargin:MARGIN,deepProbeUnknownMax:UNKNOWN_MAX,cases:cases.length},null,2));

View file

@ -6,9 +6,9 @@ function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),
function fixedNum(v,bits){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)),x=top*Math.pow(2,sh-bits);return neg?-x:x}function log2FixedAt(v,b){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-b}function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:F(Math.pow(2,l-exp)),exp}}
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
function direct(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b);let zr=0n,zi=0n;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr}return{hi,lo,refLen:limit}}
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<<BigInt(b);let zr=0n,zi=0n,refLen=limit;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;const mag=roundShift(zr*zr,b)+roundShift(zi*zi,b);if(mag>bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[refLen*2]=hr1;hi[refLen*2+1]=hi1;lo[refLen*2]=F(rr1-hr1);lo[refLen*2+1]=F(ri1-hi1);break}}return{hi,lo,refLen}}
function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))}
function perturb(ref,s,w,h,x,y,limit,strict=false){const se=spanME(s),dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w),U=2**-24;let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=64*U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+64*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30),threshold=strict?1e-4:1e-3;return ea<=threshold?{kind:'bounded',n,err:ea}:{kind:'unknown',n,reason:'error-bound',err:ea}}if(m>ref.refLen)return{kind:'unknown',n,reason:'ref'};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+64*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n,err:ea};return{kind:'unknown',n,reason:'escape-uncertain',err:ea}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n,reason:'rebase-gap'};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=64*U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n,reason:'ref-end'};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=64*U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n,reason:'range'};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n,reason:'guard'}}}
function perturb(ref,s,w,h,x,y,limit,strict=false){const se=spanME(s),dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w),U=2**-24;let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=1*U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+1*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30),threshold=strict?1e-4:1e-3;return ea<=threshold?{kind:'bounded',n,err:ea}:{kind:'unknown',n,reason:'error-bound',err:ea}}if(m>ref.refLen)return{kind:'unknown',n,reason:'ref'};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+1*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n,err:ea};return{kind:'unknown',n,reason:'escape-uncertain',err:ea}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n,reason:'rebase-gap'};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=1*U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n,reason:'ref-end'};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=1*U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n,reason:'range'};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n,reason:'guard'}}}
const cfg=[['period2-cusp-z14',1200],['swirly-seahorses-z12',2000],['period2-cusp-z20',1800],['period2-cusp-z100',2400],['period3-interior',1200],['period2-cusp-e280',2400],['period2-cusp-e400',1600]],w=17,h=11,pts=[];
for(let y=0;y<h;y++)for(let x=0;x<w;x++)pts.push([x,y]);
const report=[];

View file

@ -0,0 +1,16 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
must(/DIRECT_DS_WGSL/.test(kernels)&&/struct DS/.test(kernels)&&/centerReLo/.test(kernels),'DS-direct benchmark shader missing');
must(/direct-ds-benchmark/.test(script)&&/this\.dsDirect=/.test(script),'DS-direct pipeline missing');
must(/benchmarkDsDirect/.test(script)&&/renderTileMetaDsDirect/.test(script),'DS-direct benchmark methods missing');
must(/CROSSOVER_DEPTHS=\[0,2,4,6,8,10,12,14,16,20,30,40\]/.test(script),'round-depth crossover suite missing');
must(/runCrossoverBenchmark/.test(script)&&/referenceBuildMs/.test(script)&&/coldMedianMs/.test(script),'crossover benchmark timing/report missing');
must(/CROSSOVER_SPEED_W=256/.test(script)&&/CROSSOVER_SPEED_H=144/.test(script)&&/crossoverSpeedTiles/.test(script),'bounded representative speed tiles missing');
must(/fullMedianMs/.test(script)&&/msPerPixel/.test(script)&&/speedPixels/.test(script),'tile timing extrapolation missing');
must(/CROSSOVER_GPU_TIMEOUT_MS=15000/.test(script)&&/benchmarkTimeout/.test(script)&&/timed out after/.test(script),'benchmark watchdog missing');
must(/classMismatchRate/.test(script)&&/escapeIterationMismatchRate/.test(script),'GPU quality comparison missing');
must(/id="backendBench"/.test(html)&&/id="benchmarkDialog"/.test(html)&&/id="benchmarkSave"/.test(html),'benchmark UI missing');
const choose=(script.match(/function chooseBackend[\s\S]*?function deepNeeded/)||[''])[0];
must(!/dsDirect|backend='ds'/.test(choose),'experimental DS-direct leaked into production router');
console.log(JSON.stringify({status:'pass',check:'crossover-benchmark-contract',checks:11},null,2));

View file

@ -0,0 +1,106 @@
import fs from 'node:fs/promises';
const F=Math.fround;
const U=2**-24;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
function bitsForDepth(z){return Math.max(256,Math.ceil(z*Math.log2(10))+300)}
function snap(re,im,z){const span=`3.4e-${z}`,bits=bitsForDepth(z);return{bits,re:decFixed(re,bits),im:decFixed(im,bits),span:decFixed(span,bits),spanText:span}}
function fixedNum(v,bits){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)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
function log2FixedAt(v,b){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-b}
function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:Math.pow(2,l-exp),exp}}
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
function f32Direct(cr,ci,limit){cr=F(cr);ci=F(ci);let zr=0,zi=0;for(let n=0;n<limit;n++){const a=F(zr*zr),b=F(zi*zi);zi=F(F(F(2*zr)*zi)+ci);zr=F(F(a-b)+cr);if(F(F(zr*zr)+F(zi*zi))>4)return n+1}return limit}
// DS helpers
function q(a,b){const x=F(a+b),e=F(b-F(x-a));return[x,e]}
function sum(a,b){const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
function prod(a,b){const x=F(a*b),ca=F(4097*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(4097*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(a[1]+b[1])))}
function neg(a){return[F(-a[0]),F(-a[1])]} function sub(a,b){return add(a,neg(b))}
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(a[0]*b[1]));e=F(e+F(a[1]*b[0]));e=F(e+F(a[1]*b[1]));return q(t[0],e)}
function scaleDS(a,b){const t=prod(a[0],b);return q(t[0],F(t[1]+F(a[1]*b)))}
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]} function cmulDS(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
function mag2DS(a){return add(mul(a[0],a[0]),mul(a[1],a[1]))}
function split(x){const h=F(x);return[h,F(x-h)]}
function dsDirect(s,w,h,x,y,limit){const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),span=split(fixedNum(s.span,s.bits));const offx=(x+.5-.5*w)/w,offy=(.5*h-y-.5)/w;const cr=add(cre,scaleDS(span,F(offx))),ci=add(cim,scaleDS(span,F(offy)));let z=[[0,0],[0,0]];for(let n=0;n<limit;n++){z=cadd(cmulDS(z,z),[cr,ci]);if(cmp(mag2DS(z),[4,0])>0)return n+1}return limit}
function adjacentUniq(s,w,h){const y=Math.floor(h/2),cx=Math.floor(w/2),f32s=new Set(),dss=new Set(),cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),sp=split(fixedNum(s.span,s.bits));for(let x=cx-10;x<=cx+10;x++){const p=pixel(s,w,h,x,y),rr=fixedNum(p[0],s.bits),ii=fixedNum(p[1],s.bits);f32s.add(F(rr)+','+F(ii));const ox=(x+.5-.5*w)/w,oy=(.5*h-y-.5)/w,cr=add(cre,scaleDS(sp,F(ox))),ci=add(cim,scaleDS(sp,F(oy)));dss.add(cr[0]+':'+cr[1]+','+ci[0]+':'+ci[1])}return{f32:f32s.size,ds:dss.size}}
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<<BigInt(b);let zr=0n,zi=0n,refLen=limit;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;const mag=roundShift(zr*zr,b)+roundShift(zi*zi,b);if(mag>bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[2*refLen]=hr1;hi[2*refLen+1]=hi1;lo[2*refLen]=F(rr1-hr1);lo[2*refLen+1]=F(ri1-hi1);break}}return{hi,lo,refLen}}
function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}
function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))}
function primary(ref,s,w,h,x,y,limit){const se0=spanME(s),se={mant:F(se0.mant),exp:se0.exp},dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w);let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);return ea<=1e-3?{kind:'bounded',n}:{kind:'unknown',n}}if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n};return{kind:'unknown',n}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
function pow2DS(a,e){if(e<-126)return[0,0];if(e>126)return[F(8.507059e37),0];return[F(a[0]*2**e),F(a[1]*2**e)]}
function cscale(a,b){return[scaleDS(a[0],b),scaleDS(a[1],b)]} function cpow2(a,e){return[pow2DS(a[0],e),pow2DS(a[1],e)]} function maxabsDS(a){return Math.max(Math.abs(F(a[0][0]+a[0][1])),Math.abs(F(a[1][0]+a[1][1])))}
function correction(ref,s,w,h,x,y,limit){const se=spanME(s),mh=F(se.mant),sm=[mh,F(se.mant-mh)],iw=1/w,iwh=F(iw),inv=[iwh,F(iw-iwh)],ox=F(x+.5-.5*w),oy=F(.5*h-y-.5),dx=scaleDS(inv,ox),dy=scaleDS(inv,oy),d0=[mul(sm,dx),mul(sm,dy)];let d=d0,wv=[[0,0],[0,0]],e=se.exp,n=0,m=0,ops=0;while(true){if(n>=limit)return{kind:'bounded',n};if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,r=[[ref.hi[rp],ref.lo[rp]],[ref.hi[rp+1],ref.lo[rp+1]]],delta=cpow2(wv,e),z=cadd(r,delta),mg=mag2DS(z);if(cmp(mg,[4,0])>0)return{kind:'escaped',n};const dm=mag2DS(delta);if(m>0&&cmp(dm,[0,0])>0&&cmp(mg,dm)<0){if(se.exp-e<-96)return{kind:'unknown',n};wv=z;d=cpow2(d0,se.exp);e=0;m=0;continue}if(m>=ref.refLen)return{kind:'unknown',n};const linear=cscale(cmulDS(r,wv),2),sq=cpow2(cmulDS(wv,wv),e);wv=cadd(cadd(linear,sq),d);m++;n++;ops++;const mm=Math.max(maxabsDS(wv),maxabsDS(d));if(mm>=1e30||!Number.isFinite(mm))return{kind:'unknown',n};if(mm>65536){wv=cscale(wv,1/65536);d=cscale(d,1/65536);e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wv=cscale(wv,65536);d=cscale(d,65536);e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
function adaptiveIter(z){return Math.max(350,350+Math.floor(70*Math.sqrt(z)+15*z))}
const tracks=[
{id:'cusp',re:'-0.75',im:'0'},
{id:'seahorse',re:'-0.7453983606667815',im:'0.1125046349959942'}
];
const depths=[0,2,4,6,8,10,12,14,16,20,30,40];
const SW=21,SH=15,FULL_W=1600,FULL_H=900;
const rows=[];
for(const tr of tracks){
for(const z of depths){
const s=snap(tr.re,tr.im,z);
const limit=adaptiveIter(z);
const ref=buildRef(s,limit);
let guardMismatch=0,f32FE=0,f32FB=0,dsFE=0,dsFB=0,deepFE=0,deepFB=0,deepU=0,tested=0,f32IterMismatch=0,dsIterMismatch=0,deepIterMismatch=0;
const f32Coords=new Set(), dsCoords=new Set();
const t0=performance.now();
for(let sy=0;sy<SH;sy++){
const y=Math.round(sy*(FULL_H-1)/Math.max(1,SH-1));
for(let sx=0;sx<SW;sx++){
const x=Math.round(sx*(FULL_W-1)/Math.max(1,SW-1));
const p=pixel(s,FULL_W,FULL_H,x,y);
const g=pixel(s,FULL_W,FULL_H,x,y,s.bits+64);
const o=oracle(p[0],p[1],s.bits,limit);
const gg=oracle(g[0],g[1],s.bits+64,limit);
if(o!==gg){guardMismatch++;continue;}
tested++;
const rr=fixedNum(p[0],s.bits),ii=fixedNum(p[1],s.bits);
const fd=f32Direct(rr,ii,limit);
const dd=dsDirect(s,FULL_W,FULL_H,x,y,limit);
f32Coords.add(F(rr)+','+F(ii));
{const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),sp=split(fixedNum(s.span,s.bits)),ox=(x+.5-.5*FULL_W)/FULL_W,oy=(.5*FULL_H-y-.5)/FULL_W,crd=add(cre,scaleDS(sp,F(ox))),cid=add(cim,scaleDS(sp,F(oy)));dsCoords.add(crd[0]+':'+crd[1]+','+cid[0]+':'+cid[1]);}
const pr=primary(ref,s,FULL_W,FULL_H,x,y,limit);
const dp=pr.kind==='unknown'?correction(ref,s,FULL_W,FULL_H,x,y,limit):pr;
if(fd<limit&&o===limit)f32FE++;
if(fd===limit&&o<limit)f32FB++;
if(dd<limit&&o===limit)dsFE++;
if(dd===limit&&o<limit)dsFB++;
if(fd<limit&&o<limit&&fd!==o)f32IterMismatch++;
if(dd<limit&&o<limit&&dd!==o)dsIterMismatch++;
if(dp.kind==='unknown')deepU++;
else {
if(dp.kind==='escaped'&&o===limit)deepFE++;
if(dp.kind==='bounded'&&o<limit)deepFB++;
if(dp.kind==='escaped'&&o<limit&&dp.n!==o)deepIterMismatch++;
}
}
}
const adj=adjacentUniq(s,FULL_W,FULL_H);
const row={
track:tr.id,z,limit,tested,guardMismatch,adjacentUniqueX:adj,
f32:{falseEscaped:f32FE,falseBounded:f32FB,escapeIterationMismatch:f32IterMismatch,uniqueCoords:f32Coords.size},
ds:{falseEscaped:dsFE,falseBounded:dsFB,escapeIterationMismatch:dsIterMismatch,uniqueCoords:dsCoords.size},
deep:{falseEscaped:deepFE,falseBounded:deepFB,unknown:deepU,escapeIterationMismatch:deepIterMismatch},
modelMs:Math.round(performance.now()-t0)
};
rows.push(row);
}
}
const seahorse=rows.filter(r=>r.track==='seahorse');
const material=r=>((r.falseEscaped+r.falseBounded)/Math.max(1,r.tested))>0.01;
const firstF32=seahorse.find(r=>material({...r.f32,tested:r.tested})||r.adjacentUniqueX.f32<11)?.z??null;
const firstDs=seahorse.find(r=>material({...r.ds,tested:r.tested})||r.adjacentUniqueX.ds<11)?.z??null;
const deepIssues=seahorse.filter(r=>r.deep.falseEscaped||r.deep.falseBounded||r.deep.unknown);
const report={format:'mandelbrot-crossover-quality-v1',kind:'cpu-f32-model + BigInt P/P+64 oracle; not real GPU',viewport:[FULL_W,FULL_H],sampleGrid:[SW,SH],depths,tracks:tracks.map(t=>t.id),rows,summary:{firstMaterialF32DegradationDepth:firstF32,firstMaterialDsDegradationDepth:firstDs,deepIssueRows:deepIssues.map(r=>({z:r.z,...r.deep})),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.'}};
if(rows.some(r=>r.guardMismatch))throw new Error('P/P+64 oracle guard mismatch in crossover corpus');
await fs.mkdir(new URL('../audit/',import.meta.url),{recursive:true});
await fs.writeFile(new URL('../audit/v24-crossover-quality.json',import.meta.url),JSON.stringify(report,null,2));
console.log(JSON.stringify({status:'pass',check:'crossover-quality-advisory',summary:report.summary},null,2));

View file

@ -0,0 +1,24 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
must(/DIRECT_DS_GUARDED_WGSL/.test(kernels)&&/riskLimit:f32/.test(kernels)&&/riskQueue:array<u32>/.test(kernels)&&/atomicAdd\(&hybridStats\.enqueued,1u\)/.test(kernels),'guarded DS sensitivity queue shader missing');
must(/DIRECT_FIXED96_QUEUE_ARGS_WGSL/.test(kernels)&&/dispatchCount/.test(kernels)&&/indirectArgs\.x=\(n\+63u\)\/64u/.test(kernels),'fixed96 queue indirect-args shader missing');
must(/DIRECT_FIXED96_CORRECT_WGSL/.test(kernels)&&/struct FX96/.test(kernels)&&/fn mul32/.test(kernels)&&/Q8\.88/.test(kernels),'fixed96 correction shader missing');
const a=kernels.indexOf('const DIRECT_FIXED96_CORRECT_WGSL'),b=kernels.indexOf('// Deep path',a),fx=kernels.slice(a,b);
must(!/fn fx_bad_range/.test(fx),'invalid Q8.88 sign-extension range guard regressed');
must(/let TWO=FX96\(0u,0u,0x02000000u\)/.test(fx)&&/fx_cmp_unsigned\(fx_abs\(zr\),TWO\)>0/.test(fx)&&/write_escape\(out,n,zr,zi\)/.test(fx),'fixed96 certain-escape guard missing');
must(a>=0&&b>a&&!fx.includes('var<storage,read> refs'),'fixed96 correction unexpectedly depends on deep reference');
must(/@workgroup_size\(64\)/.test(fx)&&/let out=riskQueue\[qi\]/.test(fx)&&/atomicLoad\(&hybridStats\.dispatchCount\)/.test(fx),'fixed96 correction is not queue-indexed');
must(!/@workgroup_size\(8,8\)/.test(fx),'fixed96 correction regressed to full-screen 2D workgroups');
must(/direct-fixed96-queue-args-experiment/.test(script)&&/this\.fixed96QueueArgs=/.test(script),'fixed96 queue-args pipeline missing');
must(/direct-fixed96-correction-experiment/.test(script)&&/this\.fixed96Correct=/.test(script),'fixed96 correction pipeline missing');
must(/q88FromRational/.test(script)&&/q88Words/.test(script)&&/fixed96TileData/.test(script),'exact BigInt-to-Q8.88 packing missing');
must(/benchmarkFixed96Direct/.test(script)&&/renderTileMetaFixed96Direct/.test(script),'fixed96 benchmark methods missing');
must(/GPUBufferUsage[\s\S]*?B\.INDIRECT/.test(script)&&/dispatchWorkgroupsIndirect\(indirect,0\)/.test(script),'true sparse indirect dispatch missing');
must(/fixed96QueueIntegrity/.test(script)&&/queue integrity failure/.test(script),'queue integrity fail-closed gate missing');
must(/FIXED96_DEPTHS=\[6,8,10,12\]/.test(script)&&/FIXED96_RISK_LIMITS=\[1e11,1e12,1e13,1e14\]/.test(script),'fixed96 depth/risk sweep missing');
must(/runFixed96Experiment/.test(script)&&/failureReason/.test(script)&&/Every risk candidate/.test(script),'fixed96 experiment/full failure reporting missing');
must(/speedQueueRate/.test(script)&&/timingRepresentative/.test(script)&&/sampling-mismatch/.test(script),'speed/quality sampling consistency gate missing');
must(/id="sparseBench"/.test(html)&&/Fixed96 Direct実験/.test(html)&&/id="sparseDialog"/.test(html),'fixed96 UI missing');
const choose=(script.match(/function chooseBackend[\s\S]*?function deepNeeded/)||[''])[0];must(!/fixed96|FIXED96/.test(choose),'diagnostic fixed96 leaked into production router');
console.log(JSON.stringify({status:'pass',check:'fixed96-experiment-contract',checks:20},null,2));

View file

@ -0,0 +1,15 @@
const MASK=0xffffffffn,MOD=1n<<96n,SIGN=1n<<95n,Q=88n;
const u32=x=>Number(BigInt(x)&MASK)>>>0;
function words(x){x%=MOD;if(x<0)x+=MOD;return[u32(x),u32(x>>32n),u32(x>>64n)]}
function signed(w){let x=BigInt(w[0])+(BigInt(w[1])<<32n)+(BigInt(w[2])<<64n);return x>=SIGN?x-MOD:x}
function neg(a){let lo=(~a[0]+1)>>>0,c0=lo===0?1:0,mid=(~a[1]+c0)>>>0,c1=c0&&mid===0?1:0,hi=(~a[2]+c1)>>>0;return[lo,mid,hi]}
function isNeg(a){return (a[2]&0x80000000)!==0} function absw(a){return isNeg(a)?neg(a):a}
function add(a,b){let lo=(a[0]+b[0])>>>0,c0=lo<a[0]?1:0,m0=(a[1]+b[1])>>>0,c1=m0<a[1]?1:0,mid=(m0+c0)>>>0,c2=mid<m0?1:0,hi=(a[2]+b[2]+c1+c2)>>>0;return[lo,mid,hi]}
function mul32(a,b){const a0=a&65535,a1=a>>>16,b0=b&65535,b1=b>>>16,p0=a0*b0,p1=a0*b1,p2=a1*b0,p3=a1*b1,middle=(p0>>>16)+(p1&65535)+(p2&65535),lo=((p0&65535)|((middle&65535)<<16))>>>0,hi=(p3+(p1>>>16)+(p2>>>16)+(middle>>>16))>>>0;return[lo,hi]}
function addAt(p,idx,v){v>>>=0;while(v&&idx<6){const old=p[idx]>>>0,sum=(old+v)>>>0;p[idx]=sum;v=sum<old?1:0;idx++}}
function addProd(p,idx,a,b){const q=mul32(a,b);addAt(p,idx,q[0]);addAt(p,idx+1,q[1])}
function mulfx(a0,b0){const sign=isNeg(a0)!==isNeg(b0),a=absw(a0),b=absw(b0),p=[0,0,0,0,0,0];addProd(p,0,a[0],b[0]);addProd(p,1,a[0],b[1]);addProd(p,2,a[0],b[2]);addProd(p,1,a[1],b[0]);addProd(p,2,a[1],b[1]);addProd(p,3,a[1],b[2]);addProd(p,2,a[2],b[0]);addProd(p,3,a[2],b[1]);addProd(p,4,a[2],b[2]);let r=[((p[2]>>>24)|(p[3]<<8))>>>0,((p[3]>>>24)|(p[4]<<8))>>>0,((p[4]>>>24)|(p[5]<<8))>>>0];if(((p[2]>>>23)&1)!==0)r=add(r,[1,0,0]);return sign?neg(r):r}
function roundShift(v,b){const negv=v<0n,a=negv?-v:v,q=(a+(1n<<(b-1n)))>>b;return negv?-q:q}
let seed=0x12345678;const rand=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed};
for(let i=0;i<20000;i++){const ai=(BigInt(rand()%16000000)-8000000n)*(1n<<68n),bi=(BigInt(rand()%16000000)-8000000n)*(1n<<68n),a=words(ai),b=words(bi),got=signed(mulfx(a,b)),want=roundShift(ai*bi,Q);if(got!==want)throw new Error(`mul mismatch ${i}: ${got} != ${want}`)}
console.log(JSON.stringify({status:'pass',check:'fixed96-limb-multiply',cases:20000,format:'Q8.88'},null,2));

View file

@ -0,0 +1,24 @@
const MASK=0xffffffffn,MOD=1n<<96n,SIGN=1n<<95n,Q=88n,MIN=-(1n<<95n),MAX=(1n<<95n)-1n;
const u32=x=>Number(BigInt(x)&MASK)>>>0;
function words(x){if(x<MIN||x>MAX)throw new Error('Q8.88 overflow');if(x<0)x+=MOD;return[u32(x),u32(x>>32n),u32(x>>64n)]}
function signed(w){let x=BigInt(w[0])+(BigInt(w[1])<<32n)+(BigInt(w[2])<<64n);return x>=SIGN?x-MOD:x}
function neg(a){let lo=(~a[0]+1)>>>0,c0=lo===0?1:0,mid=(~a[1]+c0)>>>0,c1=c0&&mid===0?1:0,hi=(~a[2]+c1)>>>0;return[lo,mid,hi]}
function isNeg(a){return (a[2]&0x80000000)!==0} function absw(a){return isNeg(a)?neg(a):a}
function add(a,b){let lo=(a[0]+b[0])>>>0,c0=lo<a[0]?1:0,m0=(a[1]+b[1])>>>0,c1=m0<a[1]?1:0,mid=(m0+c0)>>>0,c2=mid<m0?1:0,hi=(a[2]+b[2]+c1+c2)>>>0;return[lo,mid,hi]}
function sub(a,b){return add(a,neg(b))}
function cmpu(a,b){for(let i=2;i>=0;i--){if(a[i]<b[i])return-1;if(a[i]>b[i])return 1}return 0}
function mul32(a,b){const a0=a&65535,a1=a>>>16,b0=b&65535,b1=b>>>16,p0=a0*b0,p1=a0*b1,p2=a1*b0,p3=a1*b1,middle=(p0>>>16)+(p1&65535)+(p2&65535),lo=((p0&65535)|((middle&65535)<<16))>>>0,hi=(p3+(p1>>>16)+(p2>>>16)+(middle>>>16))>>>0;return[lo,hi]}
function addAt(p,idx,v){v>>>=0;while(v&&idx<6){const old=p[idx]>>>0,sum=(old+v)>>>0;p[idx]=sum;v=sum<old?1:0;idx++}}
function addProd(p,idx,a,b){const q=mul32(a,b);addAt(p,idx,q[0]);addAt(p,idx+1,q[1])}
function mulfx(a0,b0){const sign=isNeg(a0)!==isNeg(b0),a=absw(a0),b=absw(b0),p=[0,0,0,0,0,0];addProd(p,0,a[0],b[0]);addProd(p,1,a[0],b[1]);addProd(p,2,a[0],b[2]);addProd(p,1,a[1],b[0]);addProd(p,2,a[1],b[1]);addProd(p,3,a[1],b[2]);addProd(p,2,a[2],b[0]);addProd(p,3,a[2],b[1]);addProd(p,4,a[2],b[2]);let r=[((p[2]>>>24)|(p[3]<<8))>>>0,((p[3]>>>24)|(p[4]<<8))>>>0,((p[4]>>>24)|(p[5]<<8))>>>0];if(((p[2]>>>23)&1)!==0)r=add(r,[1,0,0]);return sign?neg(r):r}
function roundShift(v,b=Q){const n=v<0n,a=n?-v:v,q=(a+(1n<<(b-1n)))>>b;return n?-q:q}
function exactMul(a,b){const r=roundShift(a*b);if(r<MIN||r>MAX)throw new Error('exact overflow');return r}
const TWO=words(2n<<Q),FOUR=words(4n<<Q);
function limbOrbit(cr0,ci0,limit){const cr=words(cr0),ci=words(ci0);let zr=[0,0,0],zi=[0,0,0];for(let n=1;n<=limit;n++){const zr2=mulfx(zr,zr),zi2=mulfx(zi,zi),zri=mulfx(zr,zi);zr=add(sub(zr2,zi2),cr);zi=add(add(zri,zri),ci);if(cmpu(absw(zr),TWO)>0||cmpu(absw(zi),TWO)>0)return n;const mag=add(mulfx(zr,zr),mulfx(zi,zi));if(cmpu(mag,FOUR)>0)return n}return limit}
function exactOrbit(cr,ci,limit){let zr=0n,zi=0n;const four=4n<<Q;for(let n=1;n<=limit;n++){const zr2=exactMul(zr,zr),zi2=exactMul(zi,zi),zri=exactMul(zr,zi);zr=zr2-zi2+cr;zi=2n*zri+ci;if((zr<0n?-zr:zr)>2n<<Q||(zi<0n?-zi:zi)>2n<<Q)return n;const mag=exactMul(zr,zr)+exactMul(zi,zi);if(mag>four)return n}return limit}
function qdec(x){return BigInt(Math.round(x*2**24))<<64n}
const cases=[];
for(const [re,im] of [[-0.7453983606667815,0.1125046349959942],[-0.75,0.1],[-0.743643887037151,0.13182590420533],[-1.8,0.2],[0.3,0.55],[1.2,0]]){
const cr=qdec(re),ci=qdec(im),a=limbOrbit(cr,ci,1200),b=exactOrbit(cr,ci,1200);if(a!==b)throw new Error(`orbit mismatch ${re},${im}: ${a} != ${b}`);cases.push({re,im,iter:a});
}
console.log(JSON.stringify({status:'pass',check:'fixed96-limb-orbit',cases},null,2));

View file

@ -0,0 +1,39 @@
import fs from 'node:fs/promises';
const F=Math.fround,Q=88,MOD=1n<<96n,MIN=-(1n<<95n),MAX=(1n<<95n)-1n;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const[m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
function bitsForDepth(z){return Math.max(256,Math.ceil(z*Math.log2(10))+300)}
function snap(re,im,z){const bits=bitsForDepth(z);return{bits,re:decFixed(re,bits),im:decFixed(im,bits),span:decFixed(`3.4e-${z}`,bits)}}
function fixedNum(v,bits){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)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
function q88FromRational(numer,denom,bits){let n=numer,d=denom,sh=Q-bits;if(sh>=0)n<<=BigInt(sh);else d<<=BigInt(-sh);return roundDiv(n,d)}
function qmul(a,b){const r=roundShift(a*b,Q);if(r<MIN||r>MAX)throw new Error('Q8.88 overflow');return r}
function qadd(a,b){const r=a+b;if(r<MIN||r>MAX)throw new Error('Q8.88 overflow');return r}
function fixed96Direct(s,w,h,x,y,limit){const den=4n*BigInt(w),ox4=4n*BigInt(x)+2n-2n*BigInt(w),oy4=2n*BigInt(h)-4n*BigInt(y)-2n,cr=q88FromRational(s.re*den+s.span*ox4,den,s.bits),ci=q88FromRational(s.im*den+s.span*oy4,den,s.bits);let zr=0n,zi=0n;const four=4n<<88n;for(let n=0;n<limit;n++){const zr2=qmul(zr,zr),zi2=qmul(zi,zi),ri=qmul(zr,zi);zr=qadd(qadd(zr2,-zi2),cr);zi=qadd(qadd(ri,ri),ci);if(qadd(qmul(zr,zr),qmul(zi,zi))>four)return n+1}return limit}
// DS sensitivity model matching the experimental guard.
function q(a,b){const x=F(F(a)+F(b)),e=F(F(b)-F(x-F(a)));return[x,e]}
function sum(a,b){a=F(a);b=F(b);const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
function prod(a,b){a=F(a);b=F(b);const x=F(a*b),ca=F(F(4097)*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(F(4097)*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(F(a[1])+F(b[1]))))}
function neg(a){return[F(-a[0]),F(-a[1])]} function sub(a,b){return add(a,neg(b))}
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(F(a[0])*F(b[1])));e=F(e+F(F(a[1])*F(b[0])));e=F(e+F(F(a[1])*F(b[1])));return q(t[0],e)}
function scale(a,b){const t=prod(a[0],F(b));return q(t[0],F(t[1]+F(F(a[1])*F(b))))}
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]} function cmul(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
function split(x){const h=F(x);return[h,F(x-h)]} function val(a){return F(F(a[0])+F(a[1]))}
function dsRisk(s,w,h,x,y,limit){const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),sp=split(fixedNum(s.span,s.bits)),ox=F(F(F(x+.5)-F(.5*w))/F(w)),oy=F(F(F(.5*h)-F(y+.5))/F(w)),c=[add(cre,scale(sp,ox)),add(cim,scale(sp,oy))];let z=[[0,0],[0,0]],dr=0,di=0,maxDer=0;for(let n=0;n<limit;n++){const zr=val(z[0]),zi=val(z[1]),ndr=F(F(2)*F(F(zr*dr)-F(zi*di))+F(1)),ndi=F(F(2)*F(F(zr*di)+F(zi*dr)));dr=ndr;di=ndi;maxDer=Math.max(maxDer,Math.hypot(dr,di));z=cadd(cmul(z,z),c);const mag=add(mul(z[0],z[0]),mul(z[1],z[1]));if(cmp(mag,[4,0])>0)return{n:n+1,maxDer}}return{n:limit,maxDer}}
function adaptiveIter(z){return Math.max(350,350+Math.floor(70*Math.sqrt(z)+15*z))}
const re='-0.7453983606667815',im='0.1125046349959942',depths=[6,8,10,12],risks=[1e11,1e12,1e13,1e14],W=1600,H=900,SW=31,SH=21,rows=[];
for(const z of depths){const s=snap(re,im,z),limit=adaptiveIter(z),samples=[];for(let sy=0;sy<SH;sy++){const y=Math.round(sy*(H-1)/(SH-1));for(let sx=0;sx<SW;sx++){const x=Math.round(sx*(W-1)/(SW-1)),p=pixel(s,W,H,x,y),pg=pixel(s,W,H,x,y,s.bits+64),o=oracle(p[0],p[1],s.bits,limit),g=oracle(pg[0],pg[1],s.bits+64,limit);if(o!==g){samples.push({guardMismatch:true});continue}const d=dsRisk(s,W,H,x,y,limit),f=fixed96Direct(s,W,H,x,y,limit);samples.push({guardMismatch:false,o,d,f})}}
const variants=[];for(const risk of risks){let tested=0,guardMismatch=0,uncertain=0,wrongBefore=0,wrongAfter=0;for(const a of samples){if(a.guardMismatch){guardMismatch++;continue}tested++;if((a.d.n<a.d.limit)!=(a.o<a.d.limit)){}const before=(a.d.n<limit)!=(a.o<limit);if(before)wrongBefore++;let r=a.d.n;if(a.d.maxDer>risk){uncertain++;r=a.f}if((r<limit)!=(a.o<limit))wrongAfter++}variants.push({risk,tested,guardMismatch,uncertain,uncertainRate:uncertain/Math.max(1,tested),wrongBefore,wrongAfter})}
rows.push({z,limit,variants})}
if(rows.some(r=>r.variants.some(v=>v.guardMismatch)))throw new Error('oracle guard mismatch');
for(const r of rows)if(!r.variants.some(v=>v.wrongAfter===0))throw new Error(`no safe fixed96 candidate at z${r.z}`);
const report={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:[W,H],sampleGrid:[SW,SH],rows};
await fs.mkdir(new URL('../audit/',import.meta.url),{recursive:true});await fs.writeFile(new URL('../audit/v24-fixed96-model.json',import.meta.url),JSON.stringify(report,null,2));
console.log(JSON.stringify({status:'pass',check:'fixed96-q88-model',rows:rows.map(r=>({z:r.z,variants:r.variants}))},null,2));

View file

@ -0,0 +1,22 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
const root=new URL('../',import.meta.url);
const script=await fs.readFile(new URL('script.js',root),'utf8');
const start=script.indexOf('function fixed96QueueIntegrity('),end=script.indexOf('\nfunction mulRatio',start);
if(start<0||end<=start)throw new Error('fixed96QueueIntegrity source missing');
const source=script.slice(start,end);
const ctx={};ctx.globalThis=ctx;
vm.runInNewContext(`const FIXED96_WORKGROUP_SIZE=64;${source};globalThis.check=fixed96QueueIntegrity`,ctx);
const check=ctx.check;
const stats=(selected,{overflow=0,enqueued=selected,dispatchCount=enqueued}={})=>new Uint32Array([selected,overflow,enqueued,dispatchCount,0,0,0,0]);
const fixed=(processed,{remaining=0,invalidIndex=0,staleEntry=0,corrected=processed-remaining}={})=>new Uint32Array([remaining,invalidIndex,processed,staleEntry,0,0,0,corrected]);
const args=n=>new Uint32Array([Math.ceil(n/64),1,1,0]);
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
must(check(stats(0),fixed(0),args(0),64).ok,'zero queue should be valid');
must(check(stats(20),fixed(20),args(20),64).ok,'20-entry queue should be valid');
must(check(stats(65),fixed(65),args(65),128).ok,'65-entry queue should use two workgroups');
must(!check(stats(65),fixed(65),new Uint32Array([1,1,1,0]),128).ok,'wrong indirect workgroup count was not rejected');
must(!check(stats(64,{overflow:1,enqueued:63,dispatchCount:63}),fixed(63),args(63),64).ok,'queue overflow was not rejected');
must(!check(stats(20),fixed(19,{corrected:19}),args(20),64).ok,'processed/enqueued mismatch was not rejected');
must(!check(stats(20),fixed(20,{staleEntry:1,remaining:1,corrected:19}),args(20),64).ok,'stale queue entry was not rejected');
console.log(JSON.stringify({status:'pass',check:'fixed96-queue-integrity-model',cases:7,workgroupSize:64},null,2));

View file

@ -0,0 +1,24 @@
// Regression for v24.2.6: Q8.88 values such as +1.2 were incorrectly treated
// as invalid sign extension. Model the corrected shader policy: all Q8.88
// values are valid; after each iteration, |Re|>2 or |Im|>2 is an immediate
// mathematically-certain escape, otherwise |z|^2 can be squared safely.
const Q=88n, ONE=1n<<Q, TWO=2n<<Q, FOUR=4n<<Q, MIN=-(1n<<95n), MAX=(1n<<95n)-1n;
function roundShift(v,b=Q){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(b-1n)))>>b;return neg?-q:q}
function q(x){return BigInt(Math.round(x*2**24)) << 64n} // exact enough for test constants
function mul(a,b){const r=roundShift(a*b);if(r<MIN||r>MAX)throw new Error('unexpected Q8.88 overflow');return r}
function add(a,b){const r=a+b;if(r<MIN||r>MAX)throw new Error('unexpected Q8.88 overflow');return r}
function abs(a){return a<0n?-a:a}
function iterate(cr,ci,maxIter=2000){let zr=0n,zi=0n;for(let n=1;n<=maxIter;n++){
const zr2=mul(zr,zr),zi2=mul(zi,zi),zri=mul(zr,zi);
zr=add(add(zr2,-zi2),cr);zi=add(add(zri,zri),ci);
if(abs(zr)>TWO||abs(zi)>TWO)return n;
const mag=add(mul(zr,zr),mul(zi,zi));if(mag>FOUR)return n;
}return maxIter}
// +1.2 is a valid Q8.88 value and must not be rejected merely because the
// integer byte is non-zero.
const plus12=q(1.2);if(plus12<=ONE||plus12>=TWO)throw new Error('test constant invalid');
// Escaping parameters whose orbit passes through component magnitudes >1.
for(const [re,im] of [[1.2,0],[-1.8,0.2],[0.5,0.8],[0.4,0.6]]){
const n=iterate(q(re),q(im),500);if(n>=500)throw new Error(`expected escape for ${re},${im}`);
}
console.log(JSON.stringify({status:'pass',check:'fixed96-shader-range-regression',cases:4,regression:'v24.2.6 rejected valid Q8.88 integer-byte values'},null,2));

View file

@ -1,6 +1,6 @@
const must=(x,m)=>{if(!x)throw new Error(m)};
for(const [W,H] of [[17,11],[1024,768],[16384,9216]])for(const tile of [64,512])for(const sx of [.25,.5,.75])for(const sy of [.25,.5,.75])for(const [tx,ty,lx,ly] of [[0,0,0,0],[tile,0,3,5],[tile*2,tile,7,11]]){if(tx+lx>=W||ty+ly>=H)continue;const gx=tx+lx+sx,gy=ty+ly+sy,fullX=(gx-.5*W)/W,fullY=(.5*H-gy)/W,tiledX=((tx+lx+sx)-.5*W)/W,tiledY=(.5*H-(ty+ly+sy))/W;must(Object.is(fullX,tiledX)&&Object.is(fullY,tiledY),'tile mapping seam')}
const maxFramePixels=8*1048576,fieldBytes=maxFramePixels*4;must(fieldBytes<=128*1024*1024,'default storage binding budget exceeded');
const oldQueueBytes=maxFramePixels*4,newCounterBytes=16;must(newCounterBytes<oldQueueBytes/100000,'unresolved queue was not reduced to a counter');
const oldQueueBytes=maxFramePixels*4,newCounterBytes=32;must(newCounterBytes<oldQueueBytes/100000,'unresolved queue was not reduced to a counter');
for(const [cw,ch] of [[390,844],[1920,1080],[1080,1920]]){const aspect=ch/cw,w0=16384,h0=Math.round(w0*aspect),h=Math.min(16384,h0),w=h0>16384?Math.max(64,Math.round(h/aspect)):w0;must(w<=16384&&h<=16384,'export dimension clamp failed')}
console.log(JSON.stringify({status:'pass',pixelContract:'centered',exportTile:512,strictRetryPass:false,unresolvedCounterBytes:newCounterBytes,maxExportSide:16384},null,2));

View file

@ -0,0 +1,15 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
must(/DIRECT_DS_GUARDED_WGSL/.test(kernels)&&/riskLimit:f32/.test(kernels)&&/deriv=2\.0\*cmul/.test(kernels),'guarded DS shader / derivative risk gate missing');
must(/direct-ds-guarded-experiment/.test(script)&&/this\.dsGuarded=/.test(script),'guarded DS pipeline missing');
must(/benchmarkHybrid/.test(script)&&/renderTileMetaHybrid/.test(script),'hybrid benchmark methods missing');
must(/HYBRID_DEPTHS=\[6,8,10,12\]/.test(script)&&/HYBRID_RISK_LIMITS=\[1e12,1e13,1e14\]/.test(script),'hybrid depth/risk sweep missing');
must(/runHybridExperiment/.test(script)&&/uncertainRate/.test(script)&&/speedupVsDeepCold/.test(script),'hybrid experiment/report missing');
must(/id="hybridBench"/.test(html)&&/id="hybridDialog"/.test(html)&&/id="hybridSave"/.test(html),'hybrid experiment UI missing');
must(/function openHybridDialog\(/.test(script)&&/function startHybridExperimentFromUi\(/.test(script),'hybrid UI start function missing');
must(/\$\('#hybridBench'\)\.onclick=\(\)=>\{toast\('Hybrid実験を開始'\);void startHybridExperimentFromUi\(\)\}/.test(script),'toolbar Hybrid button does not directly start experiment');
must(/\$\('#hybridStart'\)\.onclick=startHybridExperimentFromUi/.test(script),'dialog rerun button is not wired to experiment');
const choose=(script.match(/function chooseBackend[\s\S]*?function deepNeeded/)||[''])[0];
must(!/dsGuarded|hybrid-candidate|backend='hybrid'/.test(choose),'diagnostic hybrid leaked into production router');
console.log(JSON.stringify({status:'pass',check:'hybrid-experiment-contract',checks:10},null,2));

View file

@ -0,0 +1,39 @@
import fs from 'node:fs/promises';
const F=Math.fround;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const[m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
function bitsForDepth(z){return Math.max(256,Math.ceil(z*Math.log2(10))+300)}
function snap(re,im,z){const span=`3.4e-${z}`,bits=bitsForDepth(z);return{bits,re:decFixed(re,bits),im:decFixed(im,bits),span:decFixed(span,bits)}}
function fixedNum(v,bits){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)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
function q(a,b){const x=F(a+b),e=F(b-F(x-a));return[x,e]}
function sum(a,b){const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
function prod(a,b){const x=F(a*b),ca=F(4097*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(4097*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(a[1]+b[1])))}
function neg(a){return[F(-a[0]),F(-a[1])]}
function sub(a,b){return add(a,neg(b))}
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(a[0]*b[1]));e=F(e+F(a[1]*b[0]));e=F(e+F(a[1]*b[1]));return q(t[0],e)}
function scaleDS(a,b){const t=prod(a[0],b);return q(t[0],F(t[1]+F(a[1]*b)))}
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]}
function cmulDS(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
function mag2DS(a){return add(mul(a[0],a[0]),mul(a[1],a[1]))}
function split(x){const h=F(x);return[h,F(x-h)]}
function value(a){return F(a[0]+a[1])}
function dsRisk(s,w,h,x,y,limit){const cre=split(fixedNum(s.re,s.bits)),cim=split(fixedNum(s.im,s.bits)),span=split(fixedNum(s.span,s.bits)),offx=(x+.5-.5*w)/w,offy=(.5*h-y-.5)/w,cr=add(cre,scaleDS(span,F(offx))),ci=add(cim,scaleDS(span,F(offy))),c=[cr,ci];let z=[[0,0],[0,0]],dr=0,di=0,maxDer=0;for(let n=0;n<limit;n++){const zr=value(z[0]),zi=value(z[1]),ndr=2*(zr*dr-zi*di)+1,ndi=2*(zr*di+zi*dr);dr=ndr;di=ndi;maxDer=Math.max(maxDer,Math.hypot(dr,di));z=cadd(cmulDS(z,z),c);if(cmp(mag2DS(z),[4,0])>0)return{n:n+1,maxDer}}return{n:limit,maxDer}}
function adaptiveIter(z){return Math.max(350,350+Math.floor(70*Math.sqrt(z)+15*z))}
const re='-0.7453983606667815',im='0.1125046349959942',depths=[6,8,10,12],W=1600,H=900,SW=31,SH=21,riskLimit=1e13,rows=[];
for(const z of depths){const s=snap(re,im,z),limit=adaptiveIter(z);let tested=0,wrong=0,uncertain=0,caught=0,guardMismatch=0;for(let sy=0;sy<SH;sy++){const y=Math.round(sy*(H-1)/(SH-1));for(let sx=0;sx<SW;sx++){const x=Math.round(sx*(W-1)/(SW-1)),p=pixel(s,W,H,x,y),g=pixel(s,W,H,x,y,s.bits+64),o=oracle(p[0],p[1],s.bits,limit),gg=oracle(g[0],g[1],s.bits+64,limit);if(o!==gg){guardMismatch++;continue}tested++;const d=dsRisk(s,W,H,x,y,limit),bad=(d.n<limit)!=(o<limit),u=d.maxDer>riskLimit;if(bad)wrong++;if(u)uncertain++;if(bad&&u)caught++}}
rows.push({z,tested,guardMismatch,wrong,uncertain,caught,uncertainRate:uncertain/tested,catchRate:wrong?caught/wrong:1});}
if(rows.some(r=>r.guardMismatch))throw new Error('oracle guard mismatch');
for(const r of rows.slice(0,3))if(r.catchRate!==1)throw new Error(`risk gate missed DS class error at z${r.z}`);
if(!(rows[0].uncertainRate<.08&&rows[1].uncertainRate<.2&&rows[2].uncertainRate<.5))throw new Error('risk gate is too broad before z12');
if(!(rows[3].uncertainRate>.75))throw new Error('z12 should signal crossover toward Full Deep');
const report={format:'mandelbrot-hybrid-risk-model-v1',kind:'CPU Math.fround DS + BigInt P/P+64 oracle; not real GPU',viewport:[W,H],sampleGrid:[SW,SH],riskLimit,rows,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.'};
await fs.mkdir(new URL('../audit/',import.meta.url),{recursive:true});await fs.writeFile(new URL('../audit/v24-hybrid-risk-model.json',import.meta.url),JSON.stringify(report,null,2));
console.log(JSON.stringify({status:'pass',check:'hybrid-risk-model',rows},null,2));

View file

@ -0,0 +1,28 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
const source=await fs.readFile(new URL('../script.js',import.meta.url),'utf8');
const start=source.indexOf('function openHybridDialog()');
const end=source.indexOf('// ── diagnostics',start);
if(start<0||end<0)throw new Error('hybrid UI block not found');
const block=source.slice(start,end);
const el=(extra={})=>Object.assign({textContent:'',value:'',disabled:false,hidden:true,max:0,open:false,attrs:new Map(),setAttribute(k,v){this.attrs.set(k,String(v));if(k==='open')this.open=true},removeAttribute(k){this.attrs.delete(k)},showModal(){this.open=true},close(){this.open=false}},extra);
const elements={
'#hybridDialog':el(), '#hybridStart':el({textContent:'再実行'}), '#hybridBench':el(), '#hybridProgress':el(), '#hybridStatus':el(), '#hybridSave':el(), '#hybridOutput':el(), '#hybridTrack':el({value:'seahorse'}), '#hybridSamples':el({value:'2'}), '#hybridCancel':el()
};
let runCount=0,toastText='';
const ctx={
$:s=>elements[s], hybridJob:{active:false,cancelled:false,last:null}, benchmarkJob:{active:false}, HYBRID_DEPTHS:[6,8,10,12],
cancelRender(){}, state:{dirty:true}, refs:{cancelPending(){}}, markDirty(){}, toast(s){toastText=s}, hybridSummary(){return 'summary'},
async runHybridExperiment({onProgress}){runCount++;onProgress?.({index:1,total:4,z:6,phase:'probe'});return{rows:[1,2,3,4]}},
downloadBlob(){}, Blob:globalThis.Blob, Date, Math, JSON, String, Error
};
vm.createContext(ctx);vm.runInContext(block,ctx,{filename:'hybrid-ui-block.js'});
if(typeof elements['#hybridBench'].onclick!=='function')throw new Error('toolbar click handler not installed');
elements['#hybridBench'].onclick();
if(!elements['#hybridDialog'].open)throw new Error('toolbar click did not open dialog synchronously');
if(runCount!==1)throw new Error('toolbar click did not start experiment synchronously');
if(!/Hybrid実験/.test(toastText))throw new Error('toolbar click gives no visible acknowledgement');
await new Promise(r=>setTimeout(r,0));
if(!/実験完了/.test(elements['#hybridStatus'].textContent))throw new Error('experiment completion not reflected in UI: '+elements['#hybridStatus'].textContent);
if(elements['#hybridBench'].disabled)throw new Error('toolbar stayed disabled');
console.log(JSON.stringify({status:'pass',check:'hybrid-ui-runtime',runCount,dialogOpen:elements['#hybridDialog'].open,statusText:elements['#hybridStatus'].textContent,toastText},null,2));

View file

@ -1 +1,7 @@
import fs from 'node:fs/promises';const h=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');const req=['id="view"','id="processMode"','保守的 (Strict)','id="exportPrecision"','gpu-kernels.js','script.js'];for(const x of req)if(!h.includes(x))throw new Error('missing '+x);if(h.includes('<script src="kernels.js"></script>'))throw new Error('legacy kernels.js loaded');console.log(JSON.stringify({status:'pass',checks:req.length+1},null,2));
import fs from 'node:fs/promises';
const h=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
const req=['id="view"','id="processMode"','保守的 (Strict)','id="exportPrecision"','id="backendBench"','id="benchmarkDialog"','id="sparseBench"','id="sparseDialog"','mandelbrot-bundle','data-bundle="gpu-kernels"','data-bundle="app"'];
for(const x of req)if(!h.includes(x))throw new Error('missing '+x);
if(/<script\s+src=/.test(h))throw new Error('standalone index still depends on external scripts');
if(h.includes('<script src="kernels.js"></script>'))throw new Error('legacy kernels.js loaded');
console.log(JSON.stringify({status:'pass',checks:req.length+2,singleFile:true},null,2));

View file

@ -3,14 +3,16 @@ const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('scri
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
must(/const VERSION=24/.test(script),'renderer version is not 24');
must(/navigator\.gpu\.requestAdapter/.test(script),'WebGPU adapter path missing');
must(/guarded rescaled perturbation/.test(script),'guarded deep WebGPU path missing');
must(/guarded perturbation/.test(script)&&/DEEP_CORRECT_WGSL/.test(kernels)&&/correctUnknownFrame/.test(script),'guarded deep + sparse DS correction path missing');
must(/referenceWorkerSource/.test(script)&&/BigInt\(d\.re\)/.test(script),'BigInt reference worker missing');
must(!/function buildBla\(/.test(script)&&!/BlaNode/.test(kernels)&&!/useBla/.test(kernels),'unsafe BLA path is not fully quarantined');
must(!/function buildBla\(/.test(script)&&!/BlaNode/.test(kernels)&&!/useBla/.test(kernels),'unsafe BLA path is not fully removed');
must(/F32_U/.test(kernels)&&/safe_abs_error/.test(kernels)&&/errAbs/.test(kernels),'deep error-bound guard missing');
must(/struct UnresolvedHead/.test(kernels)&&/atomicAdd\(&unresolved\.remaining/.test(kernels),'unresolved counter missing');
must(!/strict_queue|QUEUE_FINALIZE_WGSL/.test(kernels)&&!/dispatchWorkgroupsIndirect|qitems|strict-indirect/.test(script),'redundant strict retry queue remains');
must(/struct UnresolvedHead/.test(kernels)&&/atomicAdd\(&unresolved\.remaining/.test(kernels)&&/REASON_ERROR_BOUND/.test(kernels),'unresolved reason counters missing');
must(/CORRECTION_MARK/.test(kernels)&&/ds_prod/.test(kernels)&&/correct_pixel/.test(kernels),'double-single correction kernel missing');
must(!/strict_queue|QUEUE_FINALIZE_WGSL/.test(kernels)&&!/qitems|strict-indirect/.test(script),'obsolete strict retry queue remains');
must(/texture_storage_2d<rgba8unorm,write>/.test(kernels),'GPU color field path missing');
must(/AA_RESOLVE_WGSL/.test(kernels)&&/renderTileRGBA2x/.test(script),'GPU 2x2 export resolve missing');
must(/correctUnknown=true/.test(script)&&/encodeCorrectionNumeric/.test(script),'export/probe DS correction integration missing');
must(/class StreamingPng/.test(script)&&/CompressionStream\('deflate'\)/.test(script)&&/async abort\(reason\)/.test(script),'streaming PNG export/abort missing');
must(/PRESENT_WGSL/.test(kernels),'GPU reprojection/presentation missing');
must(/recolorPending/.test(script)&&!/const token=\+\+state\.token,stateSnap/.test(script),'recolor race fix missing');
@ -18,15 +20,14 @@ must(/this\.context=null/.test(script)&&/webgpuCanvasClaimed=true;this\.configur
must(/このズーム深度はWebGPUが必要です/.test(script),'deep fallback rejection missing');
must(!/Math\.min\(340/.test(script),'deep exact-coordinate formatter still capped at 340 digits');
must(/uncapturederror/.test(script)&&/pushErrorScope/.test(script),'WebGPU error capture missing');
must(/gpu-kernels\.js/.test(html)&&!/src="kernels\.js"/.test(html),'index is not WebGPU-only');
must(/mandelbrot-bundle/.test(html)&&/data-bundle="gpu-kernels"/.test(html)&&/data-bundle="app"/.test(html)&&!/<script\s+src=/.test(html),'standalone index is not self-contained WebGPU bundle');
must(/numericParams/.test(script)&&/colorParams/.test(script),'persistent frame parameter buffers missing');
must(/ensureExportWorkspace/.test(script)&&/exportWorkspaceDestroy/.test(script)&&/export tile exceeds reusable workspace/.test(script),'reusable export GPU workspace missing');
must(/if\(!adapter\)adapter=await navigator\.gpu\.requestAdapter\(\)/.test(script),'adapter fallback request missing');
must(/gpuInitFailed/.test(script)&&/WebGPU初期化失敗/.test(script),'shader/pipeline failure guard missing');
must(/if\(state\.gpuInitFailed\)return null/.test(script),'failed shader initialization can be retried on every render');
must(/if\(!r\)\{if\(state\.gpuInitFailed\)/.test(script),'shader failure still enters CPU fallback');
must(/return 524288/.test(script)&&/1572864/.test(script),'responsive screen pixel budgets missing');
must(/ensureExportWorkspace/.test(script)&&/unresolveds:Array\.from\(\{length:4\}/.test(script),'reusable corrected export workspace missing');
must(/DIRECT_REQUIRED_RATIO=8/.test(script)&&/DEEP_SPEED_MARGIN=\.85/.test(script)&&/ensureBackendProbe/.test(script),'adaptive direct/deep router missing');
must(/DIRECT_DS_WGSL/.test(kernels)&&/runCrossoverBenchmark/.test(script),'diagnostic DS/crossover benchmark missing');
must(/overlap-view-probe-required/.test(script)&&/qualityPass/.test(script)&&/backendQualityTiles/.test(script)&&/overlap-probe-failed-safe-deep/.test(script),'view-specific quality probe/fail-safe gate missing');
must(!/referenceSnapAtPixel|chooseUnknownCandidate|multi-reference-refine/.test(script),'obsolete multi-reference production path remains');
must(/gpuUnavailable/.test(script)&&/return 262144/.test(script),'lightweight no-adapter CPU fallback budget missing');
for(const banned of ['DEEP_SIMD_B64','DEEP_SCALAR_B64','BLA_SIMD_B64','BLA_SCALAR_B64','deepPool','deepWasm','highPrecisionDirectPixelAsync','renderDeepAdaptive','render_perturb_rebase_rect','WebAssembly.Instance'])must(!script.includes(banned),'legacy production deep dependency remains: '+banned);
must(!/Validated direct/.test(html),'obsolete Validated UI remains');
console.log(JSON.stringify({status:'pass',rendererVersion:24,shaderVersion:'24.1.3',checks:28},null,2));
console.log(JSON.stringify({status:'pass',rendererVersion:24,shaderVersion:'24.2.8-fixed96-sparse-queue',checks:28},null,2));

View file

@ -0,0 +1,45 @@
import fs from 'node:fs/promises';
const corpus=JSON.parse(await fs.readFile(new URL('./scenes.json',import.meta.url),'utf8'));
const sc=corpus.scenes.find(s=>s.id==='swirly-seahorses-z12');
if(!sc)throw new Error('swirly scene missing');
const F=Math.fround,U=2**-24;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}
function bitsForSpan(t){const s=String(t).toLowerCase(),a=s.split('e'),m=Math.abs(Number(a[0])),e=a[1]?Number(a[1]):0,l2=Math.log2(m)+e*Math.log2(10);return Math.max(256,Math.ceil(-l2)+240)}
function snap(scene){const bits=bitsForSpan(scene.span);return{bits,re:decFixed(scene.re,bits),im:decFixed(scene.im,bits),span:decFixed(scene.span,bits)}}
function fixedNum(v,bits){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)),x=top*Math.pow(2,sh-bits);return neg?-x:x}
function log2FixedAt(v,b){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-b}
function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:Math.pow(2,l-exp),exp}}
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
function direct(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<<BigInt(b);let zr=0n,zi=0n,refLen=limit;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;const mag=roundShift(zr*zr,b)+roundShift(zi*zi,b);if(mag>bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[2*refLen]=hr1;hi[2*refLen+1]=hi1;lo[2*refLen]=F(rr1-hr1);lo[2*refLen+1]=F(ri1-hi1);break}}return{hi,lo,refLen}}
function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}
function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))}
function primary(ref,s,w,h,x,y,limit){const se0=spanME(s),se={mant:F(se0.mant),exp:se0.exp},dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w);let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);return ea<=1e-3?{kind:'bounded',n}:{kind:'unknown',n}}if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n};return{kind:'unknown',n}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
function q(a,b){const x=F(a+b),e=F(b-F(x-a));return[x,e]}
function sum(a,b){const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]}
function prod(a,b){const x=F(a*b),ca=F(4097*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(4097*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]}
function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(a[1]+b[1])))}
function neg(a){return[F(-a[0]),F(-a[1])]}
function sub(a,b){return add(a,neg(b))}
function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(a[0]*b[1]));e=F(e+F(a[1]*b[0]));e=F(e+F(a[1]*b[1]));return q(t[0],e)}
function scaleDS(a,b){const t=prod(a[0],b);return q(t[0],F(t[1]+F(a[1]*b)))}
function pow2DS(a,e){if(e<-126)return[0,0];if(e>126)return[F(8.507059e37),0];return[F(a[0]*2**e),F(a[1]*2**e)]}
function cmp(a,b){if(a[0]<b[0])return-1;if(a[0]>b[0])return 1;if(a[1]<b[1])return-1;if(a[1]>b[1])return 1;return 0}
function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]}
function cmulDS(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]}
function cscale(a,b){return[scaleDS(a[0],b),scaleDS(a[1],b)]}
function cpow2(a,e){return[pow2DS(a[0],e),pow2DS(a[1],e)]}
function mag2(a){return add(mul(a[0],a[0]),mul(a[1],a[1]))}
function val(a){return F(a[0]+a[1])}
function maxabsDS(a){return Math.max(Math.abs(val(a[0])),Math.abs(val(a[1])))}
function correction(ref,s,w,h,x,y,limit){const se=spanME(s),mh=F(se.mant),sm=[mh,F(se.mant-mh)],iw=1/w,iwh=F(iw),inv=[iwh,F(iw-iwh)],ox=F(x+.5-.5*w),oy=F(.5*h-y-.5),dx=scaleDS(inv,ox),dy=scaleDS(inv,oy),d0=[mul(sm,dx),mul(sm,dy)];let d=d0,wv=[[0,0],[0,0]],e=se.exp,n=0,m=0,ops=0;while(true){if(n>=limit)return{kind:'bounded',n};if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,r=[[ref.hi[rp],ref.lo[rp]],[ref.hi[rp+1],ref.lo[rp+1]]],delta=cpow2(wv,e),z=cadd(r,delta),mg=mag2(z);if(cmp(mg,[4,0])>0)return{kind:'escaped',n};const dm=mag2(delta);if(m>0&&cmp(dm,[0,0])>0&&cmp(mg,dm)<0){if(se.exp-e<-96)return{kind:'unknown',n};wv=z;d=cpow2(d0,se.exp);e=0;m=0;continue}if(m>=ref.refLen)return{kind:'unknown',n};const linear=cscale(cmulDS(r,wv),2),sq=cpow2(cmulDS(wv,wv),e);wv=cadd(cadd(linear,sq),d);m++;n++;ops++;const mm=Math.max(maxabsDS(wv),maxabsDS(d));if(mm>=1e30||!Number.isFinite(mm))return{kind:'unknown',n};if(mm>65536){wv=cscale(wv,1/65536);d=cscale(d,1/65536);e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wv=cscale(wv,65536);d=cscale(d,65536);e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}}
const s=snap(sc),w=61,h=39,limit=2000,ref=buildRef(s,limit);let guardMismatch=0,primaryUnknown=0,primaryFalseEscaped=0,primaryFalseBounded=0,corrected=0,remaining=0,correctionFalseEscaped=0,correctionFalseBounded=0,countMismatch=0,maxCountDiff=0;
for(let y=0;y<h;y++)for(let x=0;x<w;x++){const p=pixel(s,w,h,x,y),g=pixel(s,w,h,x,y,s.bits+64),oracle=direct(p[0],p[1],s.bits,limit),guard=direct(g[0],g[1],s.bits+64,limit);if(oracle!==guard){guardMismatch++;continue}const a=primary(ref,s,w,h,x,y,limit);if(a.kind==='escaped'&&oracle===limit)primaryFalseEscaped++;if(a.kind==='bounded'&&oracle<limit)primaryFalseBounded++;if(a.kind!=='unknown')continue;primaryUnknown++;const c=correction(ref,s,w,h,x,y,limit);if(c.kind==='unknown'){remaining++;continue}corrected++;if(c.kind==='escaped'&&oracle===limit)correctionFalseEscaped++;if(c.kind==='bounded'&&oracle<limit)correctionFalseBounded++;if(c.kind==='escaped'&&oracle<limit&&c.n!==oracle){countMismatch++;maxCountDiff=Math.max(maxCountDiff,Math.abs(c.n-oracle))}}
const report={status:'pass',kind:'sparse-double-single-correction-cpu-model-not-real-gpu',scene:sc.id,grid:[w,h],limit,referenceLength:ref.refLen,guardMismatch,primary:{unknown:primaryUnknown,falseEscaped:primaryFalseEscaped,falseBounded:primaryFalseBounded},correction:{corrected,remaining,falseEscaped:correctionFalseEscaped,falseBounded:correctionFalseBounded,escapeIterationMismatch:countMismatch,maxEscapeIterationDifference:maxCountDiff},note:'DS correction is a visual-quality recovery pass; escape iteration equality is not certified.'};
if(guardMismatch||primaryFalseEscaped||primaryFalseBounded||correctionFalseEscaped||correctionFalseBounded||remaining)throw new Error(JSON.stringify(report));
console.log(JSON.stringify(report,null,2));

View file

@ -0,0 +1,10 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url),h=await fs.readFile(new URL('index.html',root),'utf8'),dev=await fs.readFile(new URL('index.external.html',root),'utf8'),script=await fs.readFile(new URL('script.js',root),'utf8');
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
must(!/<script\s+src=/.test(h),'root standalone HTML has external script dependency');
must(/data-bundle="gpu-kernels"/.test(h)&&/data-bundle="app"/.test(h),'inline bundle markers missing');
must(/id="sparseBench"/.test(h)&&/id="sparseDialog"/.test(h),'Fixed96 UI missing in standalone');
must(/\$\('#sparseBench'\)\.onclick/.test(h)&&/runFixed96Experiment/.test(h),'Fixed96 button handler not embedded in standalone');
must(/<script src="gpu-kernels\.js"><\/script>/.test(dev)&&/<script src="script\.js"><\/script>/.test(dev),'hosted/dev template no longer has explicit source scripts');
must(/\$\('#sparseBench'\)\.onclick/.test(script),'source button handler missing');
console.log(JSON.stringify({status:'pass',check:'single-file-ui-contract',checks:6,reason:'Opening HTML from an archive temp extraction does not require sibling JS files.'},null,2));

View file

@ -0,0 +1,13 @@
import fs from 'node:fs/promises';
import crypto from 'node:crypto';
const root=new URL('../',import.meta.url);
const html=await fs.readFile(new URL('index.html',root),'utf8');
const script=await fs.readFile(new URL('script.js',root));
const kernels=await fs.readFile(new URL('gpu-kernels.js',root));
const vs=await fs.readFile(new URL('script-v24.2.6.js',root));
const vk=await fs.readFile(new URL('gpu-kernels-v24.2.6.js',root));
const h=b=>crypto.createHash('sha256').update(b).digest('hex');
if(!html.includes('script-v24.2.6.js')||!html.includes('gpu-kernels-v24.2.6.js'))throw new Error('versioned browser assets not referenced');
if(html.includes('<script src="script.js"')||html.includes('<script src="gpu-kernels.js"'))throw new Error('bare cacheable browser assets still referenced');
if(h(script)!==h(vs)||h(kernels)!==h(vk))throw new Error('versioned browser assets differ from canonical source');
console.log(JSON.stringify({status:'pass',check:'ui-cacheproof-assets',scriptSha256:h(script),kernelSha256:h(kernels)},null,2));

View file

@ -21,15 +21,15 @@ async function checkScene(api,id,re,im,span,iter,mode){
const r=await api.setView({re,im,span,baseIter:iter,adaptive:false,processMode:mode}),diag=r.diag;
if(diag.backend!=='webgpu'||diag.webgpuError)throw new Error(id+'/'+mode+': WebGPU backend unavailable/error: '+(diag.webgpuError||diag.backend));
const st=api.state(),ps=points(st.width,st.height),meta=await api.sampleMeta(ps);let guardMismatch=0,falseEscaped=0,falseBounded=0,escapeIterationMismatch=0,unknown=0;
for(let i=0;i<ps.length;i++){const [x,y]=ps[i],p=pixel(st,x,y),pg=pixel(st,x,y,st.bits+64),a=orbit(p[0],p[1],st.bits,st.iter),g=orbit(pg[0],pg[1],st.bits+64,st.iter);if(a!==g){guardMismatch++;continue}const m=meta[i],cls=(m>>>28)&3,n=m&0x0fffffff;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a<st.iter)falseBounded++;if(cls===1&&a<st.iter&&n!==a)escapeIterationMismatch++}
for(let i=0;i<ps.length;i++){const [x,y]=ps[i],p=pixel(st,x,y),pg=pixel(st,x,y,st.bits+64),a=orbit(p[0],p[1],st.bits,st.iter),g=orbit(pg[0],pg[1],st.bits+64,st.iter);if(a!==g){guardMismatch++;continue}const m=meta[i],cls=(m>>>28)&3,n=m&0x000fffff;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a<st.iter)falseBounded++;if(cls===1&&a<st.iter&&n!==a)escapeIterationMismatch++}
const row={id,mode,width:st.width,height:st.height,iter:st.iter,deep:diag.deep,guardMismatch,falseEscaped,falseBounded,escapeIterationMismatch,unknown,known:ps.length-unknown,reference:diag.reference};
if(guardMismatch||falseEscaped||falseBounded)throw new Error(id+'/'+mode+' numeric gate failed: '+JSON.stringify(row));
if(id==='swirly-seahorses-z12'){if(row.known<12)throw new Error(id+'/'+mode+': excessive UNKNOWN rate: '+JSON.stringify(row));}
else if(row.unknown!==0)throw new Error(id+'/'+mode+': stable corpus scene produced UNKNOWN samples: '+JSON.stringify(row));
if(row.unknown!==0)throw new Error(id+'/'+mode+': corrected production frame produced UNKNOWN samples: '+JSON.stringify(row));
return row;
}
async function run(){await waitApp();const api=frame.contentWindow.__MANDEL_TEST__,report=[];for(const [id,re,im,span,iter] of scenes){for(const mode of (id==='z0'?['standard']:['standard','validate'])){out.textContent='running '+id+' / '+mode+'…\n'+JSON.stringify(report,null,2);report.push(await checkScene(api,id,re,im,span,iter,mode));}}
await api.setView({re:'-0.7453983606667815',im:'0.1125046349959942',span:'3.52e-12',baseIter:2000,adaptive:false,processMode:'validate'});const denseState={...api.state(),width:61,height:39},denseMeta=await api.probeMeta({w:61,h:39,strict:true}),densePoints=[[25,12],[26,15],[27,25]],denseRegression=[];for(const[x,y]of densePoints){const p=pixel(denseState,x,y),pg=pixel(denseState,x,y,denseState.bits+64),a=orbit(p[0],p[1],denseState.bits,denseState.iter),g=orbit(pg[0],pg[1],denseState.bits+64,denseState.iter);if(a!==g)throw new Error('dense guard mismatch '+x+','+y);const m=denseMeta[y*61+x],cls=(m>>>28)&3,n=m&0x0fffffff;if(cls===1&&a===denseState.iter)throw new Error('dense false escape '+JSON.stringify({x,y,a,cls,n}));if(cls!==0&&cls!==1&&a<denseState.iter)throw new Error('dense false bounded '+JSON.stringify({x,y,a,cls,n}));denseRegression.push({x,y,oracle:a,cls,n})}
await api.setView({re:'-0.75',im:'0',span:'3.4e-20',baseIter:1000,adaptive:false,processMode:'validate'});const exportSmoke=await api.smokeExportTile({w:48,h:32,strict:true,ss:1}),exportSmokeAA=await api.smokeExportTile({w:48,h:32,strict:true,ss:2});if(exportSmoke.length!==exportSmoke.expected||exportSmokeAA.length!==exportSmokeAA.expected)throw new Error('GPU export tile readback length mismatch');if(exportSmoke.unresolved!==0||exportSmokeAA.unresolved!==0)throw new Error('GPU export smoke left unresolved samples: '+JSON.stringify({exportSmoke,exportSmokeAA}));const diag=frame.contentWindow.__MANDEL_DIAG__.snapshot();if(diag.uncapturedErrors&&diag.uncapturedErrors.length)throw new Error('uncaptured WebGPU errors: '+JSON.stringify(diag.uncapturedErrors));const result={status:'pass',kind:'real-webgpu-acceptance',date:new Date().toISOString(),shaderVersion:diag.shaderVersion,adapter:diag.adapter,limits:diag.limits,exportSmoke,exportSmokeAA,denseRegression,report};out.textContent=JSON.stringify(result,null,2);document.title='PASS v24.1 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result}
run().catch(e=>{const result={status:'fail',error:String(e&&e.stack||e)};out.textContent=JSON.stringify(result,null,2);document.title='FAIL v24.1 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result});
await api.setView({re:'-0.7453983606667815',im:'0.1125046349959942',span:'3.52e-12',baseIter:2000,adaptive:false,processMode:'validate'});const denseState={...api.state(),width:61,height:39},denseMeta=await api.probeMeta({w:61,h:39,strict:true,forceDeep:true,correctUnknown:true}),densePoints=[[25,12],[26,15],[27,25]],denseRegression=[];for(const[x,y]of densePoints){const p=pixel(denseState,x,y),pg=pixel(denseState,x,y,denseState.bits+64),a=orbit(p[0],p[1],denseState.bits,denseState.iter),g=orbit(pg[0],pg[1],denseState.bits+64,denseState.iter);if(a!==g)throw new Error('dense guard mismatch '+x+','+y);const m=denseMeta[y*61+x],cls=(m>>>28)&3,n=m&0x000fffff;if(cls===0)throw new Error('dense DS correction left known regression pixel UNKNOWN '+JSON.stringify({x,y,a,cls,n}));if(cls===1&&a===denseState.iter)throw new Error('dense false escape '+JSON.stringify({x,y,a,cls,n}));if(cls!==1&&a<denseState.iter)throw new Error('dense false bounded '+JSON.stringify({x,y,a,cls,n}));denseRegression.push({x,y,oracle:a,cls,n})}
await api.setView({re:'0.4',im:'0',span:'0.0015',baseIter:500,adaptive:false,processMode:'standard'});const routerDiag=frame.contentWindow.__MANDEL_DIAG__.snapshot(),routerDecision=routerDiag.backendDecision;if(!routerDecision||routerDecision.probe||!routerDecision.viewProbe)throw new Error('overlap view-specific backend probe did not settle: '+JSON.stringify(routerDecision));const vp=routerDecision.viewProbe;if(!vp.qualityPass||vp.classDisagreement!==0||vp.deepUnknownRate>.005)throw new Error('overlap backend quality probe failed: '+JSON.stringify(vp));const deepShouldWin=vp.predictedDeepMs<vp.predictedDirectMs*.85;if((routerDecision.backend==='deep')!==deepShouldWin)throw new Error('backend router ignored measured 15% speed margin: '+JSON.stringify(routerDecision));
await api.setView({re:'-0.75',im:'0',span:'3.4e-20',baseIter:1000,adaptive:false,processMode:'validate'});const exportSmoke=await api.smokeExportTile({w:48,h:32,strict:true,ss:1}),exportSmokeAA=await api.smokeExportTile({w:48,h:32,strict:true,ss:2});if(exportSmoke.length!==exportSmoke.expected||exportSmokeAA.length!==exportSmokeAA.expected)throw new Error('GPU export tile readback length mismatch');if(exportSmoke.unresolved!==0||exportSmokeAA.unresolved!==0)throw new Error('GPU export smoke left unresolved samples: '+JSON.stringify({exportSmoke,exportSmokeAA}));const fixed96Smoke=await api.runFixed96Experiment({track:'seahorse',depths:[8],riskLimits:[1e13],warmups:0,samples:1,qualityW:32,qualityH:24}),sr=fixed96Smoke.rows[0],sv=sr&&sr.variants&&sr.variants[0];if(!sv||!Number.isFinite(sv.fullMedianMs)||sv.remaining!==0||sv.queueIntegrity!==true||Math.abs((sv.dispatchRate||0)-(sv.uncertainRate||0))>1e-9)throw new Error('fixed96 experiment smoke failed: '+JSON.stringify(fixed96Smoke));const diag=frame.contentWindow.__MANDEL_DIAG__.snapshot();if(diag.uncapturedErrors&&diag.uncapturedErrors.length)throw new Error('uncaptured WebGPU errors: '+JSON.stringify(diag.uncapturedErrors));const result={status:'pass',kind:'real-webgpu-acceptance',date:new Date().toISOString(),shaderVersion:diag.shaderVersion,adapter:diag.adapter,limits:diag.limits,routerDecision,fixed96Smoke,exportSmoke,exportSmokeAA,denseRegression,report};out.textContent=JSON.stringify(result,null,2);document.title='PASS v24.2 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result}
run().catch(e=>{const result={status:'fail',error:String(e&&e.stack||e)};out.textContent=JSON.stringify(result,null,2);document.title='FAIL v24.2.8 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result});
})();