e
This commit is contained in:
parent
de177e8896
commit
8a2b1f6d4b
64 changed files with 10760 additions and 757 deletions
229
AUDIT_V24.md
229
AUDIT_V24.md
|
|
@ -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: 8–128
|
||||
- deep required speed advantage: 15%
|
||||
- corrected deep probe UNKNOWN max: 0.5%
|
||||
- class disagreement tolerance: 0
|
||||
- current view probe required; depth profile aloneではrouteしない
|
||||
|
||||
## Performance/迂遠処理監査
|
||||
|
||||
- full-frame high-precision CPU renderer: なし
|
||||
- multi-reference reference build loop: なし
|
||||
- redundant primary→same-f32 strict retry: なし
|
||||
- BLA precompute/evaluate: なし
|
||||
- deep correction: full dispatchだがKNOWN pixelは即return。追加reference生成なし。
|
||||
- Export: reusable 512² workspace、1 readback/tile。
|
||||
- 2×2 Export: 4 numerical samplesを1 command stream内で処理し、1 resolve/1 readback。
|
||||
- presentation/recolor: GPU field常駐。
|
||||
|
||||
## Real WebGPU acceptance
|
||||
|
||||
`tests/webgpu-acceptance.html`は以下を実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
62
CROSSOVER_BENCHMARK.md
Normal 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
70
FIXED96_EXPERIMENT.md
Normal 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 z6–z12 transition experiment. It is not proposed as an unlimited deep renderer.
|
||||
|
||||
## Measurements
|
||||
|
||||
For z6 / z8 / z10 / z12 and risk `1e11 / 1e12 / 1e13 / 1e14`, the UI reports every candidate row:
|
||||
|
||||
- extrapolated Fixed96 full-frame time,
|
||||
- Full Deep cold estimate,
|
||||
- speedup,
|
||||
- correction rate,
|
||||
- remaining UNKNOWN,
|
||||
- escaped/bounded class mismatch,
|
||||
- actual Fixed96 dispatch rate,
|
||||
- queue integrity status,
|
||||
- status/failure reason.
|
||||
|
||||
A failed candidate is never collapsed into a single `none` row.
|
||||
|
||||
## CPU preflight
|
||||
|
||||
`tests/v24-fixed96-model.mjs` compares the DS risk gate + exact Q8.88 Direct model with the BigInt P/P+64 oracle on a 1600×900-equivalent Seahorse sample grid. `tests/v24-fixed96-limb-model.mjs` separately verifies the six-limb multiplication algorithm against BigInt for 20,000 deterministic cases.
|
||||
|
||||
These are preflight tests. Real-GPU shader compilation, timing and class comparison remain decisive.
|
||||
|
||||
## Production status
|
||||
|
||||
Diagnostic only. The production router remains unchanged.
|
||||
|
||||
|
||||
## v24.2.7 range hotfix
|
||||
|
||||
v24.2.6 contained a shader bug in `fx_bad_range()`: the top Q8.88 integer byte was treated as if it had to be pure sign-extension. Valid values such as `+1.2` were therefore rejected. Escaping Mandelbrot orbits commonly pass through component magnitudes above 1, so selected correction pixels were counted as `remaining` instead of being written back.
|
||||
|
||||
v24.2.7 removes that invalid test. While the prior state is inside the bailout disk, Q8.88 multiplication is bounded. After each iteration the shader first tests `|Re(z)| > 2 || |Im(z)| > 2` as a mathematically certain escape, and only squares components when both are within ±2. This prevents both the false range rejection and large-value fixed-point wraparound.
|
||||
|
||||
|
||||
## v24.2.8 true sparse queue
|
||||
|
||||
v24.2.7 still launched a 2D Fixed96 workgroup over the whole benchmark tile and returned immediately for non-risk pixels. On SIMD/SIMT hardware this was not sparse enough: a few risky lanes could keep most of a workgroup occupied by the long integer loop.
|
||||
|
||||
v24.2.8 changes the execution topology, not the Q8.88 arithmetic. The guarded DS pass atomically appends each risky local pixel index to a queue. A one-invocation finalize shader writes `ceil(queueCount/64),1,1` to an indirect-argument buffer. The Fixed96 kernel is one-dimensional with `@workgroup_size(64)` and reads exactly one queue entry per invocation. No CPU map/readback occurs between these passes.
|
||||
|
||||
Every timing and quality tile validates queue invariants before its result is accepted. Overflow, wrong indirect arguments, invalid/stale indices or any disagreement between selected/enqueued/processed/corrected counts aborts the experiment rather than producing a misleading timing row.
|
||||
69
HYBRID_EXPERIMENT.md
Normal file
69
HYBRID_EXPERIMENT.md
Normal 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へ切り替えるべき境界を検出することも実験目的である。
|
||||
|
|
@ -1,92 +1,118 @@
|
|||
# Mandelbrot Deep Zoom v23 実装報告
|
||||
# v24.2.3 crossover benchmark implementation report
|
||||
|
||||
## 結論
|
||||
## ファイルの役割
|
||||
|
||||
`IMPROVEMENT_PROPOSAL.md`のv22向け設計判断をrenderer v23へ反映し、ローカル数値検証、WASM再現build、Hosted/Standalone成果物の生成まで実施した。提案は履歴文書であり、修正版のcoverage/Export/受入契約を現行基準とする。実ブラウザでのみ測定できる性能・表示・操作・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へ変更した。
|
||||
- wheel/pinch/pan中は再投影だけを行い、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ではP/P+64双方の固定小数点整数不等式で主カージオイド/周期2球を証明できたsampleだけを`INTERIOR_PROVEN`へ昇格する。
|
||||
- 精度優先のdeep描画では、基準軌道もP/P+64で独立再計算して疎なcheckpointとescape位置を照合し、不一致時はglobal reference全体を32 bit昇格して作り直す。
|
||||
- 1×/2×/4×/custom、1×/2×2 AA、Balanced/Validated、進捗、取消に対応する独立tile exportを実装した。
|
||||
- Balanced Exportは未確定数をmetadataへ記録して完了可能とし、Validated Exportは未確定sampleが残る場合にfinal PNGを出さず明示失敗する。
|
||||
- Export sidecarへViewSpec、precision/iteration policy、未確定sample数、backend、全kernel SHA-256、色空間、encoder、paletteを記録する。
|
||||
- version付きURL、戻る/進む、座標・spanの正確値入力/copy、Undo/Redoを実装した。
|
||||
- 初回UI、一般statusと診断status、mobile compact status、keyboard操作、live region、visible focus、44px target、reduced motion/transparencyを実装した。
|
||||
- Hosted版はcontent-hashed外部WASMとstreaming compile、Standalone版は3ファイル直開きを維持した。
|
||||
- cacheは件数ではなくmobile 96 MiB/desktop 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、SIMD/scalar flagsを固定した。
|
||||
- clang 17.0.6とNode.js v22.18.0で8 WASMを再buildした。tool配布archiveのURL/SHA-256はrepositoryに保持されていないため、再監査可能な証拠範囲はversion/flags、source hash、生成manifest、golden結果までとする。
|
||||
- shallow、deep、BLA、colorのgoldenとSIMD/scalar同値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の転送bytes/compile 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 source/ABI contract: pass
|
||||
- fixed-point precision 256/320 bit: pass
|
||||
- 基準軌道P/P+64 checkpoint一致と故意の不一致検出: pass
|
||||
- analytic interior integer proof 256/320 bit: pass
|
||||
- pixel mapping 851 samples、shallow/deep/BLA、2×/4× tile seam: pass
|
||||
- 配布shallow WASM pixel contract 1,440 samples、mismatch 0: pass
|
||||
- generated-WASM shallow/color/deep/BLA golden: pass
|
||||
- SIMD/scalar結果比較: pass
|
||||
- 本体、shallow Worker、deep WorkerのJavaScript構文: pass
|
||||
- mock DOM初期化とv23 diagnostics: pass
|
||||
- compile済みdeep/BLA/color Moduleのstructured clone・instantiate: pass
|
||||
- Node固定sceneの計測記録と処理量予算式の構造契約: pass(`audit/v23-node-performance.json`)。browser時間目標の達成を意味しない
|
||||
- Hosted/Standalone 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へ昇格させないための措置。
|
||||
|
||||
残る実機確認:
|
||||
|
||||
- mobile/desktop/4Kの表示画像とnative coverage
|
||||
- 操作反映p95、Preview/Covered/Refined所要時間
|
||||
- idle時の予約RAF/timer/background job/Canvas・DOM write 0
|
||||
- browser/GPUを含むobserved peak memory
|
||||
- Export寸法、完了前downloadなし、取消、PNG/JSON内容
|
||||
- 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が合格し、source/document 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.
|
||||
|
|
|
|||
200
MIGRATION_V24.md
200
MIGRATION_V24.md
|
|
@ -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.5M(desktop)へ縮小しました。
|
||||
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
194
README.md
|
|
@ -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 scan(9 shader。DS Direct / guarded DS / Fixed96 Direct correctionは実験専用)
|
||||
- production source/tree contract
|
||||
- BigInt reference Worker + P/P+64 checkpoint
|
||||
- f32 direct CPU model
|
||||
- view固有adaptive backend router policy
|
||||
- guarded primary deep CPU model
|
||||
- sparse double-single correction CPU model
|
||||
- BLA disabled contract
|
||||
- pixel/export geometry
|
||||
- e400/e1000 exact coordinate formatting
|
||||
- streaming PNG CRC/deflate/abort
|
||||
- real-WebGPU acceptance harnessの静的contract
|
||||
|
||||
### 実GPU 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から展開してそのまま開けます。
|
||||
|
|
|
|||
9
audit/v24-backend-policy.json
Normal file
9
audit/v24-backend-policy.json
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
25
audit/v24-crossover-gpu-status.json
Normal file
25
audit/v24-crossover-gpu-status.json
Normal 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"
|
||||
]
|
||||
}
|
||||
34
audit/v24-crossover-hotfix.json
Normal file
34
audit/v24-crossover-hotfix.json
Normal 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"
|
||||
}
|
||||
}
|
||||
766
audit/v24-crossover-quality.json
Normal file
766
audit/v24-crossover-quality.json
Normal 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."
|
||||
}
|
||||
}
|
||||
19
audit/v24-fixed96-hotfix.json
Normal file
19
audit/v24-fixed96-hotfix.json
Normal 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"
|
||||
}
|
||||
182
audit/v24-fixed96-model.json
Normal file
182
audit/v24-fixed96-model.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
31
audit/v24-fixed96-preparation.json
Normal file
31
audit/v24-fixed96-preparation.json
Normal 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"
|
||||
}
|
||||
40
audit/v24-fixed96-sparse-queue.json
Normal file
40
audit/v24-fixed96-sparse-queue.json
Normal 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"
|
||||
}
|
||||
}
|
||||
63
audit/v24-hybrid-preparation.json
Normal file
63
audit/v24-hybrid-preparation.json
Normal 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
|
||||
}
|
||||
56
audit/v24-hybrid-risk-model.json
Normal file
56
audit/v24-hybrid-risk-model.json
Normal 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."
|
||||
}
|
||||
29
audit/v24-hybrid-ui-hotfix.json
Normal file
29
audit/v24-hybrid-ui-hotfix.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
26
audit/v24-sparse-correction-model.json
Normal file
26
audit/v24-sparse-correction-model.json
Normal 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."
|
||||
}
|
||||
|
|
@ -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
598
dist/hosted/gpu-kernels-v24.2.6.js
vendored
Normal 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
|
||||
});
|
||||
})();
|
||||
590
dist/hosted/gpu-kernels.js
vendored
590
dist/hosted/gpu-kernels.js
vendored
|
|
@ -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
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
30
dist/hosted/index.html
vendored
30
dist/hosted/index.html
vendored
|
|
@ -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
454
dist/hosted/script-v24.2.6.js
vendored
Normal 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
270
dist/hosted/script.js
vendored
|
|
@ -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
598
dist/standalone/gpu-kernels-v24.2.6.js
vendored
Normal 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
|
||||
});
|
||||
})();
|
||||
396
dist/standalone/gpu-kernels.js
vendored
396
dist/standalone/gpu-kernels.js
vendored
|
|
@ -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
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
1275
dist/standalone/index.html
vendored
1275
dist/standalone/index.html
vendored
File diff suppressed because it is too large
Load diff
454
dist/standalone/script-v24.2.6.js
vendored
Normal file
454
dist/standalone/script-v24.2.6.js
vendored
Normal 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();
|
||||
})();
|
||||
282
dist/standalone/script.js
vendored
282
dist/standalone/script.js
vendored
|
|
@ -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
598
gpu-kernels-v24.2.6.js
Normal 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
|
||||
});
|
||||
})();
|
||||
590
gpu-kernels.js
590
gpu-kernels.js
|
|
@ -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
93
index.external.html
Normal 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
1275
index.html
File diff suppressed because it is too large
Load diff
|
|
@ -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
454
script-v24.2.6.js
Normal 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
270
script.js
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
10
tests/v24-backend-policy.mjs
Normal file
10
tests/v24-backend-policy.mjs
Normal 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));
|
||||
|
|
@ -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=[];
|
||||
|
|
|
|||
16
tests/v24-crossover-contract.mjs
Normal file
16
tests/v24-crossover-contract.mjs
Normal 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));
|
||||
106
tests/v24-crossover-quality.mjs
Normal file
106
tests/v24-crossover-quality.mjs
Normal 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));
|
||||
24
tests/v24-fixed96-contract.mjs
Normal file
24
tests/v24-fixed96-contract.mjs
Normal 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));
|
||||
15
tests/v24-fixed96-limb-model.mjs
Normal file
15
tests/v24-fixed96-limb-model.mjs
Normal 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));
|
||||
24
tests/v24-fixed96-limb-orbit-model.mjs
Normal file
24
tests/v24-fixed96-limb-orbit-model.mjs
Normal 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));
|
||||
39
tests/v24-fixed96-model.mjs
Normal file
39
tests/v24-fixed96-model.mjs
Normal 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));
|
||||
22
tests/v24-fixed96-queue-model.mjs
Normal file
22
tests/v24-fixed96-queue-model.mjs
Normal 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));
|
||||
24
tests/v24-fixed96-shader-range-model.mjs
Normal file
24
tests/v24-fixed96-shader-range-model.mjs
Normal 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));
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
15
tests/v24-hybrid-contract.mjs
Normal file
15
tests/v24-hybrid-contract.mjs
Normal 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));
|
||||
39
tests/v24-hybrid-risk-model.mjs
Normal file
39
tests/v24-hybrid-risk-model.mjs
Normal 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));
|
||||
28
tests/v24-hybrid-ui-runtime.mjs
Normal file
28
tests/v24-hybrid-ui-runtime.mjs
Normal 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));
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
45
tests/v24-sparse-correction-model.mjs
Normal file
45
tests/v24-sparse-correction-model.mjs
Normal 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));
|
||||
10
tests/v24-standalone-ui-contract.mjs
Normal file
10
tests/v24-standalone-ui-contract.mjs
Normal 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));
|
||||
13
tests/v24-ui-cacheproof.mjs
Normal file
13
tests/v24-ui-cacheproof.mjs
Normal 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));
|
||||
|
|
@ -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});
|
||||
})();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue