slim
This commit is contained in:
parent
8a2b1f6d4b
commit
0e1c1f04fd
158 changed files with 2578 additions and 15117 deletions
206
AUDIT.md
206
AUDIT.md
|
|
@ -1,206 +0,0 @@
|
|||
# v18 数値・性能監査
|
||||
|
||||
> **Archive notice:** この監査はv18時点の履歴資料です。現行renderer v23の受入値ではありません。v23のsource/配布baselineは`audit/v23-source-baseline.json`、実行時browser baselineは`audit/v23-browser-baseline.json`へ分離します。
|
||||
|
||||
## 目的
|
||||
|
||||
v7〜v17で積み重なった補正処理を「残っているから残す」のではなく、正しさ・速度・相互作用を測定して再判定した。
|
||||
|
||||
絶対時間は実行環境に依存するため、**誤判定率・同条件での比率・処理の計算量**を主な判断材料とする。
|
||||
|
||||
## 結論
|
||||
|
||||
| 処理 | v18判断 | 理由 |
|
||||
|---|---|---|
|
||||
| Arbitrary precision + Reference Orbit | 維持 | deep zoomの座標精度と基準軌道に必須 |
|
||||
| Perturbation + Rebasing | 維持 | 全pixelをBigIntで直接計算するより圧倒的に有利 |
|
||||
| BLA | 維持・監視強化 | deep pixel kernelの主高速化手段。ただし適切なεは画面依存 |
|
||||
| Persistent Worker / Reference | 維持 | warm stateの価値が大きい |
|
||||
| 固定距離によるReference維持 | 改廃 | 距離だけではrecenterの損得を判定できない |
|
||||
| Pilot render | 縮小して維持 | コスト予測専用なら有用。画素の代用には不正確 |
|
||||
| 数学的iteration上限を途中で切るwork cap | **廃止** | 黒/外部分類を壊す |
|
||||
| Pilotのnearest-neighborで未解決pixelを補完 | **廃止** | 境界で誤差が大きい |
|
||||
| coarse periodic interior mask | **廃止** | 効く画面が偏り、pilotコストも増える |
|
||||
| 全strip strict verification | **廃止** | deep zoomで隠れた巨大ボトルネック |
|
||||
| 1点の誤差で全画面strict再描画 | **廃止** | 局所誤差に対して再計算範囲が過大 |
|
||||
| 複数の解像度feedback controller | **統合** | controller同士が競合して粗いまま固定されることがあった |
|
||||
| Dither | **廃止** | detail detectorの勾配を人工的に増やし、余計な高精細tileを誘発 |
|
||||
| Adaptive detail refinement/cache | 維持 | idle時へ遅延すれば費用対効果が良い |
|
||||
| Autoの黒/色境界追従 | 維持 | kernelと独立で軽く、誤航行抑制に有用 |
|
||||
| Synthetic Worker Wisdom | **廃止** | 実際のWASM+BLA負荷と相関しにくい |
|
||||
| Real BLA Wisdom | 維持 | 実kernelを使ってWorker数を選べる |
|
||||
|
||||
## 1. v17のiteration work capは数学的に不正確
|
||||
|
||||
v17はdeep base描画で、表示用global iterationより小さい`computeIter`を使い、そこまで発散しなければ事実上黒候補として扱っていた。
|
||||
|
||||
Seahorse付近、48×30、global 3490 / cap 2870をBigInt直接計算で照合した。
|
||||
|
||||
- capまで残ったpixel: **1440 / 1440**
|
||||
- その後3490までに発散したpixel: **1066**
|
||||
- cap survivor中のlate escape: **74.03%**
|
||||
|
||||
つまり「2870で未発散だから黒」とみなすのは、この画面では4分の3近くを誤分類する。
|
||||
|
||||
**v18:** `computeIter = colorIter`へ戻した。負荷制限は数学的iteration数ではなく、BLA operation / perturbation operationの回数に対して行う。上限到達は`UNRESOLVED`であり黒ではない。少数なら局所再計算、多ければそのstripだけ無制限BLAへ戻す。
|
||||
|
||||
Raw: `audit/raw/workcap_truth_v17.json`
|
||||
|
||||
## 2. Pilot画像による未解決pixel補完は不正確
|
||||
|
||||
128×78のfull BLAと56×34 pilotのnearest-neighbor補完を比較。
|
||||
|
||||
- 黒/外部分類の不一致: **451 / 9984 = 4.52%**
|
||||
- escape iteration差が8超: **43.71%**
|
||||
|
||||
境界付近では低解像度pilotを「計算結果」として使えない。
|
||||
|
||||
**v18:** pilotはコスト推定にだけ使う。画面へ表示せず、未解決pixelの色・黒判定にも使わない。
|
||||
|
||||
Raw: `audit/raw/pilot_fill_audit.json`
|
||||
|
||||
## 3. Interior detectionは「追加反復」ではなく早期終了で入れる
|
||||
|
||||
以前の黒専用look-aheadは、黒pixelへ追加iterationを課すため重かった。v18では逆に、内部と判定できたpixelを**早く終了**する。
|
||||
|
||||
実装:
|
||||
|
||||
- main cardioid / period-2 bulbの解析判定
|
||||
- 軌道微分の収縮判定
|
||||
- BLA jump時はBLAの線形係数で微分も更新
|
||||
|
||||
96×72、2500 iterationでInterior detection OFF/ONを比較。黒/外部分類不一致は全テスト0。
|
||||
|
||||
| 場所 | OFF | ON | 分類不一致 |
|
||||
|---|---:|---:|---:|
|
||||
| period-3 wide | 766.7 ms | 6.0 ms | 0 |
|
||||
| period-3 mid | 438.7 ms | 2.3 ms | 0 |
|
||||
| period-2近傍 | 364.7 ms | 1.0 ms | 0 |
|
||||
| mixed interior/exterior | 282.8 ms | 16.2 ms | 0 |
|
||||
| cardioid cusp近傍 | 329.9 ms | 1.8 ms | 0 |
|
||||
|
||||
これは「黒を正しくするために重くする処理」ではなく、黒の多い画面を軽くする処理として残す価値が高い。
|
||||
|
||||
Raw: `audit/raw/interior_safety_v18.json`
|
||||
|
||||
## 4. BLA εは単一固定値にできない
|
||||
|
||||
Seahorse z≈14、160×98で保守的BLAを基準に比較。
|
||||
|
||||
| ε | 時間 | class mismatch | iteration差>1 |
|
||||
|---|---:|---:|---:|
|
||||
| 2^-23 | 約299 ms | 1.00% | 5.23% |
|
||||
| 2^-32 | 約786 ms | 0.383% | 1.16% |
|
||||
| 2^-40 | 約1218 ms | 0.083% | 0.198% |
|
||||
|
||||
一方、同じSeahorseでもz≈20では2^-23が基準と一致したテストがある。したがって「常に厳密」「常に緩い」の両方が非効率。
|
||||
|
||||
**v18:** fast BLAを使い、1フレームあたり固定少数点だけsafe BLAと照合する。誤差が出たstripだけsafe BLAで再計算する。
|
||||
|
||||
- 通常の検証はstrict perturbationではない
|
||||
- 1stripごとに何点もstrict計算しない
|
||||
- 1点の不一致で全画面をstrict再描画しない
|
||||
|
||||
Raw: `audit/raw/audit_v18_kernel.txt`
|
||||
|
||||
## 5. per-strip strict verificationは隠れたボトルネックだった
|
||||
|
||||
旧設計では各stripのBLA後にstrict perturbationを複数点実行していた。非常に深い場所ではBLA本体が数msでも、strict検証が毎strip積み重なりwall timeを支配する。
|
||||
|
||||
v18の設計:
|
||||
|
||||
1. fast BLAでstrip計算
|
||||
2. **フレーム全体で固定個数**だけsafe BLAと比較
|
||||
3. mismatchがあれば**そのstripだけ**safe BLAで再計算
|
||||
4. status failureの少数pixelだけexact fallback
|
||||
|
||||
これで検証コストが「strip数 × deep iteration」に増えにくくなった。
|
||||
|
||||
## 6. Reference recenterは距離だけで決めない
|
||||
|
||||
z≈14ではrecenterが有利なケースが多かった。
|
||||
|
||||
- 0.8画面横移動: first-frameで約**91 ms節約**
|
||||
- 0.8横+0.5縦: 約**40 ms節約**
|
||||
|
||||
しかしz≈20では、0.5画面横移動でrecenterが約**14.5 ms損**するケースもあった。
|
||||
|
||||
同じ「0.5画面離れた」でも損得が逆転するため、固定距離閾値は合理的でない。
|
||||
|
||||
**v18:**
|
||||
|
||||
- Referenceは基本的に維持
|
||||
- 実測kernel MPPが基準より悪化したときだけ、予測損失とReference rebuild EMAを比較
|
||||
- rebuildの方が安いと予測できる場合だけrecenter
|
||||
- 極端に離れた場合だけhard safety limitを使う
|
||||
|
||||
Raw: `audit/raw/reference_recenter_audit.json`, `audit/raw/reference_recenter_audit_z20.json`
|
||||
|
||||
## 7. Resolution controlを一本化
|
||||
|
||||
過去版では以下が同時に解像度へ作用していた。
|
||||
|
||||
- pilot推定
|
||||
- renderPerf MPP
|
||||
- deepScale
|
||||
- Device WisdomのresScale
|
||||
- 品質段階ごとのmin/max width
|
||||
|
||||
複数feedbackが同時に動くため、負荷変化後に過剰縮小したり、逆に最低幅に張り付いたりしやすかった。
|
||||
|
||||
**v18:** deep base resolutionは一つの時間予算モデルで決定する。pilotまたは最近の実測MPPを入力にし、detail tileは別のidle品質レイヤーとして扱う。
|
||||
|
||||
## 8. Ditherをdetail判定の前に入れない
|
||||
|
||||
Ditherはグラデーションbandingには効くが、画像のRGB勾配を人工的に増やす。detail detectorの入力へ先に適用すると「存在しない細部」を検出して追加tileを計算する。
|
||||
|
||||
**v18:** runtime ditherは削除。Canvasのhigh-quality scalingは維持。再導入するならdetail選定後の表示/export段階だけにする。
|
||||
|
||||
## 9. Real BLA Wisdomは残す
|
||||
|
||||
旧Synthetic Wisdomは単純なJS浮動小数点loopでWorker数を選んでいた。実際の負荷はWASM、BLA table、reference memory、postMessageを含むため別物。
|
||||
|
||||
**v18:** 長時間idle時のみ実際のBLA mini renderを1〜4 Workerで測定し、実スループットからWorker数を決める。解像度feedbackとは切り離した。
|
||||
|
||||
## 10. v17→v18 end-to-end確認
|
||||
|
||||
同一のclean Chromium 144 headless、780×441 viewportで、UI操作に相当するdeep base passを比較した。動的解像度を含む**実際のbase-frame pipeline比較**であり、pixel-for-pixel kernel benchmarkではない。
|
||||
|
||||
| Scene | v17 | v18 | 改善 |
|
||||
|---|---:|---:|---:|
|
||||
| Seahorse z≈14 | 1059 ms | 288 ms | **3.68×** |
|
||||
| Seahorse z≈20 | 1226 ms | 238 ms | **5.15×** |
|
||||
| Seahorse z≈100 | 983 ms | 474 ms | **2.07×** |
|
||||
| period-3 z≈20 | 1931 ms | 781 ms | **2.47×** |
|
||||
|
||||
v18のclean runではruntime exception 0。
|
||||
|
||||
Raw: `audit/raw/v17-final-browser-benchmark.json`, `audit/raw/v18-final-browser-benchmark.json`, `audit/benchmarks.json`
|
||||
|
||||
## 11. 残した処理
|
||||
|
||||
次はデータ上、削る理由が弱いので維持した。
|
||||
|
||||
- BigInt fixed-point coordinate
|
||||
- Reference Orbit cache + precision promotion
|
||||
- Perturbation / Rebasing
|
||||
- BLA
|
||||
- Series Approximation(主にfallback側)
|
||||
- Persistent Worker
|
||||
- smooth coloring LUT
|
||||
- Auto black/colour boundary tracking + safe rollback
|
||||
- world-space HQ tile cache
|
||||
- progressive detail refinement
|
||||
- high-quality Canvas scaling
|
||||
|
||||
## 12. 今後のボトルネック
|
||||
|
||||
v18ではpixel hot-loopの補正処理を大幅に整理した。さらに深い地点で残る主候補はReference OrbitのBigInt生成である。
|
||||
|
||||
特にReferenceが長く、かつ高bit精度になるケースでは、次の候補は:
|
||||
|
||||
1. Reference Orbit専用Worker
|
||||
2. 次Referenceの先読み / double buffer
|
||||
3. Reference orbitの圧縮・周期利用
|
||||
|
||||
ただしz≈20程度ではReference生成が常に主因ではないため、常時複雑化するよりtelemetryで必要なときだけ有効化する方がよい。
|
||||
182
AUDIT_V24.md
182
AUDIT_V24.md
|
|
@ -1,182 +0,0 @@
|
|||
# v24.2.3 crossover-benchmark audit
|
||||
|
||||
## 判定
|
||||
|
||||
静的contract、Node数値モデル、build/reproducibility gateはPASS。実WebGPU adapterでのv24.2.3 compile/dispatchはこのコンテナではadapterを取得できないため未実行であり、`tests/webgpu-acceptance.html`を外部実機gateとして残す。
|
||||
|
||||
## 主な修正
|
||||
|
||||
1. 深部の黒面積/黒点
|
||||
- UNKNOWN reasonを保持。
|
||||
- guarded f32 primary後にUNKNOWN限定double-single correctionを追加。
|
||||
- interior黒とUNKNOWN表示を分離。
|
||||
2. 標準描画範囲
|
||||
- direct/deep overlapを導入。
|
||||
- current-view quality/speed probeを本描画前に実行。
|
||||
- deepが15%以上高速な場合だけdeepを採用。
|
||||
3. Multi-reference
|
||||
- 全画面モデルで費用対効果が低いため撤去。
|
||||
4. Export
|
||||
- 1×/2×2 deep ExportにもDS correctionを統合。
|
||||
- 2×2はsample別unresolved counterを使用。
|
||||
5. 旧BLA
|
||||
- membership反転の既知反例があるため引き続き未搭載。
|
||||
|
||||
## CPU numeric model results
|
||||
|
||||
### Primary guarded perturbation
|
||||
|
||||
canonical 17×11 corpus:
|
||||
|
||||
- z14: false escaped 0 / false bounded 0 / UNKNOWN 0
|
||||
- z20: 0 / 0 / 0
|
||||
- z100: 0 / 0 / 0
|
||||
- period3: 0 / 0 / 0
|
||||
- e280: 0 / 0 / 0
|
||||
- e400: 0 / 0 / 0
|
||||
- swirly: false escaped 0 / false bounded 0 / UNKNOWN 49 of 187
|
||||
|
||||
Dense known regression pixels `(25,12)`, `(26,15)`, `(27,25)`はprimaryでは誤分類せずUNKNOWNへ退避。
|
||||
|
||||
### Sparse double-single correction
|
||||
|
||||
`swirly-seahorses-z12`, 61×39, 2000 iter:
|
||||
|
||||
- oracle guard mismatch: 0
|
||||
- primary UNKNOWN: 478 / 2379
|
||||
- primary false escaped: 0
|
||||
- primary false bounded: 0
|
||||
- corrected: 478
|
||||
- remaining: 0
|
||||
- correction false escaped: 0
|
||||
- correction false bounded: 0
|
||||
- escape iteration mismatch: 5
|
||||
- max escape iteration difference: 374
|
||||
|
||||
DS passはfinite-iteration visual classification recoveryとして有効だが、escape iteration equalityは保証しない。
|
||||
|
||||
## Router contract
|
||||
|
||||
- direct coordinate floor: ratio 8
|
||||
- no-probe direct margin: ratio 128
|
||||
- overlap: 8–128
|
||||
- deep required speed advantage: 15%
|
||||
- corrected deep probe UNKNOWN max: 0.5%
|
||||
- class disagreement tolerance: 0
|
||||
- current view probe required; depth profile aloneではrouteしない
|
||||
|
||||
## Performance/迂遠処理監査
|
||||
|
||||
- full-frame high-precision CPU renderer: なし
|
||||
- multi-reference reference build loop: なし
|
||||
- redundant primary→same-f32 strict retry: なし
|
||||
- BLA precompute/evaluate: なし
|
||||
- deep correction: full dispatchだがKNOWN pixelは即return。追加reference生成なし。
|
||||
- Export: reusable 512² workspace、1 readback/tile。
|
||||
- 2×2 Export: 4 numerical samplesを1 command stream内で処理し、1 resolve/1 readback。
|
||||
- presentation/recolor: GPU field常駐。
|
||||
|
||||
## Real WebGPU acceptance
|
||||
|
||||
`tests/webgpu-acceptance.html`は以下を要求する。
|
||||
|
||||
- production 6 WGSL + benchmark DS Direct 1本の計7 WGSL modules compile/pipeline creation
|
||||
- stable corpus corrected UNKNOWN = 0 on sampled pixels
|
||||
- BigInt P/P+64 guard mismatch = 0
|
||||
- false escaped / false bounded = 0
|
||||
- dense 3 regression pixelsをDS correctionでKNOWNへ回収
|
||||
- overlap view-specific routerがquality gateを通過し、15% speed ruleどおりroute
|
||||
- 1× / 2×2 Export unresolved = 0
|
||||
- uncaptured WebGPU validation/runtime error = 0
|
||||
|
||||
この環境では`navigator.gpu`/adapterを実利用できないため、上記hardware gateのみ未判定。
|
||||
|
||||
|
||||
## v24.2.3 crossover追加監査
|
||||
|
||||
- DS Direct WGSLはproduction routerへ未接続。
|
||||
- Round-depth benchmark: z0,2,4,6,8,10,12,14,16,20,30,40。
|
||||
- CPU BigInt oracle sweep: `audit/v24-crossover-quality.json`。
|
||||
- Seahorse / 1600×900相当ではf32 material degradation ≈ z6、DS ≈ z10。
|
||||
- corrected Deep CPU modelはz8で1/315 false escapeを検出したため、依然membership certificateではない。
|
||||
- 実GPU timingはこのcontainerではWebGPU adapter不成立のため未測定。実機では診断情報→Backend比較で取得する。
|
||||
|
||||
|
||||
## v24.2.3 z12 benchmark hang hotfix
|
||||
|
||||
ユーザー実機でBackend比較が `z12 · deep` から終了しない事象を確認。原因はproduction rendererの無限loopではなく、benchmarkが現在の全画面をDeep primary + DS correctionでwarmup 1回 + 2〜5回反復し、各回を単一の大規模GPU jobとして待っていたこと。1600×900級・z12では1測定だけでも最大10億回規模の反復候補となり、queue completion待ちが極端に長くなり得た。
|
||||
|
||||
修正:
|
||||
|
||||
- speed benchmarkを代表3tile(最大256×144)へ縮小。
|
||||
- actual full-resolution座標を維持したtileX/tileYで測定。
|
||||
- aggregate `ms/pixel` からcurrent canvasのfull-frame時間を換算。
|
||||
- quality gateは従来どおり5 spatial tiles。
|
||||
- GPU batch/reference buildに15 s watchdog。timeout時は明示的errorとし、GPU deviceを破棄して再初期化可能にする。
|
||||
- progress表示を `deep sample 2/3` のように細分化。
|
||||
- crossover JSON formatをv2へ更新し、`speedTiles`, `speedPixels`, `msPerPixel`, `fullMedianMs`を記録。
|
||||
|
||||
このhotfixはproduction backend routing/rendering mathを変更しない。
|
||||
|
||||
|
||||
## v24.2.4 Hybrid experiment preparation
|
||||
|
||||
- Added benchmark-only `DIRECT_DS_GUARDED_WGSL`; production router is unchanged.
|
||||
- Guard metric: approximate `|dz/dc|`; threshold sweep `1e12 / 1e13 / 1e14`.
|
||||
- Added `benchmarkHybrid()` / `renderTileMetaHybrid()` using the existing Deep double-single correction only for DS-marked UNKNOWN pixels.
|
||||
- Added z6/z8/z10/z12 experiment UI and JSON export.
|
||||
- Added CPU BigInt P/P+64 risk-model gate. At risk=1e13, observed DS class errors are all caught at z6/z8/z10 with correction rates 2.8% / 11.1% / 34.6%; z12 correction rate is 91.2%, intentionally indicating the Full-Deep crossover.
|
||||
- Added real-WebGPU acceptance smoke for the Hybrid diagnostic path.
|
||||
|
||||
|
||||
## v24.2.5 Sparse Direct experiment (Audit)
|
||||
|
||||
- The previous sparse-Full-Deep diagnostic is superseded and is not exposed in the UI.
|
||||
- Added `DIRECT_TS_CORRECT_WGSL`, a reference-free three-term f32 expansion correction shader that only processes DS sensitivity UNKNOWN pixels.
|
||||
- Added `benchmarkSparseDirect()` / `renderTileMetaSparseDirect()` and a z6/z8/z10/z12 risk sweep.
|
||||
- Full Deep remains the quality/timing baseline; the Sparse Direct candidate itself does not build or bind a reference orbit.
|
||||
- Production backend routing is unchanged.
|
||||
- Root `index.html` is now a single-file bundle with no external script dependency, specifically to keep diagnostic UI controls functional when the HTML is opened from an archive/temp extraction.
|
||||
- Static single-file UI contract and real-browser click smoke are separate gates; real WebGPU arithmetic still requires a WebGPU-capable browser.
|
||||
|
||||
- Sparse Direct coordinate packing uses exact residual subtraction from BigInt fixed-point into three f32 components; this removed the CPU TD-model class errors caused by the earlier host-Number split.
|
||||
|
||||
## v24.2.6 Fixed96 experiment audit
|
||||
|
||||
- v24.2.5 three-term f32 expansion correction is removed from the active experiment because the real-GPU quality gate produced no viable candidate at z6/z8/z10/z12.
|
||||
- New correction shader: `DIRECT_FIXED96_CORRECT_WGSL`.
|
||||
- Arithmetic is signed Q8.88 in three `u32` limbs; no floating-point expansion is used by the correction pass.
|
||||
- Exact BigInt ViewState coordinates are converted directly to Q8.88 tile base + pixel step on the host; no JS `Number` coordinate round-trip is used.
|
||||
- `tests/v24-fixed96-limb-model.mjs` verifies the limb multiplication/round-shift algorithm against BigInt over 20,000 deterministic products.
|
||||
- `tests/v24-fixed96-model.mjs` verifies the DS sensitivity mask + Q8.88 correction against the BigInt P/P+64 oracle on a 1600×900-equivalent Seahorse grid. Safe thresholds exist at z6/z8/z10/z12 in the CPU preflight.
|
||||
- Experiment output reports every risk threshold, including rejected candidates, with correction rate, remaining UNKNOWN, class mismatch and timing. The former `best risk = none` information loss is removed.
|
||||
- Production routing/rendering remains unchanged.
|
||||
- Root `index.html` remains a single-file bundle; the Fixed96 button handler is embedded and has no sibling-JS dependency when opened from an extracted ZIP.
|
||||
- Real-GPU WGSL compilation/timing remains the decisive acceptance gate.
|
||||
|
||||
|
||||
## v24.2.7 Fixed96 range hotfix audit
|
||||
|
||||
- Root cause: `fx_bad_range()` incorrectly required the Q8.88 top integer byte to be `0x00` or `0xff`, rejecting valid values with magnitude >= 1.
|
||||
- Symptom match: real-GPU experiment reported `correction ~= remain` across z6-z12.
|
||||
- Fix: remove the invalid sign-extension test; use a certain-escape component test at `|component| > 2` before fixed-point magnitude squaring.
|
||||
- Added BigInt coordinate range/step-underflow checks before GPU packing.
|
||||
- Added regression tests for valid Q8.88 values above 1, 20,000 limb multiplication cases, and full limb-orbit iteration equality against exact BigInt Q8.88.
|
||||
|
||||
|
||||
## v24.2.8 Fixed96 true sparse queue audit
|
||||
|
||||
Purpose: remove the v24.2.7 full-screen Fixed96 dispatch and make the diagnostic correction genuinely proportional to risky-pixel count.
|
||||
|
||||
Implementation checks:
|
||||
|
||||
- DS risk shader appends local pixel indices to a storage queue with atomics.
|
||||
- queue capacity equals tile pixel count; overflow is still counted and treated as fatal.
|
||||
- a dedicated finalize shader writes indirect dispatch arguments from the GPU-written queue count.
|
||||
- Fixed96 uses `@workgroup_size(64)` and `dispatchWorkgroupsIndirect`; it no longer launches 8x8 workgroups over the full tile.
|
||||
- Fixed96 validates queue indices and verifies the queued pixel is still UNKNOWN(reason 7).
|
||||
- benchmark and quality paths both run `fixed96QueueIntegrity()` and fail closed on any counter/invariant mismatch.
|
||||
- static queue-integrity model covers zero, 20, 65-entry, overflow, wrong-indirect, processed mismatch and stale-entry cases.
|
||||
- Q8.88 limb multiplication/orbit tests remain unchanged and passing; this revision changes scheduling only.
|
||||
|
||||
Real-GPU timing remains the decisive performance gate.
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
# WASM build reproducibility
|
||||
|
||||
v23では、`src/`を監査可能なkernel source、`src/abi.json`を公開ABI、
|
||||
`toolchain.lock.json`を固定toolchain契約として扱います。
|
||||
|
||||
## 互換payloadを展開する
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/extract-wasm.ps1
|
||||
```
|
||||
|
||||
現在配布している8 payloadを`dist/wasm/`へ展開し、byte数とSHA-256を
|
||||
`manifest.json`へ記録します。COLOR SIMD/scalarがbyte単位で同一ならStandaloneでは
|
||||
Base64文字列を1本へ自動集約し、異なる場合は各payloadを保持します。どちらの場合もmanifest上の2 ABI名は維持します。
|
||||
|
||||
## sourceから再生成する
|
||||
|
||||
clang 17.0.6をPATHへ置き、次を実行します。
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-wasm.ps1
|
||||
```
|
||||
|
||||
生成先は`build/wasm-v23/`です。`src/shallow_kernel.c`、
|
||||
`src/deep_kernel.c`、`src/bla_kernel_v18.c`、`src/color_kernel.c`をそれぞれ
|
||||
SIMD/scalarでcompileし、timestampを含まないsource manifestを生成します。
|
||||
|
||||
配布payloadへ昇格するときだけ`-Promote`を指定します。昇格前に
|
||||
`tests/kernel-golden.mjs`が、画素中心座標、escape iteration、magnitude、
|
||||
smooth補正、SIMD/scalar同値性を検査します。golden失敗時はコピーも
|
||||
`kernels.js`再生成も行いません。
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-wasm.ps1 -Promote
|
||||
```
|
||||
|
||||
## 信頼境界
|
||||
|
||||
clang 17.0.6とNode.js v22.18.0を作業領域内で使用し、復元sourceのcompile、
|
||||
generated-WASM golden、SIMD/scalar同値性、画素中心契約を実行済みです。tool配布archive自体の
|
||||
URL/SHA-256は現repositoryに保持されていないため、「公式archiveのhash照合」は再監査可能な証拠には
|
||||
含めません。再監査できる信頼境界はversion/flags、source hash、生成manifest、golden結果です。
|
||||
Provenance status: tool archive hash not retained.
|
||||
golden合格後に8 payloadと`kernels.js`を昇格し、Hosted/Standalone成果物も再生成しました。
|
||||
実測結果と生成manifestのSHA-256は`audit/v23-source-baseline.json`へ記録しています。
|
||||
|
||||
省略なしの再検証は、toolchainをPATHへ追加した状態で次を実行します。
|
||||
|
||||
```powershell
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/test-all.ps1 -RequireToolchain
|
||||
```
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
# Mandelbrot Deep Zoom v23 完了監査
|
||||
|
||||
## 判定
|
||||
|
||||
`IMPROVEMENT_PROPOSAL.md`のうち、Phase 0Aと計測結果に依存しないPhase 1〜3Aの実装項目はコード、build、非ブラウザ試験へ反映した。source/ABI/数値/pixel mapping/WASM Module clone/配布物の保存済みgateは合格している。v22成果物が残っていないため、v22比較項目は`not-verifiable`でありpassには含めない。
|
||||
|
||||
ただし、固定実機を必要とするbrowser acceptanceは実行環境にin-app Browserが存在しないため未実施である。性能修正後に再開した接続監査でも3回連続のgoal turnで利用可能Browser 0件となり、2026-08-22T10:48:56.1197927Zに外部状態待ちのblockerとして再確定した。Node固定sceneではdeep BLAの高いMPPを確認し、標準deepを約1.4秒の実測時間予算へ変更済みだが、これはCanvas、Worker、入力、downloadを含むbrowser gateの代替ではない。またPhase 3BとPhase 4は、提案書自身がbrowser telemetryで支配コストまたは誤差を確認した場合だけ実施すると定義した条件付き項目であり、trigger判定も未実施である。したがって本監査は「実装・非ブラウザgate合格、実機受入blocked」と判定する。
|
||||
|
||||
## 要件トレーサビリティ
|
||||
|
||||
| 提案 | 実装結果 | 主な証拠 |
|
||||
|---|---|---|
|
||||
| 6.1 イベント駆動 | 常時RAFを廃止。paint/statsをinvalidate時だけ予約し、非表示時はscheduler・render・refine・Exportを停止 | `script.js`の`scheduleFrame`、`requestScheduler`、`stopSchedulers`、`visibilitychange`。`tests/source-contract.ps1` |
|
||||
| 6.2 Canvas/計算grid分離 | 処理モード別Screen pixel budgetとeffective DPRを導入。Previewは110ms予算、cold deepは幅48px以下。標準shallowは2〜4MP、標準deepは実測MPPから約1.4秒予算へ調整、精細は4〜8MP | `screenPixelBudget`、`adaptStandardDeepBudget`、`resize`、`targetSize`、`RENDER_PROFILE` |
|
||||
| 6.3 gesture/取消 | wheel 110ms、pointer 90ms settle。操作中は再投影。省電力はPreviewで停止し、標準はCovered後の追加処理なし。精細の追加反復はescape境界候補だけをdeep 384/shallow 4,096点まで実行。非共有環境は短いtile/chunk | gesture handlers、`nextQualityDue`、`scheduleUnknownContinuation` |
|
||||
| 6.4 deep遅延生成 | main deep WASM、Module、Worker、referenceを数値条件付近まで生成しない。1 Workerから実測増員。実job wisdomを端末・kernel keyで永続化 | `prewarmDeepAssets`、`prepareDeepModules`、`addDeepWorker`、`deepWisdomStorageKey` |
|
||||
| 6.5 WASM/memory | compile済みdeep/BLA/color Moduleを一度だけ作りWorkerへclone。field buffer返却、byte ledger、Worker退役を実装。1 module/1 shared memory統合はPhase 3Bの条件付き項目 | `compileKernelPair`、Worker `init`、`memoryLedger`、`retireDeepAssets`、`tests/module-clone.mjs` |
|
||||
| 6.6 byte cache | fixed countを廃止し、mobile 96 MiB/desktop 192 MiBのmanaged byte ledgerとLRUへ変更 | `rendererMemoryBudget`、`detailCacheBudget`、`trimDetailCache` |
|
||||
| 6.7 Hosted/Standalone | content hash外部WASM+streaming compileのHosted、3ファイル直開きのStandaloneを生成。deep requestは深部到達前0の契約 | `hosted-loader.js`、`scripts/build-hosted.ps1`、`scripts/build-standalone.ps1`、`dist/` |
|
||||
| 6.8 再現build | 4 kernel source、ABI、固定clang flags、SHA-256 manifest、SIMD/scalar goldenを整備 | `src/`、`toolchain.lock.json`、`BUILD_REPRODUCIBILITY.md`、`build/wasm-v23/manifest.json` |
|
||||
| 7.1 画素中心 | JS、BigInt、shallow/deep/BLA WASM、detail、Exportを`x+0.5,y+0.5`へ統一しrenderer v23化 | `tests/pixel-mapping.mjs` 851 sample、`tests/pixel-contract.mjs` 1,440 sample |
|
||||
| 7.2 adaptive AA | 全域sample後、数値fieldの分類・分散・confidenceでquadtree 2×/4× subsample。linear RGBでresolve | `scoreDetailTiles`、`resolveSubsampleField` |
|
||||
| 7.3 field/color分離 | state、smooth、iteration、confidenceを保持。境界subsample fieldも保持。palette変更は再着色だけ | `makeField`、`colorizeField`、`recolorCurrentField` |
|
||||
| 7.4 BLA/detail精度 | Covered/detailで同一precision profile。高scoreまたはValidated detailはguarded direct。同一world座標の数値gateと視覚gateを分離 | `blaProfile`、`sendDeepDetailRect`、pixel tests |
|
||||
| 7.5 未確定分離 | `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を分離。反復を局所継続し、未確定数を表示・Export metadataへ記録 | field constants、`scheduleUnknownContinuation`、`runValidation` |
|
||||
| 7.6 誤差ベース精度 | pixel step/ULP/orbit telemetry+hysteresisでengine選択。round-to-nearest fixed point。P/P+64 directとreference checkpointを照合し、不一致時はglobal reference再構築 | `deepEngineNeeded`、`roundShift`、`highPrecisionDirectPixelAsync`、`verifyReferenceCheckpoints` |
|
||||
| 7.7 超深部 | 上限と未確定を診断表示。scaled BLA、multi-reference、可変参照長はPhase 4のtelemetry条件付き研究 | 診断UI、下記「条件付き項目」 |
|
||||
| 7.8 Export | Quick snapshotと独立tiled Exportを分離。1×/2×/4×/custom、AA、Balanced/Validated、進捗、取消、全tile後encode、JSON sidecar | `runExport`、`tests/browser-benchmark.html` |
|
||||
| 8/9 改廃・UX | 連続`q`、固定DPR/HQ幅、RGB detail、固定deep閾値、eager pool、件数cacheを廃止。ViewSpec、history、正確座標、初回UI、mobile status、Advanced、accessibilityを実装 | `index.html`、`script.js`、`tests/source-contract.ps1` |
|
||||
|
||||
## Gate結果
|
||||
|
||||
| Gate | 結果 |
|
||||
|---|---|
|
||||
| JavaScript本体/生成Worker/browser harness構文、mock DOM初期化 | pass |
|
||||
| deep/BLA/color `WebAssembly.Module` structured clone・instantiate | pass |
|
||||
| 256/320-bit direct agreement、analytic interior整数証明 | pass |
|
||||
| reference P/P+64 checkpoint agreement・故意の不一致検出 | pass |
|
||||
| Node固定scene計測/予算式契約 | pass。shallow 2 sceneと、出典付きのswirly-seahorses混在境界sceneでdeep BLAを測定し、deep標準を固定画素数から実測時間適応へ変更。保存値は端末負荷に依存する特性値であり、時間上限そのものをassertするgateではない |
|
||||
| 全backend pixel mapping・2×/4× tile seam | pass、851 sample |
|
||||
| 配布shallow SIMD pixel contract | pass、1,440 sample、mismatch 0 |
|
||||
| kernel source/ABI、payload checksum、Hosted/Standalone build | pass |
|
||||
| 実ブラウザ performance/visual/interaction/Export/accessibility | not-run。Browser bindingなし |
|
||||
|
||||
機械可読なsource基準値は`audit/v23-source-baseline.json`、未実施browser gateは`audit/v23-browser-baseline.json`に記録した。
|
||||
|
||||
追加のbrowser非依存監査は`audit/v23-browserless-baseline.json`へ保存した。固定Node.js v22.18.0で7 executable tests、source/document contract、配布hashを再実行してpassしたが、scopeは`browserless-current-artifacts`、`fullAcceptance: false`である。v22比較は`not-verifiable`、source-WASM buildはprior evidence reused、runtimeは測定値であり性能合格ではない。
|
||||
|
||||
## 条件付き項目
|
||||
|
||||
Phase 3Bの1 shared memory統合、immutable reference/BLA table共有、WASM hot loopのatomic cancel、およびPhase 4のdouble-double、interval/error ball、scaled BLA、multi-reference、WebGPUは無条件要件ではない。`IMPROVEMENT_PROPOSAL.md` 3.3、7.7、Phase 3B/4の規定どおり、固定browser計測でcopy、memory、10^-280以深、glitch、reference生成のどれが支配的か判明した場合だけ着手する。現在はModule clone、buffer pool、短いchunk、guarded directで安全な前段を実装済みだが、telemetryがないため条件成立を主張しない。
|
||||
|
||||
## Browser受入の再開条件
|
||||
|
||||
`tests/browser-benchmark.html`をHTTPで開けるin-app Browserを接続し、mobile/desktop/4Kを要求DPRの別contextで、Hosted/Standalone別に実行する。runnerはraw pixel golden、30回以上のPreview、Covered/実Refined、idle write、long task、managed/取得可能ならUA memory、deep request、非zero wheel paint、再着色、Export完了/取消/再現性、keyboard操作を採取する。observed GPU memory、visible focus、page zoom/pinch、screen-reader順序の外部証拠と統合し、全profile・全buildが揃った時点で固定実機の最終受入を判定できる。
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
# v24.2.3 Crossover Benchmark
|
||||
|
||||
## 目的
|
||||
|
||||
標準 `f32 Direct` が十分に速いのにBigInt referenceを伴うDeepへ早く切り替わっている可能性を検証する。Deepを速度目的ではなく「品質上必要になった地点」でのみ使えるよう、`f32 Direct / experimental DS Direct / corrected Deep`を同じ深度列で比較する。
|
||||
|
||||
## 品質モデル
|
||||
|
||||
- Seahorse center: `-0.7453983606667815 + 0.1125046349959942i`
|
||||
- viewport: 1600×900。21×15点を画面全域から間引き、隣接21 pixelの座標分解能も別途検査。
|
||||
- 深度: z0,2,4,6,8,10,12,14,16,20,30,40。`span = 3.4×10^-z`。
|
||||
- oracle: BigInt fixed-point P / P+64一致。
|
||||
- Deep: guarded rescaled f32 perturbation + UNKNOWNのみDS correction。
|
||||
- DS Direct: GPU候補と同じdouble-single arithmeticのCPU `Math.fround`モデル。
|
||||
|
||||
| z | f32 class error | f32 adjacent unique | DS class error | DS adjacent unique | Deep error/UNKNOWN |
|
||||
|---:|---:|---:|---:|---:|---:|
|
||||
| 0 | 0.00% | 21/21 | 0.00% | 21/21 | 0 / 0 |
|
||||
| 2 | 0.63% | 21/21 | 0.00% | 21/21 | 0 / 0 |
|
||||
| 4 | 0.63% | 21/21 | 0.00% | 21/21 | 0 / 0 |
|
||||
| 6 | 1.59% | 2/21 | 0.32% | 21/21 | 0 / 0 |
|
||||
| 8 | 1.27% | 1/21 | 0.63% | 21/21 | 1 / 0 |
|
||||
| 10 | 3.17% | 1/21 | 3.81% | 21/21 | 0 / 0 |
|
||||
| 12 | 17.14% | 1/21 | 8.57% | 21/21 | 0 / 0 |
|
||||
| 14 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 |
|
||||
| 16 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 |
|
||||
| 20 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 |
|
||||
| 30 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 |
|
||||
| 40 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 |
|
||||
|
||||
## 暫定結論
|
||||
|
||||
1. **f32 Directはz4付近までは実用候補。z6では隣接21 pixelが2値まで潰れ、品質上の限界が明確。** 現在の `ratio < 8` 強制Deepは1600px幅・この中心ではおおむねz4前後で発動するため、少なくとも「Deepを速度で選ぶ」には早すぎる。
|
||||
2. **DS Directはz8まで中間backendとして有望。z10ではclass errorが約3.8%に増え、本命Deepへ移るべき。** 隣接pixel分解能自体はz12まで維持するが、反復誤差が先に品質を制限する。
|
||||
3. **Deepは廃止できない。** f32はz6、DSもz10付近から品質が崩れる。Deepは極端な固定費を持つが、より深い領域では唯一の実用候補。
|
||||
4. Deep CPUモデルにはz8で1/315のfalse escapeが残るため、Deepもmembership certificateではない。これは既存の`membershipCertified:false`方針と整合する。
|
||||
|
||||
## 実GPU速度ベンチ
|
||||
|
||||
このコンテナではWebGPU adapterを取得できないため、GPU時間は測定できない。代わりに本体の診断情報へ **Backend比較** を追加した。
|
||||
|
||||
v24.2.2では各深度を現在のcanvas全画面でwarmup+複数回測定していたため、z12 Deepで巨大dispatchが長時間GPU queueを占有するケースがあった。v24.2.3では速度計測を現在のview座標上の代表3タイル(各最大256×144)に限定し、合計時間から `ms/pixel` を算出して現在の全画面へ換算する。品質は別の5タイルで比較する。各GPU batch/reference buildには15秒watchdogがあり、永久待ちはしない。
|
||||
|
||||
実機では同じ深度列を次の3方式により測定する。
|
||||
|
||||
- `f32`: 標準numeric kernel。
|
||||
- `DS`: experimental whole-frame double-single direct kernel。production routerには未接続。
|
||||
- `Deep warm`: reference cache済みのDeep primary + DS correction。
|
||||
- `Deep cold`: `Deep warm + verified BigInt reference build`。
|
||||
各候補のqualityはactual full-resolution座標上の5領域でcorrected Deepと比較し、class mismatch / escape-iteration mismatch / UNKNOWN率をJSONへ記録する。速度JSONにはspeedTiles / speedPixels / msPerPixel / fullMedianMsを保存する。Deep自体の絶対品質は上記BigInt CPU oracle reportを参照する。
|
||||
|
||||
## 次のrouter候補
|
||||
|
||||
実GPU結果が得られたら、backendは「速度だけ」ではなく次で選ぶ。
|
||||
|
||||
```text
|
||||
f32 quality OK → f32/DS/Deep のうち最速
|
||||
f32 NG, DS quality OK → DS/Deep のうち最速
|
||||
DS NG → Deep
|
||||
```
|
||||
|
||||
DeepにBLAがない現在、同じiteration数で`f32 Direct`よりDeep numeric kernelが速くなる理由はほぼない。Deepの役割は速度ではなく座標/反復精度であり、速度比較の本命は **DS cold cost vs Deep cold/warm cost** になる。
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
# v24.2.8 DS + True Sparse Fixed96 Direct Experiment
|
||||
|
||||
## Purpose
|
||||
|
||||
The previous three-term f32 expansion correction failed the real-GPU quality gate on every tested depth. v24.2.6 keeps the DS sensitivity gate but replaces the correction arithmetic with deterministic integer fixed-point operations.
|
||||
|
||||
Pipeline:
|
||||
|
||||
```text
|
||||
DS Direct + |dz/dc| risk gate
|
||||
-> risky pixel indices compacted into GPU u32 queue
|
||||
-> GPU finalize writes indirect workgroup count
|
||||
-> dispatchWorkgroupsIndirect
|
||||
-> signed Q8.88 Direct runs only queued pixels
|
||||
-> compare against corrected Full Deep
|
||||
```
|
||||
|
||||
The Fixed96 correction does not bind or build a deep reference.
|
||||
|
||||
## Arithmetic
|
||||
|
||||
- width: 96 bits
|
||||
- format: signed Q8.88 two's complement
|
||||
- storage: 3 × `u32`
|
||||
- multiplication: exact 32×32→64 decomposition from 16-bit partial products, 96×96 accumulation into six 32-bit limbs, then round-and-shift by 88 bits
|
||||
- coordinates: packed directly from the BigInt ViewState into Q8.88; JavaScript `Number` is not used as an intermediate
|
||||
|
||||
Q8.88 is intended only for the z6–z12 transition experiment. It is not proposed as an unlimited deep renderer.
|
||||
|
||||
## Measurements
|
||||
|
||||
For z6 / z8 / z10 / z12 and risk `1e11 / 1e12 / 1e13 / 1e14`, the UI reports every candidate row:
|
||||
|
||||
- extrapolated Fixed96 full-frame time,
|
||||
- Full Deep cold estimate,
|
||||
- speedup,
|
||||
- correction rate,
|
||||
- remaining UNKNOWN,
|
||||
- escaped/bounded class mismatch,
|
||||
- actual Fixed96 dispatch rate,
|
||||
- queue integrity status,
|
||||
- status/failure reason.
|
||||
|
||||
A failed candidate is never collapsed into a single `none` row.
|
||||
|
||||
## CPU preflight
|
||||
|
||||
`tests/v24-fixed96-model.mjs` compares the DS risk gate + exact Q8.88 Direct model with the BigInt P/P+64 oracle on a 1600×900-equivalent Seahorse sample grid. `tests/v24-fixed96-limb-model.mjs` separately verifies the six-limb multiplication algorithm against BigInt for 20,000 deterministic cases.
|
||||
|
||||
These are preflight tests. Real-GPU shader compilation, timing and class comparison remain decisive.
|
||||
|
||||
## Production status
|
||||
|
||||
Diagnostic only. The production router remains unchanged.
|
||||
|
||||
|
||||
## v24.2.7 range hotfix
|
||||
|
||||
v24.2.6 contained a shader bug in `fx_bad_range()`: the top Q8.88 integer byte was treated as if it had to be pure sign-extension. Valid values such as `+1.2` were therefore rejected. Escaping Mandelbrot orbits commonly pass through component magnitudes above 1, so selected correction pixels were counted as `remaining` instead of being written back.
|
||||
|
||||
v24.2.7 removes that invalid test. While the prior state is inside the bailout disk, Q8.88 multiplication is bounded. After each iteration the shader first tests `|Re(z)| > 2 || |Im(z)| > 2` as a mathematically certain escape, and only squares components when both are within ±2. This prevents both the false range rejection and large-value fixed-point wraparound.
|
||||
|
||||
|
||||
## v24.2.8 true sparse queue
|
||||
|
||||
v24.2.7 still launched a 2D Fixed96 workgroup over the whole benchmark tile and returned immediately for non-risk pixels. On SIMD/SIMT hardware this was not sparse enough: a few risky lanes could keep most of a workgroup occupied by the long integer loop.
|
||||
|
||||
v24.2.8 changes the execution topology, not the Q8.88 arithmetic. The guarded DS pass atomically appends each risky local pixel index to a queue. A one-invocation finalize shader writes `ceil(queueCount/64),1,1` to an indirect-argument buffer. The Fixed96 kernel is one-dimensional with `@workgroup_size(64)` and reads exactly one queue entry per invocation. No CPU map/readback occurs between these passes.
|
||||
|
||||
Every timing and quality tile validates queue invariants before its result is accepted. Overflow, wrong indirect arguments, invalid/stale indices or any disagreement between selected/enqueued/processed/corrected counts aborts the experiment rather than producing a misleading timing row.
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
# v24.2.6 DS + sparse Deep Hybrid Experiment
|
||||
|
||||
## 目的
|
||||
|
||||
v24.2.3実機計測では、Seahorse trackでDS Directはz10/z12でもFull Deepより速い一方、分類品質が不足した。次の実験ではDS Direct全画面を捨てず、数値的に危険なpixelだけDeep補正へ送ることで、z6〜z10の中間帯を高速化できるか測る。
|
||||
|
||||
production routerは変更しない。本ファイルの実験結果が採用条件を満たした場合にのみ次版で `f32 → DS/Hybrid → Full Deep` のrouterを検討する。
|
||||
|
||||
## 実験backend
|
||||
|
||||
`DS guarded direct → UNKNOWN only → existing Deep DS correction`
|
||||
|
||||
DS guarded directはorbitと同時に複素微分 `D = dz/dc` を近似追跡する。
|
||||
|
||||
```
|
||||
D[n+1] = 2 z[n] D[n] + 1
|
||||
```
|
||||
|
||||
`|D| > riskLimit` になったpixelはDS Directの分類を採用せず `FIELD_UNKNOWN / ds-sensitivity` とする。そのpixelだけ既存のBigInt reference + double-single perturbation correctionで再評価する。
|
||||
|
||||
この判定はmembership証明ではなく、DS Directの不安定化を早期に検出する実験用heuristicである。
|
||||
|
||||
## sweep
|
||||
|
||||
- 深度: z6 / z8 / z10 / z12
|
||||
- riskLimit: `1e12`, `1e13`, `1e14`
|
||||
- speed: actual full-resolution座標上の代表3tile(各最大256×144)
|
||||
- quality: 5 spatial tiles
|
||||
- quality reference: corrected Full Deep
|
||||
- cold time: verified BigInt reference buildをHybrid / Full Deep双方へ加算
|
||||
|
||||
診断情報 → **Hybrid実験を開始** を押すとダイアログ表示後に自動開始する。ダイアログ内の **再実行** で同じ設定を再度測定でき、結果はJSON保存できる。
|
||||
|
||||
## CPU事前モデル
|
||||
|
||||
1600×900相当、Seahorse、31×21点、BigInt P/P+64 oracle。`riskLimit=1e13`:
|
||||
|
||||
| depth | raw DS class error | Deep補正対象 | DS class error捕捉率 |
|
||||
|---:|---:|---:|---:|
|
||||
| z6 | 4 / 651 | 18 / 651 = 2.8% | 100% |
|
||||
| z8 | 4 / 651 | 72 / 651 = 11.1% | 100% |
|
||||
| z10 | 10 / 651 | 225 / 651 = 34.6% | 100% |
|
||||
| z12 | 51 / 651 | 594 / 651 = 91.2% | 100% |
|
||||
|
||||
このため事前仮説は次の通り。
|
||||
|
||||
- z6: Hybrid有望。
|
||||
- z8: Hybrid有望。
|
||||
- z10: 補正率約35%でもFull Deepより速いかが主要な計測点。
|
||||
- z12: 約91%をDeepへ送るためHybridの意味が薄く、Full Deepへ切り替える候補。
|
||||
|
||||
## 採用gate
|
||||
|
||||
Hybridをproduction候補にするのは、各viewで以下を全て満たす場合のみ。
|
||||
|
||||
1. corrected Full Deepとのescape/bounded class mismatch = 0。
|
||||
2. correction後remaining UNKNOWN = 0。
|
||||
3. Hybrid cold estimateがFull Deep coldより10%以上高速。
|
||||
4. correction対象率 < 60%。60%以上ならFull Deepを優先。
|
||||
5. 実GPU acceptanceでuncaptured validation/runtime error = 0。
|
||||
|
||||
最終production routerへ昇格する前には、BigInt oracleでdense boundary corpusを別途再検証する。
|
||||
|
||||
## 注意
|
||||
|
||||
- `riskLimit=1e13`はCPU事前モデルから得た候補で、固定仕様ではない。
|
||||
- `|dz/dc|`はcondition proxyであり厳密なroundoff error boundではない。
|
||||
- Hybrid correctionはreference buildを必要とする。ただしv24.2.3実機値ではreference buildはDeep全時間の小部分だったため、主要比較はGPU numeric costとなる。
|
||||
- z12でHybrid補正率が高いことを「失敗」とは扱わない。Full Deepへ切り替えるべき境界を検出することも実験目的である。
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
# v24.2.3 crossover benchmark implementation report
|
||||
|
||||
## ファイルの役割
|
||||
|
||||
- `index.html`: UI、canvas、Export dialog。WebGPU実装ロジックは持たない。
|
||||
- `script.js`: BigInt ViewState、reference Worker、WebGPU resource orchestration、backend router、screen/Export pipeline。
|
||||
- `gpu-kernels.js`: production 6 WGSL source + benchmark-only `DS Direct` 1本。DS Directはproduction routerから参照しない。
|
||||
- `tests/v24-sparse-correction-model.mjs`: WGSLと同じf32/double-single式をCPU `Math.fround`で再現し、BigInt oracleと比較する品質gate。
|
||||
- `tests/v24-backend-policy.mjs`: view-specific route policyの純粋ロジック検査。
|
||||
- `tests/webgpu-acceptance.*`: 実GPU compile/dispatch/quality/export gate。
|
||||
- `AUDIT_V24.md`: 現releaseの検査結果と未検証事項。
|
||||
- `MIGRATION_V24.md`: v23からの設計変更と、廃案を含む理由。
|
||||
|
||||
## 今回作成/変更した理由
|
||||
|
||||
深部primaryのUNKNOWNを黒として残すことが閲覧品質を下げていたため、追加referenceではなく同じreference上のdouble-single correctionへ変更した。またdirect利用範囲を固定閾値だけで狭めず、actual view上のqualityとGPU速度を測ってdeep採用を限定するためrouterを変更した。
|
||||
|
||||
## 機能検査
|
||||
|
||||
`npm test`で以下を自動実行する。
|
||||
|
||||
- JS syntax
|
||||
- WGSL reserved identifiers
|
||||
- source/tree/index contracts
|
||||
- reference Worker
|
||||
- direct f32 model
|
||||
- backend router
|
||||
- guarded deep model
|
||||
- sparse DS correction model
|
||||
- BLA disabled contract
|
||||
- geometry/export contract
|
||||
- exact deep coordinate formatter
|
||||
- streaming PNG
|
||||
- real-GPU acceptance source contract
|
||||
|
||||
## 性能設計
|
||||
|
||||
- overlap probeは本描画前に1回/current-view。
|
||||
- speed probeは最大224×160。
|
||||
- quality probeは最大72×54を5領域。
|
||||
- backend profileは補助診断であり、現在viewのquality probeを省略しない。
|
||||
- DS correctionはUNKNOWNのみ数値ループへ入り、KNOWN pixelは即return。
|
||||
- Export workspaceは512×512固定で再利用。
|
||||
|
||||
## 既知の制約
|
||||
|
||||
- double-singleはWGSLの実装差を含めmembership certificateとして扱わない。
|
||||
- 実GPU acceptanceはWebGPU adapterを取得できる環境で別途実行が必要。
|
||||
|
||||
|
||||
## v24.2.3追加作業
|
||||
|
||||
- `DIRECT_DS_WGSL`: 全画面double-single Directの速度/品質候補。
|
||||
- `runCrossoverBenchmark()`: z0〜z40のround-depth自動比較。
|
||||
- `Backend比較` dialog: 実GPUでf32 / DS / Deep warm / Deep coldを同一解像度で計時しJSON保存。
|
||||
- `tests/v24-crossover-quality.mjs`: 1600×900相当のBigInt P/P+64 oracle品質掃引。
|
||||
- `CROSSOVER_BENCHMARK.md`: 実装意図、CPU品質結果、次routerの判定基準。
|
||||
|
||||
現時点ではDS Directは診断専用であり、本体のbackend選択は変更していない。実GPU速度結果なしにproductionへ昇格させないための措置。
|
||||
|
||||
|
||||
## v24.2.4 Hybrid experiment preparation
|
||||
|
||||
- Added benchmark-only `DIRECT_DS_GUARDED_WGSL`; production router is unchanged.
|
||||
- Guard metric: approximate `|dz/dc|`; threshold sweep `1e12 / 1e13 / 1e14`.
|
||||
- Added `benchmarkHybrid()` / `renderTileMetaHybrid()` using the existing Deep double-single correction only for DS-marked UNKNOWN pixels.
|
||||
- Added z6/z8/z10/z12 experiment UI and JSON export.
|
||||
- Added CPU BigInt P/P+64 risk-model gate. At risk=1e13, observed DS class errors are all caught at z6/z8/z10 with correction rates 2.8% / 11.1% / 34.6%; z12 correction rate is 91.2%, intentionally indicating the Full-Deep crossover.
|
||||
- Added real-WebGPU acceptance smoke for the Hybrid diagnostic path.
|
||||
|
||||
|
||||
## v24.2.5 Sparse Direct experiment (Implementation)
|
||||
|
||||
- The previous sparse-Full-Deep diagnostic is superseded and is not exposed in the UI.
|
||||
- Added `DIRECT_TS_CORRECT_WGSL`, a reference-free three-term f32 expansion correction shader that only processes DS sensitivity UNKNOWN pixels.
|
||||
- Added `benchmarkSparseDirect()` / `renderTileMetaSparseDirect()` and a z6/z8/z10/z12 risk sweep.
|
||||
- Full Deep remains the quality/timing baseline; the Sparse Direct candidate itself does not build or bind a reference orbit.
|
||||
- Production backend routing is unchanged.
|
||||
- Root `index.html` is now a single-file bundle with no external script dependency, specifically to keep diagnostic UI controls functional when the HTML is opened from an archive/temp extraction.
|
||||
- Static single-file UI contract and real-browser click smoke are separate gates; real WebGPU arithmetic still requires a WebGPU-capable browser.
|
||||
|
||||
- Sparse Direct coordinate packing uses exact residual subtraction from BigInt fixed-point into three f32 components; this removed the CPU TD-model class errors caused by the earlier host-Number split.
|
||||
|
||||
## v24.2.6 Fixed96 experiment implementation
|
||||
|
||||
Purpose: test a deterministic reference-free correction tier between DS Direct and Full Deep without relying on WGSL float-expansion behavior.
|
||||
|
||||
Implemented:
|
||||
- `DIRECT_FIXED96_CORRECT_WGSL`: signed Q8.88 Direct iteration using 3×u32 values.
|
||||
- exact 32×32→64 multiplication from 16-bit partial products;
|
||||
- 96×96→192 accumulation and rounded Q8.88 reduction;
|
||||
- BigInt→Q8.88 tile coordinate packing;
|
||||
- DS risk gate + Fixed96 UNKNOWN-only correction benchmark;
|
||||
- per-risk failure reporting instead of collapsing all failures to `none`;
|
||||
- single-file ZIP UI contract;
|
||||
- CPU oracle and limb-arithmetic tests.
|
||||
|
||||
Not implemented: production router integration. The experiment must first pass on a real WebGPU adapter.
|
||||
|
||||
|
||||
## v24.2.7 Fixed96 hotfix
|
||||
|
||||
The Fixed96 experiment's real-GPU `remaining` counter exposed an implementation bug rather than an arithmetic-quality limitation. The correction shader's range check confused the 8-bit Q8.88 integer field with redundant sign-extension. The hotfix removes that check and makes bailout ordering overflow-safe: component-certain escape first, exact fixed-point magnitude only inside the ±2 component box.
|
||||
|
||||
|
||||
## v24.2.8 — Fixed96 GPU compact queue
|
||||
|
||||
### Files changed
|
||||
|
||||
- `gpu-kernels.js`: DS risk queue output, indirect-args finalize shader, 1D queue-indexed Fixed96 correction kernel.
|
||||
- `script.js`: queue/indirect buffers, indirect dispatch encoding, per-sample queue integrity verification, queue/dispatch diagnostics in the experiment result.
|
||||
- `tests/v24-fixed96-contract.mjs`: static true-sparse contract.
|
||||
- `tests/v24-fixed96-queue-model.mjs`: fail-closed queue counter/indirect argument model.
|
||||
- `tests/webgpu-acceptance.js`: real-GPU Fixed96 smoke now requires queue integrity and dispatch rate equality.
|
||||
|
||||
### Performance intent
|
||||
|
||||
The previous implementation's numerical correction fraction was sparse, but its GPU invocation topology was not. v24.2.8 makes integer iteration work proportional to the actual queue length. The DS guard remains a full-tile pass because it is the cheap classifier; only the expensive Q8.88 loop is sparse.
|
||||
|
|
@ -1,566 +0,0 @@
|
|||
# マンデルブロ集合ビューワー 軽量・高精細化 改善提案
|
||||
|
||||
- 対象baseline: Mandelbrot ∞ Zoom v22
|
||||
- 文書status: v22に対する実装前提案。採用項目はrenderer v23へ実装済みであり、現行状態と残存gateは`IMPLEMENTATION_REPORT.md`と`COMPLETION_AUDIT.md`を正とする。
|
||||
- Document status: historical-v22 / implemented-v23
|
||||
- 証拠境界: v22成果物とGit snapshotは現workspaceに残っていないため、v22の行番号・bytesは履歴記録であり再監査可能なbaselineではない。以後の比較基準はversion付きJSONとSHA-256を必須とする。
|
||||
- 作成日: 2026-08-22
|
||||
- 対象ファイル: `index.html`, `script.js`, `kernels.js`, `src/bla_kernel_v18.c`
|
||||
- 性格: 設計判断を保存する履歴文書兼、修正版の品質契約。効果値は、version付き監査JSONの引用を除き検証すべき目標または仮説である。
|
||||
|
||||
## 1. 結論
|
||||
|
||||
現行実装は、任意精度座標、基準軌道、摂動法、リベース、BLA、SIMD、局所フォールバックまで備えている。数値エンジンの骨格を捨てて全面的にGPU化するより、次の順で周辺設計を改める方が、軽量性と高精細を両立しやすい。
|
||||
|
||||
1. 待機中も続く全画面再描画を止め、イベント駆動にする。
|
||||
2. 描画品質を曖昧な数値 `q` ではなく、Reprojected / Preview / Covered / Refined / Validated の完了条件で管理する。
|
||||
3. 画面全域を、確定した表示pixel gridまでタイル描画してから、境界だけ適応的アンチエイリアスする。通常はnative DPRとし、画素予算でDPRを制限した場合は明示する。
|
||||
4. 計算結果と彩色を分離し、色変更でフラクタル計算をやり直さない。
|
||||
5. 深部WorkerとWASMを遅延生成し、固定最大長・Workerごとの重複メモリを減らす。
|
||||
6. 画素中心、BLA精度、未解決画素、出力完了条件を明文化し、「高精細」の意味を計測・監査できる状態へ変える。
|
||||
|
||||
最優先はカーネルの細かな命令最適化ではない。現状の配布物は合計138,831 bytesとすでに小さい一方、待機中の常時描画、巨大なCanvas、Workerごとの固定配列は継続的なCPU/GPU・メモリ負荷になるためである。
|
||||
|
||||
## 2. 「軽量」と「高精細」の定義
|
||||
|
||||
本提案では、目標を次のように分ける。
|
||||
|
||||
| 軸 | 意味 | 主な指標 |
|
||||
|---|---|---|
|
||||
| 配布の軽さ | 初回ロード・解析・WASMコンパイルが小さい | 転送bytes、起動時に取得/compileする資産 |
|
||||
| 実行の軽さ | 操作中と待機中にCPU/GPUを浪費しない | long task、idle CPU、Canvas draw回数、消費電力 |
|
||||
| メモリの軽さ | DPR・Worker数・参照長が増えても予算内に収める | peak memory、cache bytes、Workerごとの固定領域 |
|
||||
| ラスター精細度 | 表示ピクセルを実サンプルで覆い、境界を適切にAAする | sample coverage、resolved coverage、samples/pixel、継ぎ目 |
|
||||
| 数値精度 | 座標・反復・BLA近似の誤差を検出し、未解決を黒と混同しない | 高精度referenceとの差、unresolved数、検証モード |
|
||||
| 出力精細度 | 表示プレビューとは独立して、指定寸法を完了後に保存する | 出力寸法、AA、再現性、未完了pixel数 |
|
||||
|
||||
「Canvasが4Kサイズである」だけでは高精細とはみなさない。低解像度フレームを4K Canvasへ拡大した状態と、4K分のサンプルを実計算した状態を区別する。
|
||||
|
||||
本書のcoverageは次の2値へ分ける。sample coverageは宣言したiteration/precision policyで新ViewSpecのsampleを計算済みのtarget pixel比率であり、`UNRESOLVED`も計算済みsampleとして含む。resolved coverageは`ESCAPED`またはpolicy上の`INTERIOR_PROVEN`へ確定した比率である。Coveredはsample coverage 100%、Validatedはsample coverage 100%に加えてunresolved 0を要求する。
|
||||
|
||||
## 3. 現状評価
|
||||
|
||||
### 3.1 残す価値が高い部分
|
||||
|
||||
- BigInt固定小数点による中心・表示幅と、段階的な精度昇格 (`script.js:58-109`)
|
||||
- 可変精度BigInt固定小数で生成し、Float64表現を摂動/BLAへ渡す基準軌道 (`script.js:608-705`)
|
||||
- SIMDからscalar、さらにJS/BigIntへのフォールバック (`script.js:208-227`, `script.js:551-685`)
|
||||
- 常駐Workerのwarm cache、少数検証、strip単位の局所補修 (`script.js:296-390`)
|
||||
- main cardioid / period-2 bulbと収縮判定による内部早期終了 (`src/bla_kernel_v18.c:94-172`)
|
||||
- 操作中の既存フレーム再投影 (`script.js:154-184`, `script.js:393-424`)
|
||||
- smooth coloring、world-space detail cacheという考え方 (`script.js:187-206`, `script.js:393-531`)
|
||||
|
||||
既存のv18監査では、v17比のbase-frame改善が2.07〜5.15倍で、テストした内部早期終了は黒/外部分類不一致0だった (`AUDIT.md:60-82`, `AUDIT.md:163-176`)。この実績があるため、BLAや内部早期終了を一律に廃止する理由はない。
|
||||
|
||||
提案作成時の現行コードはv22だったのに、性能監査と最終ベンチの表題・データはv18だった。既存値は設計判断の参考には使えるが、v22の受入基準にはできない。当初は同じsceneのv22再計測を必要条件としたが、現在は不変なv22成果物が残っていないため比較を`not-verifiable`とし、version固定したv23以後のbaselineだけを回帰判定へ使う。
|
||||
|
||||
### 3.2 主要なギャップ
|
||||
|
||||
| 優先度 | ギャップ | 根拠 | 影響 |
|
||||
|---|---|---|---|
|
||||
| P0 | 静止中も永久に`requestAnimationFrame`を回す | `script.js:722-729` | idle CPU/GPU、電池、発熱 |
|
||||
| P0 | 1フレームで全Canvasと全detail cacheを再合成し、DOM統計も更新する | `script.js:413-423`, `script.js:713-716` | 画面が変わらなくても継続負荷。ドラッグ中は同一RAF内で二重paintになる |
|
||||
| P0 | 解像度制御が端末実測を使っていない | `renderPerf`は記録のみ。`targetSize()`は固定tier (`script.js:42-44`, `script.js:426-439`, `script.js:534-537`) | 小画面では過剰計算、大画面では不足 |
|
||||
| P0 | HQ全画面は最大2100×2200で、選抜領域だけ2倍 | `script.js:429-461` | 4Kでは要求gridを全域計算しない。細線を基礎画像で見失うと精細化されない |
|
||||
| P0 | 確認できるJS/direct/BLA経路が、画素中心ではなく整数格子点をサンプルする | `script.js:516-518`, `script.js:570-573`, `script.js:672-674`, `src/bla_kernel_v18.c:184-190` | 非対称なsample grid、解像度間対応の非標準化、backend差のリスク |
|
||||
| P0 | detail fast BLAがbase HQより緩い | base HQはe-32、detailは通常e-28。`strictBoundary`は現在常にfalse (`script.js:350-359`, `script.js:504-525`) | 「精細化した領域の方が数値的に粗い」可能性 |
|
||||
| P0 | PNGが現在の表示Canvasを即時保存する | `script.js:739` | 再投影中・低解像度・detail未完了でも高解像度出力に見える |
|
||||
| P1 | 起動180ms後にdeep poolを必ず作る | `script.js:296-307`, `script.js:750` | 浅部だけ見る場合もWorker、参照配列、LUTを確保 |
|
||||
| P1 | Workerごとに基準軌道とBLA表を重複保持・構築する | `script.js:249-259`, `src/bla_kernel_v18.c:3-23` | 深部使用後のpeak memoryが大きい |
|
||||
| P1 | WorkerでstripごとにRGBAを新規確保し、transfer後にmain全画面配列とImageDataへ再コピーする | `script.js:265-269`, `script.js:378-382`, `script.js:398-400` | memory bandwidth・一時領域。transfer自体はzero-copy |
|
||||
| P1 | palette/cycle/shift変更で数値計算まで再実行する | `script.js:397`, `script.js:468`, `script.js:744-747` | 深部の色調整が不必要に重い |
|
||||
| P1 | deep切替が固定ズーム指数11.5 | `script.js:20`, `script.js:707-709` | 中心値、DPR、1画素幅に対し過剰または不足 |
|
||||
| P1 | 有限反復表示として`maxIter`到達を黒にするため、精度優先時も証明済み内部と未確定を区別できない | `script.js:116-119`, `script.js:561-564`, `script.js:666-670` | Validated/Exportで確定度を説明できない。v17の途中work cap不具合とは別問題 |
|
||||
| P2 | BLAが通常doubleのspanに依存し、約10^-280以深で無効になる | `script.js:364`, `script.js:508`, `src/bla_kernel_v18.c:175-190` | 超深部で性能が崖状に低下 |
|
||||
|
||||
### 3.3 メモリ上の具体例
|
||||
|
||||
`src/bla_kernel_v18.c`の固定配列だけを合計すると、1つのBLA WASM instanceあたり約15.6 MiBになる。
|
||||
|
||||
- 出力: `counts[65536]` + `mags[65536]` ≈ 0.75 MiB
|
||||
- 参照軌道: `refs_r[150001]` + `refs_i[150001]` ≈ 2.29 MiB
|
||||
- BLA double配列5本 × 300100 ≈ 11.45 MiB
|
||||
- BLA length配列 × 300100 ≈ 1.14 MiB
|
||||
|
||||
深部job後に4 WorkerがBLA instanceを持てば、この固定領域だけで約62.5 MiBとなる。さらに各WorkerにはJS側の`RR/RI`、3パレットのLUT、deep/color WASM、出力scratchがある。
|
||||
|
||||
また、Worker内部の`RR/RI`とLUTはWASMより先に生成されるため、起動直後に4 Workerを作るだけでもtyped arrayだけで概算13 MiB超を確保する。固定最大配列を現在の`refLen`へ合わせる効果は大きい。
|
||||
|
||||
### 3.4 配布サイズ上の具体例
|
||||
|
||||
- `index.html`: 6,618 bytes
|
||||
- `script.js`: 80,077 bytes
|
||||
- `kernels.js`: 52,136 bytes
|
||||
- 実行に必要な3ファイル合計: 138,831 bytes
|
||||
- Base64部分: 51,808文字、復号後WASM合計38,845 bytes
|
||||
|
||||
絶対量はすでに小さい。Base64と復号後の約13KB差は非圧縮表現上の値であり、Brotli転送量が同量減るわけではない。外出しの主効果は独立cache、遅延取得、streaming compileであるため、先にidle負荷とメモリ重複を直す方がよい。なお`COLOR_SIMD_B64`と`COLOR_SCALAR_B64`は同一内容であり、現状は同じ2,510-byteバイナリを二重に内包している。
|
||||
|
||||
## 4. 推奨する新しい品質仕様
|
||||
|
||||
内部の連続値`q`を、利用者と実装の双方が判定できる状態機械へ置き換える。現状は初回`render(.3)`に対し分岐が`q < .3`なので、意図した最軽量tierではなく次のtierへ入る (`script.js:426-437`, `script.js:751`)。enum化はこの種の境界ずれも防ぐ。
|
||||
|
||||
| 状態 | 開始条件 | 解像度・計算 | 完了条件 | UI表示例 |
|
||||
|---|---|---|---|---|
|
||||
| Reprojected | wheel / pinch / pan継続中 | 既存フレームのtransformのみ | 入力停止待ち | プレビュー |
|
||||
| Preview | 入力停止80〜120ms後 | 時間予算内の低解像度。近似BLA可 | 最優先tileを表示 | 高速描画 |
|
||||
| Covered | Preview後 | 選択した処理モードのtarget gridを全域tile計算 | sample coverage 100%、再投影pixel 0。未確定数は別表示 | 全域描画 68% |
|
||||
| Refined | Covered後のidle | 境界・不確実tileだけ2×/4× adaptive AA | 対象tile完了 | 境界AA完了 |
|
||||
| Validated | 精度優先時またはValidated Export | 宣言した有限反復・精度policyを局所照合 | checks合格かつunresolved 0。残る場合は別状態「検証未完了・未確定N」 | 検証完了 / 検証未完了 |
|
||||
|
||||
重要な契約は次の通り。
|
||||
|
||||
- Previewは粗くてよいが、新しい未解決画素を「内部」と断定して黒にしない。
|
||||
- Coveredはズーム深度に関係なく、確定したScreen Canvas gridを新しいViewSpecの実sampleだけで全域計算する。再投影した旧frameは描画中のpresentationにだけ使い、sample coverageへ数えない。計算済み`UNRESOLVED`はsample coverageへ数えるがresolved coverageへは数えない。
|
||||
- native DPRは上限であり、標準・精細とも明示したpixel/memory budget内で使う。精細は時間適応で解像度を下げないが、memory/pixel上限は維持する。DPRを制限した場合はeffective DPRと実解像度を表示する。
|
||||
- Refinedは単なる2倍補間ではなく、複数の実サンプルをlinear-lightでresolveする。
|
||||
- Validatedは数学的なマンデルブロ集合所属証明ではなく、宣言した有限反復・precision policyの照合完了を表す。全sampleへ誤差境界または証明手続きを適用する将来モードだけを`Certified`と呼ぶ。
|
||||
- 「高精細」と「数値検証完了」を同じチェックボックスにしない。
|
||||
- Exportは表示Canvasとは独立したjobであり、view、寸法、反復方針、AA、precision tier、乱数seedを固定してから開始する。Balanced ExportとValidated Exportを区別し、前者は未確定数をmetadataへ記録して完了可能、後者だけをunresolved 0必須とする。
|
||||
|
||||
処理モードと描画状態は分ける。
|
||||
|
||||
| 処理モード | Coveredのtarget | 自動到達点 |
|
||||
|---|---|---|
|
||||
| 省電力 | 自動Coveredなし。Previewは最大約1MP | Preview |
|
||||
| 標準 | shallowはmobile/低memory約2MP、desktop約4MP。deepはPreview実測から約1.4秒のCovered予算へeffective DPRを下げる | Covered。AA・未確定追加反復は自動実行しない |
|
||||
| 精細 | mobile/低memory約4MP、desktop約8MP。上限時は実解像度を明示 | Refined。未確定追加反復は境界候補だけを上限付きで行う |
|
||||
| 精度優先 | 指定gridとprecision policy | Validated。将来はCertifiedを選択可 |
|
||||
|
||||
Exportは処理モードではなく独立jobである。Balanced / Validatedのprecision tierを選び、それぞれRefined相当 / Validated相当の完了条件を適用する。
|
||||
|
||||
## 5. 推奨アーキテクチャ
|
||||
|
||||
```text
|
||||
入力
|
||||
↓
|
||||
ViewState(BigInt座標・履歴・version)
|
||||
↓
|
||||
Render Scheduler(gesture、優先度、時間/メモリ予算、取消)
|
||||
├─ f64 SIMD backend
|
||||
├─ deep BLA / perturbation backend
|
||||
└─ high-precision verifier / fallback
|
||||
↓
|
||||
Field Cache(escaped / interior likely / interior proven / unresolved、smooth value、confidence)
|
||||
├─ Colorizer → Screen Compositor
|
||||
└─ Adaptive AA / Export
|
||||
```
|
||||
|
||||
責務を次の単位へ分ける。
|
||||
|
||||
- `view-model`: BigInt座標、pixel-to-world規約、URL version、戻る/進む
|
||||
- `scheduler`: gesture settle、tile priority、generation cancel、visibility、品質状態
|
||||
- `backend`: f64 / deep / verifierを同じtile protocolで実装
|
||||
- `field-cache`: palette非依存のpacked数値結果、未解決tileだけの継続状態、byte-budget LRU
|
||||
- `colorizer`: palette、tone mapping、最終ditherだけを担当
|
||||
- `presenter`: 再投影、tile合成、coverage表示
|
||||
- `exporter`: 指定寸法の独立tile render、進捗、取消、metadata
|
||||
|
||||
Workerへ送るjobは、最低限`viewVersion`, `tile`, `samplePattern`, `iterationPolicy`, `precisionPolicy`を持たせる。古いversionの結果は合成せず、1 tileを8〜12ms程度へ収めて取消後の滞留を限定する。
|
||||
|
||||
## 6. 軽量化の具体策
|
||||
|
||||
### 6.1 イベント駆動描画へ変更する — P0
|
||||
|
||||
現在の`loop()`は静止中も毎フレーム`paintFrame()`、`updateStats()`、次のRAF予約を行う。次へ変更する。
|
||||
|
||||
- `needsPaint` / `needsStats` / `scheduled`を設け、invalidate時だけRAFを1回予約する。
|
||||
- pointermove/wheelはイベントごとに描かず、最新transformを1 RAFに集約する。
|
||||
- render/tile完了時だけ該当rectを合成する。
|
||||
- statsは状態変化時のみ、操作中でも最大4〜10Hzに制限する。
|
||||
- `document.hidden`ではdetail、Wisdom、未表示tileを停止する。
|
||||
- 静止完了後はRAF、Canvas write、DOM writeを0回/秒にする。
|
||||
|
||||
これは実装難度が低く、画質を落とさず、待機時負荷を大きく下げる最初の変更である。
|
||||
|
||||
### 6.2 表示Canvasと計算解像度を分離する — P0
|
||||
|
||||
現状は表示Canvasを`innerWidth × min(DPR, 2.5)`で確保する一方、frameは最大2100×2200である。4K・高DPRでは巨大な表示バッファへ小さいframeを拡大するだけになり得る。
|
||||
|
||||
極端な例として、3840×2160 CSS px・DPR 2.5では表示Canvasだけで9600×5400×4 bytes、約198 MiBになる。画素数上限がないため、source frameより大幅に大きいsurfaceを常時再描画し得る。
|
||||
|
||||
- Screen Canvasには処理モード別に約1〜8MPのbyte/pixel budgetを設ける。標準shallowは2〜4MP、精細・精度優先は4〜8MPを初期値とする。標準deepはcold Previewを極小gridで測り、実測MPPから約1.4秒以内を狙うpixel budgetへ下げる。effective DPRと実寸を表示し、精細を選んだ場合はこの時間適応を使わない。
|
||||
- `effectiveDpr = min(deviceDpr, pixelBudget由来上限)`とする。
|
||||
- Covered計算はタイルで選択モードのtarget Canvas gridを100%覆う。
|
||||
- 画面より大きい4K/8K出力は表示Canvasを巨大化せず、ExporterのOffscreenCanvasまたはtile encoderで作る。
|
||||
- `renderPerf`を本当に使い、Previewだけ`targetPixels = budgetMs / EMA(msPerPixel)`で決める。
|
||||
- EMAはshallow/deepだけでなく、iteration帯、BLA有無、warm/coldを区別する。
|
||||
|
||||
Previewの目安は80〜120ms、停止後baseは400〜700msとし、その後はtileで漸進する。端末差を吸収するため、固定pixel幅ではなく時間予算から決める。
|
||||
|
||||
### 6.3 操作中の無駄な計算を止める — P0
|
||||
|
||||
- wheelをpointerと同じgestureとして扱い、入力停止80〜120msまでは再投影だけにする。
|
||||
- pinch / wheel / +−を同じsettle処理へ統合する。
|
||||
- SharedArrayBufferを使えるHosted版では、WASM hot loopが定期的に読むatomic cancel flagを設ける。
|
||||
- SharedArrayBufferが使えない構成では、tile/chunkを小さくして取消後の最悪滞留時間を制限する。
|
||||
- Preview中にwork capへ到達した画素は`UNRESOLVED`のまま残す。現状は低品質passで未解決が2%を超えると同stripを即時無制限再計算する (`script.js:265-266`) が、field/statusを導入するPhase 2以降はCoveredで継続し、Validatedでは必要に応じ高精度経路へ上げる。
|
||||
- 浅部の`.58 → .84 → 1.0`という最大3回の全面再計算は、実測が遅い端末ではPreview → Coveredの2段へ減らす。
|
||||
|
||||
### 6.4 deep資産を遅延生成する — P0/P1
|
||||
|
||||
- Hosted版の初期表示ではshallow SIMDだけを取得・compileする。Standalone版は全Base64文字列をparseするが、deep payloadの復号・compileとWorker生成は遅延する。
|
||||
- deep main-thread WASM、deep worker source、Worker poolは、数値条件が閾値へ近づいた時点で準備する。
|
||||
- Workerは最初1〜2個だけ作り、実jobのthroughputから必要時だけ増やす。
|
||||
- 現在のReal Wisdomは深部HQへ到達した各ページセッションのidle時に1回、最大4 Workerなら合計10回のmini renderを追加する (`script.js:333-348`, `script.js:698`)。通常jobから受動学習し、能動ベンチは診断時または不確実時だけにする。
|
||||
- 学習結果はkernel versionと最小限の端末帯域特性をkeyに永続化する。schema versionと保存時刻を持たせ、既定TTL 30日で失効させる。storage拒否時はsession内学習だけで動作し、raw timing履歴や過剰なfingerprint属性を保存しない。
|
||||
|
||||
### 6.5 WASMとメモリを統合・動的化する — P1
|
||||
|
||||
- deep、BLA、smooth/colorを1 module・1 memoryへ統合する。
|
||||
- `MAX_REF`、`MAX_BLA`の固定最大配列をやめ、現在の`refLen`に合わせて初期容量を決め、必要時だけ`memory.grow`する。linear memoryは縮まないため、大scene後の回収には容量bucket別instance poolの退役・再生成を使う。
|
||||
- JS `RR/RI` → deep WASM → BLA WASMという重複copyを1回へ減らす。
|
||||
- BLA tableは参照長`n`に対して概ね`2n`要素を動的確保する。
|
||||
- Hosted版ではCOOP/COEPを設定する。共有対象はbuild完了後immutableなreference、key別BLA table、tileごとに非重複なfield領域へ限定する。table公開にはready barrierを設け、scratch・統計はWorker別offsetまたは別memoryにしてdata raceを避ける。
|
||||
- 非共有版ではbuffer poolを使い、transferしたArrayBufferをWorkerへ返却して再利用する。
|
||||
- `mags`をcolor WASMへcopyする経路も統合し、kernelからpacked fieldまたはRGBAを一度だけ出す。
|
||||
|
||||
### 6.6 キャッシュを件数ではなくbytesで管理する — P1
|
||||
|
||||
`DETAIL_TILE_LIMIT=96`は、tile寸法、DPR、Canvas実装の裏側を考慮しない。次へ置き換える。
|
||||
|
||||
- mobile 96 MiB、desktop 192 MiBを、JSから管理できるtyped array / WASM / cacheの暫定論理予算として計測開始する。
|
||||
- frame、numeric field、tile canvasの推定bytes、reference、BLA、scratchをmemory ledgerへ登録する。ブラウザ内部のCanvas/GPU backing storeは正確に台帳化できないため、固定環境のprocess/UA memory観測を別指標にする。
|
||||
- LRUはbytes、現viewportからの距離、再利用見込みでevictする。
|
||||
- pan開始時の全frame cloneを避け、gesture中はimmutableな現frameを直接transformする。
|
||||
- 論理予算超過時はcacheを捨てる。ブラウザ固有のmemory-pressure通知へ依存せず、最終解像度を黙って落とさない。
|
||||
|
||||
### 6.7 配布をHosted版とStandalone版へ分ける — P2
|
||||
|
||||
推奨する標準はHosted版である。
|
||||
|
||||
- `.wasm`とmodule Workerをhash付き外部資産にする。
|
||||
- `compileStreaming` / `instantiateStreaming`、Brotli、immutable cacheを使う。
|
||||
- SIMD版を先に取得し、失敗時だけscalar版を取得する。
|
||||
- deep/BLA/colorは深部到達時だけ取得する。
|
||||
- compile済み`WebAssembly.Module`をWorkerへstructured cloneする。
|
||||
|
||||
既存の「3ファイルを直接開ける」はStandalone互換buildとして残し、build時にWASMをinlineする。これにより、軽量な通常配信とオフライン可搬性を両立できる。
|
||||
|
||||
WebGPUは最初から必須にしない。まずpalette適用、linear-light resolve、tile合成へ使い、数値演算は低ズームPreviewだけの任意backendとして比較する。fp32を深部の最終結果へ使わない。
|
||||
|
||||
### 6.8 WASM buildを再現可能にする — P1
|
||||
|
||||
現状の`src/`にはBLA C sourceしかなく、他の埋込WASMに対応するsource、build command、toolchain manifestが見当たらない。バイナリだけでは、画素座標、丸め、SIMD/scalarの同値性を十分に監査できない。
|
||||
|
||||
- 全kernel sourceと生成scriptを保存する。
|
||||
- clang等のversion、compile flags、exports ABIを固定する。
|
||||
- 生成物のSHA-256をmanifestへ記録する。
|
||||
- SIMD/scalarについて同じgolden vectorを実行する。
|
||||
- `kernels.js`またはStandalone bundleは生成物とし、手編集しない。
|
||||
|
||||
## 7. 高精細・高精度化の具体策
|
||||
|
||||
### 7.1 pixel-to-world規約を画素中心へ統一する — P0
|
||||
|
||||
確認できるJS/direct/BLA実装では整数格子式が使われている。埋込WASMを含む全backend、detail、exportで、次の規約を共有する。
|
||||
|
||||
```text
|
||||
cr = centerRe + span * ((x + 0.5) / width - 0.5)
|
||||
ci = centerIm + span * ((height / 2 - (y + 0.5)) / width)
|
||||
```
|
||||
|
||||
BigIntでは次の有理式にし、早期のdouble化を避ける。
|
||||
|
||||
```text
|
||||
cr = centerRe + span * (2*x + 1 - width) / (2*width)
|
||||
ci = centerIm + span * (height - 2*y - 1) / (2*width)
|
||||
```
|
||||
|
||||
canonical座標は上の有理数とする。fixed-point化では分子の符号と対称なround-to-nearest、exact halfはaway-from-zeroへ丸める`roundDiv`をABI契約とし、積を先に計算してから1回だけ除算する。JS f64、shallow WASM、deep WASM、BLA、direct fallback、detail、exportについて、同一pixelがcanonical座標を各backendの表現可能範囲で再現するcross-backend testを追加する。「backend差0」は整数のfixed-point表現または分類・iteration契約に対して用い、f64座標は宣言ULP以内を基準にする。整数格子式だけで現在のtile seamが実証されたわけではないが、規約を統一して将来のbackend差を防ぐ。この変更は既存URLのview中心を変えない一方、画像sampleは半画素分変化し得るため、renderer/golden image versionを上げる。
|
||||
|
||||
### 7.2 Covered完了後にadaptive AAする — P0/P1
|
||||
|
||||
現行のRGBA勾配選別は、基礎サンプルに現れなかった細いフィラメントや微小島を検出できない。また、Canvasの`imageSmoothingQuality`は再拡大補間であり、真のフラクタルAAではない。
|
||||
|
||||
新しい流れは次の通り。
|
||||
|
||||
1. 画面全域を1 sample / target screen pixelで計算する。
|
||||
2. kernelからescape iteration、smooth potential、内部/外部/未解決、confidence、可能なら距離推定値を出す。
|
||||
3. 分類境界、値の分散、距離推定、tile中央・四隅probeからquadtreeを細分化する。
|
||||
4. 必要なpixelだけ4/8/16 sampleへ増やす。
|
||||
5. sampleをlinear RGBで平均し、最後にsRGB/P3へencodeする。
|
||||
|
||||
これにより、全面4倍SSAAより少ない計算量で、ジャギー、細線、微小島を改善できる。detail選定はpalette非依存となる。ただし中央・四隅probeや距離推定も、任意に細い未サンプル構造を完全には保証しない。Refinedは誤差推定に基づくadaptive AAであり、全pixel面積の数学的被覆証明ではない。Certifiedを名乗る場合は、別途保守的なdistance/error boundが必要になる。
|
||||
|
||||
### 7.3 数値fieldと色を分離する — P1
|
||||
|
||||
全画面について保持するのは、少なくとも次のpacked fieldに限定する。
|
||||
|
||||
- state: `ESCAPED`, `INTERIOR_LIKELY`, `INTERIOR_PROVEN`, `UNRESOLVED`
|
||||
- smooth escape valueまたは固定小数へpackした値
|
||||
- confidence / precision tier
|
||||
|
||||
`zr/zi`、参照位置、微分などの継続状態は全画面に持たず、未解決pixelまたは未解決tileだけに保持する。8MP全域へ完全な継続状態を持つと、memory budgetを容易に超えるためである。
|
||||
|
||||
adaptive AAを再着色可能にするには、境界pixelのsubsampleごとのstate/smooth値とsample patternを保持する。単一の平均smooth値だけでは、非線形paletteを正しく再resolveできない。palette、cycle、shift変更時はこのfieldから安価なcolor/AA resolveだけを再実行し、フラクタル反復はやり直さない。detail cache keyからstyleを外し、geometry/field cacheとcolor cacheを分離する。
|
||||
|
||||
現状の3 palette × 2048 phase × 64 mix LUTはmainと各Workerで約1.1 MiBずつ使う。1D palette LUT + 計算式または補間へ変えれば小さくできる。色はlinear RGBまたはOKLabで生成し、最後だけ8bit化する。
|
||||
|
||||
「昼夜」paletteは周期の先頭色と末尾色が一致していない (`script.js:189-197`)。phase wrap位置に筋が出ないよう、循環paletteはendpointを一致させ、非循環paletteはping-pong mappingにする。banding対策のditherを戻す場合は、detail/AA選定後の最終表示またはExportだけに固定seedで適用する。
|
||||
|
||||
### 7.4 BLA/detailの精度逆転をなくす — P0
|
||||
|
||||
- Covered/Refinedのfast εは、少なくともbase HQと同等にする。
|
||||
- 境界scoreまたはconfidenceが悪いtileではεを厳しくするか、BLAを切ってstrict perturbation / double-doubleへ上げる。
|
||||
- 現行detail検証の「黒分類不一致3.5%以下、平均RGB差48以下」 (`script.js:476-487`) は、品質保証ではなく視覚的coherence gateにすぎない。視覚gateと数値correctness gateを分離し、後者は同一world座標を高精度referenceで再評価して比較する。low/high rasterの異なるsample位置を直接0 mismatch要件にしない。
|
||||
- fast BLAとsafe BLAは同じdouble参照軌道を使うため、「safe」を数学的厳密性の意味で使わない。
|
||||
- Balancedでは少数probe方式を残せるが、Validatedでは疑わしいtileを独立した高精度reference実装またはdouble-doubleと比較する。少数probe合格だけで未検査pixelを保証済みとは呼ばない。
|
||||
|
||||
### 7.5 未解決を黒から分離し、反復を継続可能にする — P1
|
||||
|
||||
通常の有限反復表示で`maxIter`到達を黒にすること自体は一般的な表示規約であり、v17の「global iterationより手前で打ち切ったwork cap」とは別である。本提案では、Validated/Exportの確定度を説明するため、内部表現を次へ分ける。
|
||||
|
||||
- `INTERIOR_PROVEN`: 誤差境界付き解析判定、または周期吸引を証明できたもの
|
||||
- `INTERIOR_LIKELY`: 現行のf64 cardioid/bulb判定や収縮heuristicなど、高信頼だが証明扱いしないもの
|
||||
- `ESCAPED`: 発散が確認されたもの
|
||||
- `UNRESOLVED`: iteration/work/precision上限へ達したもの
|
||||
|
||||
Coveredの描画中は未解決領域へ中立色または直前結果をpresentationとして重ねられる。旧frameの再投影はsample coverageへ数えないが、新ViewSpecでpolicy上限まで計算した`UNRESOLVED`はsample coverageへ数え、resolved coverageと未確定数で別に監査する。精細では未解決の境界tileだけ反復を段階的に継続する。これなら、旧work capを復活させずに計算量を局所化できる。
|
||||
|
||||
収縮による内部早期終了は既存監査で有効だったためBalancedでは`INTERIOR_LIKELY`として維持する。Validatedでは判定根拠をconfidenceへ記録し、policyが要求する場合は誤差境界付き解析判定・周期証明へ上げられないものを`UNRESOLVED`へ戻す。
|
||||
|
||||
### 7.6 engine選択とguard bitsを誤差ベースにする — P1
|
||||
|
||||
固定`DEEP_ZOOM_THRESHOLD=11.5`ではなく、少なくとも軸ごとに次を評価する。
|
||||
|
||||
- 1 pixelのworld-space幅
|
||||
- `centerRe`, `centerIm`付近のf64 ULP
|
||||
- 隣接pixel座標が別のdoubleになるか
|
||||
- 直近orbitの誤差・glitch率
|
||||
|
||||
`pixelStep > safety × ulp(center)`を満たす間はf64を使い、境界付近にはhysteresisを置く。これにより、不要なdeep移行を減らしつつ量子化を防げる。
|
||||
|
||||
座標のBigInt精度と基準軌道の計算精度も分ける。基準軌道をP bitとP+32/64 bitのcheckpointで比較する。単一global referenceを使う間はreference単位で精度を上げる。tile単位で上げるにはlocal referenceを新設するか、そのtileだけdirect/double-double摂動へ昇格する。全画面を常時多倍長化しない。
|
||||
|
||||
`exactDeepPixel`は固定小数積を右shiftで量子化する高精度direct計算であり、数学的なexactではない (`script.js:602-605`)。正値は切り捨て、負値は算術shiftによる負方向丸めになる。`highPrecisionDirectPixel`へ改名し、round-to-nearestを使う。Certifiedでは区間演算でescapeを証明し、内部には別の周期吸引証明を使う。
|
||||
|
||||
### 7.7 超深部の性能崖を段階的に解消する — P2
|
||||
|
||||
- BLA APIのspan、offset、cMaxを通常doubleではなくmantissa + exponent bucketへする。
|
||||
- 係数も必要に応じてscaled complexまたはdouble-doubleへ昇格する。
|
||||
- glitch tileを分割し、tile中心にlocal referenceを作るmulti-reference perturbationを研究する。
|
||||
- reference orbitをchunk化し、固定150,001点・最大140,000反復の上限をmemory budgetへ置き換える。
|
||||
- 上限へ達した場合は「∞」を暗黙に名乗らず、現在の反復/参照/精度上限と未解決数を表示する。
|
||||
|
||||
multi-referenceやdouble-doubleは高難度である。version固定した現rendererのbrowser計測で、reference生成、glitch fallback、10^-280以深のどれが実際に支配的か確認してから実装する。v22欠落分を現renderer値で遡及的に代用しない。
|
||||
|
||||
### 7.8 Exportを独立pipelineにする — P0/P1
|
||||
|
||||
`PNG`を次の2操作へ分ける。
|
||||
|
||||
- Quick snapshot: 現在見えているCanvasを保存。Previewである可能性を明示する。
|
||||
- High-quality export: 1× / 2× / 4× / custom、AA、Balanced / Validated、進捗、取消を指定する。Balancedは未確定sampleをmetadataへ記録して完了可能、Validatedはchecks合格かつunresolved 0だけをfinalとして出す。Certifiedは誤差境界経路を実装した後に追加する。
|
||||
|
||||
High-quality exportでは、固定したViewSpecをtile描画し、全tile完了後にだけencodeする。開始前に最大辺、sample数、予約bytesをpreflightし、予算超過・allocation失敗は解像度を黙って下げず明示失敗する。座標、span、bits、iteration policy、palette、AA、precision tier、backend、kernel hash、SIMD/scalar、色空間、encoder、renderer versionをPNG metadataまたはsidecar JSONへ記録する。同じ条件では規定許容差内で再現し、bit単位再現が必要な場合はscalar mathとencoderまで固定する。
|
||||
|
||||
## 8. 既存仕様の改廃
|
||||
|
||||
| 現行仕様 | 判断 | 新仕様 |
|
||||
|---|---|---|
|
||||
| 自動ズームを削除 | 維持 | 自動航法は戻さず、手動探索を軽く保つ |
|
||||
| 深度だけで解像度を下げない | 趣旨を維持 | Coveredは深度非依存でtarget grid 100%。Previewだけ時間予算で縮小可 |
|
||||
| `q`による固定4段階解像度 | 廃止 | 描画状態をReprojected / Preview / Covered / Refined / Validatedへ変更 |
|
||||
| DPR 2.5、HQ 2100×2200、direct fallback幅390/620 | 置換 | 処理モード別pixel budgetを表示。Covered/Exportは選択したgridをtileで完遂 |
|
||||
| 整数格子pixel座標 | 破壊的変更 | `(x+0.5, y+0.5)`へ統一し、renderer versionとgolden imageを更新 |
|
||||
| RGB勾配で最大36枚だけ2× | 置換 | 数値field/confidence/distanceベースのadaptive quadtree AA |
|
||||
| HQチェック1個 | 分割 | 表示品質、省電力、数値検証を別設定へ |
|
||||
| palette変更で全再計算 | 廃止 | field cacheから即時再着色 |
|
||||
| 固定z=11.5でdeep切替 | 廃止 | pixel step / ULP / orbit errorで切替 |
|
||||
| 起動直後にdeep pool生成 | 廃止 | 数値的必要時に段階生成 |
|
||||
| 深部HQ後、各ページセッションでReal Wisdomを能動測定 | 既定では廃止 | 実jobから受動学習。診断時だけ能動測定 |
|
||||
| detail cache 96件固定 | 廃止 | 管理可能領域のbyte budget LRU |
|
||||
| 表示Canvasの即時PNGだけ | 変更 | Quick snapshot + 独立High-quality export |
|
||||
| Base64 WASM 3ファイル直開き | 互換buildへ | Hosted版を標準、Standalone版を併売 |
|
||||
| SIMD → scalar → JS/BigInt fallback | 維持 | 全backendを同じtile/pixel-center contractへ統一 |
|
||||
| Reference / perturbation / rebase / BLA | 維持・強化 | 局所精度昇格、動的memory、将来multi-reference |
|
||||
| world-space cache | 維持・再設計 | style非依存field cache、quadtree key、byte budget |
|
||||
| base iteration + adaptive slider | 変更 | Auto policyは維持。生値はAdvancedへ移し、未確定数を表示 |
|
||||
| view hash / URL共有 | 強化 | version付きViewSpec、gesture settle保存、戻る/進む復元 |
|
||||
| 「∞ Zoom」表記 | 廃止 | 有限の参照長・iteration・precision上限を持つため「Deep Zoom」とし、現在の上限と未確定数を併記 |
|
||||
| UI初期非表示 | 変更 | 初回だけ操作ヒントを表示し、以後は利用者設定を復元 |
|
||||
|
||||
低優先度の整理対象として、未使用の`MILLION`、`last`、`sx/sy`、未使用`pilot`引数、常にfalseの`strictBoundary`、v22時点で同一内容だったCOLOR SIMD/scalar payloadがある。性能改善というより、仕様とコードのずれを減らすためにテスト後に削除する。実装後の再現buildではCOLOR variantsが異なるため、generatorは同一時だけ自動deduplicateする。
|
||||
|
||||
## 9. UX・再現性の変更
|
||||
|
||||
- 一般statusは「プレビュー / 全域描画 68% / 境界AA中 / 完了 / 未確定あり」とする。Covered等の内部用語は診断drawerだけに出す。
|
||||
- bit数、BLA ε、Worker数、MPPは診断drawerへ移す。
|
||||
- 深部座標表示を17桁固定 (`script.js:114`) にせず、spanから必要桁数を求める。画面上は短縮し、コピー時は正確な値を出す。
|
||||
- URL hashをversion付きにし、見た目を再現するiteration profileも保存する。端末依存の解像度やWorker数は保存しない。
|
||||
- gesture settleごとに`replaceState`、明示shareで`pushState`、`popstate/hashchange`でviewを復元する。座標/spanの正確値入力・コピーとUndo/Redoも同じViewSpecを使う。
|
||||
- `user-scalable=no`を外し、bodyはpage zoom可能にする。Canvasだけに必要なtouch-actionを限定し、アプリのpinch zoomとブラウザpage zoomの範囲を分ける。
|
||||
- rangeとlabelを関連付け、toast/statusへ`aria-live`、UI toggleへ`aria-expanded`、visible focus、44px以上のtarget、`prefers-reduced-motion/transparency`対応を付ける。
|
||||
- 初回は操作ヒントを表示し、2回目以降はUI表示状態を保存する。
|
||||
- 通常利用者には「省電力 / 標準 / 精細」を提示し、生の反復回数はAdvancedへ移す。
|
||||
- 700px以下でも進捗、未確定数、Export状態へアクセスできるcompact status sheetを出す。現行のようにstatsとhintをすべて消さない (`index.html:21`)。
|
||||
|
||||
## 10. 実装ロードマップ
|
||||
|
||||
P0/P1/P2は利用者影響のseverityであり、実装順ではない。各項目はphase、依存先、mandatory / telemetry-conditional、未通過gateを追跡する。後段実装を先行できても、前段gateを通過したとは扱わない。
|
||||
|
||||
### Phase 0A — browser非依存の証拠基準化
|
||||
|
||||
- shallow/deep/colorを含む全kernel sourceを回収または復元し、固定toolchain、compile flags、生成script、payload checksumを揃える。
|
||||
- z0 / z14 / z20 / z100 / period-3 interior / boundary / 10^-280付近をversion付き固定sceneにする。中心座標はspan相応のguard digitsを持つdecimalかexact rationalとし、precision kind、oracle、期待分類、iteration policyをscene schemaへ保存する。
|
||||
- v22成果物が残っている場合は全対象hashとbaselineをimmutableに保存する。残っていない場合は「v22比較不能」と機械可読に記録し、後からv23値で代用しない。
|
||||
- 現行`exactDeepPixel`とは独立した、複数精度一致と誤差上限を持つ高精度reference実装で、分類、iteration、smooth valueを比較する。
|
||||
- source、ABI、pixel mapping、tile seam、SIMD/scalar、解析的内部証明、Module clone、配布物hash、文書内の生成値を同じcommandで検証する。
|
||||
- v18監査はarchiveと明記し、現renderer baselineを別JSONにする。
|
||||
|
||||
Exit gate: 全kernelをsourceから再現buildでき、version付きsceneとoracleで数値baselineを再生成でき、全成果物hashと文書内の生成値が一致する。過去baseline欠落は明示的な`not-verifiable`でありpassにしない。
|
||||
|
||||
### Phase 0B — 固定browser baseline
|
||||
|
||||
- desktop、mobile相当、4Kでcold / warmを測る。
|
||||
- first preview、Covered完了、Refined完了、idle job/draw、peak memory、long taskを記録する。
|
||||
- Hosted / Standalone、SIMD / scalar、Worker / Workerなしを対象構成matrixに従って分けて実行する。
|
||||
- viewportのCSS寸法だけでDPRを模擬しない。mobile / desktop / 4Kは要求DPRを持つ別browser contextまたは別実機runとし、各runの実viewport・実DPR・browser build・OS・電源・試行数を保存する。
|
||||
- visualはraw pixelまたはlinear-light fieldのversion付きgoldenと比較し、hash文字列が生成できただけではpassにしない。
|
||||
|
||||
Exit gate: 対象profileごとのbrowser acceptanceが合格し、cold / warm、操作p95、visual、idle、long task、observed memory、Export、accessibilityのraw結果を保存する。binding不在などで未実施の場合、後続実装は進められるがbrowser性能・操作・表示の達成を主張しない。
|
||||
|
||||
### Phase 1A — Runtime quick wins
|
||||
|
||||
- event-driven paint/stats
|
||||
- wheel gesture settle
|
||||
- `visibilitychange`停止
|
||||
- deep pool遅延生成
|
||||
- Screen Canvas pixel budget
|
||||
- 初回`q=.3`境界の修正と、既存`q`のままPreview時間予算へ`renderPerf`を接続
|
||||
- Quick snapshotに品質状態を表示し、描画中の誤解を防止
|
||||
- 固定件数cacheをbyte budgetへ置換
|
||||
|
||||
Exit gate: idle時に予約RAF/timer/background job 0、wheel中full render 0、浅部だけではdeep compile 0、Canvas/管理可能memoryが予算内。
|
||||
|
||||
### Phase 1B — Pixel contract
|
||||
|
||||
- 全kernelを画素中心規約へ変更し、cross-backend testを追加する。
|
||||
- renderer/golden image versionを上げる。
|
||||
- detail BLAをbase HQ以上へ修正する。
|
||||
|
||||
Exit gate: canonical rationalに対するfixed-point整数一致とf64の宣言ULP基準、tile境界回帰0、既存URLのview中心不変を確認する。
|
||||
|
||||
### Phase 2 — Tile/field pipeline
|
||||
|
||||
- packed field形式、subsample形式、論理byte budgetを先に固定する。
|
||||
- buffer poolを導入し、field追加による一時的なpeak memory悪化を防ぐ。
|
||||
- shallowもWorker化し、全backendを共通tile protocolへする。
|
||||
- Reprojected / Preview / Covered / Refined状態機械を導入する。
|
||||
- palette非依存field bufferと即時再着色を導入する。
|
||||
- 処理モード別sample coverage 100%とresolved coverage/未確定数の分離を実装する。
|
||||
- ULPベースのshallow/deep切替を導入する。
|
||||
- `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を導入し、局所反復継続を実装する。
|
||||
|
||||
Exit gate: sample coverage 100%、再投影pixelの誤加算0、未確定数の独立計上、false `INTERIOR_PROVEN` 0、logical memory budget内。
|
||||
|
||||
### Phase 3A — Refine / Export
|
||||
|
||||
- Hosted / Standalone buildを分ける。
|
||||
- 独立High-quality export、進捗、取消、metadataを実装する。
|
||||
- adaptive AAとlinear-light resolveを導入する。
|
||||
|
||||
Exit gate: 指定寸法、完了待ち、取消、metadata、許容差内の再現性、Refinedのsample accountingが合格する。
|
||||
|
||||
### Phase 3B — 計測で必要ならWASM memory再編
|
||||
|
||||
- deep/BLA/color WASM統合と動的初期容量
|
||||
- immutable reference / key別BLA table共有、Worker別scratch
|
||||
- shared framebufferまたはbuffer poolによるcopy削減
|
||||
- 大容量instanceの退役・再生成
|
||||
|
||||
Exit gate: 対象deep sceneで速度改善を確認し、data race 0、logical/observed peak memoryが予算内。
|
||||
|
||||
### Phase 4 — 計測後の高精度研究
|
||||
|
||||
- reference orbit専用Workerと次referenceの先読み
|
||||
- double-double局所昇格
|
||||
- error ball / interval verification
|
||||
- scaled BLA
|
||||
- multi-reference perturbation
|
||||
- WebGPUによるcolor/resolve/composition、必要なら浅部Preview演算
|
||||
|
||||
Phase 4は、telemetryで支配コストまたは誤差が確認された項目だけ実施する。
|
||||
|
||||
## 11. 受入基準
|
||||
|
||||
browser時間値は、profileごとに固定したbrowser build・OS・viewport・実DPR・電源条件で、warmup後30回以上を採取してp95を算出する。単発値、同期event handler時間、異なるsceneを混ぜた分位点は代用しない。Visual golden、数値oracle、threshold policyには独立したversion IDを与え、threshold欠落時はpassにしない。
|
||||
|
||||
| 分野 | 基準案 |
|
||||
|---|---|
|
||||
| Idle | 最終更新2秒後に予約RAF/timer/background job 0、Canvas/DOM write 0回/秒。CPUは同環境blank baselineとの差を補助指標にする |
|
||||
| 操作 | 最後の実入力eventから、その入力を反映した再投影frameのpaint完了までのp95 < 16ms、計測区間のmain thread 50ms超long task 0、継続wheel中に新規full renderを開始しない |
|
||||
| Preview | settle期限到達からPreview paint完了まで、profile・sceneごとの30回以上でp95 < 120ms |
|
||||
| Covered | target Screen Canvasのsample coverage 100%、effective DPRを表示し、再投影pixelをsample coverageへ数えない。resolved coverageとunresolved数を別記録。scene別時間budgetはPhase 0B後に確定 |
|
||||
| Refined | Fine modeで実際に`REFINED`へ到達し、対象pixelは記録済み実sampleのlinear-light resolve。subsample accountingとversion付きgoldenを比較し、Canvas補間だけをAAと数えない |
|
||||
| 数値 | version付きcorpus/oracle/threshold policyを必須とする。Validatedではcorpus上のfalse `ESCAPED` 0、false `INTERIOR_PROVEN` 0、unresolved 0。escape iteration差・smooth誤差・`INTERIOR_LIKELY` mismatchはpolicyの明示閾値で判定し、閾値未定義ならfail |
|
||||
| Pixel mapping | canonical rationalとties-away-from-zeroの`roundDiv`を基準に、fixed-point backendは期待整数と一致、f64 backendは宣言ULP以内。tile境界に1px/半pxの回帰がない |
|
||||
| Export | 指定寸法どおり、全tile完了前はfinal fileを出さない、取消可能、metadata必須項目とsample countを検査。Balancedはunresolved数を記録、Validatedは0。宣言したbackend/encoder条件とversion付きgoldenの許容差内で再現 |
|
||||
| Memory | managed typed array/WASM/cacheは暫定mobile 96 MiB、desktop 192 MiB以内。Canvas/GPUを含むobserved peakは固定環境で別記録 |
|
||||
| Startup | Hosted shallow表示ではdeep取得/compile 0。Standaloneはdeep復号/compileとWorker生成0 |
|
||||
| 配布 | v22 baseline欠落のため初回比較は`not-verifiable`とする。v23の同一request集合・Content-Encoding・cache条件を新baselineとして固定し、以後の取得bytes・compile timeを回帰判定する。Hostedではdeep到達前のdeep request 0 |
|
||||
| Accessibility | keyboard-onlyで全操作を実行し、visible focus、44px target、label/name、live status、page zoomとCanvas pinchの共存、reduced motion/transparencyの実動作が合格。静的属性の存在だけではpassにしない |
|
||||
| Regression | version付き現行scene corpusをCIで実行し、速度・memory・分類・例外を履歴化する。各metricのwarn/fail閾値をpolicyへ保存する |
|
||||
|
||||
端末依存の時間値は、対象端末を固定して初めて合否に使う。絶対時間だけでなく、同一scene・同一pixel数の比率とcorrectnessを主要指標にする。browser非依存gateはsource/ABI/hash/数値oracle/pixel mapping/scene精度/document consistencyまでを証明し、paint、DPR、GPU、入力、download、実accessibilityの代替とはみなさない。
|
||||
|
||||
## 12. 推奨する最初の実装単位
|
||||
|
||||
最初の変更セットは、次の範囲に限定すると効果を測りやすい。
|
||||
|
||||
1. v22 baselineの有無を機械可読に確定し、現renderer baselineと全WASMの再現buildを用意する。
|
||||
2. 常時RAFと毎frame DOM更新を廃止する。
|
||||
3. wheelをgestureとしてデバウンスし、非表示時のjobを止める。
|
||||
4. Screen Canvasへpixel budgetを設ける。
|
||||
5. deep poolを必要時まで作らない。
|
||||
6. 既存`q`のままPreview時間予算へ`renderPerf`を接続する。
|
||||
7. version付き現renderer用のidle・gesture・startup・memory回帰テストを追加する。
|
||||
|
||||
この段階ではBLAカーネルやpixel contractをまだ変えない。まずidle負荷、入力応答、Canvas/初期memory、Preview時間を改善する。画素中心変更は、全kernelを再現buildできるPhase 1Bで行う。
|
||||
|
||||
## 付録A: 根拠データ
|
||||
|
||||
- 現行仕様: `README.md`
|
||||
- 数値・性能監査: `AUDIT.md`
|
||||
- end-to-end記録: `audit/final-browser-benchmark.json`, `audit/benchmarks.json`
|
||||
- UI/スケジューラ/renderer: `index.html`, `script.js`
|
||||
- BLA固定配列・演算: `src/bla_kernel_v18.c`
|
||||
|
||||
既存監査から今後も守るべき知見は次の通り。
|
||||
|
||||
- 数学的iterationを途中で切って黒扱いしない。
|
||||
- pilot画像を未解決pixelの計算結果として使わない。
|
||||
- 全stripを毎回strict検証しない。
|
||||
- 1点の不一致で全画面を再計算せず、tile/strip単位で補修する。
|
||||
- ditherはdetail検出前へ入れず、最終表示段だけにする。
|
||||
- Reference recenterは固定距離でなく、実測損失とbuild費を比較する。
|
||||
|
||||
## 付録B: 用語
|
||||
|
||||
| 用語 | 本文での意味 |
|
||||
|---|---|
|
||||
| BLA | Bilinear Approximation。基準軌道上の複数反復を近似的にまとめて進める高速化 |
|
||||
| perturbation | 高精度な基準軌道との差分だけを低精度で追跡する摂動法 |
|
||||
| rebase | 差分が不安定になったとき、基準軌道上の別位置へ基準を移す処理 |
|
||||
| DPR / effective DPR | OS/browserが示すdevice pixel比 / pixel budget適用後に実際に使う比率 |
|
||||
| sample / resolved coverage | 選択したScreen Canvas gridのうち、新ViewSpecでpolicy上限まで実計算済みの割合 / `ESCAPED`またはpolicy上の`INTERIOR_PROVEN`へ確定した割合 |
|
||||
| AA | Anti-Aliasing。1 pixel内の複数sampleをresolveして境界のジャギーを減らす処理 |
|
||||
| SIMD | 1命令で複数データを処理するWASM最適化 |
|
||||
| ULP | その浮動小数値付近で表現できる隣接値の間隔 |
|
||||
| EMA / MPP | 指数移動平均 / millisecond per pixel。時間予算の推定に使う |
|
||||
| cold / warm | 初回compile・cache未構築 / Worker・reference・BLA cache構築後 |
|
||||
| confidence | 近似・検証・内部判定の根拠と確からしさを表すmetadata |
|
||||
| Validated / Certified | 宣言した有限policyを照合済み / 全sampleへ誤差境界または証明手続きを適用済み |
|
||||
|
||||
## 付録C: 互換構成
|
||||
|
||||
| 構成 | 動作方針 | 主な制限 |
|
||||
|---|---|---|
|
||||
| Hosted + SIMD + Worker | 推奨構成。外部WASM、遅延取得、並列tile | Shared memoryは別途COOP/COEPが必要 |
|
||||
| Hosted、SIMDなし | scalar WASMを必要時に取得 | 速度低下。数値policyは同一 |
|
||||
| SharedArrayBufferなし | Transferable buffer poolと短いchunkを使用 | hot loop中のatomic cancel、read-only table共有なし |
|
||||
| Workerなし | main-thread WASMを時間sliceして使用 | 並列なし。Covered/Exportの完了が遅い |
|
||||
| OffscreenCanvasなし | Workerはfield/RGBA bufferを返し、main Canvasへ合成 | Export encodeと合成の一部がmain thread |
|
||||
| Standalone / `file://` | 埋込WASM互換build。deepの復号・compileは遅延 | streaming、HTTP cache、COOP/COEP、Service Workerなし |
|
||||
|
||||
最低対応browser versionとoffline cache方針は、Phase 0Bで実機matrixを作って確定する。
|
||||
157
MIGRATION_V24.md
157
MIGRATION_V24.md
|
|
@ -1,157 +0,0 @@
|
|||
# v23 → v24.2.3 WebGPU migration / crossover study
|
||||
|
||||
## 目的
|
||||
|
||||
v23のCPU/WASM deep pixel rendererとWorker poolを廃止し、画素単位のmassively parallel処理をWebGPUへ集約する。CPU高精度処理はBigInt ViewStateとreference orbit生成に限定する。
|
||||
|
||||
## 最終構成
|
||||
|
||||
```text
|
||||
BigInt ViewState
|
||||
│
|
||||
├─ Direct安全域 ─────────────── WebGPU f32 direct
|
||||
│
|
||||
└─ Deep/overlap
|
||||
│
|
||||
├─ overlap: current-view GPU quality/speed probe
|
||||
│ ├─ direct比較
|
||||
│ └─ corrected deep比較
|
||||
│
|
||||
└─ deep: BigInt reference Worker
|
||||
↓
|
||||
guarded rescaled f32 perturbation
|
||||
↓
|
||||
UNKNOWN?
|
||||
/ \
|
||||
no yes
|
||||
│ ↓
|
||||
│ double-single correction
|
||||
└────────────┘
|
||||
↓
|
||||
GPU field
|
||||
↓
|
||||
color / AA / present
|
||||
```
|
||||
|
||||
## v24.1系からの品質修正
|
||||
|
||||
v24.1のguarded f32 perturbationは誤分類を避けるためUNKNOWNへ保守退避する設計だった。境界sceneではUNKNOWNが大きく、interiorに近い暗色表示と相まって黒面積増大/黒点として見えた。
|
||||
|
||||
### Multi-reference評価
|
||||
|
||||
最初にUNKNOWN macro tileへsecondary BigInt referenceを追加する方式をモデル化した。しかし複数referenceを追加しても`swirly-seahorses-z12`全画面モデルのUNKNOWN回収率が低く、reference生成・転送・再dispatchのコストに見合わなかった。そのためproductionへは採用しない。
|
||||
|
||||
### Sparse double-single correction
|
||||
|
||||
代わりにUNKNOWNだけをdouble-single complex perturbationで再評価する。reference orbitはprimaryと共用するため追加BigInt orbitは不要。
|
||||
|
||||
61×39 dense model / 2000 iter:
|
||||
|
||||
- primary UNKNOWN 478
|
||||
- primary membership reversal 0
|
||||
- correction recovered 478
|
||||
- remaining UNKNOWN 0
|
||||
- correction membership reversal 0
|
||||
- escape iteration mismatch 5(最大374)
|
||||
|
||||
したがってcorrectionは表示品質passとして採用し、membership certificateには使わない。
|
||||
|
||||
## Backend router再設計
|
||||
|
||||
旧設計のzoom/ULP単一閾値はdeepを早く使いすぎる一方、閾値を下げるだけではdirectの境界誤分類リスクがある。
|
||||
|
||||
v24.2.1は三領域に分ける。
|
||||
|
||||
- ratio < 8: deep必須
|
||||
- ratio >= 128: direct
|
||||
- 8 <= ratio < 128: overlap
|
||||
|
||||
Overlapは本描画前にcurrent-view probeを行う。probeは中心のspeed tileと、画面5領域のquality tileをactual full-resolution pixel座標で評価する。directとDS補正済みdeepのescape/bounded分類差が0、補正後deep UNKNOWN率0.5%以下であることを要求する。そのうえでreference buildを含むdeep予測時間がdirectの85%未満ならdeep、それ以外はdirect。
|
||||
|
||||
深度binの過去profileだけでbackendを決定しない。
|
||||
|
||||
## BLAを戻さない理由
|
||||
|
||||
f64 BLAをf32係数へ量子化した旧試作は、dense swirly回帰でbounded/escaped membership反転を起こした。量子化誤差を含む安全な受入半径が未確立のため、BLA builder/evaluator/nodeはproduction treeに存在しない。
|
||||
|
||||
## Export
|
||||
|
||||
v24.2.1ではscreenだけでなくExportにもdeep correctionを適用する。
|
||||
|
||||
1×:
|
||||
|
||||
```text
|
||||
primary deep → clear stats → DS correction → color → 1 readback
|
||||
```
|
||||
|
||||
2×2:
|
||||
|
||||
```text
|
||||
4 samples × (primary deep → DS correction → color)
|
||||
→ GPU linear-light resolve
|
||||
→ 1 readback / tile
|
||||
```
|
||||
|
||||
4 sampleのunresolved counterを分離し、sidecarの`unresolvedSamples`へ合計する。
|
||||
|
||||
## 削除済み
|
||||
|
||||
- deep WASM renderer / Worker pool
|
||||
- BLA WASM / GPU BLA試作
|
||||
- BigInt direct production pixel renderer
|
||||
- multi-reference production試作
|
||||
- redundant strict retry queue
|
||||
- Canvas2D deep hot path
|
||||
- giant full-image Canvas Export
|
||||
- Validated direct UI/Export
|
||||
|
||||
## Validation contract
|
||||
|
||||
productionは`membershipCertified:false`。Strictはprimary error-bound閾値を厳しくするpolicyで、任意精度direct証明ではない。DS correctionもvisual-quality recovery passである。
|
||||
|
||||
|
||||
## v24.2.3 crossover study
|
||||
|
||||
BigInt Deepを廃止するのではなく、使用開始点を後ろへ送れるか検証するためDS Direct候補を追加した。DSはreference構築が不要なためcold latency削減の可能性があるが、WGSL double-singleのコストと品質はGPU依存である。したがってこのreleaseではbenchmark専用とし、実GPU結果を得てからrouterへの昇格を判断する。
|
||||
|
||||
## v24.2.4 diagnostic Hybrid tier
|
||||
|
||||
Crossover実機値でDS Directはz10/z12でもFull Deepより高速だったが、単独では分類品質が不足した。そこでproduction routerを変更する前に、DS Directのcondition proxy `|dz/dc|` を追跡し、危険pixelだけ既存Deep double-single correctionへ送るHybridをbenchmark専用で実装した。
|
||||
|
||||
候補routerはまだ採用しない。実験で `class mismatch=0`, `remaining UNKNOWN=0`, `Hybrid cold < 0.9 × Full Deep cold`, `correction rate < 60%` を満たすviewだけを将来のHybrid overlap候補とする。z12のCPU事前モデルではrisk=1e13で約91%が補正対象となるため、Full Deepへ切り替える境界を検出できる見込み。
|
||||
|
||||
|
||||
## v24.2.5 Sparse Direct experiment (Migration)
|
||||
|
||||
- The previous sparse-Full-Deep diagnostic is superseded and is not exposed in the UI.
|
||||
- Added `DIRECT_TS_CORRECT_WGSL`, a reference-free three-term f32 expansion correction shader that only processes DS sensitivity UNKNOWN pixels.
|
||||
- Added `benchmarkSparseDirect()` / `renderTileMetaSparseDirect()` and a z6/z8/z10/z12 risk sweep.
|
||||
- Full Deep remains the quality/timing baseline; the Sparse Direct candidate itself does not build or bind a reference orbit.
|
||||
- Production backend routing is unchanged.
|
||||
- Root `index.html` is now a single-file bundle with no external script dependency, specifically to keep diagnostic UI controls functional when the HTML is opened from an archive/temp extraction.
|
||||
- Static single-file UI contract and real-browser click smoke are separate gates; real WebGPU arithmetic still requires a WebGPU-capable browser.
|
||||
|
||||
- Sparse Direct coordinate packing uses exact residual subtraction from BigInt fixed-point into three f32 components; this removed the CPU TD-model class errors caused by the earlier host-Number split.
|
||||
|
||||
## v24.2.6 experiment direction
|
||||
|
||||
The float-expansion sparse correction branch is superseded by an integer fixed-point experiment. The transition strategy under test is now:
|
||||
|
||||
```text
|
||||
f32 Direct
|
||||
-> DS Direct
|
||||
-> DS risk gate + sparse Q8.88 Direct
|
||||
-> Full Deep
|
||||
```
|
||||
|
||||
Only the diagnostic branch is changed. Production remains on the existing direct/deep router until real-GPU timing and class gates justify promotion.
|
||||
|
||||
|
||||
## v24.2.7 experiment correction
|
||||
|
||||
Do not interpret v24.2.6 Fixed96 `remaining-unknown` measurements as a Fixed96 feasibility result. They were dominated by an invalid Q8.88 range guard. v24.2.7 is the first build in this branch whose real-GPU Fixed96 correction measurements are suitable for the crossover experiment.
|
||||
|
||||
|
||||
## v24.2.8 experiment scheduling correction
|
||||
|
||||
Fixed96 remains diagnostic and is not part of the production backend router. The correction execution changed from full-tile 2D dispatch with early returns to GPU compact-queue + indirect 1D dispatch. This isolates the experiment's arithmetic feasibility from an avoidable SIMD divergence/scheduling penalty. Adoption decisions must use v24.2.8-or-later timings; v24.2.7 Fixed96 timings are not representative of a sparse implementation.
|
||||
136
README.md
136
README.md
|
|
@ -1,136 +0,0 @@
|
|||
# Mandelbrot Deep Zoom v24.2.8 — Fixed96 True Sparse Queue Experiment
|
||||
|
||||
v24.2.8は、v24.2.7で数値バグを修正したFixed96実験を**真のsparse GPU実行**へ変更した診断版です。DS guarded passが危険pixel indexをGPU queueへcompactし、1-thread finalize passがqueue件数からindirect dispatchを生成、Fixed96 Q8.88 kernelはqueueに入ったpixelだけを処理します。guard→queue→indirect Fixed96の間にCPU readbackはありません。production routerはまだ変更していません。
|
||||
|
||||
## 数値backend
|
||||
|
||||
- **Direct**: WebGPU `f32` direct iteration。
|
||||
- **Deep primary**: BigInt reference orbit + WebGPU guarded rescaled `f32` perturbation。
|
||||
- **Deep correction**: primaryで`FIELD_UNKNOWN`になったpixelだけ、同じreferenceを用いるdouble-single perturbationで再評価。
|
||||
- **BLA**: productionでは未搭載。旧f32量子化BLAでmembership反転を再現したため再導入していません。
|
||||
- **Multi-reference**: v24.2開発中に全画面モデルを評価したが、追加referenceの生成/dispatchコストに対してUNKNOWN回収が小さかったためproductionから撤去しました。
|
||||
|
||||
Deep primaryはroundoff上界を追跡し、escape/bounded判定を安全に受理できないpixelを`UNKNOWN`にします。double-single correctionは表示品質回復用であり、任意精度membership証明ではありません。PNG sidecarの`membershipCertified`は常に`false`です。
|
||||
|
||||
## Direct / Deep router
|
||||
|
||||
単一のzoom閾値でdeepへ切り替えません。
|
||||
|
||||
1. `pixel step / f32 ULP < 8`ならdirect座標分解能が不足するためdeep必須。
|
||||
2. 比率が128以上ならdirectを即採用。
|
||||
3. 8以上128未満のoverlap帯では、本描画前に**現在のview専用GPU probe**を実行。
|
||||
4. probeではactual full-resolution座標上の複数領域をdirectと「DS補正済みdeep」で比較。
|
||||
5. deep側UNKNOWN率が0.5%以下、escape/bounded分類差0を満たした場合だけ速度比較へ進む。
|
||||
6. reference生成時間を含めてもdeepがdirectより15%以上高速な場合だけdeepを採用。それ以外はdirect。
|
||||
|
||||
過去の同深度profileだけで現在viewをdeepへ振ることはありません。profileは診断/平滑化用で、backend選択には現在viewのquality probeを要求します。
|
||||
|
||||
|
||||
## Crossover benchmark
|
||||
|
||||
診断情報の **Backend比較** から、現在のGPUで z0 / 2 / 4 / 6 / 8 / 10 / 12 / 14 / 16 / 20 / 30 / 40 を自動測定できます。
|
||||
|
||||
速度測定は全画面を反復実行しません。現在のview座標上の代表3タイル(最大256×144)を測定して `ms/pixel` を求め、現在のcanvas全体へ換算します。品質比較は別の5領域で実施します。これによりz12以降で全画面Deepを何度も実行してGPU queueが長時間塞がる問題を避けます。reference生成または1回のGPU測定が15秒を超えた場合は明示的に中断します。
|
||||
|
||||
比較するbackend:
|
||||
|
||||
- `f32`: 現行WebGPU標準。
|
||||
- `DS`: experimental whole-frame double-single Direct。**production routerには未接続**。
|
||||
- `Deep warm`: BigInt referenceをcache済みとしたDeep primary + DS correction。
|
||||
- `Deep cold`: verified BigInt reference構築 + Deep warm。
|
||||
|
||||
結果の時間欄は代表tileからの**全画面換算値**です。画面表示に加えJSON保存できます。品質のBigInt oracle掃引結果は `CROSSOVER_BENCHMARK.md` と `audit/v24-crossover-quality.json` を参照してください。CPUモデルではSeahorse trackでf32がz6付近、DS Directがz10付近からmaterial degradationを示し、DSを中間backendとして試す価値があるという結果です。
|
||||
|
||||
## Fixed96 Direct experiment
|
||||
|
||||
診断情報の **Fixed96 Direct実験** から z6 / z8 / z10 / z12 を測定できます。DS Directで `|dz/dc|` を追跡し、`1e11 / 1e12 / 1e13 / 1e14` の各risk thresholdを超えたpixelだけ、reference不要の**96-bit符号付きQ8.88固定小数点Direct**で再評価します。
|
||||
|
||||
v24.2.8では補正対象を`u32` pixel-index queueへcompactします。queue lengthから`dispatchWorkgroupsIndirect()`用の1D workgroup数をGPU内で生成し、Fixed96は**queued pixel数だけ**実行します。従来の「全画面dispatchして非対象laneをreturn」は廃止しました。
|
||||
|
||||
実験システムは各GPU sampleで次をfail-closed検証します。
|
||||
|
||||
- `selected == enqueued == dispatchCount == processed`
|
||||
- queue overflow = 0
|
||||
- indirect workgroup数 = `ceil(enqueued / 64)`
|
||||
- invalid/stale queue entry = 0
|
||||
- `corrected + remain == processed`
|
||||
|
||||
1項目でも崩れれば測定値を採用せず実験をエラー停止します。結果表には`correction`と実際の`dispatch`率を別々に表示し、`queue=ok`を確認できます。速度は代表3tileから全画面換算し、品質は5 spatial tilesでcorrected Full Deepと比較します。production routerへは未接続です。
|
||||
|
||||
### 単一ファイル起動
|
||||
|
||||
rootの `index.html` は `gpu-kernels.js` と `script.js` を内包した単一HTMLです。ZIP内のHTMLをOSが一時展開して開く場合でも、隣接JSファイルに依存しません。
|
||||
|
||||
## Deep correction
|
||||
|
||||
primary deepでUNKNOWNが残った場合、通常/精細/Strictではsparse double-single correctionを1回実行します。KNOWN pixelはshader冒頭で即returnするため、数値反復はUNKNOWNだけです。省電力モードはUNKNOWNが一定量を超えた場合のみ補正します。
|
||||
|
||||
`swirly-seahorses-z12`の61×39 CPU `Math.fround`モデルでは:
|
||||
|
||||
- primary UNKNOWN: 478 / 2379
|
||||
- primary false escape: 0
|
||||
- primary false bounded: 0
|
||||
- DS correction回収: 478 / 478
|
||||
- correction後UNKNOWN: 0
|
||||
- correction false escape / false bounded: 0 / 0
|
||||
|
||||
escape iterationは5 pixelでBigInt oracleと差があり、最大差374 iterationでした。このためDS correctionは「見た目と有限iterationの分類回復」であり、Validated相当の保証には使用しません。
|
||||
|
||||
## UNKNOWN診断
|
||||
|
||||
内部metadataではUNKNOWN理由を分離しています。
|
||||
|
||||
- error-bound
|
||||
- escape-uncertain
|
||||
- reference-end
|
||||
- rebase-gap
|
||||
- range
|
||||
- operation-limit
|
||||
|
||||
未補正UNKNOWNはinteriorの完全な黒と別色で表示され、診断欄に理由別件数が出ます。
|
||||
|
||||
## Export
|
||||
|
||||
- 512×512 reusable GPU workspace。
|
||||
- deep Exportでもprimary後にDS correctionを実行。
|
||||
- 2×2 AAは各4 sampleをGPUで数値計算→補正→彩色し、GPU resolve後に1回だけreadback。
|
||||
- 最終画像全体の巨大Canvasは作らず、scanline bandを`CompressionStream('deflate')`へ流すstreaming PNG。
|
||||
- 16384px級の片辺をtile処理可能。
|
||||
- sidecarに`unresolvedSamples`を記録。
|
||||
|
||||
## Fallback
|
||||
|
||||
WebGPUそのものが利用できない場合だけ軽量JavaScript f64 fallbackを使います。deep zoomはNumber underflow等で誤画像を出さず、「このズーム深度はWebGPUが必要です」と表示して停止します。shader/pipeline failureではCPU全画面rendererへ自動fallbackしません。
|
||||
|
||||
## Build / test
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
`npm test`には以下を含みます。
|
||||
|
||||
- WGSL reserved-word scan(9 shader。DS Direct / guarded DS / Fixed96 Direct correctionは実験専用)
|
||||
- production source/tree contract
|
||||
- BigInt reference Worker + P/P+64 checkpoint
|
||||
- f32 direct CPU model
|
||||
- view固有adaptive backend router policy
|
||||
- guarded primary deep CPU model
|
||||
- sparse double-single correction CPU model
|
||||
- BLA disabled contract
|
||||
- pixel/export geometry
|
||||
- e400/e1000 exact coordinate formatting
|
||||
- streaming PNG CRC/deflate/abort
|
||||
- real-WebGPU acceptance harnessの静的contract
|
||||
|
||||
実GPU受入は `tests/webgpu-acceptance.html` をWebGPU対応ブラウザで開いて実行します。これはdirect/deep/DS/Fixed96 Direct shader compile、DS correction、Fixed96 smoke、dense既知反例、view-specific router、1×/2×2 Export readback、uncaptured WebGPU errorを検査します。
|
||||
|
||||
## 配布
|
||||
|
||||
`npm run build`で以下を生成します。
|
||||
|
||||
- `dist/standalone/`
|
||||
- `dist/hosted/`
|
||||
|
||||
`dist/standalone/` は単一 `index.html`、`dist/hosted/` は `index.html`, `gpu-kernels.js`, `script.js` です。root `index.html` も単一ファイルで、ZIPから展開してそのまま開けます。
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
{
|
||||
"environment_note": "Absolute timings are container/Chromium specific. Use ratios and correctness rates as the main evidence.",
|
||||
"end_to_end_dynamic_base_comparison": [
|
||||
{
|
||||
"scene": "seahorse_z14",
|
||||
"v17_base_ms": 1059.0,
|
||||
"v18_base_ms": 288.0,
|
||||
"speedup_x": 3.677
|
||||
},
|
||||
{
|
||||
"scene": "seahorse_z20",
|
||||
"v17_base_ms": 1226.0,
|
||||
"v18_base_ms": 238.0,
|
||||
"speedup_x": 5.151
|
||||
},
|
||||
{
|
||||
"scene": "seahorse_z100",
|
||||
"v17_base_ms": 983.0,
|
||||
"v18_base_ms": 474.0,
|
||||
"speedup_x": 2.074
|
||||
},
|
||||
{
|
||||
"scene": "period3_z20",
|
||||
"v17_base_ms": 1931.0,
|
||||
"v18_base_ms": 781.0,
|
||||
"speedup_x": 2.472
|
||||
}
|
||||
],
|
||||
"v17_work_cap_correctness": {
|
||||
"w": 48,
|
||||
"h": 30,
|
||||
"pixels": 1440,
|
||||
"globalIter": 3490,
|
||||
"capIter": 2870,
|
||||
"capped": 1440,
|
||||
"late": 1066,
|
||||
"insideGlobal": 374,
|
||||
"lateEscapeRateAmongCapped": 0.7402777777777778,
|
||||
"elapsedMs": 6344.968723999999
|
||||
},
|
||||
"pilot_nearest_fill_accuracy": {
|
||||
"full": [
|
||||
128,
|
||||
78
|
||||
],
|
||||
"pilot": [
|
||||
56,
|
||||
34
|
||||
],
|
||||
"fullMs": 1356.671812,
|
||||
"pilotMs": 169.93951399999992,
|
||||
"classMismatch": 451,
|
||||
"classRate": 0.04517227564102564,
|
||||
"escapeIterDiffGt8": 4364,
|
||||
"diffRate": 0.437099358974359
|
||||
},
|
||||
"intrinsic_interior_detection": [
|
||||
{
|
||||
"name": "period3 wide",
|
||||
"re": "-0.1225611668766536",
|
||||
"im": "0.7448617666197442",
|
||||
"span": 0.1,
|
||||
"offMs": 766.7149390000001,
|
||||
"onMs": 5.955513999999994,
|
||||
"interior": 6912,
|
||||
"classMismatch": 0,
|
||||
"diffGt1": 0,
|
||||
"total": 6912
|
||||
},
|
||||
{
|
||||
"name": "period3 mid",
|
||||
"re": "-0.1225611668766536",
|
||||
"im": "0.7448617666197442",
|
||||
"span": 0.03,
|
||||
"offMs": 438.65803500000015,
|
||||
"onMs": 2.334043000000065,
|
||||
"interior": 6912,
|
||||
"classMismatch": 0,
|
||||
"diffGt1": 0,
|
||||
"total": 6912
|
||||
},
|
||||
{
|
||||
"name": "period2 boundary neighborhood",
|
||||
"re": "-0.75",
|
||||
"im": "0",
|
||||
"span": 0.1,
|
||||
"offMs": 364.72781799999984,
|
||||
"onMs": 1.0177550000000792,
|
||||
"interior": 6806,
|
||||
"classMismatch": 0,
|
||||
"diffGt1": 0,
|
||||
"total": 6912
|
||||
},
|
||||
{
|
||||
"name": "mixed exterior/interior",
|
||||
"re": "-0.5",
|
||||
"im": "0.5",
|
||||
"span": 0.1,
|
||||
"offMs": 282.849144,
|
||||
"onMs": 16.219244000000117,
|
||||
"interior": 5338,
|
||||
"classMismatch": 0,
|
||||
"diffGt1": 0,
|
||||
"total": 6912
|
||||
},
|
||||
{
|
||||
"name": "cardioid cusp neighborhood",
|
||||
"re": "0.25",
|
||||
"im": "0",
|
||||
"span": 0.04,
|
||||
"offMs": 329.88844100000006,
|
||||
"onMs": 1.7636029999998755,
|
||||
"interior": 6403,
|
||||
"classMismatch": 0,
|
||||
"diffGt1": 0,
|
||||
"total": 6912
|
||||
}
|
||||
],
|
||||
"reference_recenter_z14": {
|
||||
"oldRefBuildMs": 29.320617,
|
||||
"tests": [
|
||||
{
|
||||
"sx": 0.5,
|
||||
"sy": 0,
|
||||
"oldMs": 691.804547,
|
||||
"newMs": 640.2659849999995,
|
||||
"newRefBuildMs": 9.508285999999998,
|
||||
"firstFrameNetSavingMs": 42.03027600000041,
|
||||
"steadySavingMs": 51.53856200000041
|
||||
},
|
||||
{
|
||||
"sx": 0.8,
|
||||
"sy": 0,
|
||||
"oldMs": 756.002915,
|
||||
"newMs": 655.9519419999997,
|
||||
"newRefBuildMs": 8.81105800000023,
|
||||
"firstFrameNetSavingMs": 91.23991500000011,
|
||||
"steadySavingMs": 100.05097300000034
|
||||
},
|
||||
{
|
||||
"sx": 0,
|
||||
"sy": 0.8,
|
||||
"oldMs": 730.6417070000007,
|
||||
"newMs": 671.6106240000008,
|
||||
"newRefBuildMs": 8.391966000001048,
|
||||
"firstFrameNetSavingMs": 50.639116999998805,
|
||||
"steadySavingMs": 59.03108299999985
|
||||
},
|
||||
{
|
||||
"sx": 0.8,
|
||||
"sy": 0.5,
|
||||
"oldMs": 787.9931480000014,
|
||||
"newMs": 740.047842,
|
||||
"newRefBuildMs": 7.513370000000577,
|
||||
"firstFrameNetSavingMs": 40.43193600000086,
|
||||
"steadySavingMs": 47.94530600000144
|
||||
},
|
||||
{
|
||||
"sx": 1.2,
|
||||
"sy": 0,
|
||||
"oldMs": 705.7068249999975,
|
||||
"newMs": 689.7542820000017,
|
||||
"newRefBuildMs": 7.101795999999013,
|
||||
"firstFrameNetSavingMs": 8.850746999996773,
|
||||
"steadySavingMs": 15.952542999995785
|
||||
},
|
||||
{
|
||||
"sx": 1.5,
|
||||
"sy": 0.8,
|
||||
"oldMs": 793.3721589999986,
|
||||
"newMs": 733.093237000001,
|
||||
"newRefBuildMs": 7.395623000000342,
|
||||
"firstFrameNetSavingMs": 52.88329899999735,
|
||||
"steadySavingMs": 60.27892199999769
|
||||
}
|
||||
]
|
||||
},
|
||||
"reference_recenter_z20": {
|
||||
"oldRefBuildMs": 23.925890999999993,
|
||||
"tests": [
|
||||
{
|
||||
"sx": 0.5,
|
||||
"sy": 0,
|
||||
"oldMs": 100.18383699999998,
|
||||
"newMs": 105.04222300000004,
|
||||
"newRefBuildMs": 9.637189000000006,
|
||||
"firstFrameNetSavingMs": -14.49557500000006,
|
||||
"steadySavingMs": -4.858386000000053
|
||||
},
|
||||
{
|
||||
"sx": 0.8,
|
||||
"sy": 0,
|
||||
"oldMs": 106.89347099999986,
|
||||
"newMs": 93.24754299999995,
|
||||
"newRefBuildMs": 8.222010999999952,
|
||||
"firstFrameNetSavingMs": 5.42391699999996,
|
||||
"steadySavingMs": 13.645927999999913
|
||||
},
|
||||
{
|
||||
"sx": 0,
|
||||
"sy": 0.8,
|
||||
"oldMs": 110.3922050000001,
|
||||
"newMs": 98.40486799999985,
|
||||
"newRefBuildMs": 7.7547400000000835,
|
||||
"firstFrameNetSavingMs": 4.232597000000169,
|
||||
"steadySavingMs": 11.987337000000252
|
||||
},
|
||||
{
|
||||
"sx": 0.8,
|
||||
"sy": 0.5,
|
||||
"oldMs": 138.4319349999996,
|
||||
"newMs": 102.46810800000003,
|
||||
"newRefBuildMs": 8.225470999999743,
|
||||
"firstFrameNetSavingMs": 27.73835599999984,
|
||||
"steadySavingMs": 35.96382699999958
|
||||
},
|
||||
{
|
||||
"sx": 1.2,
|
||||
"sy": 0,
|
||||
"oldMs": 111.11085400000002,
|
||||
"newMs": 93.51345999999967,
|
||||
"newRefBuildMs": 8.248458000000028,
|
||||
"firstFrameNetSavingMs": 9.348936000000322,
|
||||
"steadySavingMs": 17.59739400000035
|
||||
},
|
||||
{
|
||||
"sx": 1.5,
|
||||
"sy": 0.8,
|
||||
"oldMs": 126.42733499999986,
|
||||
"newMs": 95.50466299999971,
|
||||
"newRefBuildMs": 8.381738999999925,
|
||||
"firstFrameNetSavingMs": 22.540933000000223,
|
||||
"steadySavingMs": 30.922672000000148
|
||||
}
|
||||
]
|
||||
},
|
||||
"code_metrics": {
|
||||
"v17_script_lines": 1067,
|
||||
"v17_script_bytes": 143149,
|
||||
"v18_script_lines": 805,
|
||||
"v18_script_bytes": 83427,
|
||||
"v18_kernels_lines": 12,
|
||||
"v18_kernels_bytes": 52136
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
{
|
||||
"environment": "Chromium 144 headless, 780x441 viewport, container-local",
|
||||
"scenes": [
|
||||
{
|
||||
"name": "seahorse_z14",
|
||||
"bits": 288,
|
||||
"first": {
|
||||
"render": "789 ms / 3490 回 · BLA 54.395ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.7424271106719971
|
||||
},
|
||||
"base": {
|
||||
"render": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.28958845138549805
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-28 + リベース / 288 bit",
|
||||
"renderText": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "seahorse_z20",
|
||||
"bits": 320,
|
||||
"first": {
|
||||
"render": "401 ms / 4509 回 · BLA 5.023ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.4155466556549072
|
||||
},
|
||||
"base": {
|
||||
"render": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.26917600631713867
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 320 bit",
|
||||
"renderText": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "seahorse_z100",
|
||||
"bits": 576,
|
||||
"first": {
|
||||
"render": "482 ms / 17085 回 · BLA 0.076ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.5742058753967285
|
||||
},
|
||||
"base": {
|
||||
"render": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.5136466026306152
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 576 bit",
|
||||
"renderText": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "period3_z20",
|
||||
"bits": 320,
|
||||
"first": {
|
||||
"render": "1420 ms / 4509 回 · BLA 0.395ms/kpx · 内部省略 100%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 1.5150198936462402
|
||||
},
|
||||
"base": {
|
||||
"render": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.8927633762359619
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-23 + リベース · 内部早期終了 100% / 320 bit",
|
||||
"renderText": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
|
||||
"canvas": "780x441"
|
||||
}
|
||||
],
|
||||
"errorCount": 0
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
{
|
||||
"environment": "Chromium 144 headless, 780x441 viewport, container-local",
|
||||
"scenes": [
|
||||
{
|
||||
"name": "seahorse_z14",
|
||||
"bits": 288,
|
||||
"first": {
|
||||
"render": "789 ms / 3490 回 · BLA 54.395ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.7424271106719971
|
||||
},
|
||||
"base": {
|
||||
"render": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.28958845138549805
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-28 + リベース / 288 bit",
|
||||
"renderText": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "seahorse_z20",
|
||||
"bits": 320,
|
||||
"first": {
|
||||
"render": "401 ms / 4509 回 · BLA 5.023ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.4155466556549072
|
||||
},
|
||||
"base": {
|
||||
"render": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.26917600631713867
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 320 bit",
|
||||
"renderText": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "seahorse_z100",
|
||||
"bits": 576,
|
||||
"first": {
|
||||
"render": "482 ms / 17085 回 · BLA 0.076ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.5742058753967285
|
||||
},
|
||||
"base": {
|
||||
"render": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.5136466026306152
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 576 bit",
|
||||
"renderText": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "period3_z20",
|
||||
"bits": 320,
|
||||
"first": {
|
||||
"render": "1420 ms / 4509 回 · BLA 0.395ms/kpx · 内部省略 100%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 1.5150198936462402
|
||||
},
|
||||
"base": {
|
||||
"render": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.8927633762359619
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-23 + リベース · 内部早期終了 100% / 320 bit",
|
||||
"renderText": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
|
||||
"canvas": "780x441"
|
||||
}
|
||||
],
|
||||
"errorCount": 0
|
||||
}
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
[
|
||||
{
|
||||
"z": 40,
|
||||
"global": 7752,
|
||||
"comp": 4300,
|
||||
"refBuildMs": 18.262529999999998,
|
||||
"refLen": 3246,
|
||||
"fullBlaMs": 5.190414000000004,
|
||||
"blaBuildMs": 2.432442000000009,
|
||||
"strictMs": 0,
|
||||
"blaStrict": {
|
||||
"valid": 41340,
|
||||
"classMismatch": 0,
|
||||
"classRate": 0,
|
||||
"diffGt1": 0,
|
||||
"diffRate": 0,
|
||||
"bad": 0,
|
||||
"maxDiff": 0
|
||||
},
|
||||
"work": {
|
||||
"unresolved": 0,
|
||||
"unresolvedRate": 0,
|
||||
"lateEscape": 0,
|
||||
"lateEscapeRate": 0,
|
||||
"pilotClassMismatch": 0,
|
||||
"pilotClassMismatchRate": 0,
|
||||
"meanIterErr": 0,
|
||||
"maxIterErr": 0
|
||||
},
|
||||
"safe": {
|
||||
"safeRatio": 0,
|
||||
"periodicMs": 0.08044799999998986,
|
||||
"blackRatio": 0,
|
||||
"interiorRatio": 0
|
||||
},
|
||||
"safeAcc": {
|
||||
"skipped": 0,
|
||||
"ratio": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseRate": 0
|
||||
},
|
||||
"safeRun": {
|
||||
"buildMs": 0.15029499999999985,
|
||||
"renderMs": 1.6046879999999817,
|
||||
"pixels": 41340,
|
||||
"tasks": 34
|
||||
},
|
||||
"eps": [
|
||||
{
|
||||
"e": 18,
|
||||
"ms": 1.3821410000000185,
|
||||
"build": 0.14597599999999034
|
||||
},
|
||||
{
|
||||
"e": 20,
|
||||
"ms": 1.4494080000000054,
|
||||
"build": 0.09218199999997978
|
||||
},
|
||||
{
|
||||
"e": 23,
|
||||
"ms": 1.6922059999999988,
|
||||
"build": 0.09046299999999974
|
||||
},
|
||||
{
|
||||
"e": 27,
|
||||
"ms": 1.456985000000003,
|
||||
"build": 0.09038200000000529
|
||||
},
|
||||
{
|
||||
"e": 32,
|
||||
"ms": 1.3537540000000092,
|
||||
"build": 0.0893779999999822
|
||||
}
|
||||
],
|
||||
"recenter": {
|
||||
"oldRefLen": 3246,
|
||||
"newRefLen": 3246,
|
||||
"newBuildMs": 10.052744000000018,
|
||||
"oldRenderMs": 1.324105999999972,
|
||||
"newRenderMs": 1.4144129999999961,
|
||||
"benefitMs": -0.09030700000002412,
|
||||
"netFirstFrame": -10.143051000000042
|
||||
}
|
||||
},
|
||||
{
|
||||
"z": 100,
|
||||
"global": 17085,
|
||||
"comp": 7600,
|
||||
"refBuildMs": 9.734955000000014,
|
||||
"refLen": 3246,
|
||||
"fullBlaMs": 1.3740439999999978,
|
||||
"blaBuildMs": 0.10230300000000625,
|
||||
"strictMs": 0,
|
||||
"blaStrict": {
|
||||
"valid": 41340,
|
||||
"classMismatch": 0,
|
||||
"classRate": 0,
|
||||
"diffGt1": 0,
|
||||
"diffRate": 0,
|
||||
"bad": 0,
|
||||
"maxDiff": 0
|
||||
},
|
||||
"work": {
|
||||
"unresolved": 0,
|
||||
"unresolvedRate": 0,
|
||||
"lateEscape": 0,
|
||||
"lateEscapeRate": 0,
|
||||
"pilotClassMismatch": 0,
|
||||
"pilotClassMismatchRate": 0,
|
||||
"meanIterErr": 0,
|
||||
"maxIterErr": 0
|
||||
},
|
||||
"safe": {
|
||||
"safeRatio": 0,
|
||||
"periodicMs": 0.06870000000000687,
|
||||
"blackRatio": 0,
|
||||
"interiorRatio": 0
|
||||
},
|
||||
"safeAcc": {
|
||||
"skipped": 0,
|
||||
"ratio": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseRate": 0
|
||||
},
|
||||
"safeRun": {
|
||||
"buildMs": 0.11969400000000974,
|
||||
"renderMs": 1.3758139999999912,
|
||||
"pixels": 41340,
|
||||
"tasks": 34
|
||||
},
|
||||
"eps": [
|
||||
{
|
||||
"e": 18,
|
||||
"ms": 1.3486680000000035,
|
||||
"build": 0.08054500000000075
|
||||
},
|
||||
{
|
||||
"e": 20,
|
||||
"ms": 1.3468280000000163,
|
||||
"build": 0.09072599999998943
|
||||
},
|
||||
{
|
||||
"e": 23,
|
||||
"ms": 1.3401210000000106,
|
||||
"build": 0.0852779999999882
|
||||
},
|
||||
{
|
||||
"e": 27,
|
||||
"ms": 1.6213699999999847,
|
||||
"build": 0.08418900000000917
|
||||
},
|
||||
{
|
||||
"e": 32,
|
||||
"ms": 1.4460180000000094,
|
||||
"build": 0.08714600000001838
|
||||
}
|
||||
],
|
||||
"recenter": {
|
||||
"oldRefLen": 3246,
|
||||
"newRefLen": 3246,
|
||||
"newBuildMs": 8.042026000000021,
|
||||
"oldRenderMs": 1.3196079999999881,
|
||||
"newRenderMs": 1.3709360000000004,
|
||||
"benefitMs": -0.05132800000001225,
|
||||
"netFirstFrame": -8.093354000000033
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
|
||||
seahorse-z14 iter 3490 ref 3246 build 21.53 baseline 1807.84
|
||||
{ e: 23, b: 0, p: 0, int: 0 } ms 299.14 {
|
||||
bla: 3043732,
|
||||
ptb: 11197405,
|
||||
rebase: 72197,
|
||||
interior: 0,
|
||||
unresolved: 0,
|
||||
fail: 0
|
||||
} cmp {
|
||||
cls: 157,
|
||||
clsRate: 0.010012755102040817,
|
||||
diff: 820,
|
||||
diffRate: 0.05229591836734694,
|
||||
bad: 0
|
||||
}
|
||||
{ e: 23, b: 1600, p: 2200, int: 1 } ms 373.69 {
|
||||
bla: 3043732,
|
||||
ptb: 11197405,
|
||||
rebase: 72197,
|
||||
interior: 0,
|
||||
unresolved: 0,
|
||||
fail: 0
|
||||
} cmp {
|
||||
cls: 157,
|
||||
clsRate: 0.010012755102040817,
|
||||
diff: 820,
|
||||
diffRate: 0.05229591836734694,
|
||||
bad: 0
|
||||
}
|
||||
{ e: 23, b: 800, p: 1200, int: 1 } ms 398.19 {
|
||||
bla: 3043729,
|
||||
ptb: 11189659,
|
||||
rebase: 71797,
|
||||
interior: 0,
|
||||
unresolved: 107,
|
||||
fail: 0
|
||||
} cmp {
|
||||
cls: 131,
|
||||
clsRate: 0.008354591836734693,
|
||||
diff: 747,
|
||||
diffRate: 0.04764030612244898,
|
||||
bad: 107
|
||||
}
|
||||
{ e: 32, b: 1600, p: 2200, int: 1 } ms 785.73 {
|
||||
bla: 3016859,
|
||||
ptb: 26792628,
|
||||
rebase: 72089,
|
||||
interior: 0,
|
||||
unresolved: 54,
|
||||
fail: 0
|
||||
} cmp {
|
||||
cls: 60,
|
||||
clsRate: 0.003826530612244898,
|
||||
diff: 182,
|
||||
diffRate: 0.011607142857142858,
|
||||
bad: 54
|
||||
}
|
||||
{ e: 40, b: 0, p: 0, int: 1 } ms 1217.85 {
|
||||
bla: 1091101,
|
||||
ptb: 44427881,
|
||||
rebase: 72170,
|
||||
interior: 0,
|
||||
unresolved: 0,
|
||||
fail: 0
|
||||
} cmp {
|
||||
cls: 13,
|
||||
clsRate: 0.0008290816326530612,
|
||||
diff: 31,
|
||||
diffRate: 0.0019770408163265305,
|
||||
bad: 0
|
||||
}
|
||||
|
||||
seahorse-z20 iter 4509 ref 3246 build 14.47 baseline 383.51
|
||||
{ e: 23, b: 0, p: 0, int: 0 } ms 52.83 {
|
||||
bla: 605601,
|
||||
ptb: 2085586,
|
||||
rebase: 0,
|
||||
interior: 0,
|
||||
unresolved: 0,
|
||||
fail: 0
|
||||
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 23, b: 1600, p: 2200, int: 1 } ms 60.84 {
|
||||
bla: 605601,
|
||||
ptb: 2085586,
|
||||
rebase: 0,
|
||||
interior: 0,
|
||||
unresolved: 0,
|
||||
fail: 0
|
||||
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 23, b: 800, p: 1200, int: 1 } ms 61.15 {
|
||||
bla: 605601,
|
||||
ptb: 2085586,
|
||||
rebase: 0,
|
||||
interior: 0,
|
||||
unresolved: 0,
|
||||
fail: 0
|
||||
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 32, b: 1600, p: 2200, int: 1 } ms 138.45 {
|
||||
bla: 1253140,
|
||||
ptb: 4763652,
|
||||
rebase: 0,
|
||||
interior: 0,
|
||||
unresolved: 0,
|
||||
fail: 0
|
||||
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 40, b: 0, p: 0, int: 1 } ms 253.84 {
|
||||
bla: 2443606,
|
||||
ptb: 8374654,
|
||||
rebase: 0,
|
||||
interior: 0,
|
||||
unresolved: 0,
|
||||
fail: 0
|
||||
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
|
||||
period2-z20 iter 4509 ref 4509 build 4.04 baseline 1552.21
|
||||
{ e: 23, b: 0, p: 0, int: 0 } ms 1507.19 {
|
||||
bla: 0,
|
||||
ptb: 70701120,
|
||||
rebase: 0,
|
||||
interior: 0,
|
||||
unresolved: 0,
|
||||
fail: 0
|
||||
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 23, b: 1600, p: 2200, int: 1 } ms 0.20 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 23, b: 800, p: 1200, int: 1 } ms 0.12 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 32, b: 1600, p: 2200, int: 1 } ms 0.12 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 40, b: 0, p: 0, int: 1 } ms 0.16 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
|
||||
main-z20 iter 4509 ref 4509 build 9.77 baseline 5.36
|
||||
{ e: 23, b: 0, p: 0, int: 0 } ms 2.83 {
|
||||
bla: 141120,
|
||||
ptb: 15680,
|
||||
rebase: 0,
|
||||
interior: 0,
|
||||
unresolved: 0,
|
||||
fail: 0
|
||||
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 23, b: 1600, p: 2200, int: 1 } ms 0.15 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 23, b: 800, p: 1200, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 32, b: 1600, p: 2200, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
{ e: 40, b: 0, p: 0, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
[
|
||||
{
|
||||
"name": "seahorse-z14",
|
||||
"results": [
|
||||
{
|
||||
"r": false,
|
||||
"last": 693.8000000007451,
|
||||
"e": "WASM SIMD ×2 · BLA e-28 + rebase · 局所補修 6",
|
||||
"f": 1,
|
||||
"px": 12288,
|
||||
"k": 690.1000000052154,
|
||||
"rep": 0.3,
|
||||
"un": 0,
|
||||
"int": 0,
|
||||
"bla": 162.36328125,
|
||||
"ptb": 1876.836181640625,
|
||||
"bits": 512,
|
||||
"q": 0.58
|
||||
},
|
||||
{
|
||||
"r": false,
|
||||
"last": 1999.9000000022352,
|
||||
"e": "WASM SIMD ×2 · BLA e-32 + rebase · 局所補修 12",
|
||||
"f": 2,
|
||||
"px": 36300,
|
||||
"k": 2172.9000000059605,
|
||||
"rep": 0.36363636363636365,
|
||||
"un": 0,
|
||||
"int": 0,
|
||||
"bla": 135.99895316804407,
|
||||
"ptb": 2132.273305785124,
|
||||
"bits": 512,
|
||||
"q": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "seahorse-z20",
|
||||
"results": [
|
||||
{
|
||||
"r": false,
|
||||
"last": 388.79999999701977,
|
||||
"e": "WASM SIMD ×2 · BLA e-23 + rebase",
|
||||
"f": 3,
|
||||
"px": 72463,
|
||||
"k": 330.9000000022352,
|
||||
"rep": 0,
|
||||
"un": 0,
|
||||
"int": 0,
|
||||
"bla": 38.68238963333011,
|
||||
"ptb": 133.7588424437299,
|
||||
"bits": 512,
|
||||
"q": 0.58
|
||||
},
|
||||
{
|
||||
"r": false,
|
||||
"last": 1732.699999999255,
|
||||
"e": "WASM SIMD ×2 · BLA e-32 + rebase",
|
||||
"f": 4,
|
||||
"px": 134091,
|
||||
"k": 1531.5999999977648,
|
||||
"rep": 0,
|
||||
"un": 0,
|
||||
"int": 0,
|
||||
"bla": 82.81766114056872,
|
||||
"ptb": 306.63208567316224,
|
||||
"bits": 512,
|
||||
"q": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "period3-z20",
|
||||
"results": [
|
||||
{
|
||||
"r": false,
|
||||
"last": 174.90000000223517,
|
||||
"e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%",
|
||||
"f": 5,
|
||||
"px": 202800,
|
||||
"k": 67.5,
|
||||
"rep": 0,
|
||||
"un": 0,
|
||||
"int": 1,
|
||||
"bla": 2,
|
||||
"ptb": 8,
|
||||
"bits": 512,
|
||||
"q": 0.58
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "period2-z20",
|
||||
"results": [
|
||||
{
|
||||
"r": false,
|
||||
"last": 49.19999999925494,
|
||||
"e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%",
|
||||
"f": 6,
|
||||
"px": 202800,
|
||||
"k": 4.399999998509884,
|
||||
"rep": 0,
|
||||
"un": 0,
|
||||
"int": 1,
|
||||
"bla": 0,
|
||||
"ptb": 0,
|
||||
"bits": 512,
|
||||
"q": 0.58
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cardioid-z20",
|
||||
"results": [
|
||||
{
|
||||
"r": false,
|
||||
"last": 33,
|
||||
"e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%",
|
||||
"f": 7,
|
||||
"px": 43200,
|
||||
"k": 0.5999999977648258,
|
||||
"rep": 0,
|
||||
"un": 0,
|
||||
"int": 1,
|
||||
"bla": 0,
|
||||
"ptb": 0,
|
||||
"bits": 512,
|
||||
"q": 0.58
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
[
|
||||
{
|
||||
"z": 14,
|
||||
"runs": [
|
||||
{
|
||||
"r": false,
|
||||
"last": 528.9000000022352,
|
||||
"e": "WASM SIMD \u00d72 \u00b7 BLA e-28 + rebase \u00b7 \u5c40\u6240\u88dc\u4fee 1",
|
||||
"f": 1,
|
||||
"px": 12288,
|
||||
"k": 531.7000000104308,
|
||||
"pilot": 111.89999999850988,
|
||||
"rep": 0.05
|
||||
},
|
||||
{
|
||||
"r": false,
|
||||
"last": 332.6000000014901,
|
||||
"e": "WASM SIMD \u00d72 \u00b7 BLA e-28 + rebase",
|
||||
"f": 2,
|
||||
"px": 12288,
|
||||
"k": 472.99999998882413,
|
||||
"pilot": 111.89999999850988,
|
||||
"rep": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"z": 20,
|
||||
"runs": [
|
||||
{
|
||||
"r": false,
|
||||
"last": 322.1000000014901,
|
||||
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
|
||||
"f": 3,
|
||||
"px": 56444,
|
||||
"k": 340.30000000447035,
|
||||
"pilot": 6.699999999254942,
|
||||
"rep": 0
|
||||
},
|
||||
{
|
||||
"r": false,
|
||||
"last": 216.40000000223517,
|
||||
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
|
||||
"f": 4,
|
||||
"px": 45510,
|
||||
"k": 218.69999999925494,
|
||||
"pilot": 6.699999999254942,
|
||||
"rep": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"z": 100,
|
||||
"runs": [
|
||||
{
|
||||
"r": false,
|
||||
"last": 33.70000000298023,
|
||||
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
|
||||
"f": 5,
|
||||
"px": 202800,
|
||||
"k": 16.599999994039536,
|
||||
"pilot": 0.09999999776482582,
|
||||
"rep": 0
|
||||
},
|
||||
{
|
||||
"r": false,
|
||||
"last": 46.099999997764826,
|
||||
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
|
||||
"f": 6,
|
||||
"px": 202800,
|
||||
"k": 10.899999998509884,
|
||||
"pilot": 0.09999999776482582,
|
||||
"rep": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
[
|
||||
{
|
||||
"z": 20,
|
||||
"runs": [
|
||||
{
|
||||
"r": false,
|
||||
"last": 1422.0999999977648,
|
||||
"f": 1,
|
||||
"px": 35371,
|
||||
"k": 170.7000000141561,
|
||||
"pilot": 10.700000002980232
|
||||
},
|
||||
{
|
||||
"r": false,
|
||||
"last": 1080.1000000014901,
|
||||
"f": 2,
|
||||
"px": 12288,
|
||||
"k": 57.79999999701977,
|
||||
"pilot": 10.700000002980232
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"z": 100,
|
||||
"runs": [
|
||||
{
|
||||
"r": false,
|
||||
"last": 3249.7000000029802,
|
||||
"f": 3,
|
||||
"px": 202800,
|
||||
"k": 17.099999982863665,
|
||||
"pilot": 0.19999999925494194
|
||||
},
|
||||
{
|
||||
"r": false,
|
||||
"last": 1368.800000000745,
|
||||
"f": 4,
|
||||
"px": 16170,
|
||||
"k": 2.2000000029802322,
|
||||
"pilot": 0.19999999925494194
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
[
|
||||
{
|
||||
"name": "period3 wide",
|
||||
"re": "-0.1225611668766536",
|
||||
"im": "0.7448617666197442",
|
||||
"span": 0.1,
|
||||
"offMs": 766.7149390000001,
|
||||
"onMs": 5.955513999999994,
|
||||
"interior": 6912,
|
||||
"classMismatch": 0,
|
||||
"diffGt1": 0,
|
||||
"total": 6912
|
||||
},
|
||||
{
|
||||
"name": "period3 mid",
|
||||
"re": "-0.1225611668766536",
|
||||
"im": "0.7448617666197442",
|
||||
"span": 0.03,
|
||||
"offMs": 438.65803500000015,
|
||||
"onMs": 2.334043000000065,
|
||||
"interior": 6912,
|
||||
"classMismatch": 0,
|
||||
"diffGt1": 0,
|
||||
"total": 6912
|
||||
},
|
||||
{
|
||||
"name": "period2 boundary neighborhood",
|
||||
"re": "-0.75",
|
||||
"im": "0",
|
||||
"span": 0.1,
|
||||
"offMs": 364.72781799999984,
|
||||
"onMs": 1.0177550000000792,
|
||||
"interior": 6806,
|
||||
"classMismatch": 0,
|
||||
"diffGt1": 0,
|
||||
"total": 6912
|
||||
},
|
||||
{
|
||||
"name": "mixed exterior/interior",
|
||||
"re": "-0.5",
|
||||
"im": "0.5",
|
||||
"span": 0.1,
|
||||
"offMs": 282.849144,
|
||||
"onMs": 16.219244000000117,
|
||||
"interior": 5338,
|
||||
"classMismatch": 0,
|
||||
"diffGt1": 0,
|
||||
"total": 6912
|
||||
},
|
||||
{
|
||||
"name": "cardioid cusp neighborhood",
|
||||
"re": "0.25",
|
||||
"im": "0",
|
||||
"span": 0.04,
|
||||
"offMs": 329.88844100000006,
|
||||
"onMs": 1.7636029999998755,
|
||||
"interior": 6403,
|
||||
"classMismatch": 0,
|
||||
"diffGt1": 0,
|
||||
"total": 6912
|
||||
}
|
||||
]
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
[
|
||||
{
|
||||
"exp": 14,
|
||||
"setup": {
|
||||
"bits": 768,
|
||||
"z": 14,
|
||||
"iter": 3490
|
||||
},
|
||||
"base": {
|
||||
"wall": 7315.300000000745,
|
||||
"lastRender": 7292.300000000745,
|
||||
"w": 260,
|
||||
"h": 159,
|
||||
"pilotMs": 0,
|
||||
"kernelMPP": 0,
|
||||
"interiorSkip": 0,
|
||||
"black": 0,
|
||||
"bad": 0,
|
||||
"engine": "WASM SIMD · main-thread rebase · skip 998",
|
||||
"z": 14,
|
||||
"iter": 3490,
|
||||
"compute": 2870
|
||||
},
|
||||
"safe": null
|
||||
},
|
||||
{
|
||||
"exp": 20,
|
||||
"setup": {
|
||||
"bits": 768,
|
||||
"z": 20.00000000000001,
|
||||
"iter": 4509
|
||||
},
|
||||
"base": {
|
||||
"wall": 3576.60000000149,
|
||||
"lastRender": 3555.39999999851,
|
||||
"w": 260,
|
||||
"h": 159,
|
||||
"pilotMs": 0,
|
||||
"kernelMPP": 0,
|
||||
"interiorSkip": 0,
|
||||
"black": 0,
|
||||
"bad": 0,
|
||||
"engine": "WASM SIMD · main-thread rebase · skip 998",
|
||||
"z": 20.00000000000001,
|
||||
"iter": 4509,
|
||||
"compute": 3200
|
||||
},
|
||||
"safe": null
|
||||
},
|
||||
{
|
||||
"exp": 40,
|
||||
"setup": {
|
||||
"bits": 768,
|
||||
"z": 40.00000000000002,
|
||||
"iter": 7752
|
||||
},
|
||||
"base": {
|
||||
"wall": 3800.199999999255,
|
||||
"lastRender": 3782.599999997765,
|
||||
"w": 260,
|
||||
"h": 159,
|
||||
"pilotMs": 0,
|
||||
"kernelMPP": 0,
|
||||
"interiorSkip": 0,
|
||||
"black": 0,
|
||||
"bad": 0,
|
||||
"engine": "WASM SIMD · main-thread rebase · skip 998",
|
||||
"z": 40.00000000000002,
|
||||
"iter": 7752,
|
||||
"compute": 4300
|
||||
},
|
||||
"safe": null
|
||||
},
|
||||
{
|
||||
"exp": 100,
|
||||
"setup": {
|
||||
"bits": 768,
|
||||
"z": 100,
|
||||
"iter": 17085
|
||||
},
|
||||
"base": {
|
||||
"wall": 3633.7999999970198,
|
||||
"lastRender": 3622,
|
||||
"w": 260,
|
||||
"h": 159,
|
||||
"pilotMs": 0,
|
||||
"kernelMPP": 0,
|
||||
"interiorSkip": 0,
|
||||
"black": 0,
|
||||
"bad": 0,
|
||||
"engine": "WASM SIMD · main-thread rebase · skip 998",
|
||||
"z": 100,
|
||||
"iter": 17085,
|
||||
"compute": 7600
|
||||
},
|
||||
"safe": null
|
||||
}
|
||||
]
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"full": [
|
||||
128,
|
||||
78
|
||||
],
|
||||
"pilot": [
|
||||
56,
|
||||
34
|
||||
],
|
||||
"fullMs": 1356.671812,
|
||||
"pilotMs": 169.93951399999992,
|
||||
"classMismatch": 451,
|
||||
"classRate": 0.04517227564102564,
|
||||
"escapeIterDiffGt8": 4364,
|
||||
"diffRate": 0.437099358974359
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
{
|
||||
"oldRefBuildMs": 29.320617,
|
||||
"tests": [
|
||||
{
|
||||
"sx": 0.5,
|
||||
"sy": 0,
|
||||
"oldMs": 691.804547,
|
||||
"newMs": 640.2659849999995,
|
||||
"newRefBuildMs": 9.508285999999998,
|
||||
"firstFrameNetSavingMs": 42.03027600000041,
|
||||
"steadySavingMs": 51.53856200000041
|
||||
},
|
||||
{
|
||||
"sx": 0.8,
|
||||
"sy": 0,
|
||||
"oldMs": 756.002915,
|
||||
"newMs": 655.9519419999997,
|
||||
"newRefBuildMs": 8.81105800000023,
|
||||
"firstFrameNetSavingMs": 91.23991500000011,
|
||||
"steadySavingMs": 100.05097300000034
|
||||
},
|
||||
{
|
||||
"sx": 0,
|
||||
"sy": 0.8,
|
||||
"oldMs": 730.6417070000007,
|
||||
"newMs": 671.6106240000008,
|
||||
"newRefBuildMs": 8.391966000001048,
|
||||
"firstFrameNetSavingMs": 50.639116999998805,
|
||||
"steadySavingMs": 59.03108299999985
|
||||
},
|
||||
{
|
||||
"sx": 0.8,
|
||||
"sy": 0.5,
|
||||
"oldMs": 787.9931480000014,
|
||||
"newMs": 740.047842,
|
||||
"newRefBuildMs": 7.513370000000577,
|
||||
"firstFrameNetSavingMs": 40.43193600000086,
|
||||
"steadySavingMs": 47.94530600000144
|
||||
},
|
||||
{
|
||||
"sx": 1.2,
|
||||
"sy": 0,
|
||||
"oldMs": 705.7068249999975,
|
||||
"newMs": 689.7542820000017,
|
||||
"newRefBuildMs": 7.101795999999013,
|
||||
"firstFrameNetSavingMs": 8.850746999996773,
|
||||
"steadySavingMs": 15.952542999995785
|
||||
},
|
||||
{
|
||||
"sx": 1.5,
|
||||
"sy": 0.8,
|
||||
"oldMs": 793.3721589999986,
|
||||
"newMs": 733.093237000001,
|
||||
"newRefBuildMs": 7.395623000000342,
|
||||
"firstFrameNetSavingMs": 52.88329899999735,
|
||||
"steadySavingMs": 60.27892199999769
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
{
|
||||
"oldRefBuildMs": 23.925890999999993,
|
||||
"tests": [
|
||||
{
|
||||
"sx": 0.5,
|
||||
"sy": 0,
|
||||
"oldMs": 100.18383699999998,
|
||||
"newMs": 105.04222300000004,
|
||||
"newRefBuildMs": 9.637189000000006,
|
||||
"firstFrameNetSavingMs": -14.49557500000006,
|
||||
"steadySavingMs": -4.858386000000053
|
||||
},
|
||||
{
|
||||
"sx": 0.8,
|
||||
"sy": 0,
|
||||
"oldMs": 106.89347099999986,
|
||||
"newMs": 93.24754299999995,
|
||||
"newRefBuildMs": 8.222010999999952,
|
||||
"firstFrameNetSavingMs": 5.42391699999996,
|
||||
"steadySavingMs": 13.645927999999913
|
||||
},
|
||||
{
|
||||
"sx": 0,
|
||||
"sy": 0.8,
|
||||
"oldMs": 110.3922050000001,
|
||||
"newMs": 98.40486799999985,
|
||||
"newRefBuildMs": 7.7547400000000835,
|
||||
"firstFrameNetSavingMs": 4.232597000000169,
|
||||
"steadySavingMs": 11.987337000000252
|
||||
},
|
||||
{
|
||||
"sx": 0.8,
|
||||
"sy": 0.5,
|
||||
"oldMs": 138.4319349999996,
|
||||
"newMs": 102.46810800000003,
|
||||
"newRefBuildMs": 8.225470999999743,
|
||||
"firstFrameNetSavingMs": 27.73835599999984,
|
||||
"steadySavingMs": 35.96382699999958
|
||||
},
|
||||
{
|
||||
"sx": 1.2,
|
||||
"sy": 0,
|
||||
"oldMs": 111.11085400000002,
|
||||
"newMs": 93.51345999999967,
|
||||
"newRefBuildMs": 8.248458000000028,
|
||||
"firstFrameNetSavingMs": 9.348936000000322,
|
||||
"steadySavingMs": 17.59739400000035
|
||||
},
|
||||
{
|
||||
"sx": 1.5,
|
||||
"sy": 0.8,
|
||||
"oldMs": 126.42733499999986,
|
||||
"newMs": 95.50466299999971,
|
||||
"newRefBuildMs": 8.381738999999925,
|
||||
"firstFrameNetSavingMs": 22.540933000000223,
|
||||
"steadySavingMs": 30.922672000000148
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
{
|
||||
"environment": "Chromium 144 headless, 780x441 viewport, container-local",
|
||||
"scenes": [
|
||||
{
|
||||
"name": "seahorse_z14",
|
||||
"bits": 288,
|
||||
"first": {
|
||||
"render": "835 ms / 3490 回 · BLA 8.034ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.9532754421234131
|
||||
},
|
||||
"base": {
|
||||
"render": "1059 ms / 3490 回 · BLA 9.226ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 1.098907470703125
|
||||
},
|
||||
"engine": "WASM SIMD ×2 常駐Worker · BLA e-23 + リベース · スキップ 998 / 288 bit",
|
||||
"renderText": "1059 ms / 3490 回 · BLA 9.226ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "seahorse_z20",
|
||||
"bits": 320,
|
||||
"first": {
|
||||
"render": "1574 ms / 4509 回 · BLA 1.891ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 1.7402434349060059
|
||||
},
|
||||
"base": {
|
||||
"render": "1226 ms / 4509 回 · BLA 2.126ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 1.3763623237609863
|
||||
},
|
||||
"engine": "WASM SIMD ×2 常駐Worker · BLA e-25 + リベース · スキップ 3231 / 320 bit",
|
||||
"renderText": "1226 ms / 4509 回 · BLA 2.126ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "seahorse_z100",
|
||||
"bits": 576,
|
||||
"first": {
|
||||
"render": "1224 ms / 17085 回 · BLA 0.104ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 1.4651060104370117
|
||||
},
|
||||
"base": {
|
||||
"render": "983 ms / 17085 回 · BLA 0.097ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 1.1331260204315186
|
||||
},
|
||||
"engine": "WASM SIMD ×2 常駐Worker · BLA e-23 + リベース · スキップ 3245 / 576 bit",
|
||||
"renderText": "983 ms / 17085 回 · BLA 0.097ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "period3_z20",
|
||||
"bits": 320,
|
||||
"first": {
|
||||
"render": "1675 ms / 4509 回 · BLA 53.047ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 1.8480825424194336
|
||||
},
|
||||
"base": {
|
||||
"render": "1931 ms / 4509 回 · BLA 52.519ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 2.0063276290893555
|
||||
},
|
||||
"engine": "WASM SIMD ×2 常駐Worker · BLA e-25 + リベース · スキップ 4508 / 320 bit",
|
||||
"renderText": "1931 ms / 4509 回 · BLA 52.519ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
}
|
||||
],
|
||||
"errorCount": 0
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
{
|
||||
"environment": "Chromium 144 headless, 780x441 viewport, container-local",
|
||||
"scenes": [
|
||||
{
|
||||
"name": "seahorse_z14",
|
||||
"bits": 288,
|
||||
"first": {
|
||||
"render": "789 ms / 3490 回 · BLA 54.395ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.7424271106719971
|
||||
},
|
||||
"base": {
|
||||
"render": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.28958845138549805
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-28 + リベース / 288 bit",
|
||||
"renderText": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "seahorse_z20",
|
||||
"bits": 320,
|
||||
"first": {
|
||||
"render": "401 ms / 4509 回 · BLA 5.023ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.4155466556549072
|
||||
},
|
||||
"base": {
|
||||
"render": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.26917600631713867
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 320 bit",
|
||||
"renderText": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "seahorse_z100",
|
||||
"bits": 576,
|
||||
"first": {
|
||||
"render": "482 ms / 17085 回 · BLA 0.076ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.5742058753967285
|
||||
},
|
||||
"base": {
|
||||
"render": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.5136466026306152
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 576 bit",
|
||||
"renderText": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
|
||||
"canvas": "780x441"
|
||||
},
|
||||
{
|
||||
"name": "period3_z20",
|
||||
"bits": 320,
|
||||
"first": {
|
||||
"render": "1420 ms / 4509 回 · BLA 0.395ms/kpx · 内部省略 100%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 1.5150198936462402
|
||||
},
|
||||
"base": {
|
||||
"render": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
|
||||
"badge": "深部リベース",
|
||||
"saw": true,
|
||||
"wall": 0.8927633762359619
|
||||
},
|
||||
"engine": "WASM SIMD ×2 · BLA e-23 + リベース · 内部早期終了 100% / 320 bit",
|
||||
"renderText": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
|
||||
"canvas": "780x441"
|
||||
}
|
||||
],
|
||||
"errorCount": 0
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"w": 48,
|
||||
"h": 30,
|
||||
"pixels": 1440,
|
||||
"globalIter": 3490,
|
||||
"capIter": 2870,
|
||||
"capped": 1440,
|
||||
"late": 1066,
|
||||
"insideGlobal": 374,
|
||||
"lateEscapeRateAmongCapped": 0.7402777777777778,
|
||||
"elapsedMs": 6344.968723999999
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"values": {
|
||||
"title": "Mandelbrot ∞ Zoom v18",
|
||||
"engine": "WebAssembly f64 / 256 bit",
|
||||
"render": "281 ms / 350 回",
|
||||
"badge": "準備完了",
|
||||
"canvas": "780x441",
|
||||
"black": {
|
||||
"n": 6035,
|
||||
"b": 1358,
|
||||
"uniq": 1215
|
||||
},
|
||||
"autoText": "❚❚ 自動",
|
||||
"badge2": "自動 · 境界追従",
|
||||
"render2": "53 ms / 381 回"
|
||||
},
|
||||
"errorCount": 0,
|
||||
"errors": []
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-browser-baseline-v23",
|
||||
"status": "not-run",
|
||||
"reason": "No in-app browser binding was available in this environment.",
|
||||
"lastCheckedUtc": "2026-08-22T10:48:56.1197927Z",
|
||||
"connectionAudit": {
|
||||
"consecutiveGoalTurns": 3,
|
||||
"auditCycle": "resumed-after-performance-fix",
|
||||
"availableBrowsers": 0,
|
||||
"outcome": "blocked",
|
||||
"recovery": "Connect an in-app Browser, then rerun tests/browser-benchmark.html. Unrelated browser-control backends are intentionally not substituted. Node runtime-budget coverage is available in audit/v23-node-performance.json but does not replace the browser gate."
|
||||
},
|
||||
"runner": "tests/browser-benchmark.html?profile=<mobile|desktop|4k>&target=<hosted|standalone>",
|
||||
"executionModel": "one real-DPR profile and one build target per run; iframe sizing does not emulate DPR",
|
||||
"requiredTargets": ["hosted", "standalone"],
|
||||
"requiredProfiles": ["mobile", "desktop", "4k"],
|
||||
"acceptancePending": ["visual output", "requested viewport and actual DPR fidelity", "interaction p95", "Preview/Covered/Refined timing", "Power stops at Preview", "Standard stops at Covered without AA/continuation", "idle writes", "long tasks", "observed browser/GPU peak memory", "export download/cancel", "keyboard/focus/target-size/live-status/page-zoom accessibility"]
|
||||
}
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-browserless-audit-v23",
|
||||
"generatedUtc": "2026-08-22T12:35:37.5161338Z",
|
||||
"status": "pass",
|
||||
"scope": "browserless-current-artifacts",
|
||||
"fullAcceptance": false,
|
||||
"rendererVersion": 23,
|
||||
"node": {
|
||||
"version": "v22.18.0",
|
||||
"archive": {
|
||||
"file": ".tmp-node-v22.18.0-win-x64.zip",
|
||||
"sha256": "c95d8a7e1c99e669cc08c9f1176e068c1f50847c37908fcb8c35b62482366511",
|
||||
"source": "https://nodejs.org/dist/v22.18.0/node-v22.18.0-win-x64.zip"
|
||||
}
|
||||
},
|
||||
"contracts": {
|
||||
"kernel": {
|
||||
"status": "pass",
|
||||
"modules": 4,
|
||||
"compiler": "17.0.6",
|
||||
"pixelContract": "sample centers at x + 0.5, y + 0.5"
|
||||
},
|
||||
"source": {
|
||||
"status": "pass",
|
||||
"rendererVersion": 23,
|
||||
"wasmPayloads": 8,
|
||||
"scriptBytes": 129807,
|
||||
"htmlBytes": 11395
|
||||
},
|
||||
"document": {
|
||||
"status": "pass",
|
||||
"proposalContract": "revised-v23",
|
||||
"scenes": 7,
|
||||
"viewports": 3,
|
||||
"numericPolicy": "numeric-policy-v23",
|
||||
"hashedFiles": 13,
|
||||
"browserRunner": "static-contract-pass",
|
||||
"browserlessEvidence": "pass-with-explicit-limits"
|
||||
}
|
||||
},
|
||||
"executableTests": {
|
||||
"js-syntax.mjs": {
|
||||
"status": "pass",
|
||||
"rendererVersion": 23,
|
||||
"lastPass": "preview",
|
||||
"automaticTarget": "COVERED",
|
||||
"fieldIterations": [
|
||||
7,
|
||||
100
|
||||
],
|
||||
"modeProbe": {
|
||||
"targets": {
|
||||
"power": "PREVIEW",
|
||||
"standard": "COVERED",
|
||||
"fine": "REFINED",
|
||||
"validate": "VALIDATED"
|
||||
},
|
||||
"coldDeepPreview": [
|
||||
48,
|
||||
36
|
||||
]
|
||||
},
|
||||
"referenceCheckpoints": {
|
||||
"agreement": true,
|
||||
"catchesMismatch": true,
|
||||
"count": 17,
|
||||
"bits": 320
|
||||
},
|
||||
"deepWorkerModuleInit": true,
|
||||
"browserHarnessSyntax": true,
|
||||
"sources": {
|
||||
"appBytes": 129807,
|
||||
"shallowWorkerCharacters": 1509,
|
||||
"deepWorkerCharacters": 8788
|
||||
}
|
||||
},
|
||||
"module-clone.mjs": {
|
||||
"status": "pass",
|
||||
"structuredClone": true,
|
||||
"results": [
|
||||
{
|
||||
"name": "deep-simd.wasm",
|
||||
"bytes": 4866,
|
||||
"exports": [
|
||||
"render_perturb_rebase_rect"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bla-simd.wasm",
|
||||
"bytes": 4959,
|
||||
"exports": [
|
||||
"build_bla",
|
||||
"render_bla_rect_v2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "color-simd.wasm",
|
||||
"bytes": 1387,
|
||||
"exports": [
|
||||
"smooth_batch"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"precision-reference.mjs": {
|
||||
"status": "pass",
|
||||
"precisions": [
|
||||
256,
|
||||
320
|
||||
],
|
||||
"results": [
|
||||
{
|
||||
"id": "outside",
|
||||
"p": 3,
|
||||
"guarded": 3,
|
||||
"stable": true
|
||||
},
|
||||
{
|
||||
"id": "boundary-escape",
|
||||
"p": 33,
|
||||
"guarded": 33,
|
||||
"stable": true
|
||||
},
|
||||
{
|
||||
"id": "period3-center",
|
||||
"p": 4000,
|
||||
"guarded": 4000,
|
||||
"stable": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"analytic-interior.mjs": {
|
||||
"status": "pass",
|
||||
"checked": 14,
|
||||
"precisions": [
|
||||
256,
|
||||
320
|
||||
],
|
||||
"proof": "integer cardioid/period-2 inequalities"
|
||||
},
|
||||
"pixel-mapping.mjs": {
|
||||
"status": "pass",
|
||||
"samples": 851,
|
||||
"backends": [
|
||||
"shallow",
|
||||
"deep",
|
||||
"bla"
|
||||
],
|
||||
"subsampleScales": [
|
||||
2,
|
||||
4
|
||||
],
|
||||
"contract": "centered-rational",
|
||||
"float64UlpMax": 4,
|
||||
"fixedPointRounding": "nearest-ties-away-from-zero"
|
||||
},
|
||||
"pixel-contract.mjs": {
|
||||
"status": "pass",
|
||||
"backend": "simd",
|
||||
"samples": 1440,
|
||||
"mismatches": 0,
|
||||
"contract": "(x+0.5,y+0.5)"
|
||||
},
|
||||
"runtime-budget.mjs": {
|
||||
"format": "mandelbrot-node-runtime-budget-v23",
|
||||
"generatedUtc": "2026-08-22T12:35:33.428Z",
|
||||
"node": "v22.18.0",
|
||||
"status": "measured",
|
||||
"acceptance": false,
|
||||
"sceneCorpus": "mandelbrot-scene-corpus-v2",
|
||||
"measurementNote": "Runtime characterization only; browser acceptance has separate thresholds and evidence.",
|
||||
"contracts": {
|
||||
"previewBudget110": true,
|
||||
"modeTargets": true,
|
||||
"screenBudgets": true,
|
||||
"boundedContinuation": true,
|
||||
"coldDeepCap": true,
|
||||
"measuredDeepBudget": true,
|
||||
"viewportIndependentFloor": true
|
||||
},
|
||||
"shallow": [
|
||||
{
|
||||
"id": "overview-350",
|
||||
"width": 640,
|
||||
"height": 400,
|
||||
"iterations": 350,
|
||||
"pixels": 256000,
|
||||
"medianMs": 85.583,
|
||||
"msPerMegapixel": 334.30859375,
|
||||
"meanIterations": 78.3253515625,
|
||||
"samplesMs": [
|
||||
85.583,
|
||||
88.88799999999998,
|
||||
84.14580000000001,
|
||||
84.10900000000004,
|
||||
92.09339999999997
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boundary-900",
|
||||
"width": 512,
|
||||
"height": 320,
|
||||
"iterations": 900,
|
||||
"pixels": 163840,
|
||||
"medianMs": 113.91780000000006,
|
||||
"msPerMegapixel": 695.2990722656253,
|
||||
"meanIterations": 154.49669189453124,
|
||||
"samplesMs": [
|
||||
115.31709999999998,
|
||||
113.91780000000006,
|
||||
112.1979,
|
||||
112.90809999999988,
|
||||
117.25350000000003
|
||||
]
|
||||
}
|
||||
],
|
||||
"deep": {
|
||||
"id": "swirly-seahorses-z12-bla-2000",
|
||||
"width": 256,
|
||||
"height": 144,
|
||||
"iterations": 2000,
|
||||
"pixels": 36864,
|
||||
"blaEntries": 12,
|
||||
"buildMs": 0.27119999999990796,
|
||||
"medianMs": 395.4109000000003,
|
||||
"msPerMegapixel": 10726.207139756953,
|
||||
"unresolved": 0,
|
||||
"samplesMs": [
|
||||
388.1887999999999,
|
||||
394.21360000000004,
|
||||
413.6929,
|
||||
395.4109000000003,
|
||||
396.3185000000003
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"runtime": {
|
||||
"status": "measured-not-acceptance",
|
||||
"report": "audit/v23-browserless-runtime.json",
|
||||
"reason": "Runtime values are environment/load dependent and no browser paint/input/GPU threshold is asserted."
|
||||
},
|
||||
"sourceWasmBuild": {
|
||||
"status": "prior-evidence-reused",
|
||||
"manifest": {
|
||||
"file": "build/wasm-v23/manifest.json",
|
||||
"bytes": 4193,
|
||||
"sha256": "cc388d664f642181d45a110ea5b44c47524050f8bf904ebd115e8e24c0f58a18"
|
||||
},
|
||||
"reason": "Verified generated manifest reused; fixed toolchain was removed after the successful gate."
|
||||
},
|
||||
"v22Comparison": {
|
||||
"status": "not-verifiable",
|
||||
"reason": "No immutable v22 artifacts, hashes, or versioned baseline are present."
|
||||
},
|
||||
"browserAcceptance": {
|
||||
"status": "not-run",
|
||||
"report": "audit/v23-browser-baseline.json",
|
||||
"nonSubstitutable": [
|
||||
"actual DPR and visual output",
|
||||
"transform-to-paint and Preview p95",
|
||||
"long tasks and observed browser/GPU memory",
|
||||
"download/cancel behavior",
|
||||
"keyboard/focus/page-zoom/screen-reader behavior"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-node-runtime-budget-v23",
|
||||
"generatedUtc": "2026-08-22T12:35:33.428Z",
|
||||
"node": "v22.18.0",
|
||||
"status": "measured",
|
||||
"acceptance": false,
|
||||
"sceneCorpus": "mandelbrot-scene-corpus-v2",
|
||||
"measurementNote": "Runtime characterization only; browser acceptance has separate thresholds and evidence.",
|
||||
"contracts": {
|
||||
"previewBudget110": true,
|
||||
"modeTargets": true,
|
||||
"screenBudgets": true,
|
||||
"boundedContinuation": true,
|
||||
"coldDeepCap": true,
|
||||
"measuredDeepBudget": true,
|
||||
"viewportIndependentFloor": true
|
||||
},
|
||||
"shallow": [
|
||||
{
|
||||
"id": "overview-350",
|
||||
"width": 640,
|
||||
"height": 400,
|
||||
"iterations": 350,
|
||||
"pixels": 256000,
|
||||
"medianMs": 85.583,
|
||||
"msPerMegapixel": 334.30859375,
|
||||
"meanIterations": 78.3253515625,
|
||||
"samplesMs": [
|
||||
85.583,
|
||||
88.88799999999998,
|
||||
84.14580000000001,
|
||||
84.10900000000004,
|
||||
92.09339999999997
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boundary-900",
|
||||
"width": 512,
|
||||
"height": 320,
|
||||
"iterations": 900,
|
||||
"pixels": 163840,
|
||||
"medianMs": 113.91780000000006,
|
||||
"msPerMegapixel": 695.2990722656253,
|
||||
"meanIterations": 154.49669189453124,
|
||||
"samplesMs": [
|
||||
115.31709999999998,
|
||||
113.91780000000006,
|
||||
112.1979,
|
||||
112.90809999999988,
|
||||
117.25350000000003
|
||||
]
|
||||
}
|
||||
],
|
||||
"deep": {
|
||||
"id": "swirly-seahorses-z12-bla-2000",
|
||||
"width": 256,
|
||||
"height": 144,
|
||||
"iterations": 2000,
|
||||
"pixels": 36864,
|
||||
"blaEntries": 12,
|
||||
"buildMs": 0.27119999999990796,
|
||||
"medianMs": 395.4109000000003,
|
||||
"msPerMegapixel": 10726.207139756953,
|
||||
"unresolved": 0,
|
||||
"samplesMs": [
|
||||
388.1887999999999,
|
||||
394.21360000000004,
|
||||
413.6929,
|
||||
395.4109000000003,
|
||||
396.3185000000003
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-node-runtime-budget-v23",
|
||||
"generatedUtc": "2026-08-22T10:48:04.607Z",
|
||||
"node": "v24.16.0",
|
||||
"contracts": {
|
||||
"previewBudget110": true,
|
||||
"modeTargets": true,
|
||||
"screenBudgets": true,
|
||||
"boundedContinuation": true,
|
||||
"coldDeepCap": true,
|
||||
"measuredDeepBudget": true,
|
||||
"viewportIndependentFloor": true
|
||||
},
|
||||
"shallow": [
|
||||
{
|
||||
"id": "overview-350",
|
||||
"width": 640,
|
||||
"height": 400,
|
||||
"iterations": 350,
|
||||
"pixels": 256000,
|
||||
"medianMs": 86.21379999999999,
|
||||
"msPerMegapixel": 336.77265624999995,
|
||||
"meanIterations": 78.3253515625,
|
||||
"samplesMs": [
|
||||
83.7123,
|
||||
87.21029999999999,
|
||||
86.21379999999999,
|
||||
87.21250000000003,
|
||||
85.40730000000002
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "boundary-900",
|
||||
"width": 512,
|
||||
"height": 320,
|
||||
"iterations": 900,
|
||||
"pixels": 163840,
|
||||
"medianMs": 109.8057,
|
||||
"msPerMegapixel": 670.2008056640625,
|
||||
"meanIterations": 154.49669189453124,
|
||||
"samplesMs": [
|
||||
109.8057,
|
||||
110.24979999999994,
|
||||
109.15139999999997,
|
||||
108.90229999999997,
|
||||
110.59719999999993
|
||||
]
|
||||
}
|
||||
],
|
||||
"deep": {
|
||||
"id": "seahorse-bla-2000",
|
||||
"width": 256,
|
||||
"height": 144,
|
||||
"iterations": 2000,
|
||||
"pixels": 36864,
|
||||
"blaEntries": 12,
|
||||
"buildMs": 0.4078999999999269,
|
||||
"medianMs": 722.5364,
|
||||
"msPerMegapixel": 19600.054253472223,
|
||||
"unresolved": 0,
|
||||
"samplesMs": [
|
||||
692.4027999999998,
|
||||
695.0712000000003,
|
||||
773.1455000000001,
|
||||
722.5364,
|
||||
863.2840000000006
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-source-baseline-v23",
|
||||
"generatedUtc": "2026-08-22T12:30:30.4538464Z",
|
||||
"rendererVersion": 23,
|
||||
"contract": {
|
||||
"status": "pass",
|
||||
"rendererVersion": 23,
|
||||
"wasmPayloads": 8,
|
||||
"scriptBytes": 129807,
|
||||
"htmlBytes": 11395
|
||||
},
|
||||
"hosted": {
|
||||
"firstViewFiles": [
|
||||
{
|
||||
"file": "dist/hosted/index.html",
|
||||
"bytes": 11388,
|
||||
"sha256": "e92c68d4b3c36a438675aee5fff4349c75ebd8ba8bdbc85369bfb3f55d07fd73"
|
||||
},
|
||||
{
|
||||
"file": "dist/hosted/hosted-loader.js",
|
||||
"bytes": 2338,
|
||||
"sha256": "43804e07f0603d57a4c4d210cfbfb02e5fa7adc13e2dc53c1a15e05d1d74f4c1"
|
||||
},
|
||||
{
|
||||
"file": "dist/hosted/script.js",
|
||||
"bytes": 129807,
|
||||
"sha256": "6ffebe51532fb8a141e9b1939e586d76b6c79db18786c06713e67ffe8d4ca78a"
|
||||
},
|
||||
{
|
||||
"file": "dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm",
|
||||
"bytes": 511,
|
||||
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
|
||||
}
|
||||
],
|
||||
"firstViewUncompressedBytes": 144044,
|
||||
"deepRequestContractBeforeDeepView": 0,
|
||||
"deepRequestMeasurementStatus": "not-run",
|
||||
"note": "The zero is a static loader contract, not a measured request count. Transfer compression, request count, parse, compile, and runtime timings require the fixed Hosted browser benchmark."
|
||||
},
|
||||
"standalone": {
|
||||
"files": [
|
||||
{
|
||||
"file": "dist/standalone/index.html",
|
||||
"bytes": 11395,
|
||||
"sha256": "b2017e3aac73d22d1178e2a28bb5cb1ea0035cbcae5a4ed422dbe4ff7eb951fa"
|
||||
},
|
||||
{
|
||||
"file": "dist/standalone/script.js",
|
||||
"bytes": 129807,
|
||||
"sha256": "6ffebe51532fb8a141e9b1939e586d76b6c79db18786c06713e67ffe8d4ca78a"
|
||||
},
|
||||
{
|
||||
"file": "dist/standalone/kernels.js",
|
||||
"bytes": 31389,
|
||||
"sha256": "e97726c09af138da92b331c376766c1128c10aa3c361b83750356824262ea1a0"
|
||||
}
|
||||
],
|
||||
"uncompressedBytes": 172591
|
||||
},
|
||||
"sourceBuild": {
|
||||
"files": [
|
||||
{
|
||||
"file": "src/shallow_kernel.c",
|
||||
"bytes": 1512,
|
||||
"sha256": "ec1067a7a425e180eef7bebf250730de1144e63783735c9ed22084856da096d5"
|
||||
},
|
||||
{
|
||||
"file": "src/deep_kernel.c",
|
||||
"bytes": 5076,
|
||||
"sha256": "c484aa9255a7a33eb4ce4ab90c74ec45865885b9a04fd263ce230838d3ce5137"
|
||||
},
|
||||
{
|
||||
"file": "src/bla_kernel_v18.c",
|
||||
"bytes": 8744,
|
||||
"sha256": "66724b9afee313899110380f427ba6abbf5340af5c9af1b715eb24b8cff901a9"
|
||||
},
|
||||
{
|
||||
"file": "src/color_kernel.c",
|
||||
"bytes": 1284,
|
||||
"sha256": "a40c7bd511dba94b096bf5e075f1fb95bb9f98b5154bac712f5e2bdeb1432a09"
|
||||
},
|
||||
{
|
||||
"file": "src/abi.json",
|
||||
"bytes": 899,
|
||||
"sha256": "747f6dd6a0240684e9b502ec19a90458956e2d4fc583084bfe0b3ac0e879a0da"
|
||||
},
|
||||
{
|
||||
"file": "toolchain.lock.json",
|
||||
"bytes": 381,
|
||||
"sha256": "d51122cff57d4304880db660be37988403c44770e209e51fe33e5ba30c9a7f38"
|
||||
}
|
||||
],
|
||||
"compiler": "clang 17.0.6",
|
||||
"node": "v22.18.0",
|
||||
"compiledInThisEnvironment": true,
|
||||
"goldenStatus": "pass",
|
||||
"generatedManifest": {
|
||||
"file": "build/wasm-v23/manifest.json",
|
||||
"bytes": 4193,
|
||||
"sha256": "cc388d664f642181d45a110ea5b44c47524050f8bf904ebd115e8e24c0f58a18"
|
||||
},
|
||||
"reason": "Verified generated manifest reused; fixed toolchain was removed after the successful gate."
|
||||
},
|
||||
"browserBenchmark": {
|
||||
"status": "not-run",
|
||||
"report": "audit/v23-browser-baseline.json",
|
||||
"reason": "No in-app browser binding was available in this environment."
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"status": "pass",
|
||||
"kind": "view-specific-adaptive-backend-router",
|
||||
"directRequiredRatio": 8,
|
||||
"directDefaultRatio": 128,
|
||||
"deepSpeedMargin": 0.85,
|
||||
"deepProbeUnknownMax": 0.005,
|
||||
"cases": 10
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"status": "pass",
|
||||
"kind": "bla-disabled-contract",
|
||||
"reason": "dense swirly regression demonstrated unsafe f32-quantized BLA classifications",
|
||||
"productionBla": false
|
||||
}
|
||||
|
|
@ -1,308 +0,0 @@
|
|||
{
|
||||
"status": "pass",
|
||||
"kind": "guarded-production-equation-cpu-f32-model-not-real-gpu",
|
||||
"thresholds": {
|
||||
"balanced": 0.001,
|
||||
"strict": 0.0001
|
||||
},
|
||||
"report": [
|
||||
{
|
||||
"mode": "balanced",
|
||||
"id": "period2-cusp-z14",
|
||||
"bits": 285,
|
||||
"limit": 1200,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "balanced",
|
||||
"id": "swirly-seahorses-z12",
|
||||
"bits": 279,
|
||||
"limit": 2000,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 49,
|
||||
"known": 138,
|
||||
"exactEscape": 130,
|
||||
"mismatch": [
|
||||
{
|
||||
"x": 5,
|
||||
"y": 1,
|
||||
"oracle": 675,
|
||||
"gpu": {
|
||||
"kind": "escaped",
|
||||
"n": 674,
|
||||
"err": 0.06557769039826006
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 1,
|
||||
"oracle": 636,
|
||||
"gpu": {
|
||||
"kind": "escaped",
|
||||
"n": 635,
|
||||
"err": 0.12117791268198698
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "balanced",
|
||||
"id": "period2-cusp-z20",
|
||||
"bits": 305,
|
||||
"limit": 1800,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "balanced",
|
||||
"id": "period2-cusp-z100",
|
||||
"bits": 571,
|
||||
"limit": 2400,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "balanced",
|
||||
"id": "period3-interior",
|
||||
"bits": 267,
|
||||
"limit": 1200,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "balanced",
|
||||
"id": "period2-cusp-e280",
|
||||
"bits": 1171,
|
||||
"limit": 2400,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "balanced",
|
||||
"id": "period2-cusp-e400",
|
||||
"bits": 1569,
|
||||
"limit": 1600,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "strict",
|
||||
"id": "period2-cusp-z14",
|
||||
"bits": 285,
|
||||
"limit": 1200,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "strict",
|
||||
"id": "swirly-seahorses-z12",
|
||||
"bits": 279,
|
||||
"limit": 2000,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 49,
|
||||
"known": 138,
|
||||
"exactEscape": 130,
|
||||
"mismatch": [
|
||||
{
|
||||
"x": 5,
|
||||
"y": 1,
|
||||
"oracle": 675,
|
||||
"gpu": {
|
||||
"kind": "escaped",
|
||||
"n": 674,
|
||||
"err": 0.06557769039826006
|
||||
}
|
||||
},
|
||||
{
|
||||
"x": 6,
|
||||
"y": 1,
|
||||
"oracle": 636,
|
||||
"gpu": {
|
||||
"kind": "escaped",
|
||||
"n": 635,
|
||||
"err": 0.12117791268198698
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "strict",
|
||||
"id": "period2-cusp-z20",
|
||||
"bits": 305,
|
||||
"limit": 1800,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "strict",
|
||||
"id": "period2-cusp-z100",
|
||||
"bits": 571,
|
||||
"limit": 2400,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "strict",
|
||||
"id": "period3-interior",
|
||||
"bits": 267,
|
||||
"limit": 1200,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "strict",
|
||||
"id": "period2-cusp-e280",
|
||||
"bits": 1171,
|
||||
"limit": 2400,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
},
|
||||
{
|
||||
"mode": "strict",
|
||||
"id": "period2-cusp-e400",
|
||||
"bits": 1569,
|
||||
"limit": 1600,
|
||||
"guardMismatch": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"known": 187,
|
||||
"exactEscape": 0,
|
||||
"mismatch": []
|
||||
}
|
||||
],
|
||||
"denseRegression": [
|
||||
{
|
||||
"mode": "balanced",
|
||||
"x": 25,
|
||||
"y": 12,
|
||||
"oracle": 2000,
|
||||
"gpu": {
|
||||
"kind": "unknown",
|
||||
"n": 2000,
|
||||
"reason": "error-bound",
|
||||
"err": 34362354130585895000
|
||||
}
|
||||
},
|
||||
{
|
||||
"mode": "strict",
|
||||
"x": 25,
|
||||
"y": 12,
|
||||
"oracle": 2000,
|
||||
"gpu": {
|
||||
"kind": "unknown",
|
||||
"n": 2000,
|
||||
"reason": "error-bound",
|
||||
"err": 34362354130585895000
|
||||
}
|
||||
},
|
||||
{
|
||||
"mode": "balanced",
|
||||
"x": 26,
|
||||
"y": 15,
|
||||
"oracle": 2000,
|
||||
"gpu": {
|
||||
"kind": "unknown",
|
||||
"n": 2000,
|
||||
"reason": "error-bound",
|
||||
"err": 121379814641826710000
|
||||
}
|
||||
},
|
||||
{
|
||||
"mode": "strict",
|
||||
"x": 26,
|
||||
"y": 15,
|
||||
"oracle": 2000,
|
||||
"gpu": {
|
||||
"kind": "unknown",
|
||||
"n": 2000,
|
||||
"reason": "error-bound",
|
||||
"err": 121379814641826710000
|
||||
}
|
||||
},
|
||||
{
|
||||
"mode": "balanced",
|
||||
"x": 27,
|
||||
"y": 25,
|
||||
"oracle": 1957,
|
||||
"gpu": {
|
||||
"kind": "unknown",
|
||||
"n": 2000,
|
||||
"reason": "error-bound",
|
||||
"err": 32570060988.51048
|
||||
}
|
||||
},
|
||||
{
|
||||
"mode": "strict",
|
||||
"x": 27,
|
||||
"y": 25,
|
||||
"oracle": 1957,
|
||||
"gpu": {
|
||||
"kind": "unknown",
|
||||
"n": 2000,
|
||||
"reason": "error-bound",
|
||||
"err": 32570060988.51048
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"status": "not-run",
|
||||
"reason": "container Chromium does not expose a working WebGPU adapter / GPU process initialization fails",
|
||||
"benchmark": "UI Diagnostics > Backend比較",
|
||||
"depths": [
|
||||
0,
|
||||
2,
|
||||
4,
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
12,
|
||||
14,
|
||||
16,
|
||||
20,
|
||||
30,
|
||||
40
|
||||
],
|
||||
"timings": [
|
||||
"f32 numeric",
|
||||
"DS direct numeric",
|
||||
"Deep warm numeric",
|
||||
"Deep cold = verified BigInt reference + warm numeric"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"version": "24.2.3",
|
||||
"issue": "Backend comparison could appear to hang at z12 deep because the benchmark repeated corrected Deep over the entire current canvas and waited for monolithic GPU queue completion.",
|
||||
"fix": {
|
||||
"speedTile": [256, 144],
|
||||
"speedTileCount": 3,
|
||||
"timingModel": "sum representative tile GPU queue completion time, derive msPerPixel, extrapolate to current full canvas",
|
||||
"qualityTileCount": 5,
|
||||
"gpuTimeoutMs": 15000,
|
||||
"referenceTimeoutMs": 15000,
|
||||
"productionRendererChanged": false,
|
||||
"benchmarkFormat": "mandelbrot-webgpu-crossover-v2"
|
||||
},
|
||||
"verification": {
|
||||
"wgslReserved": "pass",
|
||||
"sourceContract": "pass",
|
||||
"indexContract": "pass",
|
||||
"treeContract": "pass",
|
||||
"crossoverQuality": "pass",
|
||||
"crossoverContract": "pass",
|
||||
"referenceWorker": "pass",
|
||||
"directModel": "pass",
|
||||
"backendPolicy": "pass",
|
||||
"cpuNumericModel": "pass (individual run)",
|
||||
"sparseCorrectionModel": "pass (individual run)",
|
||||
"blaDisabledContract": "pass",
|
||||
"geometryContract": "pass",
|
||||
"coordinateFormat": "pass",
|
||||
"pngStream": "pass",
|
||||
"acceptanceContract": "pass",
|
||||
"build": "pass",
|
||||
"realWebGPU": "not available in this container"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,766 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-crossover-quality-v1",
|
||||
"kind": "cpu-f32-model + BigInt P/P+64 oracle; not real GPU",
|
||||
"viewport": [
|
||||
1600,
|
||||
900
|
||||
],
|
||||
"sampleGrid": [
|
||||
21,
|
||||
15
|
||||
],
|
||||
"depths": [
|
||||
0,
|
||||
2,
|
||||
4,
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
12,
|
||||
14,
|
||||
16,
|
||||
20,
|
||||
30,
|
||||
40
|
||||
],
|
||||
"tracks": [
|
||||
"cusp",
|
||||
"seahorse"
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 0,
|
||||
"limit": 350,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 21,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 188
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 2,
|
||||
"limit": 478,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 21,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1107
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 4,
|
||||
"limit": 550,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 21,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 851
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 6,
|
||||
"limit": 611,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 761
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 8,
|
||||
"limit": 667,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 15
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 920
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 10,
|
||||
"limit": 721,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 15
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 998
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 12,
|
||||
"limit": 772,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 15
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1005
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 14,
|
||||
"limit": 821,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 15
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1099
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 16,
|
||||
"limit": 870,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 15
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1134
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 20,
|
||||
"limit": 963,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 15
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1271
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 30,
|
||||
"limit": 1183,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 15
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1589
|
||||
},
|
||||
{
|
||||
"track": "cusp",
|
||||
"z": 40,
|
||||
"limit": 1392,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 15
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1856
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 0,
|
||||
"limit": 350,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 21,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 103
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 2,
|
||||
"limit": 478,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 21,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 2,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 2,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 366
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 4,
|
||||
"limit": 550,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 21,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 2,
|
||||
"escapeIterationMismatch": 36,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 2,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 1
|
||||
},
|
||||
"modelMs": 161
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 6,
|
||||
"limit": 611,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 2,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 5,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 104,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 1,
|
||||
"escapeIterationMismatch": 2,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 230
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 8,
|
||||
"limit": 667,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 4,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 307,
|
||||
"uniqueCoords": 8
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 1,
|
||||
"falseBounded": 1,
|
||||
"escapeIterationMismatch": 14,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 1,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 2
|
||||
},
|
||||
"modelMs": 376
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 10,
|
||||
"limit": 721,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 10,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 305,
|
||||
"uniqueCoords": 2
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 6,
|
||||
"falseBounded": 6,
|
||||
"escapeIterationMismatch": 34,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 512
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 12,
|
||||
"limit": 772,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 21
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 54,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 261,
|
||||
"uniqueCoords": 1
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 14,
|
||||
"falseBounded": 13,
|
||||
"escapeIterationMismatch": 103,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 3
|
||||
},
|
||||
"modelMs": 919
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 14,
|
||||
"limit": 821,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 1
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 315,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 1
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 315
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1158
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 16,
|
||||
"limit": 870,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 1
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 315,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 1
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 1
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1211
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 20,
|
||||
"limit": 963,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 1
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 315,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 1
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 1
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1374
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 30,
|
||||
"limit": 1183,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 1
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 315,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 1
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 1
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 1968
|
||||
},
|
||||
{
|
||||
"track": "seahorse",
|
||||
"z": 40,
|
||||
"limit": 1392,
|
||||
"tested": 315,
|
||||
"guardMismatch": 0,
|
||||
"adjacentUniqueX": {
|
||||
"f32": 1,
|
||||
"ds": 1
|
||||
},
|
||||
"f32": {
|
||||
"falseEscaped": 315,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 1
|
||||
},
|
||||
"ds": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 0,
|
||||
"uniqueCoords": 1
|
||||
},
|
||||
"deep": {
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 0
|
||||
},
|
||||
"modelMs": 2066
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"firstMaterialF32DegradationDepth": 6,
|
||||
"firstMaterialDsDegradationDepth": 10,
|
||||
"deepIssueRows": [
|
||||
{
|
||||
"z": 8,
|
||||
"falseEscaped": 1,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 2
|
||||
}
|
||||
],
|
||||
"interpretation": "On the Seahorse track at a 1600x900 viewport, f32 direct becomes materially unsuitable around z6; DS direct remains a plausible bridge through roughly z8 and degrades by z10. Corrected deep is the best of the modeled backends, but one z8 false escape means it is not a membership certificate."
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"version": "24.2.7-fixed96-hotfix",
|
||||
"rootCause": "fx_bad_range treated the Q8.88 integer byte as redundant sign extension and rejected valid values such as +1.2",
|
||||
"symptom": "correction rate approximately equaled remaining-unknown rate on the real GPU",
|
||||
"fix": [
|
||||
"removed invalid fx_bad_range check",
|
||||
"classify |Re(z)|>2 or |Im(z)|>2 as certain escape before squaring",
|
||||
"square for exact |z|^2>4 only while both components are within +/-2",
|
||||
"validate Q8.88 coordinate range and pixel-step underflow before packing"
|
||||
],
|
||||
"regressions": [
|
||||
"v24-fixed96-contract.mjs",
|
||||
"v24-fixed96-limb-model.mjs",
|
||||
"v24-fixed96-limb-orbit-model.mjs",
|
||||
"v24-fixed96-shader-range-model.mjs",
|
||||
"v24-fixed96-model.mjs"
|
||||
],
|
||||
"realWebGPU": "pending user rerun"
|
||||
}
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-fixed96-q88-model-v1",
|
||||
"kind": "Exact BigInt model of signed Q8.88 direct + DS risk gate + BigInt P/P+64 oracle; integer GPU arithmetic should be deterministic modulo implementation bugs",
|
||||
"viewport": [
|
||||
1600,
|
||||
900
|
||||
],
|
||||
"sampleGrid": [
|
||||
31,
|
||||
21
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"z": 6,
|
||||
"limit": 611,
|
||||
"variants": [
|
||||
{
|
||||
"risk": 100000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 43,
|
||||
"uncertainRate": 0.06605222734254992,
|
||||
"wrongBefore": 4,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 1000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 23,
|
||||
"uncertainRate": 0.03533026113671275,
|
||||
"wrongBefore": 4,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 10000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 18,
|
||||
"uncertainRate": 0.027649769585253458,
|
||||
"wrongBefore": 4,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 100000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 10,
|
||||
"uncertainRate": 0.015360983102918587,
|
||||
"wrongBefore": 4,
|
||||
"wrongAfter": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"z": 8,
|
||||
"limit": 667,
|
||||
"variants": [
|
||||
{
|
||||
"risk": 100000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 202,
|
||||
"uncertainRate": 0.3102918586789555,
|
||||
"wrongBefore": 4,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 1000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 126,
|
||||
"uncertainRate": 0.1935483870967742,
|
||||
"wrongBefore": 4,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 10000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 72,
|
||||
"uncertainRate": 0.11059907834101383,
|
||||
"wrongBefore": 4,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 100000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 46,
|
||||
"uncertainRate": 0.0706605222734255,
|
||||
"wrongBefore": 4,
|
||||
"wrongAfter": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"z": 10,
|
||||
"limit": 721,
|
||||
"variants": [
|
||||
{
|
||||
"risk": 100000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 648,
|
||||
"uncertainRate": 0.9953917050691244,
|
||||
"wrongBefore": 10,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 1000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 398,
|
||||
"uncertainRate": 0.6113671274961597,
|
||||
"wrongBefore": 10,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 10000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 225,
|
||||
"uncertainRate": 0.3456221198156682,
|
||||
"wrongBefore": 10,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 100000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 153,
|
||||
"uncertainRate": 0.2350230414746544,
|
||||
"wrongBefore": 10,
|
||||
"wrongAfter": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"z": 12,
|
||||
"limit": 772,
|
||||
"variants": [
|
||||
{
|
||||
"risk": 100000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 602,
|
||||
"uncertainRate": 0.9247311827956989,
|
||||
"wrongBefore": 51,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 1000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 600,
|
||||
"uncertainRate": 0.9216589861751152,
|
||||
"wrongBefore": 51,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 10000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 594,
|
||||
"uncertainRate": 0.9124423963133641,
|
||||
"wrongBefore": 51,
|
||||
"wrongAfter": 0
|
||||
},
|
||||
{
|
||||
"risk": 100000000000000,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"uncertain": 488,
|
||||
"uncertainRate": 0.7496159754224271,
|
||||
"wrongBefore": 51,
|
||||
"wrongAfter": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"status": "prepared",
|
||||
"version": "24.2.6-fixed96-experiment",
|
||||
"purpose": "DS risk gate -> sparse signed Q8.88 u32-limb direct correction -> compare with Full Deep",
|
||||
"productionRouterChanged": false,
|
||||
"standaloneUi": {
|
||||
"rootIndexSingleFile": true,
|
||||
"externalScriptTags": 0,
|
||||
"button": "Fixed96 Direct実験",
|
||||
"allRiskRowsVisible": true
|
||||
},
|
||||
"fixedPoint": {
|
||||
"bits": 96,
|
||||
"fractionBits": 88,
|
||||
"format": "signed Q8.88",
|
||||
"limbs": 3,
|
||||
"limbType": "u32"
|
||||
},
|
||||
"cpuPreflight": {
|
||||
"status": "pass",
|
||||
"report": "audit/v24-fixed96-model.json",
|
||||
"limbArithmetic": "tests/v24-fixed96-limb-model.mjs",
|
||||
"safeCandidateAtDepths": [
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
12
|
||||
]
|
||||
},
|
||||
"realWebGPU": "pending: run tests/webgpu-acceptance.html and Diagnostics > Fixed96 Direct実験 on a WebGPU-capable browser"
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
{
|
||||
"version": "24.2.8-fixed96-sparse-queue",
|
||||
"purpose": "Make the diagnostic Fixed96 correction truly sparse on GPU and fail closed on experiment-system counter/dispatch inconsistencies.",
|
||||
"productionRouterChanged": false,
|
||||
"execution": {
|
||||
"guard": "full-tile DS sensitivity pass; risky local pixel indices atomically compacted",
|
||||
"finalize": "1 invocation writes indirect args from GPU queue length",
|
||||
"correction": "1D @workgroup_size(64), dispatchWorkgroupsIndirect, one queued pixel per invocation",
|
||||
"cpuReadbackBetweenPasses": false
|
||||
},
|
||||
"integrityGate": [
|
||||
"selected == enqueued == dispatchCount == processed",
|
||||
"overflow == 0",
|
||||
"indirectWorkgroups == ceil(enqueued/64)",
|
||||
"invalidIndex == 0",
|
||||
"staleEntry == 0",
|
||||
"corrected + remain == processed"
|
||||
],
|
||||
"tests": {
|
||||
"fixed96Contract": "pass",
|
||||
"queueIntegrityModel": "pass (7 cases)",
|
||||
"wgslReserved": "pass (10 shaders)",
|
||||
"fixed96LimbMultiply": "pass (20000 cases)",
|
||||
"fixed96LimbOrbit": "pass",
|
||||
"fixed96RangeRegression": "pass",
|
||||
"singleFileUiContract": "pass",
|
||||
"sourceContract": "pass",
|
||||
"acceptanceContract": "pass",
|
||||
"realWebGPU": "pending external run",
|
||||
"samplingConsistencyGate": "pass (speed queue rate vs quality correction rate)"
|
||||
},
|
||||
"sourceSha256": {
|
||||
"script.js": "0104d437471b9ad1c3a9bc69206e1562b6841846e0e2343cde2c9ce5632d0a4d",
|
||||
"gpu-kernels.js": "364e2babe5e986dc02d17c3e8897891e34d73498cc4c84d93c7f09cc2b6f3bd6",
|
||||
"index.external.html": "23527ec2708d162bfe40c285952e170ce783cfafb63f37a5c7b5c7ecbfdec80c",
|
||||
"tests/v24-fixed96-contract.mjs": "a85f569764a5623720614c79503398e5993846fa28d0a8aec45979e0edc225ce",
|
||||
"tests/v24-fixed96-queue-model.mjs": "e29f84189314a9a15ae3e08da85f5bcf2d990ec9c6ab2e9c731ae4639d8f3d22",
|
||||
"tests/webgpu-acceptance.js": "e5580f7cfc402aa5b533169cda4a088d3f10e0086002c508bf989b1ffff19cb5"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-v24.2.4-hybrid-preparation-v1",
|
||||
"createdAt": "2026-08-23T11:36:39Z",
|
||||
"shaderVersion": "24.2.4-hybrid-experiment",
|
||||
"productionRouterUnchanged": true,
|
||||
"hybridDepths": [
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
12
|
||||
],
|
||||
"riskLimits": [
|
||||
1000000000000.0,
|
||||
10000000000000.0,
|
||||
100000000000000.0
|
||||
],
|
||||
"cpuPriorRisk1e13": {
|
||||
"z6": {
|
||||
"correctionRate": 0.027649769585253458,
|
||||
"caughtClassErrors": "4/4"
|
||||
},
|
||||
"z8": {
|
||||
"correctionRate": 0.11059907834101383,
|
||||
"caughtClassErrors": "4/4"
|
||||
},
|
||||
"z10": {
|
||||
"correctionRate": 0.3456221198156682,
|
||||
"caughtClassErrors": "10/10"
|
||||
},
|
||||
"z12": {
|
||||
"correctionRate": 0.9124423963133641,
|
||||
"caughtClassErrors": "51/51"
|
||||
}
|
||||
},
|
||||
"tests": {
|
||||
"wgslReserved": "pass",
|
||||
"hybridContract": "pass",
|
||||
"hybridRiskModel": "pass",
|
||||
"crossoverContract": "pass",
|
||||
"sourceContract": "pass",
|
||||
"treeContract": "pass",
|
||||
"referenceWorker": "pass",
|
||||
"directModel": "pass",
|
||||
"backendPolicy": "pass",
|
||||
"cpuDeepModel": "pass",
|
||||
"sparseCorrection": "pass",
|
||||
"blaDisabled": "pass",
|
||||
"geometry": "pass",
|
||||
"coordinateFormat": "pass",
|
||||
"pngStreaming": "pass",
|
||||
"acceptanceContract": "pass"
|
||||
},
|
||||
"realWebGPU": "not-run-in-container; run Hybrid実験 on target GPU",
|
||||
"hashes": {
|
||||
"source/index.html": "39427d275732e5bbce8d06253ce5f3e0a07e173081fc230cd34292050ec29827",
|
||||
"source/gpu-kernels.js": "2773c39e416c52a7919aad0146cd95cf6fc108847ba3718db9253986a4f489e5",
|
||||
"source/script.js": "d354ff6ce0937c616a02d34ed0accebe5c8c5640cc6ac98720d66f8d784359be",
|
||||
"standalone/index.html": "39427d275732e5bbce8d06253ce5f3e0a07e173081fc230cd34292050ec29827",
|
||||
"standalone/gpu-kernels.js": "2773c39e416c52a7919aad0146cd95cf6fc108847ba3718db9253986a4f489e5",
|
||||
"standalone/script.js": "d354ff6ce0937c616a02d34ed0accebe5c8c5640cc6ac98720d66f8d784359be"
|
||||
},
|
||||
"sourceStandaloneMatch": true
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-hybrid-risk-model-v1",
|
||||
"kind": "CPU Math.fround DS + BigInt P/P+64 oracle; not real GPU",
|
||||
"viewport": [
|
||||
1600,
|
||||
900
|
||||
],
|
||||
"sampleGrid": [
|
||||
31,
|
||||
21
|
||||
],
|
||||
"riskLimit": 10000000000000,
|
||||
"rows": [
|
||||
{
|
||||
"z": 6,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"wrong": 4,
|
||||
"uncertain": 18,
|
||||
"caught": 4,
|
||||
"uncertainRate": 0.027649769585253458,
|
||||
"catchRate": 1
|
||||
},
|
||||
{
|
||||
"z": 8,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"wrong": 4,
|
||||
"uncertain": 72,
|
||||
"caught": 4,
|
||||
"uncertainRate": 0.11059907834101383,
|
||||
"catchRate": 1
|
||||
},
|
||||
{
|
||||
"z": 10,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"wrong": 10,
|
||||
"uncertain": 225,
|
||||
"caught": 10,
|
||||
"uncertainRate": 0.3456221198156682,
|
||||
"catchRate": 1
|
||||
},
|
||||
{
|
||||
"z": 12,
|
||||
"tested": 651,
|
||||
"guardMismatch": 0,
|
||||
"wrong": 51,
|
||||
"uncertain": 594,
|
||||
"caught": 51,
|
||||
"uncertainRate": 0.9124423963133641,
|
||||
"catchRate": 1
|
||||
}
|
||||
],
|
||||
"interpretation": "At riskLimit=1e13 the derivative guard catches every observed DS classification error at z6/z8/z10 while sending about 3%/11%/35% of samples to Deep correction. At z12 it sends over 75%, which is a deliberate signal that Full Deep should replace the hybrid."
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-v24.2.5-hybrid-ui-hotfix-v1",
|
||||
"createdAt": "2026-08-23T11:54:13Z",
|
||||
"rootCause": "Toolbar Hybrid experiment button opened the dialog only; actual measurement required a second click on the dialog start button.",
|
||||
"fix": [
|
||||
"Toolbar click opens the dialog and auto-starts on the next animation frame.",
|
||||
"Dialog primary button is a rerun action.",
|
||||
"Concurrent Backend benchmark state is surfaced explicitly instead of silently doing nothing.",
|
||||
"Hybrid running state is surfaced explicitly.",
|
||||
"Static contract tests assert toolbar auto-start and rerun wiring."
|
||||
],
|
||||
"tests": {
|
||||
"syntax": "pass",
|
||||
"hybridContract": "pass",
|
||||
"sourceContract": "pass",
|
||||
"wgslReserved": "pass",
|
||||
"cpuNumeric": "pass",
|
||||
"sparseCorrection": "pass",
|
||||
"build": "pass"
|
||||
},
|
||||
"realWebGPU": "not-run-in-container; UI execution contract fixed independently of GPU availability",
|
||||
"sha256": {
|
||||
"script.js": "b02a47d6b6b8ea7c95e85ee26e69f877ef5c8c9cffb34e68ca37121c2fa652ac",
|
||||
"gpu-kernels.js": "6e00107df84dd6e5427774ffca2cfdf6c7bfae0989fd69054b11f500a97e048e",
|
||||
"index.html": "d12a742455fa2febf3639beddad063a3217901923b65927aeb1c21b224c00f3a",
|
||||
"package.json": "93662efdf99f04b4bb2497f6f9512ad3c617fef7209843469f8a50ed5d6a6786",
|
||||
"tests/v24-hybrid-contract.mjs": "18cb1e4b5fd9656285b021de6bea02ca767c7531cb9ee57b3cde3c38f3e887c0"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
{
|
||||
"status": "v24.2.8-fixed96-sparse-queue-real-webgpu-pending",
|
||||
"reason": "container browser does not expose a usable WebGPU adapter / GPU process initialization fails",
|
||||
"requiredExternalRuns": [
|
||||
"tests/webgpu-acceptance.html",
|
||||
"Diagnostics > Fixed96 Direct実験"
|
||||
],
|
||||
"experimentDepths": [
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
12
|
||||
],
|
||||
"riskLimits": [
|
||||
100000000000.0,
|
||||
1000000000000.0,
|
||||
10000000000000.0,
|
||||
100000000000000.0
|
||||
],
|
||||
"outputColumns": [
|
||||
"z",
|
||||
"risk",
|
||||
"fixed96 est",
|
||||
"deep cold",
|
||||
"speedup",
|
||||
"correction",
|
||||
"speed q",
|
||||
"dispatch",
|
||||
"remain",
|
||||
"Δclass",
|
||||
"queue",
|
||||
"status"
|
||||
],
|
||||
"queueAcceptance": [
|
||||
"selected == enqueued == dispatchCount == processed",
|
||||
"overflow == 0",
|
||||
"indirectWorkgroups == ceil(enqueued/64)",
|
||||
"invalidIndex == 0",
|
||||
"staleEntry == 0",
|
||||
"corrected + remain == processed"
|
||||
],
|
||||
"cpuQualityReport": "audit/v24-fixed96-model.json"
|
||||
}
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-v24.2.2-crossover-release-manifest",
|
||||
"generatedAt": "2026-08-23T10:22:38.264933+00:00",
|
||||
"files": [
|
||||
{
|
||||
"path": "AUDIT_V24.md",
|
||||
"bytes": 3906,
|
||||
"sha256": "0ac5444e8a798e183312cf4a9c4dbe08133bccf471feac13d5e6ed06c91e5133"
|
||||
},
|
||||
{
|
||||
"path": "CROSSOVER_BENCHMARK.md",
|
||||
"bytes": 3766,
|
||||
"sha256": "5f72a5a73cc73b2308e14b4a100bfd546f7067433e24db8b2b33b809f535c629"
|
||||
},
|
||||
{
|
||||
"path": "IMPLEMENTATION_REPORT.md",
|
||||
"bytes": 2869,
|
||||
"sha256": "983e5c8b217b957612f7bbabdda04c7df8da16ba28941c16a4a4b5f8e85e7cc5"
|
||||
},
|
||||
{
|
||||
"path": "MIGRATION_V24.md",
|
||||
"bytes": 4710,
|
||||
"sha256": "b4d55a30f38ba4c56e0aace0e69f00714878f5098f88d5c9a4f9e76546b24c16"
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"bytes": 6040,
|
||||
"sha256": "c651c78374c5288cd2d5739d9cd166d38840db6069d0b80b262888d333ce5c24"
|
||||
},
|
||||
{
|
||||
"path": "audit/legacy-v23/final-browser-benchmark.json",
|
||||
"bytes": 2686,
|
||||
"sha256": "bca2e141c101722f8a077d43346c2c28b5d8d7c99e70415730031065c8ab5a1c"
|
||||
},
|
||||
{
|
||||
"path": "audit/v24-backend-policy.json",
|
||||
"bytes": 205,
|
||||
"sha256": "d34d894ab9d062439e7cfe5074e47ab295defa74044ed3e2619ef0289759b2fe"
|
||||
},
|
||||
{
|
||||
"path": "audit/v24-bla-status.json",
|
||||
"bytes": 177,
|
||||
"sha256": "84587eb2183b1e6a2837f5018e74f32bc968c536d64cee7cf5fed55f7d3b21df"
|
||||
},
|
||||
{
|
||||
"path": "audit/v24-cpu-numeric-model.json",
|
||||
"bytes": 6217,
|
||||
"sha256": "8c8f7320ae29f367d4eea111eeb658a861dd0e001e5fc2653857c24b3baf71c1"
|
||||
},
|
||||
{
|
||||
"path": "audit/v24-crossover-gpu-status.json",
|
||||
"bytes": 441,
|
||||
"sha256": "dbd94c4b034c2ae521c5f591dfd745c40c9cf9d17bc08dbc00f4017216949f90"
|
||||
},
|
||||
{
|
||||
"path": "audit/v24-crossover-quality.json",
|
||||
"bytes": 16247,
|
||||
"sha256": "063deef5079d077d35d5cf349186405edcc0f17c5812f38051735a455de3ea20"
|
||||
},
|
||||
{
|
||||
"path": "audit/v24-real-webgpu-status.json",
|
||||
"bytes": 601,
|
||||
"sha256": "ec5171b4f059f4f810b64db1ba82d83979571001f17f564919322c69e59ba721"
|
||||
},
|
||||
{
|
||||
"path": "audit/v24-release-verification.json",
|
||||
"bytes": 944,
|
||||
"sha256": "b54797dfdc9e99c3e981ac39b45ef92281af5da5bb89d0ef21b837b41e4c4463"
|
||||
},
|
||||
{
|
||||
"path": "audit/v24-sparse-correction-model.json",
|
||||
"bytes": 600,
|
||||
"sha256": "0202491ac101543055d9a70c11ab3ed7bf7d253617875271c9d88c7ccd1f2acb"
|
||||
},
|
||||
{
|
||||
"path": "audit/v24-test-summary.json",
|
||||
"bytes": 1665,
|
||||
"sha256": "b7afe3205c814318d2d7646bbb235b2e4c8411ecedf746e72924cb20cfcc562b"
|
||||
},
|
||||
{
|
||||
"path": "dist/hosted/_headers",
|
||||
"bytes": 68,
|
||||
"sha256": "3d11be43c945209eb7e4503e384ac3007c42ee4022cfc23fc005f6217bda8d1b"
|
||||
},
|
||||
{
|
||||
"path": "dist/hosted/gpu-kernels.js",
|
||||
"bytes": 22425,
|
||||
"sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329"
|
||||
},
|
||||
{
|
||||
"path": "dist/hosted/index.html",
|
||||
"bytes": 13092,
|
||||
"sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
|
||||
},
|
||||
{
|
||||
"path": "dist/hosted/script.js",
|
||||
"bytes": 85737,
|
||||
"sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2"
|
||||
},
|
||||
{
|
||||
"path": "dist/standalone/gpu-kernels.js",
|
||||
"bytes": 22425,
|
||||
"sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329"
|
||||
},
|
||||
{
|
||||
"path": "dist/standalone/index.html",
|
||||
"bytes": 13092,
|
||||
"sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
|
||||
},
|
||||
{
|
||||
"path": "dist/standalone/script.js",
|
||||
"bytes": 85737,
|
||||
"sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2"
|
||||
},
|
||||
{
|
||||
"path": "gpu-kernels.js",
|
||||
"bytes": 22425,
|
||||
"sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329"
|
||||
},
|
||||
{
|
||||
"path": "index.html",
|
||||
"bytes": 13092,
|
||||
"sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
|
||||
},
|
||||
{
|
||||
"path": "package.json",
|
||||
"bytes": 199,
|
||||
"sha256": "7c8ccf08afdaef9dc91c9041a5f9b9e1d730a1a9ba237f11d46ec293dc9690ab"
|
||||
},
|
||||
{
|
||||
"path": "script.js",
|
||||
"bytes": 85737,
|
||||
"sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2"
|
||||
},
|
||||
{
|
||||
"path": "scripts/build.mjs",
|
||||
"bytes": 657,
|
||||
"sha256": "84706582dd01d6de28cbc0fcdba9758c3f6f72a3d5693b74736109b35819997f"
|
||||
},
|
||||
{
|
||||
"path": "scripts/test-all.mjs",
|
||||
"bytes": 934,
|
||||
"sha256": "c3ec078b2591cba429eef90cc4f58a7c55e97756c083ed7b2d9437342ad450b4"
|
||||
},
|
||||
{
|
||||
"path": "tests/legacy-v23/numeric-policy-v23.json",
|
||||
"bytes": 787,
|
||||
"sha256": "7afe223d27715a79e34f8c9ea11993f1c37e75bc5d49a6b3284f412d8171a00d"
|
||||
},
|
||||
{
|
||||
"path": "tests/scenes.json",
|
||||
"bytes": 3225,
|
||||
"sha256": "a92c5535c54106ce18823443244f115bf5a2a3d42abfc4ee3573f38e8234e553"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-acceptance-contract.mjs",
|
||||
"bytes": 1035,
|
||||
"sha256": "114d90b92f89b8b27f6ba35b904464908ec0e1e6393adb02ade8a8dfe5a7bbf0"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-backend-policy.mjs",
|
||||
"bytes": 1192,
|
||||
"sha256": "81cffe6276ed660210646067502ab9edcce7d0fc608048a98c06633760714b5a"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-bla-model.mjs",
|
||||
"bytes": 677,
|
||||
"sha256": "04f22c659b61635792b9662b718ec9ae6140189a3daf553ac580b841ee48bd6b"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-coordinate-format.mjs",
|
||||
"bytes": 1253,
|
||||
"sha256": "a993f04f12fceb4d38c16206636f7951d8bc4afad63a57491e0164bb3575ca3a"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-cpu-numeric-model.mjs",
|
||||
"bytes": 8452,
|
||||
"sha256": "2ee7a2ef16357db9b537da4534611ea59c80d7279980cf8957adf069093f7fd6"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-crossover-contract.mjs",
|
||||
"bytes": 1460,
|
||||
"sha256": "1b11958712e27308074123dea0376e067e4cb5dd29cf28a68d87bcf42b7e06ff"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-crossover-quality.mjs",
|
||||
"bytes": 13215,
|
||||
"sha256": "8ab9ca4da65aee229d0b5d6bdf4a068923a337ed75263514678d4b71a8273e41"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-direct-model.mjs",
|
||||
"bytes": 2452,
|
||||
"sha256": "9c9ba9cfeaf5e352059a639ba1014ae7a3b0382309a634ee128bc99e62ee3dc5"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-geometry-contract.mjs",
|
||||
"bytes": 1177,
|
||||
"sha256": "2e5e6fb87f4b7130eff8fff72fd776ef8b0d0e70e91845136a3b18cbf6c86701"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-index-contract.mjs",
|
||||
"bytes": 504,
|
||||
"sha256": "90f52bb91f12db47da1ebf39a6770cb58d5c4e2809f4d6aa2fdb1263d77cee57"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-png-stream-model.mjs",
|
||||
"bytes": 3411,
|
||||
"sha256": "ca4dee6252bf6545310ba52c1433e82e83886d04cdb1feb20c7ab08045f431a7"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-reference-worker.mjs",
|
||||
"bytes": 2140,
|
||||
"sha256": "4d637c549cdda2c28666f5bfe2925c5742103c44b752c337cadbe9bc9df6431d"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-source-contract.mjs",
|
||||
"bytes": 4239,
|
||||
"sha256": "349c6d344f5f9325cb811cbb9f63017cb2d27139e7557be829f7c85f13bb39d4"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-sparse-correction-model.mjs",
|
||||
"bytes": 9810,
|
||||
"sha256": "eb72f0aa062bcf8bf915c048ba89e58a7ad5ab338ea559cfc467ba260255c241"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-tree-contract.mjs",
|
||||
"bytes": 691,
|
||||
"sha256": "933c8c5035806584ee3ffc7645b14aa60f262920528ebb8e5e47666023174770"
|
||||
},
|
||||
{
|
||||
"path": "tests/v24-wgsl-reserved.mjs",
|
||||
"bytes": 2260,
|
||||
"sha256": "98c153ac038852d3402f36ee18ce1f8263de1a016240b4363f01d84a31f0f0ab"
|
||||
},
|
||||
{
|
||||
"path": "tests/webgpu-acceptance.html",
|
||||
"bytes": 405,
|
||||
"sha256": "89b03a08f248b048eead508fc3599057a8cf5afc1d9f846de206a69272dd95bd"
|
||||
},
|
||||
{
|
||||
"path": "tests/webgpu-acceptance.js",
|
||||
"bytes": 6884,
|
||||
"sha256": "e1ef5b21906b4da0fb7154f1e3e555c36267d8a5a7a1bd7ed0474eee147bfd13"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-v24.2.2-crossover-release-verification",
|
||||
"generatedAt": "2026-08-23T10:22:38.262457+00:00",
|
||||
"version": "24.2.2",
|
||||
"sourceHashes": {
|
||||
"script.js": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2",
|
||||
"gpu-kernels.js": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329",
|
||||
"index.html": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6"
|
||||
},
|
||||
"standaloneMatchesSource": true,
|
||||
"hostedMatchesSource": true,
|
||||
"qualitySweep": "audit/v24-crossover-quality.json",
|
||||
"gpuTiming": "pending external WebGPU adapter; use Diagnostics > Backend比較",
|
||||
"gates": {
|
||||
"wgslReserved": "pass",
|
||||
"sourceContract": "pass",
|
||||
"indexContract": "pass",
|
||||
"crossoverContract": "pass",
|
||||
"crossoverQuality": "pass",
|
||||
"cpuNumericModel": "pass individually",
|
||||
"sparseCorrectionModel": "pass individually",
|
||||
"build": "pass",
|
||||
"realWebGPU": "pending"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"status": "pass",
|
||||
"kind": "sparse-double-single-correction-cpu-model-not-real-gpu",
|
||||
"scene": "swirly-seahorses-z12",
|
||||
"grid": [
|
||||
61,
|
||||
39
|
||||
],
|
||||
"limit": 2000,
|
||||
"referenceLength": 2000,
|
||||
"guardMismatch": 0,
|
||||
"primary": {
|
||||
"unknown": 478,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0
|
||||
},
|
||||
"correction": {
|
||||
"corrected": 478,
|
||||
"remaining": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0,
|
||||
"escapeIterationMismatch": 5,
|
||||
"maxEscapeIterationDifference": 374
|
||||
},
|
||||
"note": "DS correction is a visual-quality recovery pass; escape iteration equality is not certified."
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
{
|
||||
"status": "v24.2.2-static-cpu-build-pass-real-webgpu-timing-pending",
|
||||
"rendererVersion": 24,
|
||||
"shaderVersion": "24.2.2-benchmark",
|
||||
"packageVersion": "24.2.2",
|
||||
"generatedAt": "2026-08-23T10:22:11.466258+00:00",
|
||||
"production": {
|
||||
"legacyDeepAssets": 0,
|
||||
"bigIntPixelRenderer": false,
|
||||
"bla": false,
|
||||
"multiReference": false,
|
||||
"sparseDoubleSingleCorrection": true,
|
||||
"viewSpecificBackendProbe": true,
|
||||
"correctedDeepExport": true,
|
||||
"experimentalWholeFrameDsDirect": "benchmark-only"
|
||||
},
|
||||
"crossoverQuality": {
|
||||
"firstMaterialF32DegradationDepth": 6,
|
||||
"firstMaterialDsDegradationDepth": 10,
|
||||
"deepIssueRows": [
|
||||
{
|
||||
"z": 8,
|
||||
"falseEscaped": 1,
|
||||
"falseBounded": 0,
|
||||
"unknown": 0,
|
||||
"escapeIterationMismatch": 2
|
||||
}
|
||||
],
|
||||
"interpretation": "On the Seahorse track at a 1600x900 viewport, f32 direct becomes materially unsuitable around z6; DS direct remains a plausible bridge through roughly z8 and degrades by z10. Corrected deep is the best of the modeled backends, but one z8 false escape means it is not a membership certificate."
|
||||
},
|
||||
"sparseCorrection": {
|
||||
"primaryUnknown": 478,
|
||||
"corrected": 478,
|
||||
"remaining": 0,
|
||||
"falseEscaped": 0,
|
||||
"falseBounded": 0
|
||||
},
|
||||
"build": {
|
||||
"status": "pass",
|
||||
"sourceDistHashMatch": true
|
||||
},
|
||||
"realWebGPU": {
|
||||
"status": "not-run-in-container",
|
||||
"benchmark": "Diagnostics > Backend比較",
|
||||
"hardwareTimingRequired": true
|
||||
},
|
||||
"note": "Heavy CPU model gates were also run individually. The aggregate npm test wrapper can exceed the execution harness time budget even though its constituent gates pass."
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,85 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-source-wasm-manifest-v23",
|
||||
"toolchain": {
|
||||
"format": "mandelbrot-wasm-toolchain-v1",
|
||||
"compiler": "clang",
|
||||
"version": "17.0.6",
|
||||
"target": "wasm32-unknown-unknown",
|
||||
"commonFlags": [
|
||||
"-O3",
|
||||
"-nostdlib",
|
||||
"-ffreestanding",
|
||||
"-fno-builtin",
|
||||
"-Wl,--no-entry",
|
||||
"-Wl,--export-memory",
|
||||
"-Wl,--strip-all"
|
||||
],
|
||||
"scalarFlags": [
|
||||
"-mno-simd128",
|
||||
"-fno-vectorize",
|
||||
"-fno-slp-vectorize"
|
||||
],
|
||||
"simdFlags": [
|
||||
"-msimd128"
|
||||
]
|
||||
},
|
||||
"abi": "src/abi.json",
|
||||
"payloads": [
|
||||
{
|
||||
"symbol": "WASM_SIMD_B64",
|
||||
"file": "wasm-simd.wasm",
|
||||
"bytes": 511,
|
||||
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b",
|
||||
"source": "src/shallow_kernel.c"
|
||||
},
|
||||
{
|
||||
"symbol": "WASM_SCALAR_B64",
|
||||
"file": "wasm-scalar.wasm",
|
||||
"bytes": 511,
|
||||
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b",
|
||||
"source": "src/shallow_kernel.c"
|
||||
},
|
||||
{
|
||||
"symbol": "DEEP_SIMD_B64",
|
||||
"file": "deep-simd.wasm",
|
||||
"bytes": 4866,
|
||||
"sha256": "d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214",
|
||||
"source": "src/deep_kernel.c"
|
||||
},
|
||||
{
|
||||
"symbol": "DEEP_SCALAR_B64",
|
||||
"file": "deep-scalar.wasm",
|
||||
"bytes": 5246,
|
||||
"sha256": "d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2",
|
||||
"source": "src/deep_kernel.c"
|
||||
},
|
||||
{
|
||||
"symbol": "BLA_SIMD_B64",
|
||||
"file": "bla-simd.wasm",
|
||||
"bytes": 4959,
|
||||
"sha256": "f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e",
|
||||
"source": "src/bla_kernel_v18.c"
|
||||
},
|
||||
{
|
||||
"symbol": "BLA_SCALAR_B64",
|
||||
"file": "bla-scalar.wasm",
|
||||
"bytes": 4593,
|
||||
"sha256": "4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f",
|
||||
"source": "src/bla_kernel_v18.c"
|
||||
},
|
||||
{
|
||||
"symbol": "COLOR_SIMD_B64",
|
||||
"file": "color-simd.wasm",
|
||||
"bytes": 1387,
|
||||
"sha256": "8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03",
|
||||
"source": "src/color_kernel.c"
|
||||
},
|
||||
{
|
||||
"symbol": "COLOR_SCALAR_B64",
|
||||
"file": "color-scalar.wasm",
|
||||
"bytes": 632,
|
||||
"sha256": "aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90",
|
||||
"source": "src/color_kernel.c"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
11
dist/_headers
vendored
11
dist/_headers
vendored
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
/wasm/*
|
||||
Content-Type: application/wasm
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/hosted/*
|
||||
Cache-Control: no-cache
|
||||
598
dist/hosted/gpu-kernels-v24.2.6.js
vendored
598
dist/hosted/gpu-kernels-v24.2.6.js
vendored
|
|
@ -1,598 +0,0 @@
|
|||
(()=>{'use strict';
|
||||
const COMMON=String.raw`
|
||||
const FIELD_UNKNOWN:u32=0u;
|
||||
const FIELD_ESCAPED:u32=1u;
|
||||
const FIELD_INTERIOR_LIKELY:u32=2u;
|
||||
const FIELD_INTERIOR_PROVEN:u32=3u;
|
||||
const ITER_MASK:u32=0x000fffffu;
|
||||
const REASON_SHIFT:u32=20u;
|
||||
const REASON_MASK:u32=0x0ff00000u;
|
||||
const REASON_NONE:u32=0u;
|
||||
const REASON_ERROR_BOUND:u32=1u;
|
||||
const REASON_ESCAPE_UNCERTAIN:u32=2u;
|
||||
const REASON_REFERENCE_END:u32=3u;
|
||||
const REASON_REBASE_GAP:u32=4u;
|
||||
const REASON_RANGE:u32=5u;
|
||||
const REASON_OPERATION_LIMIT:u32=6u;
|
||||
|
||||
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
|
||||
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
|
||||
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
|
||||
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
|
||||
}
|
||||
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
|
||||
const F32_U:f32=5.960464477539063e-8;
|
||||
fn pow2_safe(e:i32)->f32 {
|
||||
if(e < -126){ return 0.0; }
|
||||
if(e > 126){ return 8.507059e37; }
|
||||
return ldexp(1.0,e);
|
||||
}
|
||||
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
|
||||
let propagated=abs(errScaled*pow2_safe(scaleExp));
|
||||
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
|
||||
return propagated+reconstruction;
|
||||
}
|
||||
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
|
||||
if(e < -126){ return vec2<f32>(0.0); }
|
||||
if(e > 126){ return vec2<f32>(8.507059e37); }
|
||||
return ldexp(v,vec2<i32>(e));
|
||||
}
|
||||
fn smooth_escape(n:u32, mag2:f32)->f32{
|
||||
let u=log2(max(4.0000005,mag2));
|
||||
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
|
||||
}
|
||||
`;
|
||||
|
||||
const DIRECT_F32_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
|
||||
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
|
||||
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn analytic(cr:f32,ci:f32)->bool{
|
||||
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
|
||||
let lhs=q*(q+x); let rhs=0.25*y2;
|
||||
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
|
||||
if(lhs<rhs-margin){return true;}
|
||||
let x2=cr+1.0; let bulb=x2*x2+y2;
|
||||
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
|
||||
return bulb<0.0625-bulbMargin;
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
let scale=p.span/f32(p.fullW);
|
||||
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
|
||||
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
|
||||
if(analytic(cr,ci)){
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
|
||||
}
|
||||
var zr=0.0; var zi=0.0; var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
let zr2=zr*zr; let zi2=zi*zi;
|
||||
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
|
||||
let mag=zr*zr+zi*zi;
|
||||
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// Benchmark candidate: whole-frame double-single direct iteration.
|
||||
// This intentionally has no CPU reference-build cost. It is not selected by
|
||||
// the production router until the crossover benchmark justifies it.
|
||||
const DIRECT_DS_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
|
||||
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
|
||||
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
|
||||
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
|
||||
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn ds_quick(a:f32,b:f32)->DS{
|
||||
let q=a+b; let e=b-(q-a); return DS(q,e);
|
||||
}
|
||||
fn ds_sum(a:f32,b:f32)->DS{
|
||||
let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e);
|
||||
}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;
|
||||
let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah;
|
||||
let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh;
|
||||
var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl;
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{
|
||||
let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l));
|
||||
}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);
|
||||
var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l;
|
||||
return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{
|
||||
let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b);
|
||||
}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{
|
||||
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
|
||||
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
|
||||
}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
|
||||
return CDS(rr,ii);
|
||||
}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);
|
||||
let span=DS(p.spanHi,p.spanLo);
|
||||
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
|
||||
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
|
||||
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
|
||||
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
|
||||
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
|
||||
let c=CDS(cr,ci);
|
||||
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
z=cds_add(cds_mul(z,z),c); n+=1u;
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
|
||||
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
|
||||
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
|
||||
// can be re-evaluated by the existing deep double-single correction pass.
|
||||
// This shader is benchmark-only and is not selected by the production router.
|
||||
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
|
||||
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
|
||||
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
|
||||
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
|
||||
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
struct HybridStats{
|
||||
remaining:atomic<u32>, _a:atomic<u32>, _b:atomic<u32>, _c:atomic<u32>,
|
||||
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
|
||||
fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);}
|
||||
fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;
|
||||
let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh;
|
||||
var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{if(a.h<b.h){return -1;}if(a.h>b.h){return 1;}if(a.l<b.l){return -1;}if(a.l>b.l){return 1;}return 0;}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii);
|
||||
}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
fn mark_sensitive(out:u32,n:u32){
|
||||
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;atomicAdd(&hybridStats.remaining,1u);
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
|
||||
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
|
||||
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
|
||||
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
|
||||
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
|
||||
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
|
||||
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var deriv=vec2<f32>(0.0);var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
|
||||
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
|
||||
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
|
||||
z=cds_add(cds_mul(z,z),c);n+=1u;
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
|
||||
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
|
||||
}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
|
||||
const DEEP_PERTURB_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
|
||||
struct UnresolvedHead{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
|
||||
|
||||
fn mark_unresolved(out:u32,n:u32,reason:u32){
|
||||
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
|
||||
atomicAdd(&unresolved.remaining,1u);
|
||||
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
|
||||
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
|
||||
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
|
||||
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
|
||||
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
|
||||
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
|
||||
}
|
||||
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
||||
let dx=(gx-p.refPixelX)/f32(p.fullW);
|
||||
let dy=(p.refPixelY-gy)/f32(p.fullW);
|
||||
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
|
||||
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
|
||||
var w=vec2<f32>(0.0);
|
||||
var scaleExp=p.spanExp;
|
||||
var n=0u; var m=0u; var operations=0u;
|
||||
var errScaled=1.0*F32_U*maxabs(d);
|
||||
loop{
|
||||
if(n>=p.maxIter){
|
||||
let rpEnd=refs[min(m,p.refLen)];
|
||||
let deltaEnd=scaled_to_f32(w,scaleExp);
|
||||
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
|
||||
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
|
||||
let limit=select(1.0e-3,1.0e-4,strictMode);
|
||||
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
|
||||
return;
|
||||
}
|
||||
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let rp=refs[m];
|
||||
let delta=scaled_to_f32(w,scaleExp);
|
||||
let z=rp.hi+(rp.lo+delta);
|
||||
let mag=dot(z,z);
|
||||
if(mag>4.0){
|
||||
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
|
||||
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
|
||||
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
|
||||
}
|
||||
|
||||
// Rebase only when dc remains numerically representable in the new scale.
|
||||
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
|
||||
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
|
||||
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
|
||||
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
|
||||
errScaled+=1.0*F32_U*maxabs(d);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let r=refs[m];
|
||||
let refAbs=maxabs(r.hi)+maxabs(r.lo);
|
||||
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
|
||||
let gain=2.0*refAbs+2.0*wAbs*p2;
|
||||
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
|
||||
errScaled=gain*errScaled+roundErr;
|
||||
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
|
||||
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
|
||||
let sq=cmul(w,w)*pow2_safe(scaleExp);
|
||||
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
|
||||
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
|
||||
let mm=max(maxabs(w),maxabs(d));
|
||||
if(mm>65536.0){
|
||||
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
|
||||
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
|
||||
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
|
||||
}
|
||||
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
|
||||
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;}
|
||||
}
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
render_pixel(out,gx,gy,p.strict!=0u);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
|
||||
// perturbation. It is a visual-quality pass, not a membership certificate.
|
||||
const DEEP_CORRECT_WGSL=COMMON+String.raw`
|
||||
const CORRECTION_MARK:u32=128u;
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
|
||||
struct CorrectionStats{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
|
||||
|
||||
fn count_remaining(out:u32){
|
||||
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
|
||||
atomicAdd(&stats.remaining,1u);
|
||||
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
|
||||
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
|
||||
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
|
||||
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
|
||||
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
|
||||
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
|
||||
}
|
||||
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
|
||||
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
|
||||
}
|
||||
|
||||
fn ds_quick(a:f32,b:f32)->DS{
|
||||
let q=a+b;
|
||||
let e=b-(q-a);
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_sum(a:f32,b:f32)->DS{
|
||||
let q=a+b;
|
||||
let bb=q-a;
|
||||
let e=(a-(q-bb))+(b-bb);
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;
|
||||
let ca=4097.0*a;
|
||||
let ah=ca-(ca-a);
|
||||
let al=a-ah;
|
||||
let cb=4097.0*b;
|
||||
let bh=cb-(cb-b);
|
||||
let bl=b-bh;
|
||||
var e=ah*bh-q;
|
||||
e=e+ah*bl;
|
||||
e=e+al*bh;
|
||||
e=e+al*bl;
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{
|
||||
let q=ds_sum(a.h,b.h);
|
||||
return ds_quick(q.h,q.l+(a.l+b.l));
|
||||
}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);
|
||||
var e=q.l+a.h*b.l;
|
||||
e=e+a.l*b.h;
|
||||
e=e+a.l*b.l;
|
||||
return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{
|
||||
let q=ds_prod(a.h,b);
|
||||
return ds_quick(q.h,q.l+a.l*b);
|
||||
}
|
||||
fn ds_pow2(a:DS,e:i32)->DS{
|
||||
if(e < -126){return DS(0.0,0.0);}
|
||||
if(e > 126){return DS(8.507059e37,0.0);}
|
||||
return DS(ldexp(a.h,e),ldexp(a.l,e));
|
||||
}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{
|
||||
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
|
||||
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
|
||||
}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
|
||||
return CDS(rr,ii);
|
||||
}
|
||||
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
|
||||
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
|
||||
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
|
||||
|
||||
fn correct_pixel(out:u32,gx:f32,gy:f32){
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);
|
||||
let sm=DS(p.spanMantHi,p.spanMantLo);
|
||||
let ox=gx-p.refPixelX;
|
||||
let oy=p.refPixelY-gy;
|
||||
let dx=ds_scale(invW,ox);
|
||||
let dy=ds_scale(invW,oy);
|
||||
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
|
||||
var d=d0;
|
||||
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var scaleExp=p.spanExp;
|
||||
var n=0u; var m=0u; var operations=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
|
||||
if(m>p.refLen){count_remaining(out);return;}
|
||||
let rp=refs[m];
|
||||
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
|
||||
let delta=cds_pow2(w,scaleExp);
|
||||
let z=cds_add(r,delta);
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
|
||||
return;
|
||||
}
|
||||
let dmag=cds_mag2(delta);
|
||||
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
|
||||
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
|
||||
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
|
||||
continue;
|
||||
}
|
||||
if(m>=p.refLen){count_remaining(out);return;}
|
||||
let linear=cds_scale(cds_mul(r,w),2.0);
|
||||
let sq=cds_pow2(cds_mul(w,w),scaleExp);
|
||||
w=cds_add(cds_add(linear,sq),d);
|
||||
m+=1u; n+=1u; operations+=1u;
|
||||
let mm=max(cds_maxabs(w),cds_maxabs(d));
|
||||
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
|
||||
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
|
||||
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
|
||||
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
|
||||
}
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
correct_pixel(out,gx,gy);
|
||||
}
|
||||
`;
|
||||
|
||||
const COLOR_WGSL=String.raw`
|
||||
struct Params{
|
||||
width:u32,height:u32,palette:u32,edgeAA:u32,
|
||||
cycle:f32,shift:f32,_p0:f32,_p1:f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
|
||||
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
|
||||
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
|
||||
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
|
||||
}
|
||||
fn current_palette(t0:f32)->vec3<f32>{
|
||||
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
|
||||
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
|
||||
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
|
||||
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
|
||||
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
|
||||
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
|
||||
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
|
||||
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
|
||||
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
|
||||
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
|
||||
}
|
||||
fn base_color(i:u32)->vec3<f32>{
|
||||
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
|
||||
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
|
||||
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
|
||||
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
|
||||
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
|
||||
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
|
||||
}
|
||||
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
|
||||
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
|
||||
if(p.edgeAA!=0u){
|
||||
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
|
||||
let x=i32(gid.x); let y=i32(gid.y);
|
||||
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
|
||||
if(boundary){c=delinearize(sum/cnt);}
|
||||
}
|
||||
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const AA_RESOLVE_WGSL=String.raw`
|
||||
@group(0) @binding(0) var a:texture_2d<f32>;
|
||||
@group(0) @binding(1) var b:texture_2d<f32>;
|
||||
@group(0) @binding(2) var c:texture_2d<f32>;
|
||||
@group(0) @binding(3) var d:texture_2d<f32>;
|
||||
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
|
||||
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
|
||||
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
|
||||
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
|
||||
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
|
||||
let q=vec2<i32>(gid.xy);
|
||||
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
|
||||
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
|
||||
}
|
||||
`;
|
||||
|
||||
const PRESENT_WGSL=String.raw`
|
||||
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
|
||||
@group(0) @binding(0) var samp:sampler;
|
||||
@group(0) @binding(1) var tex:texture_2d<f32>;
|
||||
@group(0) @binding(2) var<uniform> p:Params;
|
||||
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
|
||||
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
|
||||
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
|
||||
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
|
||||
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
|
||||
}
|
||||
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
|
||||
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
|
||||
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
|
||||
}
|
||||
`;
|
||||
|
||||
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
|
||||
version:'24.2.6-hybrid-ui-cacheproof',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
|
||||
});
|
||||
})();
|
||||
704
dist/hosted/gpu-kernels.js
vendored
704
dist/hosted/gpu-kernels.js
vendored
|
|
@ -87,362 +87,77 @@ 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`
|
||||
// Fast-mode precision extension. This keeps the interactive path independent
|
||||
// of the verified Deep backend: a reduced-precision reference orbit is built
|
||||
// quickly on the CPU worker and this lightweight perturbation kernel preserves
|
||||
// sub-f32 pixel offsets once absolute Direct coordinates begin to collapse.
|
||||
const FAST_PERTURB_WGSL=COMMON+String.raw`
|
||||
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,
|
||||
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 DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn ds_quick(a:f32,b:f32)->DS{
|
||||
let q=a+b; let e=b-(q-a); return DS(q,e);
|
||||
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(4) var<storage,read_write> unresolved:array<atomic<u32>>;
|
||||
fn mark_fast_unresolved(out:u32,n:u32,reason:u32){
|
||||
fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u);
|
||||
if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);}
|
||||
}
|
||||
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;
|
||||
|
||||
fn render_fast(out:u32,gx:f32,gy:f32){
|
||||
let dx=(gx-p.refPixelX)/f32(p.fullW);
|
||||
let dy=(p.refPixelY-gy)/f32(p.fullW);
|
||||
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
|
||||
var w=vec2<f32>(0.0);
|
||||
var scaleExp=p.spanExp;
|
||||
var n=0u; var m=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){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;
|
||||
}
|
||||
`;
|
||||
if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;}
|
||||
let rp=refs[min(m,p.refLen)];
|
||||
let delta=scaled_to_f32(w,scaleExp);
|
||||
let z=rp.hi+(rp.lo+delta);
|
||||
let mag=dot(z,z);
|
||||
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
// Rebase the perturbation when it grows beyond the reference value. This
|
||||
// keeps the fast path usable across pans without any verified correction.
|
||||
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
|
||||
if(p.spanExp < -126){mark_fast_unresolved(out,n,REASON_REBASE_GAP);return;}
|
||||
w=z;
|
||||
d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp);
|
||||
scaleExp=0; m=0u;
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let r=rp;
|
||||
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
|
||||
let sq=cmul(w,w)*pow2_safe(scaleExp);
|
||||
w=linear+sq+d; m+=1u; n+=1u;
|
||||
let mm=max(maxabs(w),maxabs(d));
|
||||
if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;}
|
||||
if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;}
|
||||
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;}
|
||||
if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;}
|
||||
}
|
||||
}
|
||||
@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;
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
render_fast(out,gx,gy);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// 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,
|
||||
|
|
@ -480,7 +195,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
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 n=0u; var m=0u;
|
||||
var errScaled=1.0*F32_U*maxabs(d);
|
||||
loop{
|
||||
if(n>=p.maxIter){
|
||||
|
|
@ -492,7 +207,6 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
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);
|
||||
|
|
@ -513,7 +227,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
}
|
||||
|
||||
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let r=refs[m];
|
||||
let r=rp;
|
||||
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;
|
||||
|
|
@ -522,7 +236,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
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;
|
||||
w=linear+sq+d; m+=1u; n+=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){
|
||||
|
|
@ -531,7 +245,6 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
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)
|
||||
|
|
@ -545,6 +258,182 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
|||
`;
|
||||
|
||||
|
||||
// Idle refinement is compiled as a separate guarded-Deep module so the
|
||||
// production Fast/Deep main shaders remain byte-for-byte identical to v24.2.18.
|
||||
const DEEP_LIKELY_REFINE_WGSL=DEEP_PERTURB_WGSL.replace(
|
||||
'@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;',
|
||||
'@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var<storage,read> refineQueue:array<u32>;\n@group(1) @binding(1) var<uniform> refineBatch:RefineBatch;'
|
||||
)+String.raw`
|
||||
@compute @workgroup_size(64)
|
||||
fn refine_likely(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
let slot=refineBatch.base+gid.x;
|
||||
if(slot>=refineBatch.end){return;}
|
||||
let out=refineQueue[slot];
|
||||
if(((fieldMeta[out]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}
|
||||
let x=out%p.fullW; let y=out/p.fullW;
|
||||
render_pixel(out,f32(x)+p.sampleX,f32(y)+p.sampleY,p.strict!=0u);
|
||||
}
|
||||
`;
|
||||
|
||||
// Production Deep sparse path: keep the long-running perturbation kernel
|
||||
// untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets.
|
||||
// Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS
|
||||
// correction kernel sees less workgroup-level iteration divergence.
|
||||
//
|
||||
// Bucket state layout (24 u32 atomics):
|
||||
// [0..7] histogram counts
|
||||
// [8..15] scatter cursors (initialized from prefix offsets)
|
||||
// [16..23] immutable prefix offsets for queue accounting
|
||||
const DEEP_BUCKET_HIST_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
|
||||
var<workgroup> localCounts:array<atomic<u32>,8>;
|
||||
fn deep_bucket(packed:u32)->u32{
|
||||
let n=packed&ITER_MASK;
|
||||
return min(7u,(n*8u)/max(1u,p.maxIter));
|
||||
}
|
||||
@compute @workgroup_size(64)
|
||||
fn main(
|
||||
@builtin(local_invocation_id) lid3:vec3<u32>,
|
||||
@builtin(workgroup_id) wid:vec3<u32>
|
||||
){
|
||||
let lane=lid3.x;
|
||||
if(lane<8u){atomicStore(&localCounts[lane],0u);}
|
||||
workgroupBarrier();
|
||||
let lx=wid.x*64u+lane;
|
||||
let ly=wid.y;
|
||||
if(lx<p.tileW && ly<p.tileH){
|
||||
let out=p.outputBase+ly*p.outputStride+lx;
|
||||
let packed=fieldMeta[out];
|
||||
if(((packed>>28u)&3u)==FIELD_UNKNOWN){
|
||||
atomicAdd(&localCounts[deep_bucket(packed)],1u);
|
||||
}
|
||||
}
|
||||
workgroupBarrier();
|
||||
if(lane<8u){
|
||||
let c=atomicLoad(&localCounts[lane]);
|
||||
if(c>0u){atomicAdd(&bucketState[lane],c);}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Convert the 8-bin histogram into contiguous queue ranges, initialize each
|
||||
// scatter cursor, and produce the correction indirect-dispatch arguments.
|
||||
const DEEP_BUCKET_PREFIX_WGSL=String.raw`
|
||||
struct SparseQueueStats{
|
||||
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
|
||||
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
|
||||
};
|
||||
struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 };
|
||||
@group(0) @binding(0) var<storage,read_write> bucketState:array<atomic<u32>>;
|
||||
@group(0) @binding(1) var<storage,read_write> sparseQueueStats:SparseQueueStats;
|
||||
@group(0) @binding(2) var<storage,read_write> indirectArgs:IndirectArgs;
|
||||
@compute @workgroup_size(1)
|
||||
fn main(){
|
||||
var total=0u;
|
||||
var b=0u;
|
||||
loop{
|
||||
if(b>=8u){break;}
|
||||
let c=atomicLoad(&bucketState[b]);
|
||||
atomicStore(&bucketState[8u+b],total);
|
||||
atomicStore(&bucketState[16u+b],total);
|
||||
total+=c;
|
||||
b+=1u;
|
||||
}
|
||||
atomicStore(&sparseQueueStats.selected,total);
|
||||
atomicStore(&sparseQueueStats.overflow,0u);
|
||||
atomicStore(&sparseQueueStats.enqueued,total);
|
||||
atomicStore(&sparseQueueStats.dispatchCount,total);
|
||||
indirectArgs.x=(total+63u)/64u;
|
||||
indirectArgs.y=1u;
|
||||
indirectArgs.z=1u;
|
||||
indirectArgs._pad=0u;
|
||||
}
|
||||
`;
|
||||
|
||||
// Scatter UNKNOWN indices into the precomputed bucket ranges. Within each
|
||||
// 64-lane workgroup, workgroup-memory atomics allocate local ranks; each
|
||||
// non-empty bucket reserves one global subrange, so global atomics scale with
|
||||
// non-empty (workgroup,bucket) pairs rather than with UNKNOWN pixels.
|
||||
const DEEP_BUCKET_SCATTER_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
struct SparseQueueStats{
|
||||
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
|
||||
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
|
||||
@group(0) @binding(3) var<storage,read_write> sparseQueueStats:SparseQueueStats;
|
||||
@group(0) @binding(4) var<storage,read_write> unknownQueue:array<u32>;
|
||||
var<workgroup> localCounts:array<atomic<u32>,8>;
|
||||
var<workgroup> localRanks:array<atomic<u32>,8>;
|
||||
var<workgroup> groupBase:array<u32,8>;
|
||||
fn deep_bucket(packed:u32)->u32{
|
||||
let n=packed&ITER_MASK;
|
||||
return min(7u,(n*8u)/max(1u,p.maxIter));
|
||||
}
|
||||
@compute @workgroup_size(64)
|
||||
fn main(
|
||||
@builtin(local_invocation_id) lid3:vec3<u32>,
|
||||
@builtin(workgroup_id) wid:vec3<u32>
|
||||
){
|
||||
let lane=lid3.x;
|
||||
if(lane<8u){
|
||||
atomicStore(&localCounts[lane],0u);
|
||||
atomicStore(&localRanks[lane],0u);
|
||||
groupBase[lane]=0u;
|
||||
}
|
||||
workgroupBarrier();
|
||||
|
||||
let lx=wid.x*64u+lane;
|
||||
let ly=wid.y;
|
||||
var out=0u;
|
||||
var hit=0u;
|
||||
var bucket=0u;
|
||||
if(lx<p.tileW && ly<p.tileH){
|
||||
out=p.outputBase+ly*p.outputStride+lx;
|
||||
let packed=fieldMeta[out];
|
||||
if(((packed>>28u)&3u)==FIELD_UNKNOWN){
|
||||
hit=1u;
|
||||
bucket=deep_bucket(packed);
|
||||
atomicAdd(&localCounts[bucket],1u);
|
||||
}
|
||||
}
|
||||
workgroupBarrier();
|
||||
|
||||
if(lane<8u){
|
||||
let c=atomicLoad(&localCounts[lane]);
|
||||
if(c>0u){groupBase[lane]=atomicAdd(&bucketState[8u+lane],c);}
|
||||
}
|
||||
workgroupBarrier();
|
||||
|
||||
if(hit!=0u){
|
||||
let rank=atomicAdd(&localRanks[bucket],1u);
|
||||
let qi=groupBase[bucket]+rank;
|
||||
let capacity=p.tileW*p.tileH;
|
||||
if(qi<capacity){unknownQueue[qi]=out;}
|
||||
else{atomicAdd(&sparseQueueStats.overflow,1u);}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// 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`
|
||||
|
|
@ -659,10 +548,9 @@ fn correct_pixel(out:u32,gx:f32,gy:f32){
|
|||
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;
|
||||
var n=0u; var m=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);
|
||||
|
|
@ -682,12 +570,12 @@ fn correct_pixel(out:u32,gx:f32,gy:f32){
|
|||
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;
|
||||
m+=1u; n+=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;}
|
||||
if(scaleExp>126){count_remaining(out);return;}
|
||||
}
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
|
|
@ -701,7 +589,76 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
|||
}
|
||||
`;
|
||||
|
||||
|
||||
// Production sparse correction variant. It shares the complete DS arithmetic
|
||||
// with DEEP_CORRECT_WGSL, but consumes only indices emitted by the queued deep
|
||||
// perturbation pass and launches via dispatchWorkgroupsIndirect.
|
||||
const DEEP_CORRECT_QUEUE_WGSL=DEEP_CORRECT_WGSL
|
||||
.replace(
|
||||
`struct CorrectionStats{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};`,
|
||||
`struct CorrectionStats{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
struct SparseQueueStats{
|
||||
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
|
||||
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
|
||||
};`)
|
||||
.replace(
|
||||
`@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;`,
|
||||
`@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
|
||||
@group(0) @binding(5) var<storage,read_write> sparseQueueStats:SparseQueueStats;
|
||||
@group(0) @binding(6) var<storage,read> unknownQueue:array<u32>;`)
|
||||
.replace(
|
||||
`@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
correct_pixel(out,gx,gy);
|
||||
}`,
|
||||
`@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
let qi=gid.x;
|
||||
let queued=atomicLoad(&sparseQueueStats.dispatchCount);
|
||||
if(qi>=queued){return;}
|
||||
let out=unknownQueue[qi];
|
||||
atomicAdd(&sparseQueueStats.processed,1u);
|
||||
if(out<p.outputBase){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;}
|
||||
let local=out-p.outputBase;
|
||||
let ly=local/p.outputStride;
|
||||
let lx=local-ly*p.outputStride;
|
||||
if(lx>=p.tileW||ly>=p.tileH){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;}
|
||||
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;}
|
||||
let gx=f32(p.tileX+lx)+p.sampleX;
|
||||
let gy=f32(p.tileY+ly)+p.sampleY;
|
||||
correct_pixel(out,gx,gy);
|
||||
}`);
|
||||
|
||||
if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)')){
|
||||
throw new Error('failed to derive queued deep correction shader');
|
||||
}
|
||||
|
||||
const LIKELY_QUEUE_WGSL=COMMON+String.raw`
|
||||
struct QueueParams{width:u32,height:u32,mode:u32,sparseStride:u32};
|
||||
struct QueueStats{count:atomic<u32>,overflow:atomic<u32>,_p0:atomic<u32>,_p1:atomic<u32>};
|
||||
@group(0) @binding(0) var<uniform> p:QueueParams;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> queue:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> stats:QueueStats;
|
||||
fn is_frontier(x:u32,y:u32,r:i32)->bool{let xi=i32(x);let yi=i32(y);var oy=-1;loop{if(oy>1){break;}var ox=-1;loop{if(ox>1){break;}if(ox!=0||oy!=0){let xx=xi+ox*r;let yy=yi+oy*r;if(xx>=0&&yy>=0&&xx<i32(p.width)&&yy<i32(p.height)){let cls=(fieldMeta[u32(yy)*p.width+u32(xx)]>>28u)&3u;if(cls==FIELD_ESCAPED||cls==FIELD_UNKNOWN){return true;}}}ox+=1;}oy+=1;}return false;}
|
||||
@compute @workgroup_size(256)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){let i=gid.x;let total=p.width*p.height;if(i>=total){return;}if(((fieldMeta[i]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}let x=i%p.width;let y=i/p.width;var selected=false;if(p.mode==0u){selected=true;}else{let radius:i32=select(2,select(3,4,p.mode>=3u),p.mode>=2u);selected=is_frontier(x,y,radius);if(!selected&&p.sparseStride>0u){selected=(x%p.sparseStride==0u)&&(y%p.sparseStride==0u);}}if(!selected){return;}let slot=atomicAdd(&stats.count,1u);if(slot<total){queue[slot]=i;}else{atomicAdd(&stats.overflow,1u);}}
|
||||
`;
|
||||
|
||||
const COLOR_WGSL=String.raw`
|
||||
const FIELD_UNKNOWN:u32=0u;
|
||||
const FIELD_ESCAPED:u32=1u;
|
||||
struct Params{
|
||||
width:u32,height:u32,palette:u32,edgeAA:u32,
|
||||
cycle:f32,shift:f32,_p0:f32,_p1:f32,
|
||||
|
|
@ -726,28 +683,36 @@ fn current_palette(t0:f32)->vec3<f32>{
|
|||
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 palette_color(phase:f32)->vec3<f32>{
|
||||
if(p.palette==1u){return hsv(phase,0.92,1.0);}if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3<f32>(g);}return current_palette(phase);
|
||||
}
|
||||
fn escaped_color(m:u32,sm:f32)->vec3<f32>{
|
||||
let phase=fract(p.shift+sm*p.cycle);let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.34+0.66*pow(edge,0.38);let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv);
|
||||
}
|
||||
fn provisional_unknown(m:u32)->vec3<f32>{
|
||||
let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3<f32>(6,10,22)/255.0,vec3<f32>(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52);
|
||||
}
|
||||
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);
|
||||
let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3<f32>(0.0);}return escaped_color(m,fieldSmooth[i]);
|
||||
}
|
||||
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(gid.x>=p.width||gid.y>=p.height){return;}let i=gid.y*p.width+gid.x;let m=fieldMeta[i];let cls=(m>>28u)&3u;var c=base_color(i);let x=i32(gid.x);let y=i32(gid.y);
|
||||
if(cls==FIELD_UNKNOWN){
|
||||
var fill=vec3<f32>(0.0);var fillCount=0.0;
|
||||
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];if(((mj>>28u)&3u)!=FIELD_UNKNOWN){fill+=linearize(base_color(j));fillCount+=1.0;}}}
|
||||
if(fillCount>0.0){c=delinearize(fill/fillCount);}
|
||||
}
|
||||
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);
|
||||
var boundary=false;var sum=linearize(c);var cnt=1.0;
|
||||
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
|
||||
if(boundary){c=delinearize(sum/cnt);}
|
||||
}
|
||||
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
|
||||
const AA_RESOLVE_WGSL=String.raw`
|
||||
|
|
@ -786,7 +751,28 @@ struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
|
|||
}
|
||||
`;
|
||||
|
||||
|
||||
// v24.2.14 production screen path: defer UNKNOWN statistics out of the long
|
||||
// perturbation kernel. The existing bucket histogram pass already scans the
|
||||
// same field before sparse correction, so it can aggregate remaining/reason
|
||||
// counters per workgroup without per-UNKNOWN global atomics in the primary.
|
||||
// The counted primary is retained for export and verification paths.
|
||||
const DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u);\n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}');
|
||||
|
||||
const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL
|
||||
.replace('@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;', '@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;\n@group(0) @binding(3) var<storage,read_write> unresolvedStats:array<atomic<u32>>;')
|
||||
.replace('var<workgroup> localCounts:array<atomic<u32>,8>;', 'var<workgroup> localCounts:array<atomic<u32>,8>;\nvar<workgroup> localReasons:array<atomic<u32>,8>;')
|
||||
.replace('if(lane<8u){atomicStore(&localCounts[lane],0u);}', 'if(lane<8u){atomicStore(&localCounts[lane],0u);atomicStore(&localReasons[lane],0u);}')
|
||||
.replace('atomicAdd(&localCounts[deep_bucket(packed)],1u);', `atomicAdd(&localCounts[deep_bucket(packed)],1u);
|
||||
atomicAdd(&localReasons[0],1u);
|
||||
let reason=(packed&REASON_MASK)>>REASON_SHIFT;
|
||||
if(reason>=1u && reason<=6u){atomicAdd(&localReasons[reason],1u);}`)
|
||||
.replace('if(c>0u){atomicAdd(&bucketState[lane],c);}', `if(c>0u){atomicAdd(&bucketState[lane],c);}
|
||||
let r=atomicLoad(&localReasons[lane]);
|
||||
if(r>0u){atomicAdd(&unresolvedStats[lane],r);}`);
|
||||
if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('v24.2.14 post-stats shader derivation failed');
|
||||
|
||||
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
|
||||
version:'24.2.8-fixed96-sparse-queue',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DIRECT_FIXED96_QUEUE_ARGS_WGSL,DIRECT_FIXED96_CORRECT_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
|
||||
version:'24.2.26-baseline-idle-refinement',DIRECT_F32_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_LIKELY_REFINE_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,LIKELY_QUEUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
36
dist/hosted/hosted-loader.js
vendored
36
dist/hosted/hosted-loader.js
vendored
|
|
@ -1,36 +0,0 @@
|
|||
const wasmRoot = new URL('../wasm/', import.meta.url);
|
||||
|
||||
async function compileAsset(name) {
|
||||
const url = new URL(name, wasmRoot);
|
||||
const response = await fetch(url, { cache: 'force-cache' });
|
||||
if (!response.ok) throw new Error(`WASM fetch failed: ${name} (${response.status})`);
|
||||
if (WebAssembly.compileStreaming) {
|
||||
try { return await WebAssembly.compileStreaming(Promise.resolve(response.clone())); }
|
||||
catch { /* A proxy may have supplied the wrong MIME type; use bytes below. */ }
|
||||
}
|
||||
return WebAssembly.compile(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
let shallowModule, shallowSimd = true;
|
||||
try { shallowModule = await compileAsset('wasm-simd.d19b26c04e1b2f59.wasm'); }
|
||||
catch { shallowModule = await compileAsset('wasm-scalar.d19b26c04e1b2f59.wasm'); shallowSimd = false; }
|
||||
const asset = name => new URL(name, wasmRoot).href;
|
||||
|
||||
globalThis.MANDEL_KERNELS = Object.freeze({
|
||||
// The compiled module is structured-cloned to the shallow Worker. No Base64
|
||||
// conversion or second compile is needed in the hosted build.
|
||||
WASM_SIMD_B64: shallowModule,
|
||||
WASM_SCALAR_B64: shallowModule,
|
||||
DEEP_SIMD_B64: asset('deep-simd.d49385f06e4a8f55.wasm'),
|
||||
DEEP_SCALAR_B64: asset('deep-scalar.d133bbecc8f6dbdf.wasm'),
|
||||
BLA_SIMD_B64: asset('bla-simd.f80f136e9fb676ce.wasm'),
|
||||
BLA_SCALAR_B64: asset('bla-scalar.4f06913487044823.wasm'),
|
||||
COLOR_SIMD_B64: asset('color-simd.8cf83374ed0c680b.wasm'),
|
||||
COLOR_SCALAR_B64: asset('color-scalar.aa2914ec1acacaa2.wasm')
|
||||
});
|
||||
globalThis.MANDEL_KERNEL_META = Object.freeze({"WASM_SIMD_B64":"d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b","WASM_SCALAR_B64":"d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b","DEEP_SIMD_B64":"d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214","DEEP_SCALAR_B64":"d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2","BLA_SIMD_B64":"f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e","BLA_SCALAR_B64":"4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f","COLOR_SIMD_B64":"8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03","COLOR_SCALAR_B64":"aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90"});
|
||||
globalThis.MANDEL_HOSTED_SHALLOW_SIMD = shallowSimd;
|
||||
|
||||
const app = document.createElement('script');
|
||||
app.src = './script.js';
|
||||
document.body.appendChild(app);
|
||||
51
dist/hosted/index.html
vendored
51
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.2.8 Fixed96 Sparse Queue Experiment</title>
|
||||
<title>Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement</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}
|
||||
|
|
@ -12,15 +12,13 @@
|
|||
.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}
|
||||
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));max-height:calc(100dvh - 20px);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
|
||||
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.18);background:#000;color:#fff;min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}select option{background:#000;color:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
|
||||
.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}
|
||||
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
|
||||
.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)}
|
||||
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
|
||||
.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}}
|
||||
|
|
@ -34,20 +32,15 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
|
|||
<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="renderMode">描画方式</label><select id="renderMode"><option value="fast" selected>高速</option><option value="accurate">正確</option></select><output></output></div>
|
||||
<div class="line"><label for="processMode">負荷設定</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
|
||||
<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="cycle">色周期</label><input id="cycle" type="range" min="0" max="1000" step="1" value="532" aria-label="色周期(対数スケール)"><output id="cycleO" for="cycle">0.0080</output></div>
|
||||
<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 class="mini-actions"><button id="colorAuto" type="button" aria-pressed="false">色を自動変化</button></div>
|
||||
</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 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>
|
||||
|
|
@ -63,30 +56,6 @@ 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
454
dist/hosted/script-v24.2.6.js
vendored
|
|
@ -1,454 +0,0 @@
|
|||
(()=>{'use strict';
|
||||
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
|
||||
const $=s=>document.querySelector(s),canvas=$('#view');
|
||||
const VERSION=24,BUILD_VERSION='24.2.6',INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
|
||||
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
|
||||
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
|
||||
const DIRECT_REQUIRED_RATIO=8,DIRECT_DEFAULT_RATIO=128,DEEP_SPEED_MARGIN=.85,DEEP_PROBE_UNKNOWN_MAX=.005,NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,CORRECTION_MARK=128;
|
||||
const BACKEND_PROBE_SPEED_W=224,BACKEND_PROBE_SPEED_H=160,BACKEND_PROBE_QUALITY_W=72,BACKEND_PROBE_QUALITY_H=54;
|
||||
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
|
||||
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
|
||||
const viewHistory=[];let viewHistoryIndex=-1;
|
||||
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,backendProbes:0};
|
||||
const backendProfiles=new Map(),backendViewProbes=new Map(),backendProbePending=new Map();
|
||||
|
||||
// ── exact fixed-point view state ─────────────────────────────────────────
|
||||
function one(bits=state.bits){return 1n<<BigInt(bits)}
|
||||
function fromFrac(n,d=1n){return n*one()/d}
|
||||
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
|
||||
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
|
||||
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
|
||||
function log2Fixed(v){return log2FixedAt(v,state.bits)}
|
||||
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
|
||||
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
|
||||
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
|
||||
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
|
||||
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
|
||||
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
|
||||
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
|
||||
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
|
||||
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
|
||||
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
|
||||
function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
|
||||
function directMetrics(snap=snapshot(),width=Math.max(1,canvas.width)){const spanLog=log2FixedAt(snap.span,snap.bits),stepLog=spanLog-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp))),eligible=Number.isFinite(ratio)&&ratio>=DIRECT_REQUIRED_RATIO&&spanLog>-145;return{spanLog,stepLog,ratio,eligible,overlap:eligible&&ratio<DIRECT_DEFAULT_RATIO}}
|
||||
function zoomExpFor(snap){return Math.max(0,Math.log10(3.4)-log2FixedAt(snap.span,snap.bits)/Math.log2(10))}
|
||||
function backendProfileKey(snap,iter){const z=Math.floor(zoomExpFor(snap)*2)/2,ib=iter<600?0:iter<1500?1:iter<5000?2:3;return z.toFixed(1)+':'+ib}
|
||||
function backendViewKey(snap,iter,width,height){return[snap.bits,snap.re,snap.im,snap.span,iter,width,height].join(':')}
|
||||
function chooseBackend(snap=snapshot(),width=Math.max(1,canvas.width),iter=maxIter(),height=Math.max(1,canvas.height)){const m=directMetrics(snap,width),key=backendProfileKey(snap,iter),viewKey=backendViewKey(snap,iter,width,height),speed=backendProfiles.get(key),viewProbe=backendViewProbes.get(viewKey),pixels=Math.max(1,width*Math.max(1,height)),profile=speed?{...speed,predictedDirectMs:speed.directMsPerPixel*pixels,predictedDeepMs:speed.referenceBuildMs+speed.deepMsPerPixel*pixels}:null;let backend='deep',reason='direct-coordinate-limit';if(m.eligible){if(m.ratio>=DIRECT_DEFAULT_RATIO){backend='direct';reason='direct-wide-margin'}else if(!viewProbe){backend='probe';reason='overlap-view-probe-required'}else if(!viewProbe.qualityPass){backend='deep';reason='direct-quality-rejected'}else if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*DEEP_SPEED_MARGIN){backend='deep';reason='deep-view-probe-faster'}else{backend='direct';reason='direct-view-probe-preferred'}}return{backend,deep:backend==='deep',probe:backend==='probe',reason,key,viewKey,metrics:m,profile,viewProbe:viewProbe||null}}
|
||||
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width),height=Math.max(1,canvas.height)){const d=chooseBackend(snap,width,maxIter(),height);return d.deep||d.probe}
|
||||
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
|
||||
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
|
||||
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
|
||||
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
|
||||
|
||||
// ── iteration / quality policy ───────────────────────────────────────────
|
||||
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
|
||||
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
|
||||
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
|
||||
|
||||
// ── high precision reference worker ─────────────────────────────────────
|
||||
function referenceWorkerSource(){return String.raw`
|
||||
'use strict';
|
||||
const MAX_REF=150001,MAX_LEVELS=20;
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
|
||||
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
|
||||
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
|
||||
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
|
||||
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
|
||||
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
|
||||
`}
|
||||
class ReferenceService{
|
||||
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16}
|
||||
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
|
||||
request(snap,iter,fresh=false){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.2.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
|
||||
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
|
||||
destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
|
||||
}
|
||||
const refs=new ReferenceService();
|
||||
|
||||
// ── WebGPU renderer ──────────────────────────────────────────────────────
|
||||
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
|
||||
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
|
||||
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
|
||||
class WebGpuRenderer{
|
||||
constructor(adapter,device){
|
||||
this.adapter=adapter;this.device=device;
|
||||
const ai=adapter.info||{};
|
||||
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
|
||||
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
|
||||
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
|
||||
this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
|
||||
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
|
||||
this.ready=this.initPipelines();
|
||||
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
|
||||
}
|
||||
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
|
||||
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
|
||||
async initPipelines(){
|
||||
this.device.pushErrorScope?.('validation');
|
||||
try{
|
||||
const [dm,sdm,shm,xm,zm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('direct-ds-benchmark',G.DIRECT_DS_WGSL),this.module('direct-ds-guarded-experiment',G.DIRECT_DS_GUARDED_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
|
||||
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
|
||||
this.dsDirect=this.device.createComputePipeline({layout:'auto',compute:{module:sdm,entryPoint:'main'}});
|
||||
this.dsGuarded=this.device.createComputePipeline({layout:'auto',compute:{module:shm,entryPoint:'main'}});
|
||||
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
|
||||
this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}});
|
||||
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
|
||||
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
|
||||
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
|
||||
this.context=canvas.getContext('webgpu');
|
||||
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
|
||||
webgpuCanvasClaimed=true;this.configure();
|
||||
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
|
||||
}
|
||||
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
|
||||
ensureFrame(w,h){
|
||||
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
|
||||
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
|
||||
}
|
||||
ensureExportWorkspace(){
|
||||
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
|
||||
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
|
||||
}
|
||||
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
|
||||
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
|
||||
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
|
||||
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
|
||||
dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5){const cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),crh=Math.fround(cr),cih=Math.fround(ci),sph=Math.fround(sp),invW=1/Math.max(1,fullW),invh=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,crh,true);d.setFloat32(36,Math.fround(cr-crh),true);d.setFloat32(40,cih,true);d.setFloat32(44,Math.fround(ci-cih),true);d.setFloat32(48,sph,true);d.setFloat32(52,Math.fround(sp-sph),true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,invh,true);d.setFloat32(68,Math.fround(invW-invh),true)})}
|
||||
dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sx=.5,sy=.5){const a=this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx,sy),d=new DataView(a);d.setFloat32(72,Math.fround(riskLimit),true);return a}
|
||||
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
|
||||
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
|
||||
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDsDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsDirect.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsDirect);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDsGuardedNumeric(encoder,{pbuf,meta,smooth,stats,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsGuarded.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:stats}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsGuarded);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
|
||||
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
|
||||
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
|
||||
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
|
||||
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
|
||||
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
|
||||
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
|
||||
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
|
||||
}
|
||||
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
|
||||
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
|
||||
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
|
||||
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
|
||||
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
|
||||
async readUnresolved(){return(await this.readUnresolvedStats()).total}
|
||||
async correctUnknownFrame(snap,iter,token){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,f.w*.5,f.h*.5,1,f.w,0));const e=d.createCommandEncoder({label:'sparse-deep-correction'}),bg=d.createBindGroup({layout:this.correct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),p=e.beginComputePass();p.setPipeline(this.correct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
|
||||
|
||||
async benchmarkDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap));const e=d.createCommandEncoder({label:'ds-direct-benchmark'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;[meta,fieldSmooth,pbuf].forEach(destroy);return{gpuMs}}
|
||||
async renderTileMetaDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,n*4,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const e=d.createCommandEncoder({label:'ds-direct-meta'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});e.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,fieldSmooth,pbuf,read].forEach(destroy);return out}
|
||||
async benchmarkHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13}){
|
||||
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
|
||||
meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
|
||||
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
|
||||
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
|
||||
read=buf(d,2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
|
||||
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit));
|
||||
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,0,fullW*.5,fullH*.5,1,w,0));
|
||||
const e=d.createCommandEncoder({label:'ds-hybrid-benchmark'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
|
||||
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
|
||||
e.copyBufferToBuffer(dsStats,0,read,0,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,UNRESOLVED_BYTES,UNRESOLVED_BYTES);
|
||||
const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;
|
||||
await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),a=new Uint32Array(raw.slice(0,UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(UNRESOLVED_BYTES,2*UNRESOLVED_BYTES));read.unmap();
|
||||
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);
|
||||
return{gpuMs,uncertain:a[0]||0,remaining:b[0]||0,corrected:b[7]||0};
|
||||
}
|
||||
async renderTileMetaHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13,sampleX=.5,sampleY=.5}){
|
||||
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
|
||||
meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
|
||||
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
|
||||
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
|
||||
read=buf(d,n*4+2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
|
||||
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sampleX,sampleY));
|
||||
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,0,fullW*.5,fullH*.5,1,w,0));
|
||||
const e=d.createCommandEncoder({label:'ds-hybrid-meta'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
|
||||
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
|
||||
e.copyBufferToBuffer(meta,0,read,0,n*4);e.copyBufferToBuffer(dsStats,0,read,n*4,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,n*4+UNRESOLVED_BYTES,UNRESOLVED_BYTES);
|
||||
d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),a=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(n*4+UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES));read.unmap();
|
||||
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);out.uncertain=a[0]||0;out.remaining=b[0]||0;out.corrected=b[7]||0;return out;
|
||||
}
|
||||
|
||||
async benchmarkNumeric({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,forceStrict=false,correctUnknown=false}){await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const e=d.createCommandEncoder({label:'backend-benchmark'});if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,forceStrict?1:0));this.encodeDeepNumeric(e,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){e.clearBuffer(unresolved);this.encodeCorrectionNumeric(e,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap));this.encodeDirectNumeric(e,{pbuf,meta,smooth,w,h})}e.copyBufferToBuffer(unresolved,0,read,0,UNRESOLVED_BYTES);const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;await read.mapAsync(GPUMapMode.READ);const a=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return{gpuMs,unknown:a[0]||0,corrected:a[7]||0}}
|
||||
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
|
||||
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
|
||||
const read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
|
||||
}
|
||||
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
|
||||
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
|
||||
const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
|
||||
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes,UNRESOLVED_BYTES/4),unresolvedCount=stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
|
||||
}
|
||||
async renderTileRGBA2x({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
|
||||
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
|
||||
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
|
||||
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
|
||||
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount=0,corrected=0;for(let si=0;si<4;si++){const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4);unresolvedCount+=stats[0]||0;corrected+=stats[7]||0}ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
|
||||
}
|
||||
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext()}
|
||||
}
|
||||
|
||||
function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true}
|
||||
function backendQualityTiles(fullW,fullH){const w=Math.min(BACKEND_PROBE_QUALITY_W,fullW),h=Math.min(BACKEND_PROBE_QUALITY_H,fullH),x1=Math.max(0,fullW-w),y1=Math.max(0,fullH-h),xc=Math.max(0,Math.floor((fullW-w)/2)),yc=Math.max(0,Math.floor((fullH-h)/2));return[[0,0],[x1,0],[0,y1],[x1,y1],[xc,yc]].map(([tileX,tileY])=>({tileX,tileY,w,h}))}
|
||||
async function performBackendProbe(snap,iter,decision,token){if(!renderer||!decision.metrics.overlap||token!==state.token)return null;const fullW=canvas.width,fullH=canvas.height,sw=Math.min(BACKEND_PROBE_SPEED_W,fullW),sh=Math.min(BACKEND_PROBE_SPEED_H,fullH),sx=Math.max(0,Math.floor((fullW-sw)/2)),sy=Math.max(0,Math.floor((fullH-sh)/2)),speedPixels=sw*sh,fullPixels=Math.max(1,fullW*fullH),direct=await renderer.benchmarkNumeric({snap,iter,deep:false,deepContext:null,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh});if(token!==state.token)return null;const rb=performance.now(),ctx=await refs.request(snap,iter),buildWall=performance.now()-rb;if(token!==state.token||ctx.checkpointMismatch)return null;const deepProbe=await renderer.benchmarkNumeric({snap,iter,deep:true,deepContext:ctx,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh,correctUnknown:true});if(token!==state.token)return null;let classDisagreement=0,deepUnknown=0,knownCompared=0,qualityPixels=0,corrected=0;for(const q of backendQualityTiles(fullW,fullH)){const directMeta=await renderer.renderTileMeta({snap,iter,deep:false,deepContext:null,fullW,fullH,...q});if(token!==state.token)return null;const deepMeta=await renderer.renderTileMeta({snap,iter,deep:true,deepContext:ctx,fullW,fullH,...q,correctUnknown:true});if(token!==state.token)return null;qualityPixels+=q.w*q.h;corrected+=deepMeta.corrected||0;for(let i=0;i<deepMeta.length;i++){const dc=(deepMeta[i]>>>28)&3;if(dc===FIELD_UNKNOWN){deepUnknown++;continue}const rc=(directMeta[i]>>>28)&3;knownCompared++;if((dc===FIELD_ESCAPED)!==(rc===FIELD_ESCAPED))classDisagreement++}}const buildMs=ctx.buildMs||buildWall,directMsPerPixel=direct.gpuMs/Math.max(1,speedPixels),deepMsPerPixel=deepProbe.gpuMs/Math.max(1,speedPixels),sample={directMsPerPixel,deepMsPerPixel,referenceBuildMs:buildMs},prev=backendProfiles.get(decision.key),a=prev?.samples?0.3:1,profile={samples:(prev?.samples||0)+1,directMsPerPixel:(prev?.directMsPerPixel||sample.directMsPerPixel)*(1-a)+sample.directMsPerPixel*a,deepMsPerPixel:(prev?.deepMsPerPixel||sample.deepMsPerPixel)*(1-a)+sample.deepMsPerPixel*a,referenceBuildMs:(prev?.referenceBuildMs||sample.referenceBuildMs)*(1-a)+sample.referenceBuildMs*a};backendProfiles.set(decision.key,profile);const deepUnknownRate=deepUnknown/Math.max(1,qualityPixels),qualityPass=classDisagreement===0&&deepUnknownRate<=DEEP_PROBE_UNKNOWN_MAX,viewProbe={qualityPass,deepUnknownRate,classDisagreement,knownCompared,qualityPixels,corrected,predictedDirectMs:directMsPerPixel*fullPixels,predictedDeepMs:buildMs+deepMsPerPixel*fullPixels,ratio:decision.metrics.ratio,zoom:zoomExpFor(snap),iter,speedProbe:{w:sw,h:sh,tileX:sx,tileY:sy},qualityTiles:backendQualityTiles(fullW,fullH)};backendViewProbes.set(decision.viewKey,viewProbe);runtime.backendProbes++;return viewProbe}
|
||||
function ensureBackendProbe(snap,iter,decision,token){if(!decision.metrics.overlap)return Promise.resolve(null);const hit=backendViewProbes.get(decision.viewKey);if(hit)return Promise.resolve(hit);const pending=backendProbePending.get(decision.viewKey);if(pending)return pending;const task=performBackendProbe(snap,iter,decision,token).catch(e=>{if(token===state.token)console.warn('backend probe failed',e);return null}).finally(()=>backendProbePending.delete(decision.viewKey));backendProbePending.set(decision.viewKey,task);return task}
|
||||
|
||||
|
||||
// ── backend crossover benchmark (diagnostic only) ───────────────────────
|
||||
const CROSSOVER_TRACKS={
|
||||
seahorse:{id:'seahorse',label:'Seahorse boundary',re:'-0.7453983606667815',im:'0.1125046349959942'},
|
||||
cusp:{id:'cusp',label:'Period-2 cusp',re:'-0.75',im:'0'}
|
||||
};
|
||||
const CROSSOVER_DEPTHS=[0,2,4,6,8,10,12,14,16,20,30,40];
|
||||
const benchmarkJob={active:false,cancelled:false,last:null};
|
||||
function fixedAtDecimal(text,bits){let s=String(text).trim(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const parts=s.toLowerCase().split('e'),mant=parts[0],exp=parts[1]?parseInt(parts[1],10):0,ab=mant.split('.'),ii=ab[0]||'0',ff=ab[1]||'';let digits=(ii+ff).replace(/^0+(?=\d)/,'')||'0',places=ff.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),scale=1n<<BigInt(bits),v=(BigInt(digits)*scale+den/2n)/den;return neg?-v:v}
|
||||
function crossoverSnap(track,z){const span='3.4e-'+z,bits=Math.max(320,decimalRequiredBits(track.re)+64,decimalRequiredBits(track.im)+64,decimalRequiredBits(span)+96);return{bits,re:fixedAtDecimal(track.re,bits),im:fixedAtDecimal(track.im,bits),span:fixedAtDecimal(span,bits)}}
|
||||
function iterForZoom(z,base=350){return Math.max(base,base+Math.floor(70*Math.sqrt(Math.max(0,z))+15*Math.max(0,z)))}
|
||||
function median(values){const a=[...values].sort((x,y)=>x-y),n=a.length;return n?a.length%2?a[(n-1)>>1]:(a[n/2-1]+a[n/2])/2:NaN}
|
||||
function compareMeta(reference,candidate){let referenceUnknown=0,candidateUnknown=0,classMismatch=0,escapedCompared=0,iterMismatch=0,maxIterDelta=0,sumIterDelta=0;const n=Math.min(reference.length,candidate.length);for(let i=0;i<n;i++){const rm=reference[i]>>>0,cm=candidate[i]>>>0,rc=(rm>>>28)&3,cc=(cm>>>28)&3;if(rc===FIELD_UNKNOWN){referenceUnknown++;continue}if(cc===FIELD_UNKNOWN){candidateUnknown++;continue}const re=rc===FIELD_ESCAPED,ce=cc===FIELD_ESCAPED;if(re!==ce){classMismatch++;continue}if(re){escapedCompared++;const d=Math.abs((rm&ITER_MASK)-(cm&ITER_MASK));if(d){iterMismatch++;sumIterDelta+=d;if(d>maxIterDelta)maxIterDelta=d}}}return{pixels:n,referenceUnknown,candidateUnknown,classMismatch,classMismatchRate:classMismatch/Math.max(1,n-referenceUnknown),escapedCompared,escapeIterationMismatch:iterMismatch,escapeIterationMismatchRate:iterMismatch/Math.max(1,escapedCompared),meanEscapeIterationDelta:iterMismatch?sumIterDelta/iterMismatch:0,maxEscapeIterationDelta:maxIterDelta}}
|
||||
function crossoverQualityTiles(fullW,fullH,w,h){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[0,0],[Math.max(0,fullW-w),0],[0,Math.max(0,fullH-h)],[Math.max(0,fullW-w),Math.max(0,fullH-h)],[Math.max(0,Math.floor((fullW-w)/2)),Math.max(0,Math.floor((fullH-h)/2))]];return pts.map(([tileX,tileY])=>({tileX,tileY,w,h}))}
|
||||
function aggregateQuality(list){const out={pixels:0,referenceUnknown:0,candidateUnknown:0,classMismatch:0,escapedCompared:0,escapeIterationMismatch:0,sumEscapeIterationDelta:0,maxEscapeIterationDelta:0};for(const q of list){out.pixels+=q.pixels;out.referenceUnknown+=q.referenceUnknown;out.candidateUnknown+=q.candidateUnknown;out.classMismatch+=q.classMismatch;out.escapedCompared+=q.escapedCompared;out.escapeIterationMismatch+=q.escapeIterationMismatch;out.sumEscapeIterationDelta+=q.meanEscapeIterationDelta*q.escapeIterationMismatch;out.maxEscapeIterationDelta=Math.max(out.maxEscapeIterationDelta,q.maxEscapeIterationDelta)}out.classMismatchRate=out.classMismatch/Math.max(1,out.pixels-out.referenceUnknown);out.escapeIterationMismatchRate=out.escapeIterationMismatch/Math.max(1,out.escapedCompared);out.meanEscapeIterationDelta=out.escapeIterationMismatch?out.sumEscapeIterationDelta/out.escapeIterationMismatch:0;delete out.sumEscapeIterationDelta;return out}
|
||||
const CROSSOVER_SPEED_W=256,CROSSOVER_SPEED_H=144,CROSSOVER_GPU_TIMEOUT_MS=15000,CROSSOVER_REF_TIMEOUT_MS=15000;
|
||||
function crossoverSpeedTiles(fullW,fullH,w=CROSSOVER_SPEED_W,h=CROSSOVER_SPEED_H){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[.5,.5],[.28,.36],[.72,.64]],seen=new Set(),out=[];for(const [fx,fy] of pts){const tileX=Math.max(0,Math.min(fullW-w,Math.round(fullW*fx-w/2))),tileY=Math.max(0,Math.min(fullH-h,Math.round(fullH*fy-h/2))),k=tileX+':'+tileY;if(!seen.has(k)){seen.add(k);out.push({tileX,tileY,w,h})}}return out}
|
||||
function benchmarkTimeout(promise,ms,label,onTimeout){let timer;const timeout=new Promise((_,reject)=>{timer=setTimeout(()=>{try{onTimeout?.()}catch{}reject(new Error(label+' timed out after '+Math.round(ms/1000)+'s'))},ms)});return Promise.race([promise,timeout]).finally(()=>clearTimeout(timer))}
|
||||
async function benchmarkTileBatch(fn,tiles){let gpuMs=0,pixels=0;for(const q of tiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h}return{gpuMs,pixels}}
|
||||
async function timedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[];for(let i=0;i<samples;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'sample',index:i+1,total:samples});values.push((await fn()).gpuMs)}return{medianMs:median(values),samplesMs:values}}
|
||||
function extrapolateTiming(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels),fullMedianMs=msPerPixel*fullPixels;return{...timing,speedPixels,msPerPixel,fullMedianMs}}
|
||||
async function runCrossoverBenchmark({track='seahorse',depths=CROSSOVER_DEPTHS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
|
||||
if(benchmarkJob.active)throw new Error('benchmark already running');benchmarkJob.active=true;benchmarkJob.cancelled=false;
|
||||
try{
|
||||
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
|
||||
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown benchmark track');
|
||||
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
|
||||
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
|
||||
for(let di=0;di<depths.length;di++){
|
||||
if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z),metrics=directMetrics(snap,fullW);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'reference'});
|
||||
const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'z'+z+' reference',()=>refs.cancelPending('benchmark reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
|
||||
const timed=(name,fn)=>timedMedian(()=>gpuCall(benchmarkTileBatch(fn,speedTiles),'z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
|
||||
const tileCommon=q=>({snap,iter,fullW,fullH,...q});
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'f32'});
|
||||
const f32=extrapolateTiming(await timed('f32',q=>r.benchmarkNumeric({...tileCommon(q),deep:false,deepContext:null})),speedPixels,fullPixels);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'ds'});
|
||||
const ds=extrapolateTiming(await timed('ds',q=>r.benchmarkDsDirect(tileCommon(q))),speedPixels,fullPixels);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'deep'});
|
||||
const deep=extrapolateTiming(await timed('deep',q=>r.benchmarkNumeric({...tileCommon(q),deep:true,deepContext:ctx,correctUnknown:true})),speedPixels,fullPixels);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'quality'});
|
||||
const f32Parts=[],dsParts=[],qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH);let deepUnknownCount=0,qualityPixels=0,deepCorrected=0;
|
||||
for(const q of qTiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const qCommon={snap,iter,fullW,fullH,...q},deepMeta=await gpuCall(r.renderTileMeta({...qCommon,deep:true,deepContext:ctx,correctUnknown:true}),'z'+z+' quality deep'),f32Meta=await gpuCall(r.renderTileMeta({...qCommon,deep:false,deepContext:null}),'z'+z+' quality f32'),dsMeta=await gpuCall(r.renderTileMetaDsDirect(qCommon),'z'+z+' quality ds');qualityPixels+=q.w*q.h;deepUnknownCount+=deepMeta.unresolved||0;deepCorrected+=deepMeta.corrected||0;f32Parts.push(compareMeta(deepMeta,f32Meta));dsParts.push(compareMeta(deepMeta,dsMeta))}
|
||||
const f32Quality=aggregateQuality(f32Parts),dsQuality=aggregateQuality(dsParts),deepUnknown=deepUnknownCount/Math.max(1,qualityPixels),referenceBuildMs=ctx.buildMs||referenceWallMs;
|
||||
const row={track:tr.id,z,span:'3.4e-'+z,iter,resolution:[fullW,fullH],speedTiles,speedPixels,qualityTile:[qualityW,qualityH],qualityTiles:qTiles,directRatio:metrics.ratio,
|
||||
f32:{...f32,quality:f32Quality},ds:{...ds,quality:dsQuality},deep:{...deep,referenceBuildMs,referenceWallMs,coldMedianMs:referenceBuildMs+deep.fullMedianMs,warmMedianMs:deep.fullMedianMs,unknownRate:deepUnknown,corrected:deepCorrected}};
|
||||
const qualityLimit=.01;
|
||||
row.assessment={f32VisuallySafe:f32Quality.classMismatchRate<=qualityLimit&&metrics.ratio>=1,dsVisuallySafe:dsQuality.classMismatchRate<=qualityLimit,deepVisuallySafe:deepUnknown<=.001,
|
||||
fastestWarm:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.warmMedianMs]].sort((a,b)=>a[1]-b[1])[0][0],
|
||||
fastestCold:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.coldMedianMs]].sort((a,b)=>a[1]-b[1])[0][0]};
|
||||
rows.push(row);onProgress?.({index:di+1,total:depths.length,z,phase:'done',row});
|
||||
}
|
||||
const result={format:'mandelbrot-webgpu-crossover-v2',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Speed timing uses three bounded representative tiles and extrapolates ms/pixel to the current full resolution, preventing long full-frame Deep benchmark dispatches. Deep cold adds verified BigInt reference construction. Quality remains measured on five spatial tiles. Any single benchmark GPU batch or reference build exceeding 15 s aborts with an explicit error instead of waiting indefinitely.'};
|
||||
benchmarkJob.last=result;return result;
|
||||
}finally{benchmarkJob.active=false}
|
||||
}
|
||||
|
||||
|
||||
// ── DS + sparse Deep hybrid experiment (diagnostic only) ───────────────
|
||||
const HYBRID_DEPTHS=[6,8,10,12],HYBRID_RISK_LIMITS=[1e12,1e13,1e14];
|
||||
const hybridJob={active:false,cancelled:false,last:null};
|
||||
async function hybridTileBatch(fn,tiles){let gpuMs=0,pixels=0,uncertain=0,remaining=0,corrected=0;for(const q of tiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h;uncertain+=r.uncertain||0;remaining+=r.remaining||0;corrected+=r.corrected||0}return{gpuMs,pixels,uncertain,remaining,corrected}}
|
||||
async function hybridTimedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[],stats=[];for(let i=0;i<samples;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'sample',index:i+1,total:samples});const r=await fn();values.push(r.gpuMs);stats.push(r)}return{medianMs:median(values),samplesMs:values,sampleStats:stats}}
|
||||
function hybridExtrapolate(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels);return{...timing,speedPixels,msPerPixel,fullMedianMs:msPerPixel*fullPixels}}
|
||||
async function runHybridExperiment({track='seahorse',depths=HYBRID_DEPTHS,riskLimits=HYBRID_RISK_LIMITS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
|
||||
if(hybridJob.active||benchmarkJob.active)throw new Error('another benchmark is running');hybridJob.active=true;hybridJob.cancelled=false;
|
||||
try{
|
||||
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
|
||||
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown experiment track');
|
||||
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
|
||||
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
|
||||
for(let di=0;di<depths.length;di++){
|
||||
if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'reference'});const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'hybrid z'+z+' reference',()=>refs.cancelPending('hybrid reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
|
||||
const timed=(name,fn)=>hybridTimedMedian(()=>gpuCall(hybridTileBatch(fn,speedTiles),'hybrid z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
|
||||
const common=q=>({snap,iter,fullW,fullH,...q});
|
||||
const ds=hybridExtrapolate(await timed('DS baseline',async q=>{const x=await r.benchmarkDsDirect(common(q));return{...x}}),speedPixels,fullPixels);
|
||||
const deep=hybridExtrapolate(await timed('Full Deep',async q=>{const x=await r.benchmarkNumeric({...common(q),deep:true,deepContext:ctx,correctUnknown:true});return{...x}}),speedPixels,fullPixels);
|
||||
const qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH),deepMetas=[];let deepUnknown=0;
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'quality reference'});
|
||||
for(const q of qTiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const m=await gpuCall(r.renderTileMeta({...common(q),deep:true,deepContext:ctx,correctUnknown:true}),'hybrid z'+z+' quality deep');deepUnknown+=m.unresolved||0;deepMetas.push(m)}
|
||||
const variants=[];
|
||||
for(const riskLimit of riskLimits){
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'risk '+riskLimit.toExponential(0)});
|
||||
const timing=hybridExtrapolate(await timed('Hybrid '+riskLimit.toExponential(0),q=>r.benchmarkHybrid({...common(q),deepContext:ctx,riskLimit})),speedPixels,fullPixels);
|
||||
const parts=[];let uncertain=0,remaining=0,corrected=0,qualityPixels=0;
|
||||
for(let qi=0;qi<qTiles.length;qi++){const q=qTiles[qi],hm=await gpuCall(r.renderTileMetaHybrid({...common(q),deepContext:ctx,riskLimit}),'hybrid z'+z+' quality '+riskLimit);qualityPixels+=q.w*q.h;uncertain+=hm.uncertain||0;remaining+=hm.remaining||0;corrected+=hm.corrected||0;parts.push(compareMeta(deepMetas[qi],hm))}
|
||||
const quality=aggregateQuality(parts),referenceBuildMs=ctx.buildMs||referenceWallMs,coldMedianMs=referenceBuildMs+timing.fullMedianMs,uncertainRate=uncertain/Math.max(1,qualityPixels),remainingRate=remaining/Math.max(1,qualityPixels),safe=quality.classMismatch===0&&remaining===0;
|
||||
variants.push({riskLimit,...timing,referenceBuildMs,coldMedianMs,quality,qualityPixels,uncertain,uncertainRate,remaining,remainingRate,corrected,safe,speedupVsDeepCold:(referenceBuildMs+deep.fullMedianMs)/Math.max(1e-9,coldMedianMs)});
|
||||
}
|
||||
const referenceBuildMs=ctx.buildMs||referenceWallMs,deepCold=referenceBuildMs+deep.fullMedianMs,viable=variants.filter(v=>v.safe).sort((a,b)=>a.coldMedianMs-b.coldMedianMs),best=viable[0]||null;
|
||||
rows.push({track:tr.id,z,iter,resolution:[fullW,fullH],referenceBuildMs,ds:{...ds},deep:{...deep,coldMedianMs:deepCold,unknownRate:deepUnknown/Math.max(1,qTiles.reduce((n,q)=>n+q.w*q.h,0))},variants,bestRiskLimit:best?.riskLimit??null,bestHybridColdMs:best?.coldMedianMs??null,recommendation:best&&best.coldMedianMs<deepCold*.9&&best.uncertainRate<.6?'hybrid-candidate':'full-deep'});
|
||||
onProgress?.({index:di+1,total:depths.length,z,phase:'done'});
|
||||
}
|
||||
const result={format:'mandelbrot-ds-sparse-deep-hybrid-v1',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],riskLimits:[...riskLimits],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Diagnostic only. DS guarded direct tracks dz/dc in f32 and marks a pixel UNKNOWN when |dz/dc| exceeds riskLimit. Existing deep double-single correction re-evaluates only those UNKNOWN pixels. Full Deep+correction is the GPU quality reference. Cold timing adds the verified BigInt reference build to both Hybrid and Full Deep. Production routing is unchanged.'};
|
||||
hybridJob.last=result;return result;
|
||||
}finally{hybridJob.active=false}
|
||||
}
|
||||
|
||||
// ── GPU startup / rendering orchestration ────────────────────────────────
|
||||
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
|
||||
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
|
||||
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
|
||||
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
|
||||
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
|
||||
async function renderFrame(){
|
||||
const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height);
|
||||
state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats();
|
||||
try{
|
||||
const r=renderer||await initRenderer();if(token!==state.token)return;
|
||||
if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}
|
||||
if(decision.probe){state.lastEngine='WebGPU · direct/deep view probe';state.drawState='COVERING';updateStats();await ensureBackendProbe(snap,iter,decision,token);if(token!==state.token)return;decision=chooseBackend(snap,canvas.width,iter,canvas.height);if(decision.probe)decision={...decision,backend:'deep',deep:true,probe:false,reason:'overlap-probe-failed-safe-deep'};state.backendDecision=decision;updateStats()}
|
||||
const deep=decision.deep;let ctx=null;
|
||||
if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded perturbation'}else state.lastEngine='WebGPU · f32 direct';
|
||||
const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;
|
||||
const firstPaint=performance.now()-t0;
|
||||
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,backend:decision.backend};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=firstPaint;state.rendering=deep;state.unresolved=0;
|
||||
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();
|
||||
if(!deep){state.rendering=false;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();return}
|
||||
let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats();
|
||||
const n=canvas.width*canvas.height;if(shouldApplySparseCorrection(state.unresolved,n)){state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats();const corrected=await r.correctUnknownFrame(snap,iter,token);if(!corrected||token!==state.token)return;state.correctionPasses=1;stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;const painted=await r.recolor(token);if(!painted||token!==state.token)return;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats()}
|
||||
if(token!==state.token)return;state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · guarded perturbation'+(state.correctionPasses?' + DS correction':'');const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();
|
||||
}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
|
||||
}
|
||||
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback(深部非対応)';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
|
||||
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
|
||||
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
|
||||
|
||||
// ── interaction / view history ──────────────────────────────────────────
|
||||
function viewRect(){return canvas.getBoundingClientRect()}
|
||||
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
|
||||
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
|
||||
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
|
||||
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
|
||||
const pts=new Map();let lx=0,ly=0,pinch=0;
|
||||
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
|
||||
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
|
||||
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
|
||||
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
|
||||
|
||||
// ── URL / controls ───────────────────────────────────────────────────────
|
||||
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
|
||||
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
|
||||
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
|
||||
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
|
||||
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
|
||||
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
|
||||
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
|
||||
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
|
||||
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
|
||||
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
|
||||
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
|
||||
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
|
||||
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
|
||||
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
|
||||
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
|
||||
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
|
||||
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
|
||||
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
|
||||
|
||||
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
|
||||
const exportJob={active:false,cancelled:false};
|
||||
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
|
||||
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
|
||||
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
|
||||
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
|
||||
class StreamingPng{
|
||||
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
|
||||
async rows(filteredRows){await this.writer.write(filteredRows)}
|
||||
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
|
||||
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
|
||||
}
|
||||
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
|
||||
async function runExport(){
|
||||
if(exportJob.active)return;
|
||||
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
|
||||
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),strict=$('#exportPrecision').value==='strict';
|
||||
let ctx=null;
|
||||
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
|
||||
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
|
||||
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
|
||||
let done=0,unresolvedSamples=0;
|
||||
try{
|
||||
for(let y=0;y<h;y+=tile){
|
||||
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
|
||||
for(let x=0;x<w;x+=tile){
|
||||
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
|
||||
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
|
||||
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
|
||||
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
|
||||
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
|
||||
}
|
||||
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
|
||||
}
|
||||
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
|
||||
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
|
||||
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
|
||||
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
|
||||
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
|
||||
finally{exportJob.active=false;$('#exportStart').disabled=false}
|
||||
}
|
||||
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
|
||||
|
||||
function benchmarkSummary(result){const lines=['z | f32 est ms | DS est ms | deep warm est | deep cold est | f32 Δclass | DS Δclass | direct ratio | fastest cold','--|------------|-----------|---------------|---------------|------------|-----------|--------------|-------------'];for(const r of result.rows)lines.push([String(r.z).padStart(2),r.f32.fullMedianMs.toFixed(1),r.ds.fullMedianMs.toFixed(1),r.deep.warmMedianMs.toFixed(1),r.deep.coldMedianMs.toFixed(1),(100*r.f32.quality.classMismatchRate).toFixed(2)+'%',(100*r.ds.quality.classMismatchRate).toFixed(2)+'%',Number.isFinite(r.directRatio)?r.directRatio.toExponential(2):'n/a',r.assessment.fastestCold].join(' | '));return lines.join('\n')}
|
||||
$('#backendBench').onclick=()=>{const d=$('#benchmarkDialog');$('#benchmarkStatus').textContent='';$('#benchmarkProgress').hidden=true;$('#benchmarkOutput').value=benchmarkJob.last?benchmarkSummary(benchmarkJob.last):'';$('#benchmarkSave').disabled=!benchmarkJob.last;d.showModal?d.showModal():d.setAttribute('open','')};
|
||||
$('#benchmarkStart').onclick=async()=>{if(benchmarkJob.active)return;cancelRender();state.dirty=false;const start=$('#benchmarkStart'),progress=$('#benchmarkProgress'),status=$('#benchmarkStatus');start.disabled=true;$('#benchmarkSave').disabled=true;progress.hidden=false;progress.max=CROSSOVER_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#benchmarkOutput').value='';try{const result=await runCrossoverBenchmark({track:$('#benchmarkTrack').value,samples:Math.max(1,Number($('#benchmarkSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});progress.value=progress.max;status.textContent='測定完了 · '+result.rows.length+' depths';$('#benchmarkOutput').value=benchmarkSummary(result);$('#benchmarkSave').disabled=false}catch(e){status.textContent=String(e&&e.message||e)}finally{start.disabled=false;markDirty(false)}};
|
||||
$('#benchmarkCancel').onclick=()=>{if(benchmarkJob.active){benchmarkJob.cancelled=true;refs.cancelPending('benchmark cancelled');$('#benchmarkStatus').textContent='中止しています…'}else $('#benchmarkDialog').close()};
|
||||
$('#benchmarkSave').onclick=()=>{if(!benchmarkJob.last)return;downloadBlob(new Blob([JSON.stringify(benchmarkJob.last,null,2)],{type:'application/json'}),'mandelbrot-crossover-'+Date.now()+'.json')};
|
||||
|
||||
function hybridSummary(result){const lines=['z | best risk | hybrid cold est | deep cold est | speedup | correction | remain | Δclass | recommendation','--|-----------|-----------------|---------------|---------|------------|--------|--------|---------------'];for(const r of result.rows){const best=r.bestRiskLimit==null?null:r.variants.find(v=>v.riskLimit===r.bestRiskLimit),hy=best?best.coldMedianMs:NaN;lines.push([String(r.z).padStart(2),best?best.riskLimit.toExponential(0):'none',best?hy.toFixed(1):'n/a',r.deep.coldMedianMs.toFixed(1),best?(r.deep.coldMedianMs/hy).toFixed(2)+'×':'n/a',best?(100*best.uncertainRate).toFixed(1)+'%':'n/a',best?(100*best.remainingRate).toFixed(2)+'%':'n/a',best?(100*best.quality.classMismatchRate).toFixed(2)+'%':'n/a',r.recommendation].join(' | '))}return lines.join('\n')}
|
||||
function openHybridDialog(){const d=$('#hybridDialog');if(!d.open){try{if(typeof d.showModal==='function')d.showModal();else d.setAttribute('open','')}catch{d.setAttribute('open','')}}return d}
|
||||
async function startHybridExperimentFromUi(){
|
||||
const d=openHybridDialog(),start=$('#hybridStart'),toolbar=$('#hybridBench'),progress=$('#hybridProgress'),status=$('#hybridStatus');status.textContent='Hybrid実験を起動中…';toolbar.setAttribute('aria-busy','true');
|
||||
if(hybridJob.active){status.textContent='Hybrid実験を実行中です';toolbar.removeAttribute('aria-busy');return}
|
||||
if(benchmarkJob.active){status.textContent='Backend比較を終了または中止してから実行してください';toolbar.removeAttribute('aria-busy');return}
|
||||
cancelRender();state.dirty=false;start.disabled=true;toolbar.disabled=true;start.textContent='実行中…';$('#hybridSave').disabled=true;progress.hidden=false;progress.max=HYBRID_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#hybridOutput').value='';
|
||||
try{
|
||||
const result=await runHybridExperiment({track:$('#hybridTrack').value,samples:Math.max(1,Number($('#hybridSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});
|
||||
progress.value=progress.max;status.textContent='実験完了 · '+result.rows.length+' depths';$('#hybridOutput').value=hybridSummary(result);$('#hybridSave').disabled=false;start.textContent='再実行';
|
||||
}catch(e){status.textContent=String(e&&e.message||e);start.textContent='再実行'}
|
||||
finally{start.disabled=false;toolbar.disabled=false;toolbar.removeAttribute('aria-busy');markDirty(false)}
|
||||
}
|
||||
$('#hybridBench').onclick=()=>{toast('Hybrid実験を開始');void startHybridExperimentFromUi()};
|
||||
$('#hybridStart').onclick=startHybridExperimentFromUi;
|
||||
$('#hybridCancel').onclick=()=>{if(hybridJob.active){hybridJob.cancelled=true;refs.cancelPending('hybrid experiment cancelled');$('#hybridStatus').textContent='中止しています…'}else $('#hybridDialog').close()};
|
||||
$('#hybridSave').onclick=()=>{if(!hybridJob.last)return;downloadBlob(new Blob([JSON.stringify(hybridJob.last,null,2)],{type:'application/json'}),'mandelbrot-hybrid-'+Date.now()+'.json')};
|
||||
|
||||
|
||||
// ── diagnostics ──────────────────────────────────────────────────────────
|
||||
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend(snapshot(),Math.max(1,canvas.width),maxIter(),Math.max(1,canvas.height));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.probe?'WebGPU 判定中':decision.deep?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'深部補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.correctedPixels)status+=' · 回収 '+state.correctedPixels;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx,ratio=decision.metrics&&Number.isFinite(decision.metrics.ratio)?decision.metrics.ratio:0,prof=decision.profile,rs=state.unknownReasons,reasonText=rs?Object.entries(rs).filter(([,v])=>v).map(([k,v])=>k+':'+v).join(' '):'';$('#diagEngine').textContent='engine: '+state.lastEngine+' | route '+decision.backend+' ('+decision.reason+') | ratio '+(ratio?ratio.toExponential(2):'n/a')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'')+(reasonText?' | UNKNOWN '+reasonText:'')+(decision.viewProbe?' | view probe direct '+decision.viewProbe.predictedDirectMs.toFixed(0)+'ms / deep '+decision.viewProbe.predictedDeepMs.toFixed(0)+'ms / u '+(100*decision.viewProbe.deepUnknownRate).toFixed(2)+'% / Δclass '+decision.viewProbe.classDisagreement:'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height+' | speed profiles '+backendProfiles.size+' / view probes '+backendViewProbes.size}
|
||||
|
||||
globalThis.__MANDEL_TEST__={
|
||||
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
|
||||
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
|
||||
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
|
||||
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w,h):!!forceDeep;let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
|
||||
async runCrossoverBenchmark(options={}){return runCrossoverBenchmark(options)},
|
||||
async runHybridExperiment(options={}){return runHybridExperiment(options)},
|
||||
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
|
||||
};
|
||||
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,buildVersion:BUILD_VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:(state.backendDecision||chooseBackend()).deep,backendDecision:state.backendDecision,backendProfiles:Array.from(backendProfiles.entries()),backendViewProbes:Array.from(backendViewProbes.entries()),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,unknownReasons:state.unknownReasons,correctionPasses:state.correctionPasses,correctedPixels:state.correctedPixels,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
|
||||
|
||||
// ── boot / teardown ──────────────────────────────────────────────────────
|
||||
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
|
||||
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
|
||||
})();
|
||||
399
dist/hosted/script.js
vendored
399
dist/hosted/script.js
vendored
|
|
@ -2,15 +2,14 @@
|
|||
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 CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000;
|
||||
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 NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96,REFINE_QUEUE_PARAM_BYTES=16,REFINE_STATS_BYTES=16,REFINE_BATCH_BYTES=16;
|
||||
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorCycleDir:1,colorShiftDir:1,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,refinementRunning:false,refinementStage:0,refinementQueue:0,refinedIter:0};
|
||||
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,idleRefineTimer=0,lastWrittenHash='',navigationHash='';
|
||||
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();
|
||||
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,idleRefineBatches:0,idleRefineQueued:0};
|
||||
|
||||
// ── exact fixed-point view state ─────────────────────────────────────────
|
||||
function one(bits=state.bits){return 1n<<BigInt(bits)}
|
||||
|
|
@ -30,11 +29,6 @@ function f32ToFixed(x,bits){
|
|||
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)}
|
||||
|
|
@ -44,20 +38,21 @@ function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.sp
|
|||
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 f32Ulp(x){x=Math.abs(Math.fround(x));if(!Number.isFinite(x))return Infinity;if(x===0)return Math.pow(2,-149);if(x<Math.pow(2,-126))return Math.pow(2,-149);return Math.pow(2,Math.floor(Math.log2(x))-23)}
|
||||
function directPixelRatio(snap,w){const cr=Math.abs(fixedNum(snap.re,snap.bits)),ci=Math.abs(fixedNum(snap.im,snap.bits)),sp=Math.abs(fixedNum(snap.span,snap.bits)),step=sp/Math.max(1,w),scale=Math.max(cr,ci,sp*.75,Math.pow(2,-126)),ulp=f32Ulp(scale);return step/ulp}
|
||||
function fastNeedsExtended(snap,w){return !(directPixelRatio(snap,w)>=4)}
|
||||
function chooseBackend(snap=snapshot(),w=canvas.width){const deep=state.renderMode==='accurate',fastExtended=!deep&&fastNeedsExtended(snap,w);return{backend:deep?'deep':fastExtended?'fast-extended':'direct',deep,fastExtended,probe:false,reason:deep?'manual-accurate':fastExtended?'manual-fast-extended':'manual-fast'}}
|
||||
function deepNeeded(){return state.renderMode==='accurate'}
|
||||
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,renderMode:state.renderMode}}
|
||||
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()}
|
||||
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,renderMode:v.renderMode||state.renderMode});ensurePrecision();syncControls();saveHash(false);markDirty()}
|
||||
|
||||
// ── 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 effectiveColorCycle(iter=maxIter()){const base=Math.max(1,state.baseIter),effective=Math.max(base,Number(iter)||base);return state.cycle*base/effective}
|
||||
function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))}
|
||||
function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)}
|
||||
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)}}
|
||||
|
||||
|
|
@ -69,19 +64,45 @@ 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 orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n;for(let n=0;n<iter;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)return n+1}return 0}
|
||||
function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]}
|
||||
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.score){best={...c,escape,score}}if(escape===0)break}return best}
|
||||
function 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)})}}
|
||||
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceBits=d.bits,sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),limit=Math.min(MAX_REF-1,d.iter),bits=sourceBits+64,centerRef=orbit(bits,sourceRe<<64n,sourceIm<<64n,limit),chosen=chooseReference(sourceBits,sourceRe,sourceIm,sourceSpan,w,h,limit,centerRef.escape),re=chosen.re<<64n,im=chosen.im<<64n,ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,limit),v=verify(sourceBits,chosen.re,chosen.im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
|
||||
`}
|
||||
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})})}
|
||||
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
|
||||
request(snap,iter,width=1,height=1,fresh=false){const key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,'guarded-perturb-v24.2.26'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})}
|
||||
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();
|
||||
|
||||
function fastReferenceBits(snap){const need=Math.ceil(Math.max(0,-log2FixedAt(snap.span,snap.bits)))+48;return Math.max(96,Math.min(snap.bits,need))}
|
||||
function fastReferenceWorkerSource(){return String.raw`
|
||||
'use strict';
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundShift(v,b){if(b<=0)return v;const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
|
||||
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
|
||||
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<<BigInt(d):roundShift(v,-d)}
|
||||
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
|
||||
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n;for(let n=0;n<iter;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)return n+1}return 0}
|
||||
function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]}
|
||||
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.score){best={...c,escape,score}}if(escape===0)break}return best}
|
||||
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
|
||||
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),bits=d.targetBits,centerRe=requant(sourceRe,d.sourceBits,bits),centerIm=requant(sourceIm,d.sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,d.iter),chosen=chooseReference(d.sourceBits,sourceRe,sourceIm,sourceSpan,w,h,d.iter,centerRef.escape),re=requant(chosen.re,d.sourceBits,bits),im=requant(chosen.im,d.sourceBits,bits),ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,d.iter),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
|
||||
`}
|
||||
class FastReferenceService{
|
||||
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=12}
|
||||
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([fastReferenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'fast reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
|
||||
request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.2.26'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})}
|
||||
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
|
||||
destroy(){this.cancelPending('destroyed');this.cache.clear()}
|
||||
}
|
||||
const fastRefs=new FastReferenceService();
|
||||
|
||||
// ── 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{}}
|
||||
|
|
@ -93,7 +114,7 @@ class WebGpuRenderer{
|
|||
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'});
|
||||
this.frame=null;this.deepCtx=null;this.fastCtx=null;this.refineDeepCtx=null;this.exportWs=null;this.refinePipelinePromise=null;this.likelyQueue=null;this.deepLikelyRefine=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
|
||||
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()});
|
||||
|
|
@ -103,14 +124,18 @@ class WebGpuRenderer{
|
|||
async initPipelines(){
|
||||
this.device.pushErrorScope?.('validation');
|
||||
try{
|
||||
const [dm,sdm,shm,qm,fm,xm,zm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('direct-ds-benchmark',G.DIRECT_DS_WGSL),this.module('direct-ds-guarded-experiment',G.DIRECT_DS_GUARDED_WGSL),this.module('direct-fixed96-queue-args-experiment',G.DIRECT_FIXED96_QUEUE_ARGS_WGSL),this.module('direct-fixed96-correction-experiment',G.DIRECT_FIXED96_CORRECT_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
|
||||
const [dm,fm,xm,xhm,xpm,xsm,zm,zqm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
|
||||
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.fast=this.device.createComputePipeline({layout:'auto',compute:{module:fm,entryPoint:'main'}});
|
||||
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
|
||||
const [xpsm,xhsm]=await Promise.all([this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL),this.module('deep-bucket-histogram-stats-production',G.DEEP_BUCKET_HIST_STATS_WGSL)]);
|
||||
this.deepPostStats=this.device.createComputePipeline({layout:'auto',compute:{module:xpsm,entryPoint:'main'}});
|
||||
this.deepBucketHist=this.device.createComputePipeline({layout:'auto',compute:{module:xhm,entryPoint:'main'}});
|
||||
this.deepBucketHistStats=this.device.createComputePipeline({layout:'auto',compute:{module:xhsm,entryPoint:'main'}});
|
||||
this.deepBucketPrefix=this.device.createComputePipeline({layout:'auto',compute:{module:xpm,entryPoint:'main'}});
|
||||
this.deepBucketScatter=this.device.createComputePipeline({layout:'auto',compute:{module:xsm,entryPoint:'main'}});
|
||||
this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}});
|
||||
this.correctQueued=this.device.createComputePipeline({layout:'auto',compute:{module:zqm,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'}});
|
||||
|
|
@ -119,11 +144,15 @@ class WebGpuRenderer{
|
|||
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}
|
||||
async ensureRefinePipelines(){if(this.likelyQueue&&this.deepLikelyRefine)return;if(this.refinePipelinePromise)return this.refinePipelinePromise;this.refinePipelinePromise=(async()=>{const [lqm,drm]=await Promise.all([this.module('likely-refine-queue',G.LIKELY_QUEUE_WGSL),this.module('deep-likely-refine',G.DEEP_LIKELY_REFINE_WGSL)]);this.likelyQueue=this.device.createComputePipeline({layout:'auto',compute:{module:lqm,entryPoint:'main'}});this.deepLikelyRefine=this.device.createComputePipeline({layout:'auto',compute:{module:drm,entryPoint:'refine_likely'}})})().finally(()=>{this.refinePipelinePromise=null});return this.refinePipelinePromise}
|
||||
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','refineQueue','refineStats','refineQueueParams','refineBatchParams','refineRead','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
|
||||
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;
|
||||
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,refineQueue:null,refineStats:null,refineQueueParams:null,refineBatchParams:null,refineRead:null,numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
|
||||
}
|
||||
ensureDeepBucketState(f=this.frame){if(!f)return null;if(!f.deepBucketState){const d=this.device,B=GPUBufferUsage;f.deepBucketState=buf(d,DEEP_BUCKET_STATE_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-bucket-state')}return f}
|
||||
ensureDeepQueueWorkspace(f=this.frame){if(!f)return null;this.ensureDeepBucketState(f);if(f.deepQueue&&f.deepQueueStats&&f.deepIndirect)return f;const d=this.device,B=GPUBufferUsage;f.deepQueueStats=buf(d,SPARSE_QUEUE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue-stats');f.deepQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue');f.deepIndirect=buf(d,SPARSE_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'deep-correction-indirect');return f}
|
||||
ensureRefineWorkspace(f=this.frame){if(!f)return null;if(f.refineQueue&&f.refineStats&&f.refineQueueParams&&f.refineBatchParams&&f.refineRead)return f;const d=this.device,B=GPUBufferUsage;f.refineQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-queue');f.refineStats=buf(d,REFINE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-stats');f.refineQueueParams=buf(d,REFINE_QUEUE_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-queue-params');f.refineBatchParams=buf(d,REFINE_BATCH_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-batch-params');f.refineRead=buf(d,REFINE_STATS_BYTES,B.COPY_DST|B.MAP_READ,'likely-refine-readback');return f}
|
||||
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;
|
||||
|
|
@ -131,187 +160,86 @@ class WebGpuRenderer{
|
|||
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}
|
||||
setRefineDeepContext(ctx){if(this.refineDeepCtx&&this.refineDeepCtx.key===ctx.key)return;this.destroyRefineDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'idle-refinement-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.refineDeepCtx={...ctx,refsB}}
|
||||
destroyRefineDeepContext(){if(this.refineDeepCtx)destroy(this.refineDeepCtx.refsB);this.refineDeepCtx=null}
|
||||
setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}}
|
||||
destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null}
|
||||
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)})}
|
||||
fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,0,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
|
||||
colorParamsData(w,h,iter=state.fieldView?.iter??maxIter()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,effectiveColorCycle(iter),true);d.setFloat32(20,state.shift,true)})}
|
||||
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()}
|
||||
encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,pipeline=this.deepPostStats,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
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()}
|
||||
encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.fast.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fast);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHist.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHist);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
|
||||
encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHistStats.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
|
||||
encodeDeepBucketPrefix(encoder,{bucketState,queueStats,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketPrefix.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:bucketState}},{binding:1,resource:{buffer:queueStats}},{binding:2,resource:{buffer:indirect}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketPrefix);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end()}
|
||||
encodeDeepBucketScatter(encoder,{pbuf,meta,bucketState,queueStats,queue,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
|
||||
encodeQueuedDeepCorrection(encoder,{pbuf,meta,smooth,unresolved,queueStats,queue,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.correctQueued.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{binding:5,resource:{buffer:queueStats}},{binding:6,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.correctQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()}
|
||||
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 computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null){
|
||||
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep){this.setDeepContext(deepContext);this.ensureDeepBucketState(f)}else if(fastExtended){this.setFastContext(fastContext)}d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep)d.queue.writeBuffer(f.deepBucketState,0,new Uint32Array(DEEP_BUCKET_STATE_BYTES/4));
|
||||
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;const params=deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,refX,refY):fastExtended?this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap);d.queue.writeBuffer(f.numericParams,0,params);
|
||||
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
|
||||
if(deep){this.encodeDeepPostStatsNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeDeepBucketHistogramStats(encoder,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,unresolved:f.unresolved,w:f.w,h:f.h});}
|
||||
else if(fastExtended){this.encodeFastNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});}
|
||||
else{this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});}
|
||||
if(!deep){const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();}
|
||||
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;if(!deep)[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
|
||||
}
|
||||
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}
|
||||
async buildLikelyRefineQueue(token,mode=1,sparseStride=4){await this.ready;await this.ensureRefinePipelines();if(token!==state.token)return{count:0,overflow:0,cancelled:true};const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;d.queue.writeBuffer(f.refineStats,0,new Uint32Array(REFINE_STATS_BYTES/4));d.queue.writeBuffer(f.refineQueueParams,0,new Uint32Array([f.w,f.h,mode>>>0,sparseStride>>>0]));const e=d.createCommandEncoder({label:'likely-refine-queue-build'}),bg=d.createBindGroup({layout:this.likelyQueue.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.refineQueueParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.refineQueue}},{binding:3,resource:{buffer:f.refineStats}}]}),pass=e.beginComputePass();pass.setPipeline(this.likelyQueue);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.n/256));pass.end();e.copyBufferToBuffer(f.refineStats,0,f.refineRead,0,REFINE_STATS_BYTES);d.queue.submit([e.finish()]);await f.refineRead.mapAsync(GPUMapMode.READ);const a=new Uint32Array(f.refineRead.getMappedRange().slice(0));f.refineRead.unmap();if(token!==state.token)return{count:0,overflow:0,cancelled:true};return{count:Math.min(f.n,a[0]||0),overflow:a[1]||0,cancelled:false}}
|
||||
async refineLikelyBatch({snap,iter,deepContext,token,base,end,forceStrict=false}){await this.ready;await this.ensureRefinePipelines();if(token!==state.token||base>=end)return false;const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setRefineDeepContext(deepContext);const ref=referencePixelForSource(deepContext?.source,snap,f.w,f.h);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,ref.x,ref.y,0,f.w,0));d.queue.writeBuffer(f.refineBatchParams,0,new Uint32Array([base>>>0,end>>>0,0,0]));const pipeline=this.deepLikelyRefine,bg0=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.refineDeepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),bg1=d.createBindGroup({layout:pipeline.getBindGroupLayout(1),entries:[{binding:0,resource:{buffer:f.refineQueue}},{binding:1,resource:{buffer:f.refineBatchParams}}]}),e=d.createCommandEncoder({label:'likely-idle-refine'}),pass=e.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg0);pass.setBindGroup(1,bg1);pass.dispatchWorkgroups(Math.ceil((end-base)/64));pass.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.idleRefineBatches++;return true}
|
||||
async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
|
||||
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 correctUnknownFrame(snap,iter,token,referencePixel=null){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');this.ensureDeepQueueWorkspace(f);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.deepQueueStats,0,new Uint32Array(SPARSE_QUEUE_STATS_BYTES/4));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array(SPARSE_INDIRECT_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,1,f.w,0));const e=d.createCommandEncoder({label:'deep-bucketed-correction'});this.encodeDeepBucketPrefix(e,{bucketState:f.deepBucketState,queueStats:f.deepQueueStats,indirect:f.deepIndirect});this.encodeDeepBucketScatter(e,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,queueStats:f.deepQueueStats,queue:f.deepQueue,w:f.w,h:f.h});this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
|
||||
|
||||
async benchmarkDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap));const e=d.createCommandEncoder({label:'ds-direct-benchmark'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;[meta,fieldSmooth,pbuf].forEach(destroy);return{gpuMs}}
|
||||
async renderTileMetaDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,n*4,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const e=d.createCommandEncoder({label:'ds-direct-meta'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});e.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,fieldSmooth,pbuf,read].forEach(destroy);return out}
|
||||
async benchmarkFixed96Direct({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13}){
|
||||
await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,
|
||||
meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
|
||||
guardStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),correctStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
|
||||
riskQueue=buf(d,n*4,B.STORAGE|B.COPY_DST,'fixed96-risk-queue'),indirect=buf(d,FIXED96_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'fixed96-indirect'),
|
||||
guardPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),correctPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
|
||||
read=buf(d,2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES,B.COPY_DST|B.MAP_READ);
|
||||
d.queue.writeBuffer(guardStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(correctStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(indirect,0,new Uint32Array(FIXED96_INDIRECT_BYTES/4));
|
||||
d.queue.writeBuffer(guardPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit));d.queue.writeBuffer(correctPbuf,0,this.fixed96Params(w,h,fullW,fullH,tileX,tileY,iter,snap));
|
||||
const e=d.createCommandEncoder({label:'ds-fixed96-queued-benchmark'});this.encodeDsGuardedNumeric(e,{pbuf:guardPbuf,meta,smooth:fieldSmooth,stats:guardStats,queue:riskQueue,w,h});this.encodeFixed96QueueArgs(e,{guardStats,indirect});this.encodeFixed96CorrectionNumeric(e,{pbuf:correctPbuf,meta,smooth:fieldSmooth,stats:correctStats,guardStats,queue:riskQueue,indirect});
|
||||
e.copyBufferToBuffer(guardStats,0,read,0,UNRESOLVED_BYTES);e.copyBufferToBuffer(correctStats,0,read,UNRESOLVED_BYTES,UNRESOLVED_BYTES);e.copyBufferToBuffer(indirect,0,read,2*UNRESOLVED_BYTES,FIXED96_INDIRECT_BYTES);
|
||||
const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;
|
||||
await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),a=new Uint32Array(raw.slice(0,UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(UNRESOLVED_BYTES,2*UNRESOLVED_BYTES)),args=new Uint32Array(raw.slice(2*UNRESOLVED_BYTES,2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES));read.unmap();
|
||||
const integrity=fixed96QueueIntegrity(a,b,args,n);[meta,fieldSmooth,guardStats,correctStats,riskQueue,indirect,guardPbuf,correctPbuf,read].forEach(destroy);if(!integrity.ok)throw new Error('Fixed96 queue integrity failure: '+integrity.issues.join(', '));return{gpuMs,uncertain:integrity.selected,remaining:integrity.remaining,corrected:integrity.corrected,queue:integrity};
|
||||
}
|
||||
async renderTileMetaFixed96Direct({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13,sampleX=.5,sampleY=.5}){
|
||||
await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,
|
||||
meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
|
||||
guardStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),correctStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
|
||||
riskQueue=buf(d,n*4,B.STORAGE|B.COPY_DST,'fixed96-risk-queue-meta'),indirect=buf(d,FIXED96_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'fixed96-indirect-meta'),
|
||||
guardPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),correctPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
|
||||
read=buf(d,n*4+2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES,B.COPY_DST|B.MAP_READ);
|
||||
d.queue.writeBuffer(guardStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(correctStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(indirect,0,new Uint32Array(FIXED96_INDIRECT_BYTES/4));
|
||||
d.queue.writeBuffer(guardPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sampleX,sampleY));d.queue.writeBuffer(correctPbuf,0,this.fixed96Params(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));
|
||||
const e=d.createCommandEncoder({label:'ds-fixed96-queued-meta'});this.encodeDsGuardedNumeric(e,{pbuf:guardPbuf,meta,smooth:fieldSmooth,stats:guardStats,queue:riskQueue,w,h});this.encodeFixed96QueueArgs(e,{guardStats,indirect});this.encodeFixed96CorrectionNumeric(e,{pbuf:correctPbuf,meta,smooth:fieldSmooth,stats:correctStats,guardStats,queue:riskQueue,indirect});
|
||||
e.copyBufferToBuffer(meta,0,read,0,n*4);e.copyBufferToBuffer(guardStats,0,read,n*4,UNRESOLVED_BYTES);e.copyBufferToBuffer(correctStats,0,read,n*4+UNRESOLVED_BYTES,UNRESOLVED_BYTES);e.copyBufferToBuffer(indirect,0,read,n*4+2*UNRESOLVED_BYTES,FIXED96_INDIRECT_BYTES);
|
||||
d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),a=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(n*4+UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES)),args=new Uint32Array(raw.slice(n*4+2*UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES+FIXED96_INDIRECT_BYTES));read.unmap();
|
||||
const integrity=fixed96QueueIntegrity(a,b,args,n);[meta,fieldSmooth,guardStats,correctStats,riskQueue,indirect,guardPbuf,correctPbuf,read].forEach(destroy);if(!integrity.ok)throw new Error('Fixed96 queue integrity failure: '+integrity.issues.join(', '));out.uncertain=integrity.selected;out.remaining=integrity.remaining;out.corrected=integrity.corrected;out.queue=integrity;return out;
|
||||
}
|
||||
|
||||
async benchmarkNumeric({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,forceStrict=false,correctUnknown=false}){await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const e=d.createCommandEncoder({label:'backend-benchmark'});if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,forceStrict?1:0));this.encodeDeepNumeric(e,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){e.clearBuffer(unresolved);this.encodeCorrectionNumeric(e,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap));this.encodeDirectNumeric(e,{pbuf,meta,smooth,w,h})}e.copyBufferToBuffer(unresolved,0,read,0,UNRESOLVED_BYTES);const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;await read.mapAsync(GPUMapMode.READ);const a=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return{gpuMs,unknown:a[0]||0,corrected:a[7]||0}}
|
||||
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
|
||||
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
|
||||
async renderTileMeta({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
|
||||
await this.ready;const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
|
||||
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();
|
||||
async renderTileRGBA({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
|
||||
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
|
||||
const ca=this.colorParamsData(w,h,iter),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
|
||||
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()}
|
||||
async renderTileRGBA2x({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
|
||||
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h,iter);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
|
||||
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
|
||||
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()}
|
||||
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyRefineDeepContext();this.destroyDeepContext();this.destroyFastContext()}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
function referencePixelForSource(src,snap,w,h){if(!src)return{x:w*.5,y:h*.5};const b=Math.max(snap.bits,src.bits),span=align(snap.span,snap.bits,b),dr=align(src.re,src.bits,b)-align(snap.re,snap.bits,b),di=align(src.im,src.bits,b)-align(snap.im,snap.bits,b);return{x:w*.5+fixedRatio(dr,span)*w,y:h*.5-fixedRatio(di,span)*w}}
|
||||
function reusableDeepReference(snap,iter,w,h){const ctx=renderer&&renderer.deepCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}}
|
||||
function reusableFastReference(snap,iter,w,h){const ctx=renderer&&renderer.fastCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}}
|
||||
|
||||
// ── 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 sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span}
|
||||
function cancelIdleRefinement(){if(idleRefineTimer){clearTimeout(idleRefineTimer);idleRefineTimer=0}state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0}
|
||||
function idleRefinementTargets(baseIter){if(baseIter>=8000)return[];const z=zoomExp(),mode=state.processMode,mul=mode==='power'?.75:mode==='fine'?1.25:mode==='validate'?1.4:1,cap=Math.min(8000,Math.max(baseIter*2,Math.round((1200+300*z)*mul))),raw=mode==='power'?[Math.min(cap,baseIter*2)]:[Math.min(cap,baseIter*2),Math.min(cap,baseIter*4),cap],out=[];for(const v of raw){const n=Math.max(baseIter+1,Math.floor(v));if(n>baseIter&&(!out.length||n!==out[out.length-1]))out.push(n)}return out}
|
||||
function idleRefinementBatchBudget(){const m=state.processMode;if(m==='power')return 900000;if(m==='fine')return 2800000;if(m==='validate')return 1800000;return 1800000}
|
||||
function idleRefinementQueuePolicy(stage){if(stage<=0)return{mode:1,stride:4};if(stage===1)return{mode:2,stride:8};return{mode:3,stride:16}}
|
||||
function idleRefinementSliceMs(){return state.processMode==='power'?100:state.processMode==='fine'?220:160}
|
||||
function idleRefinementPauseMs(){return state.processMode==='power'?160:80}
|
||||
function canIdleRefine(token,snap,baseIter){return token===state.token&&renderer&&!state.rendering&&!state.pointerActive&&!state.wheelActive&&sameSnapshot(snap,snapshot())&&state.fieldView&&state.fieldView.iter===baseIter&&state.fieldView.w===canvas.width&&state.fieldView.h===canvas.height}
|
||||
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
async function paintIdleRefinement(token,snap,baseIter){while(state.recoloring&&canIdleRefine(token,snap,baseIter))await sleep(12);if(!canIdleRefine(token,snap,baseIter))return false;await recolor();while((state.recoloring||state.recolorPending)&&canIdleRefine(token,snap,baseIter))await sleep(8);return canIdleRefine(token,snap,baseIter)}
|
||||
function scheduleIdleRefinement(snap,baseIter,decision,token){cancelIdleRefinement();if(!renderer||(!decision.deep&&!decision.fastExtended))return;const targets=idleRefinementTargets(baseIter);if(!targets.length)return;idleRefineTimer=setTimeout(()=>{idleRefineTimer=0;runIdleRefinement(snap,baseIter,decision,token,targets)},320)}
|
||||
async function runIdleRefinement(snap,baseIter,decision,token,targets){if(!canIdleRefine(token,snap,baseIter))return;state.refinementRunning=true;state.refinedIter=baseIter;updateStats();try{for(let stage=0;stage<targets.length;stage++){if(!canIdleRefine(token,snap,baseIter))return;const targetIter=targets[stage];state.refinementStage=stage+1;state.refinedIter=targetIter;const deepCtx=await refs.request(snap,targetIter,canvas.width,canvas.height);if(!canIdleRefine(token,snap,baseIter))return;if(deepCtx.checkpointMismatch)throw new Error('idle refinement reference guard checkpoint mismatch');const policy=idleRefinementQueuePolicy(stage),q=await renderer.buildLikelyRefineQueue(token,policy.mode,policy.stride);if(!canIdleRefine(token,snap,baseIter)||q.cancelled)return;state.refinementQueue=q.count;runtime.idleRefineQueued+=q.count;updateStats();if(q.count<=0)continue;const budget=idleRefinementBatchBudget(),batchPixels=Math.max(64,Math.floor(Math.max(64,budget/Math.max(1,targetIter))/64)*64);let base=0,lastPaint=performance.now();while(base<q.count){if(!canIdleRefine(token,snap,baseIter))return;const end=Math.min(q.count,base+batchPixels),ok=await renderer.refineLikelyBatch({snap,iter:targetIter,deepContext:deepCtx,token,base,end,forceStrict:state.processMode==='validate'});if(!ok||!canIdleRefine(token,snap,baseIter))return;base=end;state.refinementQueue=q.count-base;const now=performance.now();if(now-lastPaint>=idleRefinementSliceMs()){const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;updateStats();lastPaint=performance.now();await sleep(idleRefinementPauseMs())}}const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;state.fieldView.refinedIter=targetIter;updateStats();await sleep(idleRefinementPauseMs())}}catch(e){if(token===state.token){console.warn('idle refinement stopped:',e);state.gpuError=String(e&&e.message||e)}}finally{if(token===state.token){state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0;updateStats()}}}
|
||||
|
||||
function cancelRender(){cancelIdleRefinement();state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled')}
|
||||
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(){
|
||||
|
|
@ -320,20 +248,33 @@ async function renderFrame(){
|
|||
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}
|
||||
const deep=decision.deep,fastExtended=decision.fastExtended;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false;
|
||||
if(deep){const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height);if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 正確 reference再利用'}else{state.lastEngine='WebGPU · 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · 正確 perturbation'}}
|
||||
else if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 高速拡張 reference再利用'}else{state.lastEngine='WebGPU · 高速拡張準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height);state.lastEngine='WebGPU · 高速拡張'}}
|
||||
else state.lastEngine='WebGPU · 高速';
|
||||
const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate',referencePixel,fastExtended,fastCtx);if(!ok)return;
|
||||
if(!deep){
|
||||
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false;
|
||||
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});
|
||||
if(fastExtended){const stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons}
|
||||
const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token);return;
|
||||
}
|
||||
// Accurate mode keeps the previous committed frame visible while the new
|
||||
// primary field is checked/corrected. The incomplete primary texture lives
|
||||
// only in frame.back and is never presented to the viewer.
|
||||
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();
|
||||
const n=canvas.width*canvas.height;
|
||||
if(shouldApplySparseCorrection(state.unresolved,n)){
|
||||
state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats();
|
||||
const corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return;state.correctionPasses=1;
|
||||
stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;
|
||||
}
|
||||
const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return;
|
||||
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · '+(referenceReused?'正確 reference再利用':'正確 reference自動選択')+(state.correctionPasses?' + DS correction':'');
|
||||
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token);
|
||||
}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)}
|
||||
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · 正確モード非対応';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*effectiveColorCycle(iter)+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback(正確モード非対応)';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
|
||||
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())}
|
||||
|
||||
|
|
@ -350,22 +291,23 @@ canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;update
|
|||
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 saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');p.set('rm',state.renderMode);const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
|
||||
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';if(p.has('rm'))state.renderMode=p.get('rm')==='accurate'?'accurate':'fast';ensurePrecision();return true}catch{return false}}
|
||||
function syncHistoryButtons(){}
|
||||
function syncControls(){$('#renderMode').value=state.renderMode;$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()}
|
||||
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{}};
|
||||
let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0;
|
||||
function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色アニメ停止':'色を自動変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')}
|
||||
function stopColorAuto(){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton()}
|
||||
function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;const cmin=.001,cmax=.05,smin=0,smax=1;state.cycle+=state.colorCycleDir*.0006*dt;state.shift+=state.colorShiftDir*.012*dt;if(state.cycle>=cmax){state.cycle=cmax;state.colorCycleDir=-1}else if(state.cycle<=cmin){state.cycle=cmin;state.colorCycleDir=1}if(state.shift>=smax){state.shift=smax;state.colorShiftDir=-1}else if(state.shift<=smin){state.shift=smin;state.colorShiftDir=1}$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;recolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)}
|
||||
$('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf)colorAutoRaf=requestAnimationFrame(colorAutoStep);else if(!state.colorAuto)stopColorAuto()};
|
||||
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
|
||||
$('#renderMode').onchange=e=>{state.renderMode=e.target.value==='accurate'?'accurate':'fast';try{localStorage.setItem('mandelbrot.renderMode',state.renderMode)}catch{}markDirty()};
|
||||
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
|
||||
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()}});
|
||||
|
||||
|
|
@ -385,9 +327,9 @@ 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,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{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),fastExtended=!deep&&fastNeedsExtended(snap,w),strict=$('#exportPrecision').value==='strict';
|
||||
let ctx=null,fastCtx=null;
|
||||
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}else if(fastExtended){$('#exportStatus').textContent='高速拡張参照を準備中…';fastCtx=await fastRefs.request(snap,iter,w,h)}
|
||||
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;
|
||||
|
|
@ -396,7 +338,7 @@ async function runExport(){
|
|||
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 result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
|
||||
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:'');
|
||||
|
|
@ -404,7 +346,7 @@ async function runExport(){
|
|||
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};
|
||||
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':fastExtended?'reduced-reference + fast-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
|
||||
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)}
|
||||
|
|
@ -412,33 +354,18 @@ 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)),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}
|
||||
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.deep?'WebGPU 正確':decision.fastExtended?'WebGPU 高速':'WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'正確描画の補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.refinementRunning)status+=' · 補足精密化 '+state.refinementStage+' ('+state.refinedIter+' iter, 残 '+state.refinementQueue+')';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status}
|
||||
|
||||
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 setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard',renderMode='fast'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.renderMode=renderMode==='accurate'?'accurate':'fast';state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,state:this.state()}},
|
||||
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
|
||||
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
|
||||
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w,h):!!forceDeep;let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
|
||||
async runCrossoverBenchmark(options={}){return runCrossoverBenchmark(options)},
|
||||
async runFixed96Experiment(options={}){return runFixed96Experiment(options)},
|
||||
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
|
||||
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter(),renderMode:state.renderMode,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused}),
|
||||
async panPixels(dx,dy){pan(dx,dy);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test pan timeout');return this.state()},
|
||||
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded():!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
|
||||
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
|
||||
};
|
||||
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:(state.backendDecision||chooseBackend()).deep,backendDecision:state.backendDecision,backendProfiles:Array.from(backendProfiles.entries()),backendViewProbes:Array.from(backendViewProbes.entries()),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,unknownReasons:state.unknownReasons,correctionPasses:state.correctionPasses,correctedPixels:state.correctedPixels,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
|
||||
|
||||
// ── boot / teardown ──────────────────────────────────────────────────────
|
||||
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
|
||||
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
|
||||
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();refs.destroy();fastRefs.destroy();if(renderer)renderer.destroy()},{once:true});
|
||||
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}state.renderMode=localStorage.getItem('mandelbrot.renderMode')==='accurate'?'accurate':'fast'}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
|
||||
})();
|
||||
|
|
|
|||
598
dist/standalone/gpu-kernels-v24.2.6.js
vendored
598
dist/standalone/gpu-kernels-v24.2.6.js
vendored
|
|
@ -1,598 +0,0 @@
|
|||
(()=>{'use strict';
|
||||
const COMMON=String.raw`
|
||||
const FIELD_UNKNOWN:u32=0u;
|
||||
const FIELD_ESCAPED:u32=1u;
|
||||
const FIELD_INTERIOR_LIKELY:u32=2u;
|
||||
const FIELD_INTERIOR_PROVEN:u32=3u;
|
||||
const ITER_MASK:u32=0x000fffffu;
|
||||
const REASON_SHIFT:u32=20u;
|
||||
const REASON_MASK:u32=0x0ff00000u;
|
||||
const REASON_NONE:u32=0u;
|
||||
const REASON_ERROR_BOUND:u32=1u;
|
||||
const REASON_ESCAPE_UNCERTAIN:u32=2u;
|
||||
const REASON_REFERENCE_END:u32=3u;
|
||||
const REASON_REBASE_GAP:u32=4u;
|
||||
const REASON_RANGE:u32=5u;
|
||||
const REASON_OPERATION_LIMIT:u32=6u;
|
||||
|
||||
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
|
||||
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
|
||||
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
|
||||
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
|
||||
}
|
||||
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
|
||||
const F32_U:f32=5.960464477539063e-8;
|
||||
fn pow2_safe(e:i32)->f32 {
|
||||
if(e < -126){ return 0.0; }
|
||||
if(e > 126){ return 8.507059e37; }
|
||||
return ldexp(1.0,e);
|
||||
}
|
||||
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
|
||||
let propagated=abs(errScaled*pow2_safe(scaleExp));
|
||||
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
|
||||
return propagated+reconstruction;
|
||||
}
|
||||
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
|
||||
if(e < -126){ return vec2<f32>(0.0); }
|
||||
if(e > 126){ return vec2<f32>(8.507059e37); }
|
||||
return ldexp(v,vec2<i32>(e));
|
||||
}
|
||||
fn smooth_escape(n:u32, mag2:f32)->f32{
|
||||
let u=log2(max(4.0000005,mag2));
|
||||
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
|
||||
}
|
||||
`;
|
||||
|
||||
const DIRECT_F32_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
|
||||
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
|
||||
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn analytic(cr:f32,ci:f32)->bool{
|
||||
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
|
||||
let lhs=q*(q+x); let rhs=0.25*y2;
|
||||
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
|
||||
if(lhs<rhs-margin){return true;}
|
||||
let x2=cr+1.0; let bulb=x2*x2+y2;
|
||||
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
|
||||
return bulb<0.0625-bulbMargin;
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
let scale=p.span/f32(p.fullW);
|
||||
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
|
||||
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
|
||||
if(analytic(cr,ci)){
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
|
||||
}
|
||||
var zr=0.0; var zi=0.0; var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
let zr2=zr*zr; let zi2=zi*zi;
|
||||
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
|
||||
let mag=zr*zr+zi*zi;
|
||||
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// Benchmark candidate: whole-frame double-single direct iteration.
|
||||
// This intentionally has no CPU reference-build cost. It is not selected by
|
||||
// the production router until the crossover benchmark justifies it.
|
||||
const DIRECT_DS_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
|
||||
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
|
||||
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
|
||||
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
|
||||
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn ds_quick(a:f32,b:f32)->DS{
|
||||
let q=a+b; let e=b-(q-a); return DS(q,e);
|
||||
}
|
||||
fn ds_sum(a:f32,b:f32)->DS{
|
||||
let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e);
|
||||
}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;
|
||||
let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah;
|
||||
let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh;
|
||||
var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl;
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{
|
||||
let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l));
|
||||
}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);
|
||||
var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l;
|
||||
return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{
|
||||
let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b);
|
||||
}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{
|
||||
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
|
||||
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
|
||||
}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
|
||||
return CDS(rr,ii);
|
||||
}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);
|
||||
let span=DS(p.spanHi,p.spanLo);
|
||||
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
|
||||
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
|
||||
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
|
||||
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
|
||||
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
|
||||
let c=CDS(cr,ci);
|
||||
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
z=cds_add(cds_mul(z,z),c); n+=1u;
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
|
||||
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
|
||||
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
|
||||
// can be re-evaluated by the existing deep double-single correction pass.
|
||||
// This shader is benchmark-only and is not selected by the production router.
|
||||
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
|
||||
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
|
||||
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
|
||||
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
|
||||
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
struct HybridStats{
|
||||
remaining:atomic<u32>, _a:atomic<u32>, _b:atomic<u32>, _c:atomic<u32>,
|
||||
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
|
||||
fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);}
|
||||
fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;
|
||||
let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh;
|
||||
var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{if(a.h<b.h){return -1;}if(a.h>b.h){return 1;}if(a.l<b.l){return -1;}if(a.l>b.l){return 1;}return 0;}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii);
|
||||
}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
fn mark_sensitive(out:u32,n:u32){
|
||||
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;atomicAdd(&hybridStats.remaining,1u);
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
|
||||
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
|
||||
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
|
||||
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
|
||||
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
|
||||
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
|
||||
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var deriv=vec2<f32>(0.0);var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
|
||||
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
|
||||
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
|
||||
z=cds_add(cds_mul(z,z),c);n+=1u;
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
|
||||
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
|
||||
}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
|
||||
const DEEP_PERTURB_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
|
||||
struct UnresolvedHead{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
|
||||
|
||||
fn mark_unresolved(out:u32,n:u32,reason:u32){
|
||||
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
|
||||
atomicAdd(&unresolved.remaining,1u);
|
||||
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
|
||||
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
|
||||
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
|
||||
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
|
||||
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
|
||||
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
|
||||
}
|
||||
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
||||
let dx=(gx-p.refPixelX)/f32(p.fullW);
|
||||
let dy=(p.refPixelY-gy)/f32(p.fullW);
|
||||
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
|
||||
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
|
||||
var w=vec2<f32>(0.0);
|
||||
var scaleExp=p.spanExp;
|
||||
var n=0u; var m=0u; var operations=0u;
|
||||
var errScaled=1.0*F32_U*maxabs(d);
|
||||
loop{
|
||||
if(n>=p.maxIter){
|
||||
let rpEnd=refs[min(m,p.refLen)];
|
||||
let deltaEnd=scaled_to_f32(w,scaleExp);
|
||||
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
|
||||
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
|
||||
let limit=select(1.0e-3,1.0e-4,strictMode);
|
||||
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
|
||||
return;
|
||||
}
|
||||
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let rp=refs[m];
|
||||
let delta=scaled_to_f32(w,scaleExp);
|
||||
let z=rp.hi+(rp.lo+delta);
|
||||
let mag=dot(z,z);
|
||||
if(mag>4.0){
|
||||
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
|
||||
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
|
||||
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
|
||||
}
|
||||
|
||||
// Rebase only when dc remains numerically representable in the new scale.
|
||||
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
|
||||
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
|
||||
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
|
||||
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
|
||||
errScaled+=1.0*F32_U*maxabs(d);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let r=refs[m];
|
||||
let refAbs=maxabs(r.hi)+maxabs(r.lo);
|
||||
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
|
||||
let gain=2.0*refAbs+2.0*wAbs*p2;
|
||||
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
|
||||
errScaled=gain*errScaled+roundErr;
|
||||
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
|
||||
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
|
||||
let sq=cmul(w,w)*pow2_safe(scaleExp);
|
||||
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
|
||||
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
|
||||
let mm=max(maxabs(w),maxabs(d));
|
||||
if(mm>65536.0){
|
||||
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
|
||||
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
|
||||
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
|
||||
}
|
||||
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
|
||||
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;}
|
||||
}
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
render_pixel(out,gx,gy,p.strict!=0u);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
|
||||
// perturbation. It is a visual-quality pass, not a membership certificate.
|
||||
const DEEP_CORRECT_WGSL=COMMON+String.raw`
|
||||
const CORRECTION_MARK:u32=128u;
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
|
||||
struct CorrectionStats{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
|
||||
|
||||
fn count_remaining(out:u32){
|
||||
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
|
||||
atomicAdd(&stats.remaining,1u);
|
||||
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
|
||||
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
|
||||
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
|
||||
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
|
||||
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
|
||||
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
|
||||
}
|
||||
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
|
||||
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
|
||||
}
|
||||
|
||||
fn ds_quick(a:f32,b:f32)->DS{
|
||||
let q=a+b;
|
||||
let e=b-(q-a);
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_sum(a:f32,b:f32)->DS{
|
||||
let q=a+b;
|
||||
let bb=q-a;
|
||||
let e=(a-(q-bb))+(b-bb);
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;
|
||||
let ca=4097.0*a;
|
||||
let ah=ca-(ca-a);
|
||||
let al=a-ah;
|
||||
let cb=4097.0*b;
|
||||
let bh=cb-(cb-b);
|
||||
let bl=b-bh;
|
||||
var e=ah*bh-q;
|
||||
e=e+ah*bl;
|
||||
e=e+al*bh;
|
||||
e=e+al*bl;
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{
|
||||
let q=ds_sum(a.h,b.h);
|
||||
return ds_quick(q.h,q.l+(a.l+b.l));
|
||||
}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);
|
||||
var e=q.l+a.h*b.l;
|
||||
e=e+a.l*b.h;
|
||||
e=e+a.l*b.l;
|
||||
return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{
|
||||
let q=ds_prod(a.h,b);
|
||||
return ds_quick(q.h,q.l+a.l*b);
|
||||
}
|
||||
fn ds_pow2(a:DS,e:i32)->DS{
|
||||
if(e < -126){return DS(0.0,0.0);}
|
||||
if(e > 126){return DS(8.507059e37,0.0);}
|
||||
return DS(ldexp(a.h,e),ldexp(a.l,e));
|
||||
}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{
|
||||
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
|
||||
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
|
||||
}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
|
||||
return CDS(rr,ii);
|
||||
}
|
||||
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
|
||||
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
|
||||
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
|
||||
|
||||
fn correct_pixel(out:u32,gx:f32,gy:f32){
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);
|
||||
let sm=DS(p.spanMantHi,p.spanMantLo);
|
||||
let ox=gx-p.refPixelX;
|
||||
let oy=p.refPixelY-gy;
|
||||
let dx=ds_scale(invW,ox);
|
||||
let dy=ds_scale(invW,oy);
|
||||
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
|
||||
var d=d0;
|
||||
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var scaleExp=p.spanExp;
|
||||
var n=0u; var m=0u; var operations=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
|
||||
if(m>p.refLen){count_remaining(out);return;}
|
||||
let rp=refs[m];
|
||||
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
|
||||
let delta=cds_pow2(w,scaleExp);
|
||||
let z=cds_add(r,delta);
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
|
||||
return;
|
||||
}
|
||||
let dmag=cds_mag2(delta);
|
||||
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
|
||||
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
|
||||
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
|
||||
continue;
|
||||
}
|
||||
if(m>=p.refLen){count_remaining(out);return;}
|
||||
let linear=cds_scale(cds_mul(r,w),2.0);
|
||||
let sq=cds_pow2(cds_mul(w,w),scaleExp);
|
||||
w=cds_add(cds_add(linear,sq),d);
|
||||
m+=1u; n+=1u; operations+=1u;
|
||||
let mm=max(cds_maxabs(w),cds_maxabs(d));
|
||||
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
|
||||
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
|
||||
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
|
||||
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
|
||||
}
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
correct_pixel(out,gx,gy);
|
||||
}
|
||||
`;
|
||||
|
||||
const COLOR_WGSL=String.raw`
|
||||
struct Params{
|
||||
width:u32,height:u32,palette:u32,edgeAA:u32,
|
||||
cycle:f32,shift:f32,_p0:f32,_p1:f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
|
||||
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
|
||||
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
|
||||
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
|
||||
}
|
||||
fn current_palette(t0:f32)->vec3<f32>{
|
||||
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
|
||||
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
|
||||
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
|
||||
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
|
||||
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
|
||||
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
|
||||
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
|
||||
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
|
||||
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
|
||||
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
|
||||
}
|
||||
fn base_color(i:u32)->vec3<f32>{
|
||||
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
|
||||
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
|
||||
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
|
||||
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
|
||||
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
|
||||
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
|
||||
}
|
||||
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
|
||||
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
|
||||
if(p.edgeAA!=0u){
|
||||
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
|
||||
let x=i32(gid.x); let y=i32(gid.y);
|
||||
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
|
||||
if(boundary){c=delinearize(sum/cnt);}
|
||||
}
|
||||
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const AA_RESOLVE_WGSL=String.raw`
|
||||
@group(0) @binding(0) var a:texture_2d<f32>;
|
||||
@group(0) @binding(1) var b:texture_2d<f32>;
|
||||
@group(0) @binding(2) var c:texture_2d<f32>;
|
||||
@group(0) @binding(3) var d:texture_2d<f32>;
|
||||
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
|
||||
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
|
||||
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
|
||||
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
|
||||
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
|
||||
let q=vec2<i32>(gid.xy);
|
||||
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
|
||||
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
|
||||
}
|
||||
`;
|
||||
|
||||
const PRESENT_WGSL=String.raw`
|
||||
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
|
||||
@group(0) @binding(0) var samp:sampler;
|
||||
@group(0) @binding(1) var tex:texture_2d<f32>;
|
||||
@group(0) @binding(2) var<uniform> p:Params;
|
||||
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
|
||||
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
|
||||
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
|
||||
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
|
||||
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
|
||||
}
|
||||
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
|
||||
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
|
||||
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
|
||||
}
|
||||
`;
|
||||
|
||||
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
|
||||
version:'24.2.6-hybrid-ui-cacheproof',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
|
||||
});
|
||||
})();
|
||||
598
dist/standalone/gpu-kernels.js
vendored
598
dist/standalone/gpu-kernels.js
vendored
|
|
@ -1,598 +0,0 @@
|
|||
(()=>{'use strict';
|
||||
const COMMON=String.raw`
|
||||
const FIELD_UNKNOWN:u32=0u;
|
||||
const FIELD_ESCAPED:u32=1u;
|
||||
const FIELD_INTERIOR_LIKELY:u32=2u;
|
||||
const FIELD_INTERIOR_PROVEN:u32=3u;
|
||||
const ITER_MASK:u32=0x000fffffu;
|
||||
const REASON_SHIFT:u32=20u;
|
||||
const REASON_MASK:u32=0x0ff00000u;
|
||||
const REASON_NONE:u32=0u;
|
||||
const REASON_ERROR_BOUND:u32=1u;
|
||||
const REASON_ESCAPE_UNCERTAIN:u32=2u;
|
||||
const REASON_REFERENCE_END:u32=3u;
|
||||
const REASON_REBASE_GAP:u32=4u;
|
||||
const REASON_RANGE:u32=5u;
|
||||
const REASON_OPERATION_LIMIT:u32=6u;
|
||||
|
||||
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
|
||||
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
|
||||
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
|
||||
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
|
||||
}
|
||||
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
|
||||
const F32_U:f32=5.960464477539063e-8;
|
||||
fn pow2_safe(e:i32)->f32 {
|
||||
if(e < -126){ return 0.0; }
|
||||
if(e > 126){ return 8.507059e37; }
|
||||
return ldexp(1.0,e);
|
||||
}
|
||||
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
|
||||
let propagated=abs(errScaled*pow2_safe(scaleExp));
|
||||
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
|
||||
return propagated+reconstruction;
|
||||
}
|
||||
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
|
||||
if(e < -126){ return vec2<f32>(0.0); }
|
||||
if(e > 126){ return vec2<f32>(8.507059e37); }
|
||||
return ldexp(v,vec2<i32>(e));
|
||||
}
|
||||
fn smooth_escape(n:u32, mag2:f32)->f32{
|
||||
let u=log2(max(4.0000005,mag2));
|
||||
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
|
||||
}
|
||||
`;
|
||||
|
||||
const DIRECT_F32_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
|
||||
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
|
||||
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn analytic(cr:f32,ci:f32)->bool{
|
||||
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
|
||||
let lhs=q*(q+x); let rhs=0.25*y2;
|
||||
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
|
||||
if(lhs<rhs-margin){return true;}
|
||||
let x2=cr+1.0; let bulb=x2*x2+y2;
|
||||
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
|
||||
return bulb<0.0625-bulbMargin;
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
let scale=p.span/f32(p.fullW);
|
||||
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
|
||||
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
|
||||
if(analytic(cr,ci)){
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
|
||||
}
|
||||
var zr=0.0; var zi=0.0; var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
let zr2=zr*zr; let zi2=zi*zi;
|
||||
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
|
||||
let mag=zr*zr+zi*zi;
|
||||
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// Benchmark candidate: whole-frame double-single direct iteration.
|
||||
// This intentionally has no CPU reference-build cost. It is not selected by
|
||||
// the production router until the crossover benchmark justifies it.
|
||||
const DIRECT_DS_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
|
||||
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
|
||||
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
|
||||
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
|
||||
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn ds_quick(a:f32,b:f32)->DS{
|
||||
let q=a+b; let e=b-(q-a); return DS(q,e);
|
||||
}
|
||||
fn ds_sum(a:f32,b:f32)->DS{
|
||||
let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e);
|
||||
}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;
|
||||
let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah;
|
||||
let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh;
|
||||
var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl;
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{
|
||||
let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l));
|
||||
}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);
|
||||
var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l;
|
||||
return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{
|
||||
let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b);
|
||||
}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{
|
||||
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
|
||||
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
|
||||
}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
|
||||
return CDS(rr,ii);
|
||||
}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);
|
||||
let span=DS(p.spanHi,p.spanLo);
|
||||
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
|
||||
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
|
||||
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
|
||||
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
|
||||
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
|
||||
let c=CDS(cr,ci);
|
||||
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
z=cds_add(cds_mul(z,z),c); n+=1u;
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
|
||||
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
|
||||
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
|
||||
// can be re-evaluated by the existing deep double-single correction pass.
|
||||
// This shader is benchmark-only and is not selected by the production router.
|
||||
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
|
||||
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
|
||||
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
|
||||
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
|
||||
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
struct HybridStats{
|
||||
remaining:atomic<u32>, _a:atomic<u32>, _b:atomic<u32>, _c:atomic<u32>,
|
||||
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
|
||||
fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);}
|
||||
fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;
|
||||
let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh;
|
||||
var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{if(a.h<b.h){return -1;}if(a.h>b.h){return 1;}if(a.l<b.l){return -1;}if(a.l>b.l){return 1;}return 0;}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii);
|
||||
}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
fn mark_sensitive(out:u32,n:u32){
|
||||
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;atomicAdd(&hybridStats.remaining,1u);
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
|
||||
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
|
||||
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
|
||||
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
|
||||
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
|
||||
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
|
||||
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var deriv=vec2<f32>(0.0);var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
|
||||
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
|
||||
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
|
||||
z=cds_add(cds_mul(z,z),c);n+=1u;
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
|
||||
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
|
||||
}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
|
||||
const DEEP_PERTURB_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
|
||||
struct UnresolvedHead{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
|
||||
|
||||
fn mark_unresolved(out:u32,n:u32,reason:u32){
|
||||
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
|
||||
atomicAdd(&unresolved.remaining,1u);
|
||||
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
|
||||
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
|
||||
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
|
||||
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
|
||||
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
|
||||
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
|
||||
}
|
||||
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
||||
let dx=(gx-p.refPixelX)/f32(p.fullW);
|
||||
let dy=(p.refPixelY-gy)/f32(p.fullW);
|
||||
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
|
||||
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
|
||||
var w=vec2<f32>(0.0);
|
||||
var scaleExp=p.spanExp;
|
||||
var n=0u; var m=0u; var operations=0u;
|
||||
var errScaled=1.0*F32_U*maxabs(d);
|
||||
loop{
|
||||
if(n>=p.maxIter){
|
||||
let rpEnd=refs[min(m,p.refLen)];
|
||||
let deltaEnd=scaled_to_f32(w,scaleExp);
|
||||
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
|
||||
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
|
||||
let limit=select(1.0e-3,1.0e-4,strictMode);
|
||||
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
|
||||
return;
|
||||
}
|
||||
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let rp=refs[m];
|
||||
let delta=scaled_to_f32(w,scaleExp);
|
||||
let z=rp.hi+(rp.lo+delta);
|
||||
let mag=dot(z,z);
|
||||
if(mag>4.0){
|
||||
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
|
||||
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
|
||||
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
|
||||
}
|
||||
|
||||
// Rebase only when dc remains numerically representable in the new scale.
|
||||
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
|
||||
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
|
||||
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
|
||||
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
|
||||
errScaled+=1.0*F32_U*maxabs(d);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let r=refs[m];
|
||||
let refAbs=maxabs(r.hi)+maxabs(r.lo);
|
||||
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
|
||||
let gain=2.0*refAbs+2.0*wAbs*p2;
|
||||
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
|
||||
errScaled=gain*errScaled+roundErr;
|
||||
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
|
||||
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
|
||||
let sq=cmul(w,w)*pow2_safe(scaleExp);
|
||||
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
|
||||
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
|
||||
let mm=max(maxabs(w),maxabs(d));
|
||||
if(mm>65536.0){
|
||||
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
|
||||
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
|
||||
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
|
||||
}
|
||||
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
|
||||
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;}
|
||||
}
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
render_pixel(out,gx,gy,p.strict!=0u);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
|
||||
// perturbation. It is a visual-quality pass, not a membership certificate.
|
||||
const DEEP_CORRECT_WGSL=COMMON+String.raw`
|
||||
const CORRECTION_MARK:u32=128u;
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
|
||||
struct CorrectionStats{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
|
||||
|
||||
fn count_remaining(out:u32){
|
||||
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
|
||||
atomicAdd(&stats.remaining,1u);
|
||||
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
|
||||
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
|
||||
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
|
||||
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
|
||||
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
|
||||
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
|
||||
}
|
||||
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
|
||||
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
|
||||
}
|
||||
|
||||
fn ds_quick(a:f32,b:f32)->DS{
|
||||
let q=a+b;
|
||||
let e=b-(q-a);
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_sum(a:f32,b:f32)->DS{
|
||||
let q=a+b;
|
||||
let bb=q-a;
|
||||
let e=(a-(q-bb))+(b-bb);
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;
|
||||
let ca=4097.0*a;
|
||||
let ah=ca-(ca-a);
|
||||
let al=a-ah;
|
||||
let cb=4097.0*b;
|
||||
let bh=cb-(cb-b);
|
||||
let bl=b-bh;
|
||||
var e=ah*bh-q;
|
||||
e=e+ah*bl;
|
||||
e=e+al*bh;
|
||||
e=e+al*bl;
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{
|
||||
let q=ds_sum(a.h,b.h);
|
||||
return ds_quick(q.h,q.l+(a.l+b.l));
|
||||
}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);
|
||||
var e=q.l+a.h*b.l;
|
||||
e=e+a.l*b.h;
|
||||
e=e+a.l*b.l;
|
||||
return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{
|
||||
let q=ds_prod(a.h,b);
|
||||
return ds_quick(q.h,q.l+a.l*b);
|
||||
}
|
||||
fn ds_pow2(a:DS,e:i32)->DS{
|
||||
if(e < -126){return DS(0.0,0.0);}
|
||||
if(e > 126){return DS(8.507059e37,0.0);}
|
||||
return DS(ldexp(a.h,e),ldexp(a.l,e));
|
||||
}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{
|
||||
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
|
||||
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
|
||||
}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
|
||||
return CDS(rr,ii);
|
||||
}
|
||||
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
|
||||
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
|
||||
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
|
||||
|
||||
fn correct_pixel(out:u32,gx:f32,gy:f32){
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);
|
||||
let sm=DS(p.spanMantHi,p.spanMantLo);
|
||||
let ox=gx-p.refPixelX;
|
||||
let oy=p.refPixelY-gy;
|
||||
let dx=ds_scale(invW,ox);
|
||||
let dy=ds_scale(invW,oy);
|
||||
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
|
||||
var d=d0;
|
||||
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var scaleExp=p.spanExp;
|
||||
var n=0u; var m=0u; var operations=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
|
||||
if(m>p.refLen){count_remaining(out);return;}
|
||||
let rp=refs[m];
|
||||
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
|
||||
let delta=cds_pow2(w,scaleExp);
|
||||
let z=cds_add(r,delta);
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
|
||||
return;
|
||||
}
|
||||
let dmag=cds_mag2(delta);
|
||||
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
|
||||
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
|
||||
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
|
||||
continue;
|
||||
}
|
||||
if(m>=p.refLen){count_remaining(out);return;}
|
||||
let linear=cds_scale(cds_mul(r,w),2.0);
|
||||
let sq=cds_pow2(cds_mul(w,w),scaleExp);
|
||||
w=cds_add(cds_add(linear,sq),d);
|
||||
m+=1u; n+=1u; operations+=1u;
|
||||
let mm=max(cds_maxabs(w),cds_maxabs(d));
|
||||
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
|
||||
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
|
||||
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
|
||||
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
|
||||
}
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
correct_pixel(out,gx,gy);
|
||||
}
|
||||
`;
|
||||
|
||||
const COLOR_WGSL=String.raw`
|
||||
struct Params{
|
||||
width:u32,height:u32,palette:u32,edgeAA:u32,
|
||||
cycle:f32,shift:f32,_p0:f32,_p1:f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
|
||||
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
|
||||
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
|
||||
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
|
||||
}
|
||||
fn current_palette(t0:f32)->vec3<f32>{
|
||||
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
|
||||
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
|
||||
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
|
||||
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
|
||||
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
|
||||
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
|
||||
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
|
||||
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
|
||||
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
|
||||
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
|
||||
}
|
||||
fn base_color(i:u32)->vec3<f32>{
|
||||
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
|
||||
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
|
||||
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
|
||||
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
|
||||
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
|
||||
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
|
||||
}
|
||||
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
|
||||
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
|
||||
if(p.edgeAA!=0u){
|
||||
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
|
||||
let x=i32(gid.x); let y=i32(gid.y);
|
||||
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
|
||||
if(boundary){c=delinearize(sum/cnt);}
|
||||
}
|
||||
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const AA_RESOLVE_WGSL=String.raw`
|
||||
@group(0) @binding(0) var a:texture_2d<f32>;
|
||||
@group(0) @binding(1) var b:texture_2d<f32>;
|
||||
@group(0) @binding(2) var c:texture_2d<f32>;
|
||||
@group(0) @binding(3) var d:texture_2d<f32>;
|
||||
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
|
||||
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
|
||||
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
|
||||
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
|
||||
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
|
||||
let q=vec2<i32>(gid.xy);
|
||||
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
|
||||
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
|
||||
}
|
||||
`;
|
||||
|
||||
const PRESENT_WGSL=String.raw`
|
||||
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
|
||||
@group(0) @binding(0) var samp:sampler;
|
||||
@group(0) @binding(1) var tex:texture_2d<f32>;
|
||||
@group(0) @binding(2) var<uniform> p:Params;
|
||||
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
|
||||
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
|
||||
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
|
||||
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
|
||||
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
|
||||
}
|
||||
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
|
||||
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
|
||||
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
|
||||
}
|
||||
`;
|
||||
|
||||
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
|
||||
version:'24.2.6-hybrid-ui-cacheproof',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
|
||||
});
|
||||
})();
|
||||
1156
dist/standalone/index.html
vendored
1156
dist/standalone/index.html
vendored
File diff suppressed because it is too large
Load diff
22
dist/standalone/kernels.js
vendored
22
dist/standalone/kernels.js
vendored
File diff suppressed because one or more lines are too long
454
dist/standalone/script-v24.2.6.js
vendored
454
dist/standalone/script-v24.2.6.js
vendored
|
|
@ -1,454 +0,0 @@
|
|||
(()=>{'use strict';
|
||||
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
|
||||
const $=s=>document.querySelector(s),canvas=$('#view');
|
||||
const VERSION=24,BUILD_VERSION='24.2.6',INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
|
||||
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
|
||||
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
|
||||
const DIRECT_REQUIRED_RATIO=8,DIRECT_DEFAULT_RATIO=128,DEEP_SPEED_MARGIN=.85,DEEP_PROBE_UNKNOWN_MAX=.005,NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,CORRECTION_MARK=128;
|
||||
const BACKEND_PROBE_SPEED_W=224,BACKEND_PROBE_SPEED_H=160,BACKEND_PROBE_QUALITY_W=72,BACKEND_PROBE_QUALITY_H=54;
|
||||
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
|
||||
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
|
||||
const viewHistory=[];let viewHistoryIndex=-1;
|
||||
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,backendProbes:0};
|
||||
const backendProfiles=new Map(),backendViewProbes=new Map(),backendProbePending=new Map();
|
||||
|
||||
// ── exact fixed-point view state ─────────────────────────────────────────
|
||||
function one(bits=state.bits){return 1n<<BigInt(bits)}
|
||||
function fromFrac(n,d=1n){return n*one()/d}
|
||||
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
|
||||
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
|
||||
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
|
||||
function log2Fixed(v){return log2FixedAt(v,state.bits)}
|
||||
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
|
||||
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
|
||||
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
|
||||
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
|
||||
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
|
||||
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
|
||||
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
|
||||
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
|
||||
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
|
||||
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
|
||||
function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
|
||||
function directMetrics(snap=snapshot(),width=Math.max(1,canvas.width)){const spanLog=log2FixedAt(snap.span,snap.bits),stepLog=spanLog-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp))),eligible=Number.isFinite(ratio)&&ratio>=DIRECT_REQUIRED_RATIO&&spanLog>-145;return{spanLog,stepLog,ratio,eligible,overlap:eligible&&ratio<DIRECT_DEFAULT_RATIO}}
|
||||
function zoomExpFor(snap){return Math.max(0,Math.log10(3.4)-log2FixedAt(snap.span,snap.bits)/Math.log2(10))}
|
||||
function backendProfileKey(snap,iter){const z=Math.floor(zoomExpFor(snap)*2)/2,ib=iter<600?0:iter<1500?1:iter<5000?2:3;return z.toFixed(1)+':'+ib}
|
||||
function backendViewKey(snap,iter,width,height){return[snap.bits,snap.re,snap.im,snap.span,iter,width,height].join(':')}
|
||||
function chooseBackend(snap=snapshot(),width=Math.max(1,canvas.width),iter=maxIter(),height=Math.max(1,canvas.height)){const m=directMetrics(snap,width),key=backendProfileKey(snap,iter),viewKey=backendViewKey(snap,iter,width,height),speed=backendProfiles.get(key),viewProbe=backendViewProbes.get(viewKey),pixels=Math.max(1,width*Math.max(1,height)),profile=speed?{...speed,predictedDirectMs:speed.directMsPerPixel*pixels,predictedDeepMs:speed.referenceBuildMs+speed.deepMsPerPixel*pixels}:null;let backend='deep',reason='direct-coordinate-limit';if(m.eligible){if(m.ratio>=DIRECT_DEFAULT_RATIO){backend='direct';reason='direct-wide-margin'}else if(!viewProbe){backend='probe';reason='overlap-view-probe-required'}else if(!viewProbe.qualityPass){backend='deep';reason='direct-quality-rejected'}else if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*DEEP_SPEED_MARGIN){backend='deep';reason='deep-view-probe-faster'}else{backend='direct';reason='direct-view-probe-preferred'}}return{backend,deep:backend==='deep',probe:backend==='probe',reason,key,viewKey,metrics:m,profile,viewProbe:viewProbe||null}}
|
||||
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width),height=Math.max(1,canvas.height)){const d=chooseBackend(snap,width,maxIter(),height);return d.deep||d.probe}
|
||||
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
|
||||
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
|
||||
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
|
||||
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
|
||||
|
||||
// ── iteration / quality policy ───────────────────────────────────────────
|
||||
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
|
||||
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
|
||||
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
|
||||
|
||||
// ── high precision reference worker ─────────────────────────────────────
|
||||
function referenceWorkerSource(){return String.raw`
|
||||
'use strict';
|
||||
const MAX_REF=150001,MAX_LEVELS=20;
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
|
||||
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
|
||||
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
|
||||
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
|
||||
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
|
||||
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
|
||||
`}
|
||||
class ReferenceService{
|
||||
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16}
|
||||
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
|
||||
request(snap,iter,fresh=false){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.2.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
|
||||
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
|
||||
destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
|
||||
}
|
||||
const refs=new ReferenceService();
|
||||
|
||||
// ── WebGPU renderer ──────────────────────────────────────────────────────
|
||||
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
|
||||
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
|
||||
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
|
||||
class WebGpuRenderer{
|
||||
constructor(adapter,device){
|
||||
this.adapter=adapter;this.device=device;
|
||||
const ai=adapter.info||{};
|
||||
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
|
||||
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
|
||||
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
|
||||
this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
|
||||
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
|
||||
this.ready=this.initPipelines();
|
||||
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
|
||||
}
|
||||
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
|
||||
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
|
||||
async initPipelines(){
|
||||
this.device.pushErrorScope?.('validation');
|
||||
try{
|
||||
const [dm,sdm,shm,xm,zm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('direct-ds-benchmark',G.DIRECT_DS_WGSL),this.module('direct-ds-guarded-experiment',G.DIRECT_DS_GUARDED_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
|
||||
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
|
||||
this.dsDirect=this.device.createComputePipeline({layout:'auto',compute:{module:sdm,entryPoint:'main'}});
|
||||
this.dsGuarded=this.device.createComputePipeline({layout:'auto',compute:{module:shm,entryPoint:'main'}});
|
||||
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
|
||||
this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}});
|
||||
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
|
||||
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
|
||||
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
|
||||
this.context=canvas.getContext('webgpu');
|
||||
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
|
||||
webgpuCanvasClaimed=true;this.configure();
|
||||
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
|
||||
}
|
||||
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
|
||||
ensureFrame(w,h){
|
||||
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
|
||||
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
|
||||
}
|
||||
ensureExportWorkspace(){
|
||||
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
|
||||
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
|
||||
}
|
||||
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
|
||||
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
|
||||
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
|
||||
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
|
||||
dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5){const cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),crh=Math.fround(cr),cih=Math.fround(ci),sph=Math.fround(sp),invW=1/Math.max(1,fullW),invh=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,crh,true);d.setFloat32(36,Math.fround(cr-crh),true);d.setFloat32(40,cih,true);d.setFloat32(44,Math.fround(ci-cih),true);d.setFloat32(48,sph,true);d.setFloat32(52,Math.fround(sp-sph),true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,invh,true);d.setFloat32(68,Math.fround(invW-invh),true)})}
|
||||
dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sx=.5,sy=.5){const a=this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx,sy),d=new DataView(a);d.setFloat32(72,Math.fround(riskLimit),true);return a}
|
||||
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
|
||||
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
|
||||
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDsDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsDirect.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsDirect);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDsGuardedNumeric(encoder,{pbuf,meta,smooth,stats,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsGuarded.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:stats}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsGuarded);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
|
||||
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
|
||||
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
|
||||
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
|
||||
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
|
||||
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
|
||||
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
|
||||
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
|
||||
}
|
||||
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
|
||||
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
|
||||
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
|
||||
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
|
||||
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
|
||||
async readUnresolved(){return(await this.readUnresolvedStats()).total}
|
||||
async correctUnknownFrame(snap,iter,token){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,f.w*.5,f.h*.5,1,f.w,0));const e=d.createCommandEncoder({label:'sparse-deep-correction'}),bg=d.createBindGroup({layout:this.correct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),p=e.beginComputePass();p.setPipeline(this.correct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
|
||||
|
||||
async benchmarkDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap));const e=d.createCommandEncoder({label:'ds-direct-benchmark'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;[meta,fieldSmooth,pbuf].forEach(destroy);return{gpuMs}}
|
||||
async renderTileMetaDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,n*4,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const e=d.createCommandEncoder({label:'ds-direct-meta'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});e.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,fieldSmooth,pbuf,read].forEach(destroy);return out}
|
||||
async benchmarkHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13}){
|
||||
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
|
||||
meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
|
||||
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
|
||||
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
|
||||
read=buf(d,2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
|
||||
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit));
|
||||
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,0,fullW*.5,fullH*.5,1,w,0));
|
||||
const e=d.createCommandEncoder({label:'ds-hybrid-benchmark'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
|
||||
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
|
||||
e.copyBufferToBuffer(dsStats,0,read,0,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,UNRESOLVED_BYTES,UNRESOLVED_BYTES);
|
||||
const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;
|
||||
await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),a=new Uint32Array(raw.slice(0,UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(UNRESOLVED_BYTES,2*UNRESOLVED_BYTES));read.unmap();
|
||||
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);
|
||||
return{gpuMs,uncertain:a[0]||0,remaining:b[0]||0,corrected:b[7]||0};
|
||||
}
|
||||
async renderTileMetaHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13,sampleX=.5,sampleY=.5}){
|
||||
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
|
||||
meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
|
||||
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
|
||||
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
|
||||
read=buf(d,n*4+2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
|
||||
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sampleX,sampleY));
|
||||
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,0,fullW*.5,fullH*.5,1,w,0));
|
||||
const e=d.createCommandEncoder({label:'ds-hybrid-meta'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
|
||||
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
|
||||
e.copyBufferToBuffer(meta,0,read,0,n*4);e.copyBufferToBuffer(dsStats,0,read,n*4,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,n*4+UNRESOLVED_BYTES,UNRESOLVED_BYTES);
|
||||
d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),a=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(n*4+UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES));read.unmap();
|
||||
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);out.uncertain=a[0]||0;out.remaining=b[0]||0;out.corrected=b[7]||0;return out;
|
||||
}
|
||||
|
||||
async benchmarkNumeric({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,forceStrict=false,correctUnknown=false}){await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const e=d.createCommandEncoder({label:'backend-benchmark'});if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,forceStrict?1:0));this.encodeDeepNumeric(e,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){e.clearBuffer(unresolved);this.encodeCorrectionNumeric(e,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap));this.encodeDirectNumeric(e,{pbuf,meta,smooth,w,h})}e.copyBufferToBuffer(unresolved,0,read,0,UNRESOLVED_BYTES);const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;await read.mapAsync(GPUMapMode.READ);const a=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return{gpuMs,unknown:a[0]||0,corrected:a[7]||0}}
|
||||
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
|
||||
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
|
||||
const read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
|
||||
}
|
||||
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
|
||||
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
|
||||
const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
|
||||
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes,UNRESOLVED_BYTES/4),unresolvedCount=stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
|
||||
}
|
||||
async renderTileRGBA2x({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
|
||||
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
|
||||
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
|
||||
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
|
||||
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount=0,corrected=0;for(let si=0;si<4;si++){const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4);unresolvedCount+=stats[0]||0;corrected+=stats[7]||0}ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
|
||||
}
|
||||
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext()}
|
||||
}
|
||||
|
||||
function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true}
|
||||
function backendQualityTiles(fullW,fullH){const w=Math.min(BACKEND_PROBE_QUALITY_W,fullW),h=Math.min(BACKEND_PROBE_QUALITY_H,fullH),x1=Math.max(0,fullW-w),y1=Math.max(0,fullH-h),xc=Math.max(0,Math.floor((fullW-w)/2)),yc=Math.max(0,Math.floor((fullH-h)/2));return[[0,0],[x1,0],[0,y1],[x1,y1],[xc,yc]].map(([tileX,tileY])=>({tileX,tileY,w,h}))}
|
||||
async function performBackendProbe(snap,iter,decision,token){if(!renderer||!decision.metrics.overlap||token!==state.token)return null;const fullW=canvas.width,fullH=canvas.height,sw=Math.min(BACKEND_PROBE_SPEED_W,fullW),sh=Math.min(BACKEND_PROBE_SPEED_H,fullH),sx=Math.max(0,Math.floor((fullW-sw)/2)),sy=Math.max(0,Math.floor((fullH-sh)/2)),speedPixels=sw*sh,fullPixels=Math.max(1,fullW*fullH),direct=await renderer.benchmarkNumeric({snap,iter,deep:false,deepContext:null,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh});if(token!==state.token)return null;const rb=performance.now(),ctx=await refs.request(snap,iter),buildWall=performance.now()-rb;if(token!==state.token||ctx.checkpointMismatch)return null;const deepProbe=await renderer.benchmarkNumeric({snap,iter,deep:true,deepContext:ctx,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh,correctUnknown:true});if(token!==state.token)return null;let classDisagreement=0,deepUnknown=0,knownCompared=0,qualityPixels=0,corrected=0;for(const q of backendQualityTiles(fullW,fullH)){const directMeta=await renderer.renderTileMeta({snap,iter,deep:false,deepContext:null,fullW,fullH,...q});if(token!==state.token)return null;const deepMeta=await renderer.renderTileMeta({snap,iter,deep:true,deepContext:ctx,fullW,fullH,...q,correctUnknown:true});if(token!==state.token)return null;qualityPixels+=q.w*q.h;corrected+=deepMeta.corrected||0;for(let i=0;i<deepMeta.length;i++){const dc=(deepMeta[i]>>>28)&3;if(dc===FIELD_UNKNOWN){deepUnknown++;continue}const rc=(directMeta[i]>>>28)&3;knownCompared++;if((dc===FIELD_ESCAPED)!==(rc===FIELD_ESCAPED))classDisagreement++}}const buildMs=ctx.buildMs||buildWall,directMsPerPixel=direct.gpuMs/Math.max(1,speedPixels),deepMsPerPixel=deepProbe.gpuMs/Math.max(1,speedPixels),sample={directMsPerPixel,deepMsPerPixel,referenceBuildMs:buildMs},prev=backendProfiles.get(decision.key),a=prev?.samples?0.3:1,profile={samples:(prev?.samples||0)+1,directMsPerPixel:(prev?.directMsPerPixel||sample.directMsPerPixel)*(1-a)+sample.directMsPerPixel*a,deepMsPerPixel:(prev?.deepMsPerPixel||sample.deepMsPerPixel)*(1-a)+sample.deepMsPerPixel*a,referenceBuildMs:(prev?.referenceBuildMs||sample.referenceBuildMs)*(1-a)+sample.referenceBuildMs*a};backendProfiles.set(decision.key,profile);const deepUnknownRate=deepUnknown/Math.max(1,qualityPixels),qualityPass=classDisagreement===0&&deepUnknownRate<=DEEP_PROBE_UNKNOWN_MAX,viewProbe={qualityPass,deepUnknownRate,classDisagreement,knownCompared,qualityPixels,corrected,predictedDirectMs:directMsPerPixel*fullPixels,predictedDeepMs:buildMs+deepMsPerPixel*fullPixels,ratio:decision.metrics.ratio,zoom:zoomExpFor(snap),iter,speedProbe:{w:sw,h:sh,tileX:sx,tileY:sy},qualityTiles:backendQualityTiles(fullW,fullH)};backendViewProbes.set(decision.viewKey,viewProbe);runtime.backendProbes++;return viewProbe}
|
||||
function ensureBackendProbe(snap,iter,decision,token){if(!decision.metrics.overlap)return Promise.resolve(null);const hit=backendViewProbes.get(decision.viewKey);if(hit)return Promise.resolve(hit);const pending=backendProbePending.get(decision.viewKey);if(pending)return pending;const task=performBackendProbe(snap,iter,decision,token).catch(e=>{if(token===state.token)console.warn('backend probe failed',e);return null}).finally(()=>backendProbePending.delete(decision.viewKey));backendProbePending.set(decision.viewKey,task);return task}
|
||||
|
||||
|
||||
// ── backend crossover benchmark (diagnostic only) ───────────────────────
|
||||
const CROSSOVER_TRACKS={
|
||||
seahorse:{id:'seahorse',label:'Seahorse boundary',re:'-0.7453983606667815',im:'0.1125046349959942'},
|
||||
cusp:{id:'cusp',label:'Period-2 cusp',re:'-0.75',im:'0'}
|
||||
};
|
||||
const CROSSOVER_DEPTHS=[0,2,4,6,8,10,12,14,16,20,30,40];
|
||||
const benchmarkJob={active:false,cancelled:false,last:null};
|
||||
function fixedAtDecimal(text,bits){let s=String(text).trim(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const parts=s.toLowerCase().split('e'),mant=parts[0],exp=parts[1]?parseInt(parts[1],10):0,ab=mant.split('.'),ii=ab[0]||'0',ff=ab[1]||'';let digits=(ii+ff).replace(/^0+(?=\d)/,'')||'0',places=ff.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),scale=1n<<BigInt(bits),v=(BigInt(digits)*scale+den/2n)/den;return neg?-v:v}
|
||||
function crossoverSnap(track,z){const span='3.4e-'+z,bits=Math.max(320,decimalRequiredBits(track.re)+64,decimalRequiredBits(track.im)+64,decimalRequiredBits(span)+96);return{bits,re:fixedAtDecimal(track.re,bits),im:fixedAtDecimal(track.im,bits),span:fixedAtDecimal(span,bits)}}
|
||||
function iterForZoom(z,base=350){return Math.max(base,base+Math.floor(70*Math.sqrt(Math.max(0,z))+15*Math.max(0,z)))}
|
||||
function median(values){const a=[...values].sort((x,y)=>x-y),n=a.length;return n?a.length%2?a[(n-1)>>1]:(a[n/2-1]+a[n/2])/2:NaN}
|
||||
function compareMeta(reference,candidate){let referenceUnknown=0,candidateUnknown=0,classMismatch=0,escapedCompared=0,iterMismatch=0,maxIterDelta=0,sumIterDelta=0;const n=Math.min(reference.length,candidate.length);for(let i=0;i<n;i++){const rm=reference[i]>>>0,cm=candidate[i]>>>0,rc=(rm>>>28)&3,cc=(cm>>>28)&3;if(rc===FIELD_UNKNOWN){referenceUnknown++;continue}if(cc===FIELD_UNKNOWN){candidateUnknown++;continue}const re=rc===FIELD_ESCAPED,ce=cc===FIELD_ESCAPED;if(re!==ce){classMismatch++;continue}if(re){escapedCompared++;const d=Math.abs((rm&ITER_MASK)-(cm&ITER_MASK));if(d){iterMismatch++;sumIterDelta+=d;if(d>maxIterDelta)maxIterDelta=d}}}return{pixels:n,referenceUnknown,candidateUnknown,classMismatch,classMismatchRate:classMismatch/Math.max(1,n-referenceUnknown),escapedCompared,escapeIterationMismatch:iterMismatch,escapeIterationMismatchRate:iterMismatch/Math.max(1,escapedCompared),meanEscapeIterationDelta:iterMismatch?sumIterDelta/iterMismatch:0,maxEscapeIterationDelta:maxIterDelta}}
|
||||
function crossoverQualityTiles(fullW,fullH,w,h){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[0,0],[Math.max(0,fullW-w),0],[0,Math.max(0,fullH-h)],[Math.max(0,fullW-w),Math.max(0,fullH-h)],[Math.max(0,Math.floor((fullW-w)/2)),Math.max(0,Math.floor((fullH-h)/2))]];return pts.map(([tileX,tileY])=>({tileX,tileY,w,h}))}
|
||||
function aggregateQuality(list){const out={pixels:0,referenceUnknown:0,candidateUnknown:0,classMismatch:0,escapedCompared:0,escapeIterationMismatch:0,sumEscapeIterationDelta:0,maxEscapeIterationDelta:0};for(const q of list){out.pixels+=q.pixels;out.referenceUnknown+=q.referenceUnknown;out.candidateUnknown+=q.candidateUnknown;out.classMismatch+=q.classMismatch;out.escapedCompared+=q.escapedCompared;out.escapeIterationMismatch+=q.escapeIterationMismatch;out.sumEscapeIterationDelta+=q.meanEscapeIterationDelta*q.escapeIterationMismatch;out.maxEscapeIterationDelta=Math.max(out.maxEscapeIterationDelta,q.maxEscapeIterationDelta)}out.classMismatchRate=out.classMismatch/Math.max(1,out.pixels-out.referenceUnknown);out.escapeIterationMismatchRate=out.escapeIterationMismatch/Math.max(1,out.escapedCompared);out.meanEscapeIterationDelta=out.escapeIterationMismatch?out.sumEscapeIterationDelta/out.escapeIterationMismatch:0;delete out.sumEscapeIterationDelta;return out}
|
||||
const CROSSOVER_SPEED_W=256,CROSSOVER_SPEED_H=144,CROSSOVER_GPU_TIMEOUT_MS=15000,CROSSOVER_REF_TIMEOUT_MS=15000;
|
||||
function crossoverSpeedTiles(fullW,fullH,w=CROSSOVER_SPEED_W,h=CROSSOVER_SPEED_H){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[.5,.5],[.28,.36],[.72,.64]],seen=new Set(),out=[];for(const [fx,fy] of pts){const tileX=Math.max(0,Math.min(fullW-w,Math.round(fullW*fx-w/2))),tileY=Math.max(0,Math.min(fullH-h,Math.round(fullH*fy-h/2))),k=tileX+':'+tileY;if(!seen.has(k)){seen.add(k);out.push({tileX,tileY,w,h})}}return out}
|
||||
function benchmarkTimeout(promise,ms,label,onTimeout){let timer;const timeout=new Promise((_,reject)=>{timer=setTimeout(()=>{try{onTimeout?.()}catch{}reject(new Error(label+' timed out after '+Math.round(ms/1000)+'s'))},ms)});return Promise.race([promise,timeout]).finally(()=>clearTimeout(timer))}
|
||||
async function benchmarkTileBatch(fn,tiles){let gpuMs=0,pixels=0;for(const q of tiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h}return{gpuMs,pixels}}
|
||||
async function timedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[];for(let i=0;i<samples;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'sample',index:i+1,total:samples});values.push((await fn()).gpuMs)}return{medianMs:median(values),samplesMs:values}}
|
||||
function extrapolateTiming(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels),fullMedianMs=msPerPixel*fullPixels;return{...timing,speedPixels,msPerPixel,fullMedianMs}}
|
||||
async function runCrossoverBenchmark({track='seahorse',depths=CROSSOVER_DEPTHS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
|
||||
if(benchmarkJob.active)throw new Error('benchmark already running');benchmarkJob.active=true;benchmarkJob.cancelled=false;
|
||||
try{
|
||||
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
|
||||
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown benchmark track');
|
||||
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
|
||||
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
|
||||
for(let di=0;di<depths.length;di++){
|
||||
if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z),metrics=directMetrics(snap,fullW);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'reference'});
|
||||
const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'z'+z+' reference',()=>refs.cancelPending('benchmark reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
|
||||
const timed=(name,fn)=>timedMedian(()=>gpuCall(benchmarkTileBatch(fn,speedTiles),'z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
|
||||
const tileCommon=q=>({snap,iter,fullW,fullH,...q});
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'f32'});
|
||||
const f32=extrapolateTiming(await timed('f32',q=>r.benchmarkNumeric({...tileCommon(q),deep:false,deepContext:null})),speedPixels,fullPixels);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'ds'});
|
||||
const ds=extrapolateTiming(await timed('ds',q=>r.benchmarkDsDirect(tileCommon(q))),speedPixels,fullPixels);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'deep'});
|
||||
const deep=extrapolateTiming(await timed('deep',q=>r.benchmarkNumeric({...tileCommon(q),deep:true,deepContext:ctx,correctUnknown:true})),speedPixels,fullPixels);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'quality'});
|
||||
const f32Parts=[],dsParts=[],qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH);let deepUnknownCount=0,qualityPixels=0,deepCorrected=0;
|
||||
for(const q of qTiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const qCommon={snap,iter,fullW,fullH,...q},deepMeta=await gpuCall(r.renderTileMeta({...qCommon,deep:true,deepContext:ctx,correctUnknown:true}),'z'+z+' quality deep'),f32Meta=await gpuCall(r.renderTileMeta({...qCommon,deep:false,deepContext:null}),'z'+z+' quality f32'),dsMeta=await gpuCall(r.renderTileMetaDsDirect(qCommon),'z'+z+' quality ds');qualityPixels+=q.w*q.h;deepUnknownCount+=deepMeta.unresolved||0;deepCorrected+=deepMeta.corrected||0;f32Parts.push(compareMeta(deepMeta,f32Meta));dsParts.push(compareMeta(deepMeta,dsMeta))}
|
||||
const f32Quality=aggregateQuality(f32Parts),dsQuality=aggregateQuality(dsParts),deepUnknown=deepUnknownCount/Math.max(1,qualityPixels),referenceBuildMs=ctx.buildMs||referenceWallMs;
|
||||
const row={track:tr.id,z,span:'3.4e-'+z,iter,resolution:[fullW,fullH],speedTiles,speedPixels,qualityTile:[qualityW,qualityH],qualityTiles:qTiles,directRatio:metrics.ratio,
|
||||
f32:{...f32,quality:f32Quality},ds:{...ds,quality:dsQuality},deep:{...deep,referenceBuildMs,referenceWallMs,coldMedianMs:referenceBuildMs+deep.fullMedianMs,warmMedianMs:deep.fullMedianMs,unknownRate:deepUnknown,corrected:deepCorrected}};
|
||||
const qualityLimit=.01;
|
||||
row.assessment={f32VisuallySafe:f32Quality.classMismatchRate<=qualityLimit&&metrics.ratio>=1,dsVisuallySafe:dsQuality.classMismatchRate<=qualityLimit,deepVisuallySafe:deepUnknown<=.001,
|
||||
fastestWarm:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.warmMedianMs]].sort((a,b)=>a[1]-b[1])[0][0],
|
||||
fastestCold:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.coldMedianMs]].sort((a,b)=>a[1]-b[1])[0][0]};
|
||||
rows.push(row);onProgress?.({index:di+1,total:depths.length,z,phase:'done',row});
|
||||
}
|
||||
const result={format:'mandelbrot-webgpu-crossover-v2',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Speed timing uses three bounded representative tiles and extrapolates ms/pixel to the current full resolution, preventing long full-frame Deep benchmark dispatches. Deep cold adds verified BigInt reference construction. Quality remains measured on five spatial tiles. Any single benchmark GPU batch or reference build exceeding 15 s aborts with an explicit error instead of waiting indefinitely.'};
|
||||
benchmarkJob.last=result;return result;
|
||||
}finally{benchmarkJob.active=false}
|
||||
}
|
||||
|
||||
|
||||
// ── DS + sparse Deep hybrid experiment (diagnostic only) ───────────────
|
||||
const HYBRID_DEPTHS=[6,8,10,12],HYBRID_RISK_LIMITS=[1e12,1e13,1e14];
|
||||
const hybridJob={active:false,cancelled:false,last:null};
|
||||
async function hybridTileBatch(fn,tiles){let gpuMs=0,pixels=0,uncertain=0,remaining=0,corrected=0;for(const q of tiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h;uncertain+=r.uncertain||0;remaining+=r.remaining||0;corrected+=r.corrected||0}return{gpuMs,pixels,uncertain,remaining,corrected}}
|
||||
async function hybridTimedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[],stats=[];for(let i=0;i<samples;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'sample',index:i+1,total:samples});const r=await fn();values.push(r.gpuMs);stats.push(r)}return{medianMs:median(values),samplesMs:values,sampleStats:stats}}
|
||||
function hybridExtrapolate(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels);return{...timing,speedPixels,msPerPixel,fullMedianMs:msPerPixel*fullPixels}}
|
||||
async function runHybridExperiment({track='seahorse',depths=HYBRID_DEPTHS,riskLimits=HYBRID_RISK_LIMITS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
|
||||
if(hybridJob.active||benchmarkJob.active)throw new Error('another benchmark is running');hybridJob.active=true;hybridJob.cancelled=false;
|
||||
try{
|
||||
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
|
||||
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown experiment track');
|
||||
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
|
||||
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
|
||||
for(let di=0;di<depths.length;di++){
|
||||
if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'reference'});const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'hybrid z'+z+' reference',()=>refs.cancelPending('hybrid reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
|
||||
const timed=(name,fn)=>hybridTimedMedian(()=>gpuCall(hybridTileBatch(fn,speedTiles),'hybrid z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
|
||||
const common=q=>({snap,iter,fullW,fullH,...q});
|
||||
const ds=hybridExtrapolate(await timed('DS baseline',async q=>{const x=await r.benchmarkDsDirect(common(q));return{...x}}),speedPixels,fullPixels);
|
||||
const deep=hybridExtrapolate(await timed('Full Deep',async q=>{const x=await r.benchmarkNumeric({...common(q),deep:true,deepContext:ctx,correctUnknown:true});return{...x}}),speedPixels,fullPixels);
|
||||
const qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH),deepMetas=[];let deepUnknown=0;
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'quality reference'});
|
||||
for(const q of qTiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const m=await gpuCall(r.renderTileMeta({...common(q),deep:true,deepContext:ctx,correctUnknown:true}),'hybrid z'+z+' quality deep');deepUnknown+=m.unresolved||0;deepMetas.push(m)}
|
||||
const variants=[];
|
||||
for(const riskLimit of riskLimits){
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'risk '+riskLimit.toExponential(0)});
|
||||
const timing=hybridExtrapolate(await timed('Hybrid '+riskLimit.toExponential(0),q=>r.benchmarkHybrid({...common(q),deepContext:ctx,riskLimit})),speedPixels,fullPixels);
|
||||
const parts=[];let uncertain=0,remaining=0,corrected=0,qualityPixels=0;
|
||||
for(let qi=0;qi<qTiles.length;qi++){const q=qTiles[qi],hm=await gpuCall(r.renderTileMetaHybrid({...common(q),deepContext:ctx,riskLimit}),'hybrid z'+z+' quality '+riskLimit);qualityPixels+=q.w*q.h;uncertain+=hm.uncertain||0;remaining+=hm.remaining||0;corrected+=hm.corrected||0;parts.push(compareMeta(deepMetas[qi],hm))}
|
||||
const quality=aggregateQuality(parts),referenceBuildMs=ctx.buildMs||referenceWallMs,coldMedianMs=referenceBuildMs+timing.fullMedianMs,uncertainRate=uncertain/Math.max(1,qualityPixels),remainingRate=remaining/Math.max(1,qualityPixels),safe=quality.classMismatch===0&&remaining===0;
|
||||
variants.push({riskLimit,...timing,referenceBuildMs,coldMedianMs,quality,qualityPixels,uncertain,uncertainRate,remaining,remainingRate,corrected,safe,speedupVsDeepCold:(referenceBuildMs+deep.fullMedianMs)/Math.max(1e-9,coldMedianMs)});
|
||||
}
|
||||
const referenceBuildMs=ctx.buildMs||referenceWallMs,deepCold=referenceBuildMs+deep.fullMedianMs,viable=variants.filter(v=>v.safe).sort((a,b)=>a.coldMedianMs-b.coldMedianMs),best=viable[0]||null;
|
||||
rows.push({track:tr.id,z,iter,resolution:[fullW,fullH],referenceBuildMs,ds:{...ds},deep:{...deep,coldMedianMs:deepCold,unknownRate:deepUnknown/Math.max(1,qTiles.reduce((n,q)=>n+q.w*q.h,0))},variants,bestRiskLimit:best?.riskLimit??null,bestHybridColdMs:best?.coldMedianMs??null,recommendation:best&&best.coldMedianMs<deepCold*.9&&best.uncertainRate<.6?'hybrid-candidate':'full-deep'});
|
||||
onProgress?.({index:di+1,total:depths.length,z,phase:'done'});
|
||||
}
|
||||
const result={format:'mandelbrot-ds-sparse-deep-hybrid-v1',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],riskLimits:[...riskLimits],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Diagnostic only. DS guarded direct tracks dz/dc in f32 and marks a pixel UNKNOWN when |dz/dc| exceeds riskLimit. Existing deep double-single correction re-evaluates only those UNKNOWN pixels. Full Deep+correction is the GPU quality reference. Cold timing adds the verified BigInt reference build to both Hybrid and Full Deep. Production routing is unchanged.'};
|
||||
hybridJob.last=result;return result;
|
||||
}finally{hybridJob.active=false}
|
||||
}
|
||||
|
||||
// ── GPU startup / rendering orchestration ────────────────────────────────
|
||||
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
|
||||
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
|
||||
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
|
||||
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
|
||||
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
|
||||
async function renderFrame(){
|
||||
const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height);
|
||||
state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats();
|
||||
try{
|
||||
const r=renderer||await initRenderer();if(token!==state.token)return;
|
||||
if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}
|
||||
if(decision.probe){state.lastEngine='WebGPU · direct/deep view probe';state.drawState='COVERING';updateStats();await ensureBackendProbe(snap,iter,decision,token);if(token!==state.token)return;decision=chooseBackend(snap,canvas.width,iter,canvas.height);if(decision.probe)decision={...decision,backend:'deep',deep:true,probe:false,reason:'overlap-probe-failed-safe-deep'};state.backendDecision=decision;updateStats()}
|
||||
const deep=decision.deep;let ctx=null;
|
||||
if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded perturbation'}else state.lastEngine='WebGPU · f32 direct';
|
||||
const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;
|
||||
const firstPaint=performance.now()-t0;
|
||||
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,backend:decision.backend};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=firstPaint;state.rendering=deep;state.unresolved=0;
|
||||
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();
|
||||
if(!deep){state.rendering=false;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();return}
|
||||
let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats();
|
||||
const n=canvas.width*canvas.height;if(shouldApplySparseCorrection(state.unresolved,n)){state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats();const corrected=await r.correctUnknownFrame(snap,iter,token);if(!corrected||token!==state.token)return;state.correctionPasses=1;stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;const painted=await r.recolor(token);if(!painted||token!==state.token)return;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats()}
|
||||
if(token!==state.token)return;state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · guarded perturbation'+(state.correctionPasses?' + DS correction':'');const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();
|
||||
}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
|
||||
}
|
||||
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback(深部非対応)';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
|
||||
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
|
||||
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
|
||||
|
||||
// ── interaction / view history ──────────────────────────────────────────
|
||||
function viewRect(){return canvas.getBoundingClientRect()}
|
||||
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
|
||||
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
|
||||
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
|
||||
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
|
||||
const pts=new Map();let lx=0,ly=0,pinch=0;
|
||||
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
|
||||
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
|
||||
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
|
||||
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
|
||||
|
||||
// ── URL / controls ───────────────────────────────────────────────────────
|
||||
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
|
||||
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
|
||||
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
|
||||
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
|
||||
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
|
||||
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
|
||||
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
|
||||
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
|
||||
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
|
||||
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
|
||||
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
|
||||
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
|
||||
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
|
||||
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
|
||||
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
|
||||
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
|
||||
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
|
||||
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
|
||||
|
||||
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
|
||||
const exportJob={active:false,cancelled:false};
|
||||
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
|
||||
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
|
||||
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
|
||||
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
|
||||
class StreamingPng{
|
||||
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
|
||||
async rows(filteredRows){await this.writer.write(filteredRows)}
|
||||
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
|
||||
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
|
||||
}
|
||||
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
|
||||
async function runExport(){
|
||||
if(exportJob.active)return;
|
||||
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
|
||||
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),strict=$('#exportPrecision').value==='strict';
|
||||
let ctx=null;
|
||||
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
|
||||
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
|
||||
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
|
||||
let done=0,unresolvedSamples=0;
|
||||
try{
|
||||
for(let y=0;y<h;y+=tile){
|
||||
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
|
||||
for(let x=0;x<w;x+=tile){
|
||||
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
|
||||
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
|
||||
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
|
||||
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
|
||||
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
|
||||
}
|
||||
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
|
||||
}
|
||||
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
|
||||
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
|
||||
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
|
||||
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
|
||||
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
|
||||
finally{exportJob.active=false;$('#exportStart').disabled=false}
|
||||
}
|
||||
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
|
||||
|
||||
function benchmarkSummary(result){const lines=['z | f32 est ms | DS est ms | deep warm est | deep cold est | f32 Δclass | DS Δclass | direct ratio | fastest cold','--|------------|-----------|---------------|---------------|------------|-----------|--------------|-------------'];for(const r of result.rows)lines.push([String(r.z).padStart(2),r.f32.fullMedianMs.toFixed(1),r.ds.fullMedianMs.toFixed(1),r.deep.warmMedianMs.toFixed(1),r.deep.coldMedianMs.toFixed(1),(100*r.f32.quality.classMismatchRate).toFixed(2)+'%',(100*r.ds.quality.classMismatchRate).toFixed(2)+'%',Number.isFinite(r.directRatio)?r.directRatio.toExponential(2):'n/a',r.assessment.fastestCold].join(' | '));return lines.join('\n')}
|
||||
$('#backendBench').onclick=()=>{const d=$('#benchmarkDialog');$('#benchmarkStatus').textContent='';$('#benchmarkProgress').hidden=true;$('#benchmarkOutput').value=benchmarkJob.last?benchmarkSummary(benchmarkJob.last):'';$('#benchmarkSave').disabled=!benchmarkJob.last;d.showModal?d.showModal():d.setAttribute('open','')};
|
||||
$('#benchmarkStart').onclick=async()=>{if(benchmarkJob.active)return;cancelRender();state.dirty=false;const start=$('#benchmarkStart'),progress=$('#benchmarkProgress'),status=$('#benchmarkStatus');start.disabled=true;$('#benchmarkSave').disabled=true;progress.hidden=false;progress.max=CROSSOVER_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#benchmarkOutput').value='';try{const result=await runCrossoverBenchmark({track:$('#benchmarkTrack').value,samples:Math.max(1,Number($('#benchmarkSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});progress.value=progress.max;status.textContent='測定完了 · '+result.rows.length+' depths';$('#benchmarkOutput').value=benchmarkSummary(result);$('#benchmarkSave').disabled=false}catch(e){status.textContent=String(e&&e.message||e)}finally{start.disabled=false;markDirty(false)}};
|
||||
$('#benchmarkCancel').onclick=()=>{if(benchmarkJob.active){benchmarkJob.cancelled=true;refs.cancelPending('benchmark cancelled');$('#benchmarkStatus').textContent='中止しています…'}else $('#benchmarkDialog').close()};
|
||||
$('#benchmarkSave').onclick=()=>{if(!benchmarkJob.last)return;downloadBlob(new Blob([JSON.stringify(benchmarkJob.last,null,2)],{type:'application/json'}),'mandelbrot-crossover-'+Date.now()+'.json')};
|
||||
|
||||
function hybridSummary(result){const lines=['z | best risk | hybrid cold est | deep cold est | speedup | correction | remain | Δclass | recommendation','--|-----------|-----------------|---------------|---------|------------|--------|--------|---------------'];for(const r of result.rows){const best=r.bestRiskLimit==null?null:r.variants.find(v=>v.riskLimit===r.bestRiskLimit),hy=best?best.coldMedianMs:NaN;lines.push([String(r.z).padStart(2),best?best.riskLimit.toExponential(0):'none',best?hy.toFixed(1):'n/a',r.deep.coldMedianMs.toFixed(1),best?(r.deep.coldMedianMs/hy).toFixed(2)+'×':'n/a',best?(100*best.uncertainRate).toFixed(1)+'%':'n/a',best?(100*best.remainingRate).toFixed(2)+'%':'n/a',best?(100*best.quality.classMismatchRate).toFixed(2)+'%':'n/a',r.recommendation].join(' | '))}return lines.join('\n')}
|
||||
function openHybridDialog(){const d=$('#hybridDialog');if(!d.open){try{if(typeof d.showModal==='function')d.showModal();else d.setAttribute('open','')}catch{d.setAttribute('open','')}}return d}
|
||||
async function startHybridExperimentFromUi(){
|
||||
const d=openHybridDialog(),start=$('#hybridStart'),toolbar=$('#hybridBench'),progress=$('#hybridProgress'),status=$('#hybridStatus');status.textContent='Hybrid実験を起動中…';toolbar.setAttribute('aria-busy','true');
|
||||
if(hybridJob.active){status.textContent='Hybrid実験を実行中です';toolbar.removeAttribute('aria-busy');return}
|
||||
if(benchmarkJob.active){status.textContent='Backend比較を終了または中止してから実行してください';toolbar.removeAttribute('aria-busy');return}
|
||||
cancelRender();state.dirty=false;start.disabled=true;toolbar.disabled=true;start.textContent='実行中…';$('#hybridSave').disabled=true;progress.hidden=false;progress.max=HYBRID_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#hybridOutput').value='';
|
||||
try{
|
||||
const result=await runHybridExperiment({track:$('#hybridTrack').value,samples:Math.max(1,Number($('#hybridSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});
|
||||
progress.value=progress.max;status.textContent='実験完了 · '+result.rows.length+' depths';$('#hybridOutput').value=hybridSummary(result);$('#hybridSave').disabled=false;start.textContent='再実行';
|
||||
}catch(e){status.textContent=String(e&&e.message||e);start.textContent='再実行'}
|
||||
finally{start.disabled=false;toolbar.disabled=false;toolbar.removeAttribute('aria-busy');markDirty(false)}
|
||||
}
|
||||
$('#hybridBench').onclick=()=>{toast('Hybrid実験を開始');void startHybridExperimentFromUi()};
|
||||
$('#hybridStart').onclick=startHybridExperimentFromUi;
|
||||
$('#hybridCancel').onclick=()=>{if(hybridJob.active){hybridJob.cancelled=true;refs.cancelPending('hybrid experiment cancelled');$('#hybridStatus').textContent='中止しています…'}else $('#hybridDialog').close()};
|
||||
$('#hybridSave').onclick=()=>{if(!hybridJob.last)return;downloadBlob(new Blob([JSON.stringify(hybridJob.last,null,2)],{type:'application/json'}),'mandelbrot-hybrid-'+Date.now()+'.json')};
|
||||
|
||||
|
||||
// ── diagnostics ──────────────────────────────────────────────────────────
|
||||
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend(snapshot(),Math.max(1,canvas.width),maxIter(),Math.max(1,canvas.height));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.probe?'WebGPU 判定中':decision.deep?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'深部補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.correctedPixels)status+=' · 回収 '+state.correctedPixels;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx,ratio=decision.metrics&&Number.isFinite(decision.metrics.ratio)?decision.metrics.ratio:0,prof=decision.profile,rs=state.unknownReasons,reasonText=rs?Object.entries(rs).filter(([,v])=>v).map(([k,v])=>k+':'+v).join(' '):'';$('#diagEngine').textContent='engine: '+state.lastEngine+' | route '+decision.backend+' ('+decision.reason+') | ratio '+(ratio?ratio.toExponential(2):'n/a')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'')+(reasonText?' | UNKNOWN '+reasonText:'')+(decision.viewProbe?' | view probe direct '+decision.viewProbe.predictedDirectMs.toFixed(0)+'ms / deep '+decision.viewProbe.predictedDeepMs.toFixed(0)+'ms / u '+(100*decision.viewProbe.deepUnknownRate).toFixed(2)+'% / Δclass '+decision.viewProbe.classDisagreement:'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height+' | speed profiles '+backendProfiles.size+' / view probes '+backendViewProbes.size}
|
||||
|
||||
globalThis.__MANDEL_TEST__={
|
||||
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
|
||||
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
|
||||
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
|
||||
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w,h):!!forceDeep;let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
|
||||
async runCrossoverBenchmark(options={}){return runCrossoverBenchmark(options)},
|
||||
async runHybridExperiment(options={}){return runHybridExperiment(options)},
|
||||
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
|
||||
};
|
||||
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,buildVersion:BUILD_VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:(state.backendDecision||chooseBackend()).deep,backendDecision:state.backendDecision,backendProfiles:Array.from(backendProfiles.entries()),backendViewProbes:Array.from(backendViewProbes.entries()),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,unknownReasons:state.unknownReasons,correctionPasses:state.correctionPasses,correctedPixels:state.correctedPixels,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
|
||||
|
||||
// ── boot / teardown ──────────────────────────────────────────────────────
|
||||
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
|
||||
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
|
||||
})();
|
||||
454
dist/standalone/script.js
vendored
454
dist/standalone/script.js
vendored
|
|
@ -1,454 +0,0 @@
|
|||
(()=>{'use strict';
|
||||
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
|
||||
const $=s=>document.querySelector(s),canvas=$('#view');
|
||||
const VERSION=24,BUILD_VERSION='24.2.6',INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
|
||||
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
|
||||
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
|
||||
const DIRECT_REQUIRED_RATIO=8,DIRECT_DEFAULT_RATIO=128,DEEP_SPEED_MARGIN=.85,DEEP_PROBE_UNKNOWN_MAX=.005,NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,CORRECTION_MARK=128;
|
||||
const BACKEND_PROBE_SPEED_W=224,BACKEND_PROBE_SPEED_H=160,BACKEND_PROBE_QUALITY_W=72,BACKEND_PROBE_QUALITY_H=54;
|
||||
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
|
||||
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
|
||||
const viewHistory=[];let viewHistoryIndex=-1;
|
||||
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,backendProbes:0};
|
||||
const backendProfiles=new Map(),backendViewProbes=new Map(),backendProbePending=new Map();
|
||||
|
||||
// ── exact fixed-point view state ─────────────────────────────────────────
|
||||
function one(bits=state.bits){return 1n<<BigInt(bits)}
|
||||
function fromFrac(n,d=1n){return n*one()/d}
|
||||
function roundDivSigned(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
|
||||
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
|
||||
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
|
||||
function log2Fixed(v){return log2FixedAt(v,state.bits)}
|
||||
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
|
||||
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
|
||||
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
|
||||
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
|
||||
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
|
||||
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
|
||||
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
|
||||
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
|
||||
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
|
||||
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
|
||||
function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
|
||||
function directMetrics(snap=snapshot(),width=Math.max(1,canvas.width)){const spanLog=log2FixedAt(snap.span,snap.bits),stepLog=spanLog-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp))),eligible=Number.isFinite(ratio)&&ratio>=DIRECT_REQUIRED_RATIO&&spanLog>-145;return{spanLog,stepLog,ratio,eligible,overlap:eligible&&ratio<DIRECT_DEFAULT_RATIO}}
|
||||
function zoomExpFor(snap){return Math.max(0,Math.log10(3.4)-log2FixedAt(snap.span,snap.bits)/Math.log2(10))}
|
||||
function backendProfileKey(snap,iter){const z=Math.floor(zoomExpFor(snap)*2)/2,ib=iter<600?0:iter<1500?1:iter<5000?2:3;return z.toFixed(1)+':'+ib}
|
||||
function backendViewKey(snap,iter,width,height){return[snap.bits,snap.re,snap.im,snap.span,iter,width,height].join(':')}
|
||||
function chooseBackend(snap=snapshot(),width=Math.max(1,canvas.width),iter=maxIter(),height=Math.max(1,canvas.height)){const m=directMetrics(snap,width),key=backendProfileKey(snap,iter),viewKey=backendViewKey(snap,iter,width,height),speed=backendProfiles.get(key),viewProbe=backendViewProbes.get(viewKey),pixels=Math.max(1,width*Math.max(1,height)),profile=speed?{...speed,predictedDirectMs:speed.directMsPerPixel*pixels,predictedDeepMs:speed.referenceBuildMs+speed.deepMsPerPixel*pixels}:null;let backend='deep',reason='direct-coordinate-limit';if(m.eligible){if(m.ratio>=DIRECT_DEFAULT_RATIO){backend='direct';reason='direct-wide-margin'}else if(!viewProbe){backend='probe';reason='overlap-view-probe-required'}else if(!viewProbe.qualityPass){backend='deep';reason='direct-quality-rejected'}else if(viewProbe.predictedDeepMs<viewProbe.predictedDirectMs*DEEP_SPEED_MARGIN){backend='deep';reason='deep-view-probe-faster'}else{backend='direct';reason='direct-view-probe-preferred'}}return{backend,deep:backend==='deep',probe:backend==='probe',reason,key,viewKey,metrics:m,profile,viewProbe:viewProbe||null}}
|
||||
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width),height=Math.max(1,canvas.height)){const d=chooseBackend(snap,width,maxIter(),height);return d.deep||d.probe}
|
||||
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
|
||||
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
|
||||
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
|
||||
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
|
||||
|
||||
// ── iteration / quality policy ───────────────────────────────────────────
|
||||
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
|
||||
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
|
||||
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
|
||||
|
||||
// ── high precision reference worker ─────────────────────────────────────
|
||||
function referenceWorkerSource(){return String.raw`
|
||||
'use strict';
|
||||
const MAX_REF=150001,MAX_LEVELS=20;
|
||||
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
||||
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
|
||||
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
|
||||
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
|
||||
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
|
||||
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
|
||||
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
|
||||
`}
|
||||
class ReferenceService{
|
||||
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16}
|
||||
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
|
||||
request(snap,iter,fresh=false){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.2.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
|
||||
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
|
||||
destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
|
||||
}
|
||||
const refs=new ReferenceService();
|
||||
|
||||
// ── WebGPU renderer ──────────────────────────────────────────────────────
|
||||
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
|
||||
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
|
||||
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
|
||||
class WebGpuRenderer{
|
||||
constructor(adapter,device){
|
||||
this.adapter=adapter;this.device=device;
|
||||
const ai=adapter.info||{};
|
||||
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
|
||||
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
|
||||
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
|
||||
this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
|
||||
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
|
||||
this.ready=this.initPipelines();
|
||||
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
|
||||
}
|
||||
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
|
||||
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
|
||||
async initPipelines(){
|
||||
this.device.pushErrorScope?.('validation');
|
||||
try{
|
||||
const [dm,sdm,shm,xm,zm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('direct-ds-benchmark',G.DIRECT_DS_WGSL),this.module('direct-ds-guarded-experiment',G.DIRECT_DS_GUARDED_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
|
||||
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
|
||||
this.dsDirect=this.device.createComputePipeline({layout:'auto',compute:{module:sdm,entryPoint:'main'}});
|
||||
this.dsGuarded=this.device.createComputePipeline({layout:'auto',compute:{module:shm,entryPoint:'main'}});
|
||||
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
|
||||
this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}});
|
||||
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
|
||||
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
|
||||
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
|
||||
this.context=canvas.getContext('webgpu');
|
||||
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
|
||||
webgpuCanvasClaimed=true;this.configure();
|
||||
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
|
||||
}
|
||||
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
|
||||
ensureFrame(w,h){
|
||||
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
|
||||
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
|
||||
}
|
||||
ensureExportWorkspace(){
|
||||
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
|
||||
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
|
||||
}
|
||||
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
|
||||
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
|
||||
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
|
||||
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
|
||||
dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5){const cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),crh=Math.fround(cr),cih=Math.fround(ci),sph=Math.fround(sp),invW=1/Math.max(1,fullW),invh=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,crh,true);d.setFloat32(36,Math.fround(cr-crh),true);d.setFloat32(40,cih,true);d.setFloat32(44,Math.fround(ci-cih),true);d.setFloat32(48,sph,true);d.setFloat32(52,Math.fround(sp-sph),true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,invh,true);d.setFloat32(68,Math.fround(invW-invh),true)})}
|
||||
dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sx=.5,sy=.5){const a=this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx,sy),d=new DataView(a);d.setFloat32(72,Math.fround(riskLimit),true);return a}
|
||||
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
|
||||
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
|
||||
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDsDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsDirect.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsDirect);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDsGuardedNumeric(encoder,{pbuf,meta,smooth,stats,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.dsGuarded.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:{buffer:stats}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.dsGuarded);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})}
|
||||
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
|
||||
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
|
||||
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
|
||||
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
|
||||
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
|
||||
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
|
||||
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
|
||||
}
|
||||
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
|
||||
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
|
||||
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
|
||||
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
|
||||
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,B=GPUBufferUsage,r=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
|
||||
async readUnresolved(){return(await this.readUnresolvedStats()).total}
|
||||
async correctUnknownFrame(snap,iter,token){await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!this.deepCtx)throw new Error('deep reference context is missing');d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,f.w*.5,f.h*.5,1,f.w,0));const e=d.createCommandEncoder({label:'sparse-deep-correction'}),bg=d.createBindGroup({layout:this.correct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),p=e.beginComputePass();p.setPipeline(this.correct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
|
||||
|
||||
async benchmarkDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap));const e=d.createCommandEncoder({label:'ds-direct-benchmark'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;[meta,fieldSmooth,pbuf].forEach(destroy);return{gpuMs}}
|
||||
async renderTileMetaDsDirect({snap,iter,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5}){await this.ready;const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,n*4,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(pbuf,0,this.dsDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const e=d.createCommandEncoder({label:'ds-direct-meta'});this.encodeDsDirectNumeric(e,{pbuf,meta,smooth:fieldSmooth,w,h});e.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,fieldSmooth,pbuf,read].forEach(destroy);return out}
|
||||
async benchmarkHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13}){
|
||||
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
|
||||
meta=buf(d,n*4,B.STORAGE|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
|
||||
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
|
||||
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
|
||||
read=buf(d,2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
|
||||
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit));
|
||||
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,0,fullW*.5,fullH*.5,1,w,0));
|
||||
const e=d.createCommandEncoder({label:'ds-hybrid-benchmark'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
|
||||
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
|
||||
e.copyBufferToBuffer(dsStats,0,read,0,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,UNRESOLVED_BYTES,UNRESOLVED_BYTES);
|
||||
const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;
|
||||
await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),a=new Uint32Array(raw.slice(0,UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(UNRESOLVED_BYTES,2*UNRESOLVED_BYTES));read.unmap();
|
||||
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);
|
||||
return{gpuMs,uncertain:a[0]||0,remaining:b[0]||0,corrected:b[7]||0};
|
||||
}
|
||||
async renderTileMetaHybrid({snap,iter,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,riskLimit=1e13,sampleX=.5,sampleY=.5}){
|
||||
await this.ready;this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,
|
||||
meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),fieldSmooth=buf(d,n*4,B.STORAGE|B.COPY_DST),
|
||||
dsStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),corStats=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),
|
||||
dsPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),deepPbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),
|
||||
read=buf(d,n*4+2*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);
|
||||
d.queue.writeBuffer(dsStats,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(corStats,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
d.queue.writeBuffer(dsPbuf,0,this.dsGuardedParams(w,h,fullW,fullH,tileX,tileY,iter,snap,riskLimit,sampleX,sampleY));
|
||||
d.queue.writeBuffer(deepPbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,0,fullW*.5,fullH*.5,1,w,0));
|
||||
const e=d.createCommandEncoder({label:'ds-hybrid-meta'});this.encodeDsGuardedNumeric(e,{pbuf:dsPbuf,meta,smooth:fieldSmooth,stats:dsStats,w,h});
|
||||
this.encodeCorrectionNumeric(e,{pbuf:deepPbuf,meta,smooth:fieldSmooth,unresolved:corStats,w,h});
|
||||
e.copyBufferToBuffer(meta,0,read,0,n*4);e.copyBufferToBuffer(dsStats,0,read,n*4,UNRESOLVED_BYTES);e.copyBufferToBuffer(corStats,0,read,n*4+UNRESOLVED_BYTES,UNRESOLVED_BYTES);
|
||||
d.queue.submit([e.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),a=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES)),b=new Uint32Array(raw.slice(n*4+UNRESOLVED_BYTES,n*4+2*UNRESOLVED_BYTES));read.unmap();
|
||||
[meta,fieldSmooth,dsStats,corStats,dsPbuf,deepPbuf,read].forEach(destroy);out.uncertain=a[0]||0;out.remaining=b[0]||0;out.corrected=b[7]||0;return out;
|
||||
}
|
||||
|
||||
async benchmarkNumeric({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,forceStrict=false,correctUnknown=false}){await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),read=buf(d,UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const e=d.createCommandEncoder({label:'backend-benchmark'});if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,forceStrict?1:0));this.encodeDeepNumeric(e,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){e.clearBuffer(unresolved);this.encodeCorrectionNumeric(e,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap));this.encodeDirectNumeric(e,{pbuf,meta,smooth,w,h})}e.copyBufferToBuffer(unresolved,0,read,0,UNRESOLVED_BYTES);const cmd=e.finish(),t0=performance.now();d.queue.submit([cmd]);await d.queue.onSubmittedWorkDone();const gpuMs=performance.now()-t0;await read.mapAsync(GPUMapMode.READ);const a=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return{gpuMs,unknown:a[0]||0,corrected:a[7]||0}}
|
||||
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
|
||||
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
|
||||
const read=buf(d,n*4+UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);encoder.copyBufferToBuffer(unresolved,0,read,n*4,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const raw=read.getMappedRange(),out=new Uint32Array(raw.slice(0,n*4)),stats=new Uint32Array(raw.slice(n*4,n*4+UNRESOLVED_BYTES));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);out.unresolved=stats[0]||0;out.corrected=stats[7]||0;return out;
|
||||
}
|
||||
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false,correctUnknown=true}){
|
||||
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
|
||||
const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
|
||||
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes,UNRESOLVED_BYTES/4),unresolvedCount=stats[0]||0,corrected=stats[7]||0;ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
|
||||
}
|
||||
async renderTileRGBA2x({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,forceStrict=false,correctUnknown=true}){
|
||||
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);
|
||||
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}
|
||||
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
|
||||
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolvedCount=0,corrected=0;for(let si=0;si<4;si++){const stats=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4);unresolvedCount+=stats[0]||0;corrected+=stats[7]||0}ws.read.unmap();return{rgba:out,unresolved:unresolvedCount,corrected};
|
||||
}
|
||||
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext()}
|
||||
}
|
||||
|
||||
function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true}
|
||||
function backendQualityTiles(fullW,fullH){const w=Math.min(BACKEND_PROBE_QUALITY_W,fullW),h=Math.min(BACKEND_PROBE_QUALITY_H,fullH),x1=Math.max(0,fullW-w),y1=Math.max(0,fullH-h),xc=Math.max(0,Math.floor((fullW-w)/2)),yc=Math.max(0,Math.floor((fullH-h)/2));return[[0,0],[x1,0],[0,y1],[x1,y1],[xc,yc]].map(([tileX,tileY])=>({tileX,tileY,w,h}))}
|
||||
async function performBackendProbe(snap,iter,decision,token){if(!renderer||!decision.metrics.overlap||token!==state.token)return null;const fullW=canvas.width,fullH=canvas.height,sw=Math.min(BACKEND_PROBE_SPEED_W,fullW),sh=Math.min(BACKEND_PROBE_SPEED_H,fullH),sx=Math.max(0,Math.floor((fullW-sw)/2)),sy=Math.max(0,Math.floor((fullH-sh)/2)),speedPixels=sw*sh,fullPixels=Math.max(1,fullW*fullH),direct=await renderer.benchmarkNumeric({snap,iter,deep:false,deepContext:null,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh});if(token!==state.token)return null;const rb=performance.now(),ctx=await refs.request(snap,iter),buildWall=performance.now()-rb;if(token!==state.token||ctx.checkpointMismatch)return null;const deepProbe=await renderer.benchmarkNumeric({snap,iter,deep:true,deepContext:ctx,fullW,fullH,tileX:sx,tileY:sy,w:sw,h:sh,correctUnknown:true});if(token!==state.token)return null;let classDisagreement=0,deepUnknown=0,knownCompared=0,qualityPixels=0,corrected=0;for(const q of backendQualityTiles(fullW,fullH)){const directMeta=await renderer.renderTileMeta({snap,iter,deep:false,deepContext:null,fullW,fullH,...q});if(token!==state.token)return null;const deepMeta=await renderer.renderTileMeta({snap,iter,deep:true,deepContext:ctx,fullW,fullH,...q,correctUnknown:true});if(token!==state.token)return null;qualityPixels+=q.w*q.h;corrected+=deepMeta.corrected||0;for(let i=0;i<deepMeta.length;i++){const dc=(deepMeta[i]>>>28)&3;if(dc===FIELD_UNKNOWN){deepUnknown++;continue}const rc=(directMeta[i]>>>28)&3;knownCompared++;if((dc===FIELD_ESCAPED)!==(rc===FIELD_ESCAPED))classDisagreement++}}const buildMs=ctx.buildMs||buildWall,directMsPerPixel=direct.gpuMs/Math.max(1,speedPixels),deepMsPerPixel=deepProbe.gpuMs/Math.max(1,speedPixels),sample={directMsPerPixel,deepMsPerPixel,referenceBuildMs:buildMs},prev=backendProfiles.get(decision.key),a=prev?.samples?0.3:1,profile={samples:(prev?.samples||0)+1,directMsPerPixel:(prev?.directMsPerPixel||sample.directMsPerPixel)*(1-a)+sample.directMsPerPixel*a,deepMsPerPixel:(prev?.deepMsPerPixel||sample.deepMsPerPixel)*(1-a)+sample.deepMsPerPixel*a,referenceBuildMs:(prev?.referenceBuildMs||sample.referenceBuildMs)*(1-a)+sample.referenceBuildMs*a};backendProfiles.set(decision.key,profile);const deepUnknownRate=deepUnknown/Math.max(1,qualityPixels),qualityPass=classDisagreement===0&&deepUnknownRate<=DEEP_PROBE_UNKNOWN_MAX,viewProbe={qualityPass,deepUnknownRate,classDisagreement,knownCompared,qualityPixels,corrected,predictedDirectMs:directMsPerPixel*fullPixels,predictedDeepMs:buildMs+deepMsPerPixel*fullPixels,ratio:decision.metrics.ratio,zoom:zoomExpFor(snap),iter,speedProbe:{w:sw,h:sh,tileX:sx,tileY:sy},qualityTiles:backendQualityTiles(fullW,fullH)};backendViewProbes.set(decision.viewKey,viewProbe);runtime.backendProbes++;return viewProbe}
|
||||
function ensureBackendProbe(snap,iter,decision,token){if(!decision.metrics.overlap)return Promise.resolve(null);const hit=backendViewProbes.get(decision.viewKey);if(hit)return Promise.resolve(hit);const pending=backendProbePending.get(decision.viewKey);if(pending)return pending;const task=performBackendProbe(snap,iter,decision,token).catch(e=>{if(token===state.token)console.warn('backend probe failed',e);return null}).finally(()=>backendProbePending.delete(decision.viewKey));backendProbePending.set(decision.viewKey,task);return task}
|
||||
|
||||
|
||||
// ── backend crossover benchmark (diagnostic only) ───────────────────────
|
||||
const CROSSOVER_TRACKS={
|
||||
seahorse:{id:'seahorse',label:'Seahorse boundary',re:'-0.7453983606667815',im:'0.1125046349959942'},
|
||||
cusp:{id:'cusp',label:'Period-2 cusp',re:'-0.75',im:'0'}
|
||||
};
|
||||
const CROSSOVER_DEPTHS=[0,2,4,6,8,10,12,14,16,20,30,40];
|
||||
const benchmarkJob={active:false,cancelled:false,last:null};
|
||||
function fixedAtDecimal(text,bits){let s=String(text).trim(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const parts=s.toLowerCase().split('e'),mant=parts[0],exp=parts[1]?parseInt(parts[1],10):0,ab=mant.split('.'),ii=ab[0]||'0',ff=ab[1]||'';let digits=(ii+ff).replace(/^0+(?=\d)/,'')||'0',places=ff.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),scale=1n<<BigInt(bits),v=(BigInt(digits)*scale+den/2n)/den;return neg?-v:v}
|
||||
function crossoverSnap(track,z){const span='3.4e-'+z,bits=Math.max(320,decimalRequiredBits(track.re)+64,decimalRequiredBits(track.im)+64,decimalRequiredBits(span)+96);return{bits,re:fixedAtDecimal(track.re,bits),im:fixedAtDecimal(track.im,bits),span:fixedAtDecimal(span,bits)}}
|
||||
function iterForZoom(z,base=350){return Math.max(base,base+Math.floor(70*Math.sqrt(Math.max(0,z))+15*Math.max(0,z)))}
|
||||
function median(values){const a=[...values].sort((x,y)=>x-y),n=a.length;return n?a.length%2?a[(n-1)>>1]:(a[n/2-1]+a[n/2])/2:NaN}
|
||||
function compareMeta(reference,candidate){let referenceUnknown=0,candidateUnknown=0,classMismatch=0,escapedCompared=0,iterMismatch=0,maxIterDelta=0,sumIterDelta=0;const n=Math.min(reference.length,candidate.length);for(let i=0;i<n;i++){const rm=reference[i]>>>0,cm=candidate[i]>>>0,rc=(rm>>>28)&3,cc=(cm>>>28)&3;if(rc===FIELD_UNKNOWN){referenceUnknown++;continue}if(cc===FIELD_UNKNOWN){candidateUnknown++;continue}const re=rc===FIELD_ESCAPED,ce=cc===FIELD_ESCAPED;if(re!==ce){classMismatch++;continue}if(re){escapedCompared++;const d=Math.abs((rm&ITER_MASK)-(cm&ITER_MASK));if(d){iterMismatch++;sumIterDelta+=d;if(d>maxIterDelta)maxIterDelta=d}}}return{pixels:n,referenceUnknown,candidateUnknown,classMismatch,classMismatchRate:classMismatch/Math.max(1,n-referenceUnknown),escapedCompared,escapeIterationMismatch:iterMismatch,escapeIterationMismatchRate:iterMismatch/Math.max(1,escapedCompared),meanEscapeIterationDelta:iterMismatch?sumIterDelta/iterMismatch:0,maxEscapeIterationDelta:maxIterDelta}}
|
||||
function crossoverQualityTiles(fullW,fullH,w,h){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[0,0],[Math.max(0,fullW-w),0],[0,Math.max(0,fullH-h)],[Math.max(0,fullW-w),Math.max(0,fullH-h)],[Math.max(0,Math.floor((fullW-w)/2)),Math.max(0,Math.floor((fullH-h)/2))]];return pts.map(([tileX,tileY])=>({tileX,tileY,w,h}))}
|
||||
function aggregateQuality(list){const out={pixels:0,referenceUnknown:0,candidateUnknown:0,classMismatch:0,escapedCompared:0,escapeIterationMismatch:0,sumEscapeIterationDelta:0,maxEscapeIterationDelta:0};for(const q of list){out.pixels+=q.pixels;out.referenceUnknown+=q.referenceUnknown;out.candidateUnknown+=q.candidateUnknown;out.classMismatch+=q.classMismatch;out.escapedCompared+=q.escapedCompared;out.escapeIterationMismatch+=q.escapeIterationMismatch;out.sumEscapeIterationDelta+=q.meanEscapeIterationDelta*q.escapeIterationMismatch;out.maxEscapeIterationDelta=Math.max(out.maxEscapeIterationDelta,q.maxEscapeIterationDelta)}out.classMismatchRate=out.classMismatch/Math.max(1,out.pixels-out.referenceUnknown);out.escapeIterationMismatchRate=out.escapeIterationMismatch/Math.max(1,out.escapedCompared);out.meanEscapeIterationDelta=out.escapeIterationMismatch?out.sumEscapeIterationDelta/out.escapeIterationMismatch:0;delete out.sumEscapeIterationDelta;return out}
|
||||
const CROSSOVER_SPEED_W=256,CROSSOVER_SPEED_H=144,CROSSOVER_GPU_TIMEOUT_MS=15000,CROSSOVER_REF_TIMEOUT_MS=15000;
|
||||
function crossoverSpeedTiles(fullW,fullH,w=CROSSOVER_SPEED_W,h=CROSSOVER_SPEED_H){w=Math.min(w,fullW);h=Math.min(h,fullH);const pts=[[.5,.5],[.28,.36],[.72,.64]],seen=new Set(),out=[];for(const [fx,fy] of pts){const tileX=Math.max(0,Math.min(fullW-w,Math.round(fullW*fx-w/2))),tileY=Math.max(0,Math.min(fullH-h,Math.round(fullH*fy-h/2))),k=tileX+':'+tileY;if(!seen.has(k)){seen.add(k);out.push({tileX,tileY,w,h})}}return out}
|
||||
function benchmarkTimeout(promise,ms,label,onTimeout){let timer;const timeout=new Promise((_,reject)=>{timer=setTimeout(()=>{try{onTimeout?.()}catch{}reject(new Error(label+' timed out after '+Math.round(ms/1000)+'s'))},ms)});return Promise.race([promise,timeout]).finally(()=>clearTimeout(timer))}
|
||||
async function benchmarkTileBatch(fn,tiles){let gpuMs=0,pixels=0;for(const q of tiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h}return{gpuMs,pixels}}
|
||||
async function timedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[];for(let i=0;i<samples;i++){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');onStep?.({kind:'sample',index:i+1,total:samples});values.push((await fn()).gpuMs)}return{medianMs:median(values),samplesMs:values}}
|
||||
function extrapolateTiming(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels),fullMedianMs=msPerPixel*fullPixels;return{...timing,speedPixels,msPerPixel,fullMedianMs}}
|
||||
async function runCrossoverBenchmark({track='seahorse',depths=CROSSOVER_DEPTHS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
|
||||
if(benchmarkJob.active)throw new Error('benchmark already running');benchmarkJob.active=true;benchmarkJob.cancelled=false;
|
||||
try{
|
||||
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
|
||||
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown benchmark track');
|
||||
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
|
||||
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
|
||||
for(let di=0;di<depths.length;di++){
|
||||
if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z),metrics=directMetrics(snap,fullW);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'reference'});
|
||||
const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'z'+z+' reference',()=>refs.cancelPending('benchmark reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
|
||||
const timed=(name,fn)=>timedMedian(()=>gpuCall(benchmarkTileBatch(fn,speedTiles),'z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
|
||||
const tileCommon=q=>({snap,iter,fullW,fullH,...q});
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'f32'});
|
||||
const f32=extrapolateTiming(await timed('f32',q=>r.benchmarkNumeric({...tileCommon(q),deep:false,deepContext:null})),speedPixels,fullPixels);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'ds'});
|
||||
const ds=extrapolateTiming(await timed('ds',q=>r.benchmarkDsDirect(tileCommon(q))),speedPixels,fullPixels);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'deep'});
|
||||
const deep=extrapolateTiming(await timed('deep',q=>r.benchmarkNumeric({...tileCommon(q),deep:true,deepContext:ctx,correctUnknown:true})),speedPixels,fullPixels);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'quality'});
|
||||
const f32Parts=[],dsParts=[],qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH);let deepUnknownCount=0,qualityPixels=0,deepCorrected=0;
|
||||
for(const q of qTiles){if(benchmarkJob.cancelled)throw new Error('benchmark cancelled');const qCommon={snap,iter,fullW,fullH,...q},deepMeta=await gpuCall(r.renderTileMeta({...qCommon,deep:true,deepContext:ctx,correctUnknown:true}),'z'+z+' quality deep'),f32Meta=await gpuCall(r.renderTileMeta({...qCommon,deep:false,deepContext:null}),'z'+z+' quality f32'),dsMeta=await gpuCall(r.renderTileMetaDsDirect(qCommon),'z'+z+' quality ds');qualityPixels+=q.w*q.h;deepUnknownCount+=deepMeta.unresolved||0;deepCorrected+=deepMeta.corrected||0;f32Parts.push(compareMeta(deepMeta,f32Meta));dsParts.push(compareMeta(deepMeta,dsMeta))}
|
||||
const f32Quality=aggregateQuality(f32Parts),dsQuality=aggregateQuality(dsParts),deepUnknown=deepUnknownCount/Math.max(1,qualityPixels),referenceBuildMs=ctx.buildMs||referenceWallMs;
|
||||
const row={track:tr.id,z,span:'3.4e-'+z,iter,resolution:[fullW,fullH],speedTiles,speedPixels,qualityTile:[qualityW,qualityH],qualityTiles:qTiles,directRatio:metrics.ratio,
|
||||
f32:{...f32,quality:f32Quality},ds:{...ds,quality:dsQuality},deep:{...deep,referenceBuildMs,referenceWallMs,coldMedianMs:referenceBuildMs+deep.fullMedianMs,warmMedianMs:deep.fullMedianMs,unknownRate:deepUnknown,corrected:deepCorrected}};
|
||||
const qualityLimit=.01;
|
||||
row.assessment={f32VisuallySafe:f32Quality.classMismatchRate<=qualityLimit&&metrics.ratio>=1,dsVisuallySafe:dsQuality.classMismatchRate<=qualityLimit,deepVisuallySafe:deepUnknown<=.001,
|
||||
fastestWarm:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.warmMedianMs]].sort((a,b)=>a[1]-b[1])[0][0],
|
||||
fastestCold:[['f32',row.f32.fullMedianMs],['ds',row.ds.fullMedianMs],['deep',row.deep.coldMedianMs]].sort((a,b)=>a[1]-b[1])[0][0]};
|
||||
rows.push(row);onProgress?.({index:di+1,total:depths.length,z,phase:'done',row});
|
||||
}
|
||||
const result={format:'mandelbrot-webgpu-crossover-v2',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Speed timing uses three bounded representative tiles and extrapolates ms/pixel to the current full resolution, preventing long full-frame Deep benchmark dispatches. Deep cold adds verified BigInt reference construction. Quality remains measured on five spatial tiles. Any single benchmark GPU batch or reference build exceeding 15 s aborts with an explicit error instead of waiting indefinitely.'};
|
||||
benchmarkJob.last=result;return result;
|
||||
}finally{benchmarkJob.active=false}
|
||||
}
|
||||
|
||||
|
||||
// ── DS + sparse Deep hybrid experiment (diagnostic only) ───────────────
|
||||
const HYBRID_DEPTHS=[6,8,10,12],HYBRID_RISK_LIMITS=[1e12,1e13,1e14];
|
||||
const hybridJob={active:false,cancelled:false,last:null};
|
||||
async function hybridTileBatch(fn,tiles){let gpuMs=0,pixels=0,uncertain=0,remaining=0,corrected=0;for(const q of tiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const r=await fn(q);gpuMs+=r.gpuMs;pixels+=q.w*q.h;uncertain+=r.uncertain||0;remaining+=r.remaining||0;corrected+=r.corrected||0}return{gpuMs,pixels,uncertain,remaining,corrected}}
|
||||
async function hybridTimedMedian(fn,warmups=1,samples=3,onStep=null){for(let i=0;i<warmups;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'warmup',index:i+1,total:warmups});await fn()}const values=[],stats=[];for(let i=0;i<samples;i++){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');onStep?.({kind:'sample',index:i+1,total:samples});const r=await fn();values.push(r.gpuMs);stats.push(r)}return{medianMs:median(values),samplesMs:values,sampleStats:stats}}
|
||||
function hybridExtrapolate(timing,speedPixels,fullPixels){const msPerPixel=timing.medianMs/Math.max(1,speedPixels);return{...timing,speedPixels,msPerPixel,fullMedianMs:msPerPixel*fullPixels}}
|
||||
async function runHybridExperiment({track='seahorse',depths=HYBRID_DEPTHS,riskLimits=HYBRID_RISK_LIMITS,warmups=1,samples=3,qualityW=96,qualityH=64,onProgress=null}={}){
|
||||
if(hybridJob.active||benchmarkJob.active)throw new Error('another benchmark is running');hybridJob.active=true;hybridJob.cancelled=false;
|
||||
try{
|
||||
const r=renderer||await initRenderer();if(!r)throw new Error('WebGPU renderer unavailable');await r.ready;
|
||||
const tr=typeof track==='string'?CROSSOVER_TRACKS[track]:track;if(!tr)throw new Error('unknown experiment track');
|
||||
const fullW=Math.max(64,canvas.width),fullH=Math.max(64,canvas.height),fullPixels=fullW*fullH,speedTiles=crossoverSpeedTiles(fullW,fullH),speedPixels=speedTiles.reduce((n,q)=>n+q.w*q.h,0),rows=[];
|
||||
const gpuCall=(promise,label)=>benchmarkTimeout(promise,CROSSOVER_GPU_TIMEOUT_MS,label,()=>{try{r.device.destroy()}catch{}});
|
||||
for(let di=0;di<depths.length;di++){
|
||||
if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const z=Number(depths[di]),snap=crossoverSnap(tr,z),iter=iterForZoom(z);
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'reference'});const refStart=performance.now(),ctx=await benchmarkTimeout(refs.request(snap,iter,true),CROSSOVER_REF_TIMEOUT_MS,'hybrid z'+z+' reference',()=>refs.cancelPending('hybrid reference timeout')),referenceWallMs=performance.now()-refStart;if(ctx.checkpointMismatch)throw new Error('reference checkpoint mismatch at z'+z);
|
||||
const timed=(name,fn)=>hybridTimedMedian(()=>gpuCall(hybridTileBatch(fn,speedTiles),'hybrid z'+z+' '+name),warmups,samples,step=>onProgress?.({index:di,total:depths.length,z,phase:name+' '+step.kind+' '+step.index+'/'+step.total}));
|
||||
const common=q=>({snap,iter,fullW,fullH,...q});
|
||||
const ds=hybridExtrapolate(await timed('DS baseline',async q=>{const x=await r.benchmarkDsDirect(common(q));return{...x}}),speedPixels,fullPixels);
|
||||
const deep=hybridExtrapolate(await timed('Full Deep',async q=>{const x=await r.benchmarkNumeric({...common(q),deep:true,deepContext:ctx,correctUnknown:true});return{...x}}),speedPixels,fullPixels);
|
||||
const qTiles=crossoverQualityTiles(fullW,fullH,qualityW,qualityH),deepMetas=[];let deepUnknown=0;
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'quality reference'});
|
||||
for(const q of qTiles){if(hybridJob.cancelled)throw new Error('hybrid experiment cancelled');const m=await gpuCall(r.renderTileMeta({...common(q),deep:true,deepContext:ctx,correctUnknown:true}),'hybrid z'+z+' quality deep');deepUnknown+=m.unresolved||0;deepMetas.push(m)}
|
||||
const variants=[];
|
||||
for(const riskLimit of riskLimits){
|
||||
onProgress?.({index:di,total:depths.length,z,phase:'risk '+riskLimit.toExponential(0)});
|
||||
const timing=hybridExtrapolate(await timed('Hybrid '+riskLimit.toExponential(0),q=>r.benchmarkHybrid({...common(q),deepContext:ctx,riskLimit})),speedPixels,fullPixels);
|
||||
const parts=[];let uncertain=0,remaining=0,corrected=0,qualityPixels=0;
|
||||
for(let qi=0;qi<qTiles.length;qi++){const q=qTiles[qi],hm=await gpuCall(r.renderTileMetaHybrid({...common(q),deepContext:ctx,riskLimit}),'hybrid z'+z+' quality '+riskLimit);qualityPixels+=q.w*q.h;uncertain+=hm.uncertain||0;remaining+=hm.remaining||0;corrected+=hm.corrected||0;parts.push(compareMeta(deepMetas[qi],hm))}
|
||||
const quality=aggregateQuality(parts),referenceBuildMs=ctx.buildMs||referenceWallMs,coldMedianMs=referenceBuildMs+timing.fullMedianMs,uncertainRate=uncertain/Math.max(1,qualityPixels),remainingRate=remaining/Math.max(1,qualityPixels),safe=quality.classMismatch===0&&remaining===0;
|
||||
variants.push({riskLimit,...timing,referenceBuildMs,coldMedianMs,quality,qualityPixels,uncertain,uncertainRate,remaining,remainingRate,corrected,safe,speedupVsDeepCold:(referenceBuildMs+deep.fullMedianMs)/Math.max(1e-9,coldMedianMs)});
|
||||
}
|
||||
const referenceBuildMs=ctx.buildMs||referenceWallMs,deepCold=referenceBuildMs+deep.fullMedianMs,viable=variants.filter(v=>v.safe).sort((a,b)=>a.coldMedianMs-b.coldMedianMs),best=viable[0]||null;
|
||||
rows.push({track:tr.id,z,iter,resolution:[fullW,fullH],referenceBuildMs,ds:{...ds},deep:{...deep,coldMedianMs:deepCold,unknownRate:deepUnknown/Math.max(1,qTiles.reduce((n,q)=>n+q.w*q.h,0))},variants,bestRiskLimit:best?.riskLimit??null,bestHybridColdMs:best?.coldMedianMs??null,recommendation:best&&best.coldMedianMs<deepCold*.9&&best.uncertainRate<.6?'hybrid-candidate':'full-deep'});
|
||||
onProgress?.({index:di+1,total:depths.length,z,phase:'done'});
|
||||
}
|
||||
const result={format:'mandelbrot-ds-sparse-deep-hybrid-v1',shaderVersion:G.version,createdAt:new Date().toISOString(),adapter:r.adapterInfo,limits:r.adapterLimits,track:tr,depths:[...depths],riskLimits:[...riskLimits],warmups,samples,resolution:[fullW,fullH],speedTiles,speedPixels,rows,note:'Diagnostic only. DS guarded direct tracks dz/dc in f32 and marks a pixel UNKNOWN when |dz/dc| exceeds riskLimit. Existing deep double-single correction re-evaluates only those UNKNOWN pixels. Full Deep+correction is the GPU quality reference. Cold timing adds the verified BigInt reference build to both Hybrid and Full Deep. Production routing is unchanged.'};
|
||||
hybridJob.last=result;return result;
|
||||
}finally{hybridJob.active=false}
|
||||
}
|
||||
|
||||
// ── GPU startup / rendering orchestration ────────────────────────────────
|
||||
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
|
||||
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
|
||||
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
|
||||
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
|
||||
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
|
||||
async function renderFrame(){
|
||||
const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height);
|
||||
state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats();
|
||||
try{
|
||||
const r=renderer||await initRenderer();if(token!==state.token)return;
|
||||
if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}
|
||||
if(decision.probe){state.lastEngine='WebGPU · direct/deep view probe';state.drawState='COVERING';updateStats();await ensureBackendProbe(snap,iter,decision,token);if(token!==state.token)return;decision=chooseBackend(snap,canvas.width,iter,canvas.height);if(decision.probe)decision={...decision,backend:'deep',deep:true,probe:false,reason:'overlap-probe-failed-safe-deep'};state.backendDecision=decision;updateStats()}
|
||||
const deep=decision.deep;let ctx=null;
|
||||
if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded perturbation'}else state.lastEngine='WebGPU · f32 direct';
|
||||
const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;
|
||||
const firstPaint=performance.now()-t0;
|
||||
state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,backend:decision.backend};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=firstPaint;state.rendering=deep;state.unresolved=0;
|
||||
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();
|
||||
if(!deep){state.rendering=false;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();return}
|
||||
let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats();
|
||||
const n=canvas.width*canvas.height;if(shouldApplySparseCorrection(state.unresolved,n)){state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats();const corrected=await r.correctUnknownFrame(snap,iter,token);if(!corrected||token!==state.token)return;state.correctionPasses=1;stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected;const painted=await r.recolor(token);if(!painted||token!==state.token)return;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats()}
|
||||
if(token!==state.token)return;state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · guarded perturbation'+(state.correctionPasses?' + DS correction':'');const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();
|
||||
}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}
|
||||
}
|
||||
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback(深部非対応)';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
|
||||
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
|
||||
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
|
||||
|
||||
// ── interaction / view history ──────────────────────────────────────────
|
||||
function viewRect(){return canvas.getBoundingClientRect()}
|
||||
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
|
||||
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
|
||||
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
|
||||
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
|
||||
const pts=new Map();let lx=0,ly=0,pinch=0;
|
||||
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
|
||||
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
|
||||
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
|
||||
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
|
||||
|
||||
// ── URL / controls ───────────────────────────────────────────────────────
|
||||
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
|
||||
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
|
||||
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
|
||||
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
|
||||
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
|
||||
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
|
||||
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
|
||||
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
|
||||
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
|
||||
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
|
||||
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
|
||||
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
|
||||
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
|
||||
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
|
||||
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
|
||||
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
|
||||
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
|
||||
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
|
||||
|
||||
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
|
||||
const exportJob={active:false,cancelled:false};
|
||||
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
|
||||
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
|
||||
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
|
||||
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
|
||||
class StreamingPng{
|
||||
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
|
||||
async rows(filteredRows){await this.writer.write(filteredRows)}
|
||||
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
|
||||
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
|
||||
}
|
||||
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
|
||||
async function runExport(){
|
||||
if(exportJob.active)return;
|
||||
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
|
||||
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),strict=$('#exportPrecision').value==='strict';
|
||||
let ctx=null;
|
||||
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
|
||||
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
|
||||
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
|
||||
let done=0,unresolvedSamples=0;
|
||||
try{
|
||||
for(let y=0;y<h;y+=tile){
|
||||
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
|
||||
for(let x=0;x<w;x+=tile){
|
||||
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
|
||||
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
|
||||
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
|
||||
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
|
||||
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
|
||||
}
|
||||
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
|
||||
}
|
||||
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
|
||||
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
|
||||
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
|
||||
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
|
||||
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
|
||||
finally{exportJob.active=false;$('#exportStart').disabled=false}
|
||||
}
|
||||
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
|
||||
|
||||
function benchmarkSummary(result){const lines=['z | f32 est ms | DS est ms | deep warm est | deep cold est | f32 Δclass | DS Δclass | direct ratio | fastest cold','--|------------|-----------|---------------|---------------|------------|-----------|--------------|-------------'];for(const r of result.rows)lines.push([String(r.z).padStart(2),r.f32.fullMedianMs.toFixed(1),r.ds.fullMedianMs.toFixed(1),r.deep.warmMedianMs.toFixed(1),r.deep.coldMedianMs.toFixed(1),(100*r.f32.quality.classMismatchRate).toFixed(2)+'%',(100*r.ds.quality.classMismatchRate).toFixed(2)+'%',Number.isFinite(r.directRatio)?r.directRatio.toExponential(2):'n/a',r.assessment.fastestCold].join(' | '));return lines.join('\n')}
|
||||
$('#backendBench').onclick=()=>{const d=$('#benchmarkDialog');$('#benchmarkStatus').textContent='';$('#benchmarkProgress').hidden=true;$('#benchmarkOutput').value=benchmarkJob.last?benchmarkSummary(benchmarkJob.last):'';$('#benchmarkSave').disabled=!benchmarkJob.last;d.showModal?d.showModal():d.setAttribute('open','')};
|
||||
$('#benchmarkStart').onclick=async()=>{if(benchmarkJob.active)return;cancelRender();state.dirty=false;const start=$('#benchmarkStart'),progress=$('#benchmarkProgress'),status=$('#benchmarkStatus');start.disabled=true;$('#benchmarkSave').disabled=true;progress.hidden=false;progress.max=CROSSOVER_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#benchmarkOutput').value='';try{const result=await runCrossoverBenchmark({track:$('#benchmarkTrack').value,samples:Math.max(1,Number($('#benchmarkSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});progress.value=progress.max;status.textContent='測定完了 · '+result.rows.length+' depths';$('#benchmarkOutput').value=benchmarkSummary(result);$('#benchmarkSave').disabled=false}catch(e){status.textContent=String(e&&e.message||e)}finally{start.disabled=false;markDirty(false)}};
|
||||
$('#benchmarkCancel').onclick=()=>{if(benchmarkJob.active){benchmarkJob.cancelled=true;refs.cancelPending('benchmark cancelled');$('#benchmarkStatus').textContent='中止しています…'}else $('#benchmarkDialog').close()};
|
||||
$('#benchmarkSave').onclick=()=>{if(!benchmarkJob.last)return;downloadBlob(new Blob([JSON.stringify(benchmarkJob.last,null,2)],{type:'application/json'}),'mandelbrot-crossover-'+Date.now()+'.json')};
|
||||
|
||||
function hybridSummary(result){const lines=['z | best risk | hybrid cold est | deep cold est | speedup | correction | remain | Δclass | recommendation','--|-----------|-----------------|---------------|---------|------------|--------|--------|---------------'];for(const r of result.rows){const best=r.bestRiskLimit==null?null:r.variants.find(v=>v.riskLimit===r.bestRiskLimit),hy=best?best.coldMedianMs:NaN;lines.push([String(r.z).padStart(2),best?best.riskLimit.toExponential(0):'none',best?hy.toFixed(1):'n/a',r.deep.coldMedianMs.toFixed(1),best?(r.deep.coldMedianMs/hy).toFixed(2)+'×':'n/a',best?(100*best.uncertainRate).toFixed(1)+'%':'n/a',best?(100*best.remainingRate).toFixed(2)+'%':'n/a',best?(100*best.quality.classMismatchRate).toFixed(2)+'%':'n/a',r.recommendation].join(' | '))}return lines.join('\n')}
|
||||
function openHybridDialog(){const d=$('#hybridDialog');if(!d.open){try{if(typeof d.showModal==='function')d.showModal();else d.setAttribute('open','')}catch{d.setAttribute('open','')}}return d}
|
||||
async function startHybridExperimentFromUi(){
|
||||
const d=openHybridDialog(),start=$('#hybridStart'),toolbar=$('#hybridBench'),progress=$('#hybridProgress'),status=$('#hybridStatus');status.textContent='Hybrid実験を起動中…';toolbar.setAttribute('aria-busy','true');
|
||||
if(hybridJob.active){status.textContent='Hybrid実験を実行中です';toolbar.removeAttribute('aria-busy');return}
|
||||
if(benchmarkJob.active){status.textContent='Backend比較を終了または中止してから実行してください';toolbar.removeAttribute('aria-busy');return}
|
||||
cancelRender();state.dirty=false;start.disabled=true;toolbar.disabled=true;start.textContent='実行中…';$('#hybridSave').disabled=true;progress.hidden=false;progress.max=HYBRID_DEPTHS.length;progress.value=0;status.textContent='準備中…';$('#hybridOutput').value='';
|
||||
try{
|
||||
const result=await runHybridExperiment({track:$('#hybridTrack').value,samples:Math.max(1,Number($('#hybridSamples').value)||3),onProgress:p=>{progress.value=Math.min(progress.max,p.index||0);status.textContent='z'+p.z+' · '+p.phase}});
|
||||
progress.value=progress.max;status.textContent='実験完了 · '+result.rows.length+' depths';$('#hybridOutput').value=hybridSummary(result);$('#hybridSave').disabled=false;start.textContent='再実行';
|
||||
}catch(e){status.textContent=String(e&&e.message||e);start.textContent='再実行'}
|
||||
finally{start.disabled=false;toolbar.disabled=false;toolbar.removeAttribute('aria-busy');markDirty(false)}
|
||||
}
|
||||
$('#hybridBench').onclick=()=>{toast('Hybrid実験を開始');void startHybridExperimentFromUi()};
|
||||
$('#hybridStart').onclick=startHybridExperimentFromUi;
|
||||
$('#hybridCancel').onclick=()=>{if(hybridJob.active){hybridJob.cancelled=true;refs.cancelPending('hybrid experiment cancelled');$('#hybridStatus').textContent='中止しています…'}else $('#hybridDialog').close()};
|
||||
$('#hybridSave').onclick=()=>{if(!hybridJob.last)return;downloadBlob(new Blob([JSON.stringify(hybridJob.last,null,2)],{type:'application/json'}),'mandelbrot-hybrid-'+Date.now()+'.json')};
|
||||
|
||||
|
||||
// ── diagnostics ──────────────────────────────────────────────────────────
|
||||
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend(snapshot(),Math.max(1,canvas.width),maxIter(),Math.max(1,canvas.height));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.probe?'WebGPU 判定中':decision.deep?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'深部補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.correctedPixels)status+=' · 回収 '+state.correctedPixels;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx,ratio=decision.metrics&&Number.isFinite(decision.metrics.ratio)?decision.metrics.ratio:0,prof=decision.profile,rs=state.unknownReasons,reasonText=rs?Object.entries(rs).filter(([,v])=>v).map(([k,v])=>k+':'+v).join(' '):'';$('#diagEngine').textContent='engine: '+state.lastEngine+' | route '+decision.backend+' ('+decision.reason+') | ratio '+(ratio?ratio.toExponential(2):'n/a')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'')+(reasonText?' | UNKNOWN '+reasonText:'')+(decision.viewProbe?' | view probe direct '+decision.viewProbe.predictedDirectMs.toFixed(0)+'ms / deep '+decision.viewProbe.predictedDeepMs.toFixed(0)+'ms / u '+(100*decision.viewProbe.deepUnknownRate).toFixed(2)+'% / Δclass '+decision.viewProbe.classDisagreement:'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height+' | speed profiles '+backendProfiles.size+' / view probes '+backendViewProbes.size}
|
||||
|
||||
globalThis.__MANDEL_TEST__={
|
||||
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
|
||||
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
|
||||
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
|
||||
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w,h):!!forceDeep;let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
|
||||
async runCrossoverBenchmark(options={}){return runCrossoverBenchmark(options)},
|
||||
async runHybridExperiment(options={}){return runHybridExperiment(options)},
|
||||
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
|
||||
};
|
||||
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,buildVersion:BUILD_VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:(state.backendDecision||chooseBackend()).deep,backendDecision:state.backendDecision,backendProfiles:Array.from(backendProfiles.entries()),backendViewProbes:Array.from(backendViewProbes.entries()),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,unknownReasons:state.unknownReasons,correctionPasses:state.correctionPasses,correctedPixels:state.correctedPixels,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
|
||||
|
||||
// ── boot / teardown ──────────────────────────────────────────────────────
|
||||
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
|
||||
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
|
||||
})();
|
||||
BIN
dist/wasm/bla-scalar.4f06913487044823.wasm
vendored
BIN
dist/wasm/bla-scalar.4f06913487044823.wasm
vendored
Binary file not shown.
BIN
dist/wasm/bla-scalar.wasm
vendored
BIN
dist/wasm/bla-scalar.wasm
vendored
Binary file not shown.
BIN
dist/wasm/bla-simd.f80f136e9fb676ce.wasm
vendored
BIN
dist/wasm/bla-simd.f80f136e9fb676ce.wasm
vendored
Binary file not shown.
BIN
dist/wasm/bla-simd.wasm
vendored
BIN
dist/wasm/bla-simd.wasm
vendored
Binary file not shown.
BIN
dist/wasm/color-scalar.aa2914ec1acacaa2.wasm
vendored
BIN
dist/wasm/color-scalar.aa2914ec1acacaa2.wasm
vendored
Binary file not shown.
BIN
dist/wasm/color-scalar.wasm
vendored
BIN
dist/wasm/color-scalar.wasm
vendored
Binary file not shown.
BIN
dist/wasm/color-simd.8cf83374ed0c680b.wasm
vendored
BIN
dist/wasm/color-simd.8cf83374ed0c680b.wasm
vendored
Binary file not shown.
BIN
dist/wasm/color-simd.wasm
vendored
BIN
dist/wasm/color-simd.wasm
vendored
Binary file not shown.
BIN
dist/wasm/deep-scalar.d133bbecc8f6dbdf.wasm
vendored
BIN
dist/wasm/deep-scalar.d133bbecc8f6dbdf.wasm
vendored
Binary file not shown.
BIN
dist/wasm/deep-scalar.wasm
vendored
BIN
dist/wasm/deep-scalar.wasm
vendored
Binary file not shown.
BIN
dist/wasm/deep-simd.d49385f06e4a8f55.wasm
vendored
BIN
dist/wasm/deep-simd.d49385f06e4a8f55.wasm
vendored
Binary file not shown.
BIN
dist/wasm/deep-simd.wasm
vendored
BIN
dist/wasm/deep-simd.wasm
vendored
Binary file not shown.
55
dist/wasm/manifest.json
vendored
55
dist/wasm/manifest.json
vendored
|
|
@ -1,55 +0,0 @@
|
|||
{
|
||||
"format": "mandelbrot-wasm-manifest-v1",
|
||||
"generatedUtc": "2026-08-22T12:30:26.3193366Z",
|
||||
"input": "kernels.js",
|
||||
"payloads": [
|
||||
{
|
||||
"symbol": "WASM_SIMD_B64",
|
||||
"file": "wasm-simd.wasm",
|
||||
"bytes": 511,
|
||||
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
|
||||
},
|
||||
{
|
||||
"symbol": "WASM_SCALAR_B64",
|
||||
"file": "wasm-scalar.wasm",
|
||||
"bytes": 511,
|
||||
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
|
||||
},
|
||||
{
|
||||
"symbol": "DEEP_SIMD_B64",
|
||||
"file": "deep-simd.wasm",
|
||||
"bytes": 4866,
|
||||
"sha256": "d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214"
|
||||
},
|
||||
{
|
||||
"symbol": "DEEP_SCALAR_B64",
|
||||
"file": "deep-scalar.wasm",
|
||||
"bytes": 5246,
|
||||
"sha256": "d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2"
|
||||
},
|
||||
{
|
||||
"symbol": "BLA_SIMD_B64",
|
||||
"file": "bla-simd.wasm",
|
||||
"bytes": 4959,
|
||||
"sha256": "f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e"
|
||||
},
|
||||
{
|
||||
"symbol": "BLA_SCALAR_B64",
|
||||
"file": "bla-scalar.wasm",
|
||||
"bytes": 4593,
|
||||
"sha256": "4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f"
|
||||
},
|
||||
{
|
||||
"symbol": "COLOR_SIMD_B64",
|
||||
"file": "color-simd.wasm",
|
||||
"bytes": 1387,
|
||||
"sha256": "8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03"
|
||||
},
|
||||
{
|
||||
"symbol": "COLOR_SCALAR_B64",
|
||||
"file": "color-scalar.wasm",
|
||||
"bytes": 632,
|
||||
"sha256": "aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
dist/wasm/wasm-scalar.d19b26c04e1b2f59.wasm
vendored
BIN
dist/wasm/wasm-scalar.d19b26c04e1b2f59.wasm
vendored
Binary file not shown.
BIN
dist/wasm/wasm-scalar.wasm
vendored
BIN
dist/wasm/wasm-scalar.wasm
vendored
Binary file not shown.
BIN
dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm
vendored
BIN
dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm
vendored
Binary file not shown.
BIN
dist/wasm/wasm-simd.wasm
vendored
BIN
dist/wasm/wasm-simd.wasm
vendored
Binary file not shown.
15
docs/CHANGELOG.md
Normal file
15
docs/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# CHANGELOG
|
||||
|
||||
## v24.2.26
|
||||
|
||||
- v24.2.23〜v24.2.24 のLOD / frontier全画面再計算系を撤回。
|
||||
- v24.2.18 の Direct / Fast / Deep / Color WGSL を完全復元。
|
||||
- v24.2.18 の `maxIter` と fallback を復元。
|
||||
- 一次描画後だけ起動する GPU-only `INTERIOR_LIKELY` queue を追加。
|
||||
- 補足精密化は高速モードでも guarded Deep で実行。
|
||||
- 精密化シェーダーは一次表示後に遅延コンパイル。
|
||||
- queue候補は境界近傍 + 疎なscreen-space seedに限定。
|
||||
- CPUへ戻す精密化情報は16 byte統計のみ。全meta readbackなし。
|
||||
- 標準精密化batchを最大1.8M sample-iterationsに制限。
|
||||
- 自動色変化速度を v24.2.18 の1/10へ変更。
|
||||
- baseline一致・batch予算・品質単調性の専用テストを追加。
|
||||
117
docs/README.md
Normal file
117
docs/README.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# Mandelbrot Deep Zoom v24.2.26
|
||||
|
||||
## 目的
|
||||
|
||||
v24.2.23〜v24.2.24 系で発生した「品質低下・速度低下」を撤回し、**v24.2.18 の一次描画を基準線として復元**した版です。
|
||||
|
||||
この版の合格条件は次の4点です。
|
||||
|
||||
1. 一次表示は v24.2.18 より遅くしない。
|
||||
2. 補足精密化を停止しても v24.2.18 より品質を下げない。
|
||||
3. 通常描画ループで fieldMeta 全体を GPU→CPU readback しない。
|
||||
4. 黒い `INTERIOR_LIKELY` の追加判定は一次表示後・アイドル時だけ行う。
|
||||
|
||||
## 一次描画
|
||||
|
||||
一次描画の以下のWGSLは v24.2.18 と **SHA-256まで一致**しています。
|
||||
|
||||
- `DIRECT_F32_WGSL`
|
||||
- `FAST_PERTURB_WGSL`
|
||||
- `DEEP_PERTURB_WGSL`
|
||||
- `COLOR_WGSL`
|
||||
|
||||
`maxIter()` も v24.2.18 と同じです。
|
||||
|
||||
```text
|
||||
baseIter + floor(70*sqrt(zoomExp) + 15*zoomExp)
|
||||
```
|
||||
|
||||
1920×1080 / 標準負荷の代表条件では内部描画は約 1672×941 = 1,573,352 px です。
|
||||
|
||||
| zoom | 一次 iter | 一次描画の最悪 sample-iterations |
|
||||
|---:|---:|---:|
|
||||
| 10^6 | 611 | 0.961 B |
|
||||
| 10^8 | 667 | 1.049 B |
|
||||
| 10^12 | 772 | 1.215 B |
|
||||
| 10^20 | 963 | 1.515 B |
|
||||
|
||||
これは v24.2.18 と同じ計算量です。補足精密化用WGSLも起動時にはコンパイルせず、一次描画完了から320ms後に初めて遅延コンパイルします。
|
||||
|
||||
## アイドル補足精密化
|
||||
|
||||
一次描画後にだけ `FIELD_INTERIOR_LIKELY` を追加判定します。
|
||||
|
||||
### GPU-only queue
|
||||
|
||||
全 `fieldMeta` をCPUへ戻しません。GPU上で候補インデックスqueueを作り、CPUへ戻すのは **16 byte のqueue統計だけ**です。
|
||||
|
||||
候補は次のように絞ります。
|
||||
|
||||
- stage 1: 境界近傍 + 4px間隔 seed
|
||||
- stage 2: 境界近傍 + 8px間隔 seed
|
||||
- stage 3: 境界近傍 + 16px間隔 seed
|
||||
|
||||
境界判定は各候補につき最大8点だけ参照します。広い真内部を毎回全面再計算する方式にはしていません。1920×1080/標準の代表内部解像度では、純粋な疎seedだけなら stage 1/2/3 はそれぞれ約98,648 / 24,662 / 6,195画素です。10^8 の場合、疎seed部分の合計は約0.220B sample-iterationsで、一次描画最悪値約1.049Bの約21%です。境界画素はこれに追加されますが、batch上限と休止でGPU占有を制限します。
|
||||
|
||||
### 数値エンジン
|
||||
|
||||
高速モードの一次表示は従来のFast経路ですが、**アイドル精密化は高速モードでも guarded Deep を使用**します。精密化用reference bufferは通常描画の `deepCtx / fastCtx` と分離しているため、精密化後もv24.2.18のpan reference再利用を壊しません。
|
||||
|
||||
これにより、一次描画で `INTERIOR_LIKELY` だった点だけを保守的に再評価します。精密化シェーダーは実行直前にも対象がまだ `INTERIOR_LIKELY` か確認するため、既存の `ESCAPED` / `INTERIOR_PROVEN` を上書きしません。
|
||||
|
||||
### 反復ターゲット
|
||||
|
||||
標準モードでは概ね次の3段階です。
|
||||
|
||||
| zoom | 一次 | stage 1 | stage 2 | stage 3 |
|
||||
|---:|---:|---:|---:|---:|
|
||||
| 10^6 | 611 | 1222 | 2444 | 3000 |
|
||||
| 10^8 | 667 | 1334 | 2668 | 3600 |
|
||||
| 10^12 | 772 | 1544 | 3088 | 4800 |
|
||||
| 10^20 | 963 | 1926 | 3852 | 7200 |
|
||||
|
||||
### 1 GPU batch の上限
|
||||
|
||||
標準モードの補足精密化は **1.8 million sample-iterations / batch 以下**に丸めます。
|
||||
|
||||
例: `10^8`
|
||||
|
||||
- 1334 iter: 1344 pixels / batch → 1,792,896 sample-iterations
|
||||
- 2668 iter: 640 pixels / batch → 1,707,520
|
||||
- 3600 iter: 448 pixels / batch → 1,612,800
|
||||
|
||||
ユーザー操作が入ると `state.token` が変わり、次batchへ進む前に中断します。また一定時間ごとにGPUを休ませます。
|
||||
|
||||
## 品質の単調性
|
||||
|
||||
補足精密化前の画像は v24.2.18 と同じです。
|
||||
|
||||
精密化は `FIELD_INTERIOR_LIKELY` のみを対象とし、既に確定した画素には触れません。さらに再着色時も一次描画の `baseIter` を色周期正規化に使うため、精密化によって既存の外部色が一斉に変化することを避けています。
|
||||
|
||||
## 色操作
|
||||
|
||||
v24.2.18 のズーム相対色周期と対数スライダーを維持しています。`色を自動変化` の速度のみ、要求どおり v24.2.18 の **1/10** にしています。
|
||||
|
||||
## fallback
|
||||
|
||||
JavaScript fallback は v24.2.18 と SHA-256一致です。疑似等高線・縞模様は追加していません。
|
||||
|
||||
## 検査
|
||||
|
||||
```bash
|
||||
npm test
|
||||
npm run build
|
||||
```
|
||||
|
||||
`tests/v24-baseline-idle-refinement-model.mjs` が以下を検査します。
|
||||
|
||||
- 一次WGSL 4本の v24.2.18 SHA-256一致
|
||||
- fallback の v24.2.18 SHA-256一致
|
||||
- `maxIter()` の基準線維持
|
||||
- 精密化WGSLの遅延コンパイル
|
||||
- 全meta readback不使用
|
||||
- 16 byte queue統計readback
|
||||
- 既存確定画素を上書きしない契約
|
||||
- 各倍率で1batchが予算を超えないこと
|
||||
|
||||
実GPUのfpsやdevice-lost有無は、このコンテナでWebGPUが初期化できない場合は保証できません。静的・CPUモデル結果と実機測定は区別します。
|
||||
101
docs/VALIDATION_REPORT.md
Normal file
101
docs/VALIDATION_REPORT.md
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# v24.2.26 検査報告
|
||||
|
||||
## 結論
|
||||
|
||||
v24.2.26 は v24.2.18 を一次描画の基準線として復元し、黒い `INTERIOR_LIKELY` の追加判定だけを一次表示後のアイドルGPU処理として分離した。
|
||||
|
||||
一次描画の品質・数値計算量を変えないことを、ソースハッシュ・式・静的契約で確認した。実GPU上のfps/device-lostは、この実行環境でWebGPU/EGLを初期化できなかったため未測定である。
|
||||
|
||||
## 一次描画の同一性
|
||||
|
||||
v24.2.18 と SHA-256 が一致する本番WGSL:
|
||||
|
||||
| Shader | SHA-256 |
|
||||
|---|---|
|
||||
| DIRECT_F32_WGSL | `59301bf0c5ba3dda4a4055335b32d4a3f84a21991be30f8efc74deb5a5f11ccf` |
|
||||
| FAST_PERTURB_WGSL | `03b45713b1bbb7b56e18bd8fdf0f2d8161d7ed25adf02482b41c1038dc16e3b8` |
|
||||
| DEEP_PERTURB_WGSL | `2865a015d5c1a9b459085b7044e23563fcc5f0eec5362634249b6604a47e660a` |
|
||||
| COLOR_WGSL | `cccac7bfb69c64093c46c3468ff9d685d680ba3a88de0d1ea57168d91a067acf` |
|
||||
|
||||
JavaScript fallback も v24.2.18 と一致:
|
||||
|
||||
`689a6f56aedd416b5e512b17ab6a3ddebcf7c73494e0a8a600ce74a06d8073a2`
|
||||
|
||||
`maxIter()` は v24.2.18 と同じ:
|
||||
|
||||
`baseIter + floor(70*sqrt(zoomExp) + 15*zoomExp)`
|
||||
|
||||
補足精密化用WGSLは本番pipeline初期化ではコンパイルしない。一次表示完了後、320msのアイドル遅延後に必要な場合だけ遅延コンパイルする。
|
||||
|
||||
## 代表計算量
|
||||
|
||||
1920×1080 CSS / 標準負荷では内部描画を約1672×941 = 1,573,352画素とする。
|
||||
|
||||
| zoom | 一次iter | 一次最悪 sample-iterations |
|
||||
|---:|---:|---:|
|
||||
| 10^6 | 611 | 961,318,072 |
|
||||
| 10^8 | 667 | 1,049,425,784 |
|
||||
| 10^12 | 772 | 1,214,627,744 |
|
||||
| 10^20 | 963 | 1,515,137,976 |
|
||||
|
||||
これはv24.2.18と同一。
|
||||
|
||||
標準モードのアイドル補足精密化は1バッチ1,800,000 sample-iterations以下に制限する。
|
||||
|
||||
| zoom | refine iter | pixels/batch | 最悪 sample-iterations |
|
||||
|---:|---:|---:|---:|
|
||||
| 10^6 | 1222 | 1472 | 1,798,784 |
|
||||
| 10^6 | 2444 | 704 | 1,720,576 |
|
||||
| 10^6 | 3000 | 576 | 1,728,000 |
|
||||
| 10^8 | 1334 | 1344 | 1,792,896 |
|
||||
| 10^8 | 2668 | 640 | 1,707,520 |
|
||||
| 10^8 | 3600 | 448 | 1,612,800 |
|
||||
| 10^12 | 1544 | 1152 | 1,778,688 |
|
||||
| 10^12 | 3088 | 576 | 1,778,688 |
|
||||
| 10^12 | 4800 | 320 | 1,536,000 |
|
||||
| 10^20 | 1926 | 896 | 1,725,696 |
|
||||
| 10^20 | 3852 | 448 | 1,725,696 |
|
||||
| 10^20 | 7200 | 192 | 1,382,400 |
|
||||
|
||||
10^8 の最初のrefine batchは一次描画最悪値の約0.171%。1バッチを巨大dispatchにしないことを優先している。
|
||||
|
||||
## 候補量モデル
|
||||
|
||||
代表解像度で境界判定以外の疎seedだけを数えると:
|
||||
|
||||
- stride 4: 約98,648画素
|
||||
- stride 8: 約24,662画素
|
||||
- stride 16: 約6,195画素
|
||||
|
||||
10^8 でこの疎seed全部を3段階処理した場合は約0.220B sample-iterationsで、一次描画最悪値1.049Bの約20.9%。実際には境界候補も加わるが、処理は小batchに分割し、一定時間ごとに休止し、ユーザー操作で中止する。
|
||||
|
||||
## 品質契約
|
||||
|
||||
- 一次画像はv24.2.18と同じ本番シェーダーで生成する。
|
||||
- refinement対象は `FIELD_INTERIOR_LIKELY` のみ。
|
||||
- refinement shaderは実行時にも対象classを再確認する。
|
||||
- `FIELD_ESCAPED` / `FIELD_INTERIOR_PROVEN` は上書きしない。
|
||||
- 高速モードのidle refinementもguarded Deepを使う。
|
||||
- refinement用reference bufferを通常描画の `deepCtx / fastCtx` と分離し、pan reference reuseを壊さない。
|
||||
- 再着色は一次描画の色周期スケールを維持し、既存の外部色を一斉に動かさない。
|
||||
- JavaScript fallbackに疑似等高線を追加しない。
|
||||
|
||||
## 自動テスト
|
||||
|
||||
最終ソースに対して以下を実行し、すべてPASS:
|
||||
|
||||
```text
|
||||
npm test -> RC=0
|
||||
npm run build -> RC=0
|
||||
npm test -> RC=0 (build後再検査)
|
||||
node --check script.js -> PASS
|
||||
node --check gpu-kernels.js -> PASS
|
||||
```
|
||||
|
||||
専用モデル `tests/v24-baseline-idle-refinement-model.mjs` もPASSし、上記ハッシュ、batch上限、readback契約、品質単調性を検査した。
|
||||
|
||||
## 実WebGPU検査の制約
|
||||
|
||||
このコンテナのChromiumではGPUプロセスがEGL/ANGLE初期化に失敗したため、実WebGPUのfps・device lost試験は完了できなかった。観測した主な失敗は `EGL_NOT_INITIALIZED` / `SwANGLE failed` 系である。
|
||||
|
||||
したがって「実機でv24.2.18より何%速い」という主張はしない。保証しているのは一次描画経路の構造的な基準線復元と、idle refinementのbatch仕事量上限である。
|
||||
|
|
@ -1,598 +0,0 @@
|
|||
(()=>{'use strict';
|
||||
const COMMON=String.raw`
|
||||
const FIELD_UNKNOWN:u32=0u;
|
||||
const FIELD_ESCAPED:u32=1u;
|
||||
const FIELD_INTERIOR_LIKELY:u32=2u;
|
||||
const FIELD_INTERIOR_PROVEN:u32=3u;
|
||||
const ITER_MASK:u32=0x000fffffu;
|
||||
const REASON_SHIFT:u32=20u;
|
||||
const REASON_MASK:u32=0x0ff00000u;
|
||||
const REASON_NONE:u32=0u;
|
||||
const REASON_ERROR_BOUND:u32=1u;
|
||||
const REASON_ESCAPE_UNCERTAIN:u32=2u;
|
||||
const REASON_REFERENCE_END:u32=3u;
|
||||
const REASON_REBASE_GAP:u32=4u;
|
||||
const REASON_RANGE:u32=5u;
|
||||
const REASON_OPERATION_LIMIT:u32=6u;
|
||||
|
||||
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
|
||||
fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); }
|
||||
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
|
||||
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
|
||||
}
|
||||
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
|
||||
const F32_U:f32=5.960464477539063e-8;
|
||||
fn pow2_safe(e:i32)->f32 {
|
||||
if(e < -126){ return 0.0; }
|
||||
if(e > 126){ return 8.507059e37; }
|
||||
return ldexp(1.0,e);
|
||||
}
|
||||
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
|
||||
let propagated=abs(errScaled*pow2_safe(scaleExp));
|
||||
let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
|
||||
return propagated+reconstruction;
|
||||
}
|
||||
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
|
||||
if(e < -126){ return vec2<f32>(0.0); }
|
||||
if(e > 126){ return vec2<f32>(8.507059e37); }
|
||||
return ldexp(v,vec2<i32>(e));
|
||||
}
|
||||
fn smooth_escape(n:u32, mag2:f32)->f32{
|
||||
let u=log2(max(4.0000005,mag2));
|
||||
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
|
||||
}
|
||||
`;
|
||||
|
||||
const DIRECT_F32_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
|
||||
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
|
||||
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn analytic(cr:f32,ci:f32)->bool{
|
||||
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
|
||||
let lhs=q*(q+x); let rhs=0.25*y2;
|
||||
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
|
||||
if(lhs<rhs-margin){return true;}
|
||||
let x2=cr+1.0; let bulb=x2*x2+y2;
|
||||
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
|
||||
return bulb<0.0625-bulbMargin;
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
let scale=p.span/f32(p.fullW);
|
||||
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
|
||||
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
|
||||
if(analytic(cr,ci)){
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
|
||||
}
|
||||
var zr=0.0; var zi=0.0; var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
let zr2=zr*zr; let zi2=zi*zi;
|
||||
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
|
||||
let mag=zr*zr+zi*zi;
|
||||
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// Benchmark candidate: whole-frame double-single direct iteration.
|
||||
// This intentionally has no CPU reference-build cost. It is not selected by
|
||||
// the production router until the crossover benchmark justifies it.
|
||||
const DIRECT_DS_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
|
||||
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
|
||||
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
|
||||
invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32,
|
||||
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn ds_quick(a:f32,b:f32)->DS{
|
||||
let q=a+b; let e=b-(q-a); return DS(q,e);
|
||||
}
|
||||
fn ds_sum(a:f32,b:f32)->DS{
|
||||
let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e);
|
||||
}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;
|
||||
let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah;
|
||||
let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh;
|
||||
var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl;
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{
|
||||
let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l));
|
||||
}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);
|
||||
var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l;
|
||||
return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{
|
||||
let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b);
|
||||
}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{
|
||||
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
|
||||
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
|
||||
}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
|
||||
return CDS(rr,ii);
|
||||
}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);
|
||||
let span=DS(p.spanHi,p.spanLo);
|
||||
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
|
||||
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
|
||||
let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy);
|
||||
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
|
||||
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
|
||||
let c=CDS(cr,ci);
|
||||
var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
z=cds_add(cds_mul(z,z),c); n+=1u;
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
|
||||
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
|
||||
// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels
|
||||
// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and
|
||||
// can be re-evaluated by the existing deep double-single correction pass.
|
||||
// This shader is benchmark-only and is not selected by the production router.
|
||||
const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, _flags:u32,
|
||||
centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32,
|
||||
spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32,
|
||||
invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32,
|
||||
_p2:f32, _p3:f32, _p4:f32, _p5:f32,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
struct HybridStats{
|
||||
remaining:atomic<u32>, _a:atomic<u32>, _b:atomic<u32>, _c:atomic<u32>,
|
||||
_d:atomic<u32>, _e:atomic<u32>, _f:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(3) var<storage,read_write> hybridStats:HybridStats;
|
||||
fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);}
|
||||
fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;
|
||||
let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh;
|
||||
var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{if(a.h<b.h){return -1;}if(a.h>b.h){return 1;}if(a.l<b.l){return -1;}if(a.l>b.l){return 1;}return 0;}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii);
|
||||
}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
fn mark_sensitive(out:u32,n:u32){
|
||||
fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;atomicAdd(&hybridStats.remaining,1u);
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=gid.y*p.tileW+gid.x;
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo);
|
||||
let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW);
|
||||
let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY);
|
||||
let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy);
|
||||
let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx));
|
||||
let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy));
|
||||
let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var deriv=vec2<f32>(0.0);var n=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){break;}
|
||||
let zv=vec2<f32>(ds_value(z.r),ds_value(z.i));
|
||||
deriv=2.0*cmul(zv,deriv)+vec2<f32>(1.0,0.0);
|
||||
if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;}
|
||||
z=cds_add(cds_mul(z,z),c);n+=1u;
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);
|
||||
fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;
|
||||
}
|
||||
}
|
||||
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;
|
||||
}
|
||||
`;
|
||||
|
||||
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
|
||||
const DEEP_PERTURB_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
|
||||
struct UnresolvedHead{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
|
||||
|
||||
fn mark_unresolved(out:u32,n:u32,reason:u32){
|
||||
fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;
|
||||
atomicAdd(&unresolved.remaining,1u);
|
||||
if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}
|
||||
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}
|
||||
else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}
|
||||
else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}
|
||||
else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}
|
||||
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}
|
||||
}
|
||||
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
||||
let dx=(gx-p.refPixelX)/f32(p.fullW);
|
||||
let dy=(p.refPixelY-gy)/f32(p.fullW);
|
||||
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
|
||||
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
|
||||
var w=vec2<f32>(0.0);
|
||||
var scaleExp=p.spanExp;
|
||||
var n=0u; var m=0u; var operations=0u;
|
||||
var errScaled=1.0*F32_U*maxabs(d);
|
||||
loop{
|
||||
if(n>=p.maxIter){
|
||||
let rpEnd=refs[min(m,p.refLen)];
|
||||
let deltaEnd=scaled_to_f32(w,scaleExp);
|
||||
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
|
||||
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
|
||||
let limit=select(1.0e-3,1.0e-4,strictMode);
|
||||
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);}
|
||||
return;
|
||||
}
|
||||
if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let rp=refs[m];
|
||||
let delta=scaled_to_f32(w,scaleExp);
|
||||
let z=rp.hi+(rp.lo+delta);
|
||||
let mag=dot(z,z);
|
||||
if(mag>4.0){
|
||||
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
|
||||
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
|
||||
mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return;
|
||||
}
|
||||
|
||||
// Rebase only when dc remains numerically representable in the new scale.
|
||||
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
|
||||
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n,REASON_REBASE_GAP);return;}
|
||||
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
|
||||
w=z; d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u;
|
||||
errScaled+=1.0*F32_U*maxabs(d);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let r=refs[m];
|
||||
let refAbs=maxabs(r.hi)+maxabs(r.lo);
|
||||
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
|
||||
let gain=2.0*refAbs+2.0*wAbs*p2;
|
||||
let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
|
||||
errScaled=gain*errScaled+roundErr;
|
||||
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
|
||||
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
|
||||
let sq=cmul(w,w)*pow2_safe(scaleExp);
|
||||
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
|
||||
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;}
|
||||
let mm=max(maxabs(w),maxabs(d));
|
||||
if(mm>65536.0){
|
||||
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
|
||||
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
|
||||
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
|
||||
}
|
||||
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;}
|
||||
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;}
|
||||
}
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
render_pixel(out,gx,gy,p.strict!=0u);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single
|
||||
// perturbation. It is a visual-quality pass, not a membership certificate.
|
||||
const DEEP_CORRECT_WGSL=COMMON+String.raw`
|
||||
const CORRECTION_MARK:u32=128u;
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
|
||||
struct CorrectionStats{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
struct DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
|
||||
|
||||
fn count_remaining(out:u32){
|
||||
let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu;
|
||||
atomicAdd(&stats.remaining,1u);
|
||||
if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);}
|
||||
else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);}
|
||||
else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);}
|
||||
else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);}
|
||||
else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);}
|
||||
else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);}
|
||||
}
|
||||
fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){
|
||||
fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u);
|
||||
}
|
||||
|
||||
fn ds_quick(a:f32,b:f32)->DS{
|
||||
let q=a+b;
|
||||
let e=b-(q-a);
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_sum(a:f32,b:f32)->DS{
|
||||
let q=a+b;
|
||||
let bb=q-a;
|
||||
let e=(a-(q-bb))+(b-bb);
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_prod(a:f32,b:f32)->DS{
|
||||
let q=a*b;
|
||||
let ca=4097.0*a;
|
||||
let ah=ca-(ca-a);
|
||||
let al=a-ah;
|
||||
let cb=4097.0*b;
|
||||
let bh=cb-(cb-b);
|
||||
let bl=b-bh;
|
||||
var e=ah*bh-q;
|
||||
e=e+ah*bl;
|
||||
e=e+al*bh;
|
||||
e=e+al*bl;
|
||||
return DS(q,e);
|
||||
}
|
||||
fn ds_add(a:DS,b:DS)->DS{
|
||||
let q=ds_sum(a.h,b.h);
|
||||
return ds_quick(q.h,q.l+(a.l+b.l));
|
||||
}
|
||||
fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);}
|
||||
fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));}
|
||||
fn ds_mul(a:DS,b:DS)->DS{
|
||||
let q=ds_prod(a.h,b.h);
|
||||
var e=q.l+a.h*b.l;
|
||||
e=e+a.l*b.h;
|
||||
e=e+a.l*b.l;
|
||||
return ds_quick(q.h,e);
|
||||
}
|
||||
fn ds_scale(a:DS,b:f32)->DS{
|
||||
let q=ds_prod(a.h,b);
|
||||
return ds_quick(q.h,q.l+a.l*b);
|
||||
}
|
||||
fn ds_pow2(a:DS,e:i32)->DS{
|
||||
if(e < -126){return DS(0.0,0.0);}
|
||||
if(e > 126){return DS(8.507059e37,0.0);}
|
||||
return DS(ldexp(a.h,e),ldexp(a.l,e));
|
||||
}
|
||||
fn ds_cmp(a:DS,b:DS)->i32{
|
||||
if(a.h<b.h){return -1;} if(a.h>b.h){return 1;}
|
||||
if(a.l<b.l){return -1;} if(a.l>b.l){return 1;} return 0;
|
||||
}
|
||||
fn ds_value(a:DS)->f32{return a.h+a.l;}
|
||||
fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));}
|
||||
fn cds_mul(a:CDS,b:CDS)->CDS{
|
||||
let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i));
|
||||
let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));
|
||||
return CDS(rr,ii);
|
||||
}
|
||||
fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));}
|
||||
fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));}
|
||||
fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));}
|
||||
fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));}
|
||||
fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<<REASON_SHIFT);}
|
||||
|
||||
fn correct_pixel(out:u32,gx:f32,gy:f32){
|
||||
let invW=DS(p.invFullWHi,p.invFullWLo);
|
||||
let sm=DS(p.spanMantHi,p.spanMantLo);
|
||||
let ox=gx-p.refPixelX;
|
||||
let oy=p.refPixelY-gy;
|
||||
let dx=ds_scale(invW,ox);
|
||||
let dy=ds_scale(invW,oy);
|
||||
let d0=CDS(ds_mul(sm,dx),ds_mul(sm,dy));
|
||||
var d=d0;
|
||||
var w=CDS(DS(0.0,0.0),DS(0.0,0.0));
|
||||
var scaleExp=p.spanExp;
|
||||
var n=0u; var m=0u; var operations=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;}
|
||||
if(m>p.refLen){count_remaining(out);return;}
|
||||
let rp=refs[m];
|
||||
let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y));
|
||||
let delta=cds_pow2(w,scaleExp);
|
||||
let z=cds_add(r,delta);
|
||||
let mag=cds_mag2(z);
|
||||
if(ds_cmp(mag,DS(4.0,0.0))>0){
|
||||
accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag))));
|
||||
return;
|
||||
}
|
||||
let dmag=cds_mag2(delta);
|
||||
if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){
|
||||
if(p.spanExp-scaleExp < -96){count_remaining(out);return;}
|
||||
w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u;
|
||||
continue;
|
||||
}
|
||||
if(m>=p.refLen){count_remaining(out);return;}
|
||||
let linear=cds_scale(cds_mul(r,w),2.0);
|
||||
let sq=cds_pow2(cds_mul(w,w),scaleExp);
|
||||
w=cds_add(cds_add(linear,sq),d);
|
||||
m+=1u; n+=1u; operations+=1u;
|
||||
let mm=max(cds_maxabs(w),cds_maxabs(d));
|
||||
if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;}
|
||||
if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;}
|
||||
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;}
|
||||
if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;}
|
||||
}
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
correct_pixel(out,gx,gy);
|
||||
}
|
||||
`;
|
||||
|
||||
const COLOR_WGSL=String.raw`
|
||||
struct Params{
|
||||
width:u32,height:u32,palette:u32,edgeAA:u32,
|
||||
cycle:f32,shift:f32,_p0:f32,_p1:f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
|
||||
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
|
||||
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
|
||||
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
|
||||
}
|
||||
fn current_palette(t0:f32)->vec3<f32>{
|
||||
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
|
||||
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
|
||||
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
|
||||
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
|
||||
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
|
||||
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
|
||||
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
|
||||
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
|
||||
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
|
||||
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
|
||||
}
|
||||
fn base_color(i:u32)->vec3<f32>{
|
||||
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
|
||||
if(cls==0u){return vec3<f32>(43,20,58)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
|
||||
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
|
||||
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
|
||||
let n=f32(m&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
|
||||
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
|
||||
}
|
||||
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
|
||||
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
|
||||
if(p.edgeAA!=0u){
|
||||
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
|
||||
let x=i32(gid.x); let y=i32(gid.y);
|
||||
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
|
||||
if(boundary){c=delinearize(sum/cnt);}
|
||||
}
|
||||
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
const AA_RESOLVE_WGSL=String.raw`
|
||||
@group(0) @binding(0) var a:texture_2d<f32>;
|
||||
@group(0) @binding(1) var b:texture_2d<f32>;
|
||||
@group(0) @binding(2) var c:texture_2d<f32>;
|
||||
@group(0) @binding(3) var d:texture_2d<f32>;
|
||||
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
|
||||
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
|
||||
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
|
||||
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
|
||||
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
|
||||
@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
|
||||
let q=vec2<i32>(gid.xy);
|
||||
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
|
||||
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
|
||||
}
|
||||
`;
|
||||
|
||||
const PRESENT_WGSL=String.raw`
|
||||
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
|
||||
@group(0) @binding(0) var samp:sampler;
|
||||
@group(0) @binding(1) var tex:texture_2d<f32>;
|
||||
@group(0) @binding(2) var<uniform> p:Params;
|
||||
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
|
||||
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
|
||||
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
|
||||
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
|
||||
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
|
||||
}
|
||||
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
|
||||
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
|
||||
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
|
||||
}
|
||||
`;
|
||||
|
||||
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
|
||||
version:'24.2.6-hybrid-ui-cacheproof',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
|
||||
});
|
||||
})();
|
||||
704
gpu-kernels.js
704
gpu-kernels.js
|
|
@ -87,362 +87,77 @@ 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`
|
||||
// Fast-mode precision extension. This keeps the interactive path independent
|
||||
// of the verified Deep backend: a reduced-precision reference orbit is built
|
||||
// quickly on the CPU worker and this lightweight perturbation kernel preserves
|
||||
// sub-f32 pixel offsets once absolute Direct coordinates begin to collapse.
|
||||
const FAST_PERTURB_WGSL=COMMON+String.raw`
|
||||
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,
|
||||
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 DS{ h:f32, l:f32 };
|
||||
struct CDS{ r:DS, i:DS };
|
||||
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn ds_quick(a:f32,b:f32)->DS{
|
||||
let q=a+b; let e=b-(q-a); return DS(q,e);
|
||||
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
|
||||
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
@group(0) @binding(4) var<storage,read_write> unresolved:array<atomic<u32>>;
|
||||
fn mark_fast_unresolved(out:u32,n:u32,reason:u32){
|
||||
fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u);
|
||||
if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);}
|
||||
}
|
||||
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;
|
||||
|
||||
fn render_fast(out:u32,gx:f32,gy:f32){
|
||||
let dx=(gx-p.refPixelX)/f32(p.fullW);
|
||||
let dy=(p.refPixelY-gy)/f32(p.fullW);
|
||||
var d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
|
||||
var w=vec2<f32>(0.0);
|
||||
var scaleExp=p.spanExp;
|
||||
var n=0u; var m=0u;
|
||||
loop{
|
||||
if(n>=p.maxIter){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;
|
||||
}
|
||||
`;
|
||||
if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;}
|
||||
let rp=refs[min(m,p.refLen)];
|
||||
let delta=scaled_to_f32(w,scaleExp);
|
||||
let z=rp.hi+(rp.lo+delta);
|
||||
let mag=dot(z,z);
|
||||
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
// Rebase the perturbation when it grows beyond the reference value. This
|
||||
// keeps the fast path usable across pans without any verified correction.
|
||||
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
|
||||
if(p.spanExp < -126){mark_fast_unresolved(out,n,REASON_REBASE_GAP);return;}
|
||||
w=z;
|
||||
d=scaled_to_f32(vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp);
|
||||
scaleExp=0; m=0u;
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let r=rp;
|
||||
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
|
||||
let sq=cmul(w,w)*pow2_safe(scaleExp);
|
||||
w=linear+sq+d; m+=1u; n+=1u;
|
||||
let mm=max(maxabs(w),maxabs(d));
|
||||
if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;}
|
||||
if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;}
|
||||
else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;}
|
||||
if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;}
|
||||
}
|
||||
}
|
||||
@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;
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
render_fast(out,gx,gy);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
// 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,
|
||||
|
|
@ -480,7 +195,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
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 n=0u; var m=0u;
|
||||
var errScaled=1.0*F32_U*maxabs(d);
|
||||
loop{
|
||||
if(n>=p.maxIter){
|
||||
|
|
@ -492,7 +207,6 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
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);
|
||||
|
|
@ -513,7 +227,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
}
|
||||
|
||||
if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;}
|
||||
let r=refs[m];
|
||||
let r=rp;
|
||||
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;
|
||||
|
|
@ -522,7 +236,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
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;
|
||||
w=linear+sq+d; m+=1u; n+=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){
|
||||
|
|
@ -531,7 +245,6 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
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)
|
||||
|
|
@ -545,6 +258,182 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
|||
`;
|
||||
|
||||
|
||||
// Idle refinement is compiled as a separate guarded-Deep module so the
|
||||
// production Fast/Deep main shaders remain byte-for-byte identical to v24.2.18.
|
||||
const DEEP_LIKELY_REFINE_WGSL=DEEP_PERTURB_WGSL.replace(
|
||||
'@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;',
|
||||
'@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var<storage,read> refineQueue:array<u32>;\n@group(1) @binding(1) var<uniform> refineBatch:RefineBatch;'
|
||||
)+String.raw`
|
||||
@compute @workgroup_size(64)
|
||||
fn refine_likely(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
let slot=refineBatch.base+gid.x;
|
||||
if(slot>=refineBatch.end){return;}
|
||||
let out=refineQueue[slot];
|
||||
if(((fieldMeta[out]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}
|
||||
let x=out%p.fullW; let y=out/p.fullW;
|
||||
render_pixel(out,f32(x)+p.sampleX,f32(y)+p.sampleY,p.strict!=0u);
|
||||
}
|
||||
`;
|
||||
|
||||
// Production Deep sparse path: keep the long-running perturbation kernel
|
||||
// untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets.
|
||||
// Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS
|
||||
// correction kernel sees less workgroup-level iteration divergence.
|
||||
//
|
||||
// Bucket state layout (24 u32 atomics):
|
||||
// [0..7] histogram counts
|
||||
// [8..15] scatter cursors (initialized from prefix offsets)
|
||||
// [16..23] immutable prefix offsets for queue accounting
|
||||
const DEEP_BUCKET_HIST_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
|
||||
var<workgroup> localCounts:array<atomic<u32>,8>;
|
||||
fn deep_bucket(packed:u32)->u32{
|
||||
let n=packed&ITER_MASK;
|
||||
return min(7u,(n*8u)/max(1u,p.maxIter));
|
||||
}
|
||||
@compute @workgroup_size(64)
|
||||
fn main(
|
||||
@builtin(local_invocation_id) lid3:vec3<u32>,
|
||||
@builtin(workgroup_id) wid:vec3<u32>
|
||||
){
|
||||
let lane=lid3.x;
|
||||
if(lane<8u){atomicStore(&localCounts[lane],0u);}
|
||||
workgroupBarrier();
|
||||
let lx=wid.x*64u+lane;
|
||||
let ly=wid.y;
|
||||
if(lx<p.tileW && ly<p.tileH){
|
||||
let out=p.outputBase+ly*p.outputStride+lx;
|
||||
let packed=fieldMeta[out];
|
||||
if(((packed>>28u)&3u)==FIELD_UNKNOWN){
|
||||
atomicAdd(&localCounts[deep_bucket(packed)],1u);
|
||||
}
|
||||
}
|
||||
workgroupBarrier();
|
||||
if(lane<8u){
|
||||
let c=atomicLoad(&localCounts[lane]);
|
||||
if(c>0u){atomicAdd(&bucketState[lane],c);}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Convert the 8-bin histogram into contiguous queue ranges, initialize each
|
||||
// scatter cursor, and produce the correction indirect-dispatch arguments.
|
||||
const DEEP_BUCKET_PREFIX_WGSL=String.raw`
|
||||
struct SparseQueueStats{
|
||||
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
|
||||
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
|
||||
};
|
||||
struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 };
|
||||
@group(0) @binding(0) var<storage,read_write> bucketState:array<atomic<u32>>;
|
||||
@group(0) @binding(1) var<storage,read_write> sparseQueueStats:SparseQueueStats;
|
||||
@group(0) @binding(2) var<storage,read_write> indirectArgs:IndirectArgs;
|
||||
@compute @workgroup_size(1)
|
||||
fn main(){
|
||||
var total=0u;
|
||||
var b=0u;
|
||||
loop{
|
||||
if(b>=8u){break;}
|
||||
let c=atomicLoad(&bucketState[b]);
|
||||
atomicStore(&bucketState[8u+b],total);
|
||||
atomicStore(&bucketState[16u+b],total);
|
||||
total+=c;
|
||||
b+=1u;
|
||||
}
|
||||
atomicStore(&sparseQueueStats.selected,total);
|
||||
atomicStore(&sparseQueueStats.overflow,0u);
|
||||
atomicStore(&sparseQueueStats.enqueued,total);
|
||||
atomicStore(&sparseQueueStats.dispatchCount,total);
|
||||
indirectArgs.x=(total+63u)/64u;
|
||||
indirectArgs.y=1u;
|
||||
indirectArgs.z=1u;
|
||||
indirectArgs._pad=0u;
|
||||
}
|
||||
`;
|
||||
|
||||
// Scatter UNKNOWN indices into the precomputed bucket ranges. Within each
|
||||
// 64-lane workgroup, workgroup-memory atomics allocate local ranks; each
|
||||
// non-empty bucket reserves one global subrange, so global atomics scale with
|
||||
// non-empty (workgroup,bucket) pairs rather than with UNKNOWN pixels.
|
||||
const DEEP_BUCKET_SCATTER_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
|
||||
strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32,
|
||||
spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32,
|
||||
refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32,
|
||||
invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32,
|
||||
};
|
||||
struct SparseQueueStats{
|
||||
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
|
||||
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
|
||||
};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;
|
||||
@group(0) @binding(3) var<storage,read_write> sparseQueueStats:SparseQueueStats;
|
||||
@group(0) @binding(4) var<storage,read_write> unknownQueue:array<u32>;
|
||||
var<workgroup> localCounts:array<atomic<u32>,8>;
|
||||
var<workgroup> localRanks:array<atomic<u32>,8>;
|
||||
var<workgroup> groupBase:array<u32,8>;
|
||||
fn deep_bucket(packed:u32)->u32{
|
||||
let n=packed&ITER_MASK;
|
||||
return min(7u,(n*8u)/max(1u,p.maxIter));
|
||||
}
|
||||
@compute @workgroup_size(64)
|
||||
fn main(
|
||||
@builtin(local_invocation_id) lid3:vec3<u32>,
|
||||
@builtin(workgroup_id) wid:vec3<u32>
|
||||
){
|
||||
let lane=lid3.x;
|
||||
if(lane<8u){
|
||||
atomicStore(&localCounts[lane],0u);
|
||||
atomicStore(&localRanks[lane],0u);
|
||||
groupBase[lane]=0u;
|
||||
}
|
||||
workgroupBarrier();
|
||||
|
||||
let lx=wid.x*64u+lane;
|
||||
let ly=wid.y;
|
||||
var out=0u;
|
||||
var hit=0u;
|
||||
var bucket=0u;
|
||||
if(lx<p.tileW && ly<p.tileH){
|
||||
out=p.outputBase+ly*p.outputStride+lx;
|
||||
let packed=fieldMeta[out];
|
||||
if(((packed>>28u)&3u)==FIELD_UNKNOWN){
|
||||
hit=1u;
|
||||
bucket=deep_bucket(packed);
|
||||
atomicAdd(&localCounts[bucket],1u);
|
||||
}
|
||||
}
|
||||
workgroupBarrier();
|
||||
|
||||
if(lane<8u){
|
||||
let c=atomicLoad(&localCounts[lane]);
|
||||
if(c>0u){groupBase[lane]=atomicAdd(&bucketState[8u+lane],c);}
|
||||
}
|
||||
workgroupBarrier();
|
||||
|
||||
if(hit!=0u){
|
||||
let rank=atomicAdd(&localRanks[bucket],1u);
|
||||
let qi=groupBase[bucket]+rank;
|
||||
let capacity=p.tileW*p.tileH;
|
||||
if(qi<capacity){unknownQueue[qi]=out;}
|
||||
else{atomicAdd(&sparseQueueStats.overflow,1u);}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// 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`
|
||||
|
|
@ -659,10 +548,9 @@ fn correct_pixel(out:u32,gx:f32,gy:f32){
|
|||
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;
|
||||
var n=0u; var m=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);
|
||||
|
|
@ -682,12 +570,12 @@ fn correct_pixel(out:u32,gx:f32,gy:f32){
|
|||
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;
|
||||
m+=1u; n+=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;}
|
||||
if(scaleExp>126){count_remaining(out);return;}
|
||||
}
|
||||
}
|
||||
@compute @workgroup_size(8,8)
|
||||
|
|
@ -701,7 +589,76 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
|||
}
|
||||
`;
|
||||
|
||||
|
||||
// Production sparse correction variant. It shares the complete DS arithmetic
|
||||
// with DEEP_CORRECT_WGSL, but consumes only indices emitted by the queued deep
|
||||
// perturbation pass and launches via dispatchWorkgroupsIndirect.
|
||||
const DEEP_CORRECT_QUEUE_WGSL=DEEP_CORRECT_WGSL
|
||||
.replace(
|
||||
`struct CorrectionStats{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};`,
|
||||
`struct CorrectionStats{
|
||||
remaining:atomic<u32>, errorBound:atomic<u32>, escapeUncertain:atomic<u32>, referenceEnd:atomic<u32>,
|
||||
rebaseGap:atomic<u32>, rangeFailure:atomic<u32>, operationLimit:atomic<u32>, corrected:atomic<u32>,
|
||||
};
|
||||
struct SparseQueueStats{
|
||||
selected:atomic<u32>, overflow:atomic<u32>, enqueued:atomic<u32>, dispatchCount:atomic<u32>,
|
||||
invalidIndex:atomic<u32>, staleEntry:atomic<u32>, processed:atomic<u32>, _reserved:atomic<u32>,
|
||||
};`)
|
||||
.replace(
|
||||
`@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;`,
|
||||
`@group(0) @binding(4) var<storage,read_write> stats:CorrectionStats;
|
||||
@group(0) @binding(5) var<storage,read_write> sparseQueueStats:SparseQueueStats;
|
||||
@group(0) @binding(6) var<storage,read> unknownQueue:array<u32>;`)
|
||||
.replace(
|
||||
`@compute @workgroup_size(8,8)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
correct_pixel(out,gx,gy);
|
||||
}`,
|
||||
`@compute @workgroup_size(64)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
let qi=gid.x;
|
||||
let queued=atomicLoad(&sparseQueueStats.dispatchCount);
|
||||
if(qi>=queued){return;}
|
||||
let out=unknownQueue[qi];
|
||||
atomicAdd(&sparseQueueStats.processed,1u);
|
||||
if(out<p.outputBase){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;}
|
||||
let local=out-p.outputBase;
|
||||
let ly=local/p.outputStride;
|
||||
let lx=local-ly*p.outputStride;
|
||||
if(lx>=p.tileW||ly>=p.tileH){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;}
|
||||
if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;}
|
||||
let gx=f32(p.tileX+lx)+p.sampleX;
|
||||
let gy=f32(p.tileY+ly)+p.sampleY;
|
||||
correct_pixel(out,gx,gy);
|
||||
}`);
|
||||
|
||||
if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)')){
|
||||
throw new Error('failed to derive queued deep correction shader');
|
||||
}
|
||||
|
||||
const LIKELY_QUEUE_WGSL=COMMON+String.raw`
|
||||
struct QueueParams{width:u32,height:u32,mode:u32,sparseStride:u32};
|
||||
struct QueueStats{count:atomic<u32>,overflow:atomic<u32>,_p0:atomic<u32>,_p1:atomic<u32>};
|
||||
@group(0) @binding(0) var<uniform> p:QueueParams;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> queue:array<u32>;
|
||||
@group(0) @binding(3) var<storage,read_write> stats:QueueStats;
|
||||
fn is_frontier(x:u32,y:u32,r:i32)->bool{let xi=i32(x);let yi=i32(y);var oy=-1;loop{if(oy>1){break;}var ox=-1;loop{if(ox>1){break;}if(ox!=0||oy!=0){let xx=xi+ox*r;let yy=yi+oy*r;if(xx>=0&&yy>=0&&xx<i32(p.width)&&yy<i32(p.height)){let cls=(fieldMeta[u32(yy)*p.width+u32(xx)]>>28u)&3u;if(cls==FIELD_ESCAPED||cls==FIELD_UNKNOWN){return true;}}}ox+=1;}oy+=1;}return false;}
|
||||
@compute @workgroup_size(256)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){let i=gid.x;let total=p.width*p.height;if(i>=total){return;}if(((fieldMeta[i]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}let x=i%p.width;let y=i/p.width;var selected=false;if(p.mode==0u){selected=true;}else{let radius:i32=select(2,select(3,4,p.mode>=3u),p.mode>=2u);selected=is_frontier(x,y,radius);if(!selected&&p.sparseStride>0u){selected=(x%p.sparseStride==0u)&&(y%p.sparseStride==0u);}}if(!selected){return;}let slot=atomicAdd(&stats.count,1u);if(slot<total){queue[slot]=i;}else{atomicAdd(&stats.overflow,1u);}}
|
||||
`;
|
||||
|
||||
const COLOR_WGSL=String.raw`
|
||||
const FIELD_UNKNOWN:u32=0u;
|
||||
const FIELD_ESCAPED:u32=1u;
|
||||
struct Params{
|
||||
width:u32,height:u32,palette:u32,edgeAA:u32,
|
||||
cycle:f32,shift:f32,_p0:f32,_p1:f32,
|
||||
|
|
@ -726,28 +683,36 @@ fn current_palette(t0:f32)->vec3<f32>{
|
|||
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 palette_color(phase:f32)->vec3<f32>{
|
||||
if(p.palette==1u){return hsv(phase,0.92,1.0);}if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3<f32>(g);}return current_palette(phase);
|
||||
}
|
||||
fn escaped_color(m:u32,sm:f32)->vec3<f32>{
|
||||
let phase=fract(p.shift+sm*p.cycle);let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.34+0.66*pow(edge,0.38);let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv);
|
||||
}
|
||||
fn provisional_unknown(m:u32)->vec3<f32>{
|
||||
let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3<f32>(6,10,22)/255.0,vec3<f32>(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52);
|
||||
}
|
||||
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);
|
||||
let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3<f32>(0.0);}return escaped_color(m,fieldSmooth[i]);
|
||||
}
|
||||
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(gid.x>=p.width||gid.y>=p.height){return;}let i=gid.y*p.width+gid.x;let m=fieldMeta[i];let cls=(m>>28u)&3u;var c=base_color(i);let x=i32(gid.x);let y=i32(gid.y);
|
||||
if(cls==FIELD_UNKNOWN){
|
||||
var fill=vec3<f32>(0.0);var fillCount=0.0;
|
||||
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];if(((mj>>28u)&3u)!=FIELD_UNKNOWN){fill+=linearize(base_color(j));fillCount+=1.0;}}}
|
||||
if(fillCount>0.0){c=delinearize(fill/fillCount);}
|
||||
}
|
||||
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);
|
||||
var boundary=false;var sum=linearize(c);var cnt=1.0;
|
||||
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
|
||||
if(boundary){c=delinearize(sum/cnt);}
|
||||
}
|
||||
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
|
||||
}
|
||||
`;
|
||||
`
|
||||
|
||||
|
||||
const AA_RESOLVE_WGSL=String.raw`
|
||||
|
|
@ -786,7 +751,28 @@ struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
|
|||
}
|
||||
`;
|
||||
|
||||
|
||||
// v24.2.14 production screen path: defer UNKNOWN statistics out of the long
|
||||
// perturbation kernel. The existing bucket histogram pass already scans the
|
||||
// same field before sparse correction, so it can aggregate remaining/reason
|
||||
// counters per workgroup without per-UNKNOWN global atomics in the primary.
|
||||
// The counted primary is retained for export and verification paths.
|
||||
const DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u);\n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}');
|
||||
|
||||
const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL
|
||||
.replace('@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;', '@group(0) @binding(2) var<storage,read_write> bucketState:array<atomic<u32>>;\n@group(0) @binding(3) var<storage,read_write> unresolvedStats:array<atomic<u32>>;')
|
||||
.replace('var<workgroup> localCounts:array<atomic<u32>,8>;', 'var<workgroup> localCounts:array<atomic<u32>,8>;\nvar<workgroup> localReasons:array<atomic<u32>,8>;')
|
||||
.replace('if(lane<8u){atomicStore(&localCounts[lane],0u);}', 'if(lane<8u){atomicStore(&localCounts[lane],0u);atomicStore(&localReasons[lane],0u);}')
|
||||
.replace('atomicAdd(&localCounts[deep_bucket(packed)],1u);', `atomicAdd(&localCounts[deep_bucket(packed)],1u);
|
||||
atomicAdd(&localReasons[0],1u);
|
||||
let reason=(packed&REASON_MASK)>>REASON_SHIFT;
|
||||
if(reason>=1u && reason<=6u){atomicAdd(&localReasons[reason],1u);}`)
|
||||
.replace('if(c>0u){atomicAdd(&bucketState[lane],c);}', `if(c>0u){atomicAdd(&bucketState[lane],c);}
|
||||
let r=atomicLoad(&localReasons[lane]);
|
||||
if(r>0u){atomicAdd(&unresolvedStats[lane],r);}`);
|
||||
if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('v24.2.14 post-stats shader derivation failed');
|
||||
|
||||
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
|
||||
version:'24.2.8-fixed96-sparse-queue',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DIRECT_FIXED96_QUEUE_ARGS_WGSL,DIRECT_FIXED96_CORRECT_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
|
||||
version:'24.2.26-baseline-idle-refinement',DIRECT_F32_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_LIKELY_REFINE_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,LIKELY_QUEUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
/*
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
X-Content-Type-Options: nosniff
|
||||
|
||||
/wasm/*
|
||||
Content-Type: application/wasm
|
||||
Cache-Control: public, max-age=31536000, immutable
|
||||
|
||||
/hosted/*
|
||||
Cache-Control: no-cache
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
const wasmRoot = new URL('../wasm/', import.meta.url);
|
||||
|
||||
async function compileAsset(name) {
|
||||
const url = new URL(name, wasmRoot);
|
||||
const response = await fetch(url, { cache: 'force-cache' });
|
||||
if (!response.ok) throw new Error(`WASM fetch failed: ${name} (${response.status})`);
|
||||
if (WebAssembly.compileStreaming) {
|
||||
try { return await WebAssembly.compileStreaming(Promise.resolve(response.clone())); }
|
||||
catch { /* A proxy may have supplied the wrong MIME type; use bytes below. */ }
|
||||
}
|
||||
return WebAssembly.compile(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
let shallowModule, shallowSimd = true;
|
||||
try { shallowModule = await compileAsset('wasm-simd.wasm'); }
|
||||
catch { shallowModule = await compileAsset('wasm-scalar.wasm'); shallowSimd = false; }
|
||||
const asset = name => new URL(name, wasmRoot).href;
|
||||
|
||||
globalThis.MANDEL_KERNELS = Object.freeze({
|
||||
// The compiled module is structured-cloned to the shallow Worker. No Base64
|
||||
// conversion or second compile is needed in the hosted build.
|
||||
WASM_SIMD_B64: shallowModule,
|
||||
WASM_SCALAR_B64: shallowModule,
|
||||
DEEP_SIMD_B64: asset('deep-simd.wasm'),
|
||||
DEEP_SCALAR_B64: asset('deep-scalar.wasm'),
|
||||
BLA_SIMD_B64: asset('bla-simd.wasm'),
|
||||
BLA_SCALAR_B64: asset('bla-scalar.wasm'),
|
||||
COLOR_SIMD_B64: asset('color-simd.wasm'),
|
||||
COLOR_SCALAR_B64: asset('color-scalar.wasm')
|
||||
});
|
||||
globalThis.MANDEL_KERNEL_META = Object.freeze(/*__KERNEL_META__*/{});
|
||||
globalThis.MANDEL_HOSTED_SHALLOW_SIMD = shallowSimd;
|
||||
|
||||
const app = document.createElement('script');
|
||||
app.src = './script.js';
|
||||
document.body.appendChild(app);
|
||||
|
|
@ -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.2.8 Fixed96 Sparse Queue Experiment</title>
|
||||
<title>Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement</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}
|
||||
|
|
@ -12,15 +12,13 @@
|
|||
.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}
|
||||
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));max-height:calc(100dvh - 20px);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
|
||||
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.18);background:#000;color:#fff;min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}select option{background:#000;color:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
|
||||
.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}
|
||||
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
|
||||
.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)}
|
||||
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
|
||||
.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}}
|
||||
|
|
@ -34,20 +32,15 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
|
|||
<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="renderMode">描画方式</label><select id="renderMode"><option value="fast" selected>高速</option><option value="accurate">正確</option></select><output></output></div>
|
||||
<div class="line"><label for="processMode">負荷設定</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
|
||||
<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="cycle">色周期</label><input id="cycle" type="range" min="0" max="1000" step="1" value="532" aria-label="色周期(対数スケール)"><output id="cycleO" for="cycle">0.0080</output></div>
|
||||
<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 class="mini-actions"><button id="colorAuto" type="button" aria-pressed="false">色を自動変化</button></div>
|
||||
</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 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>
|
||||
|
|
@ -63,30 +56,6 @@ 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>
|
||||
|
|
|
|||
1156
index.html
1156
index.html
File diff suppressed because it is too large
Load diff
22
kernels.js
22
kernels.js
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue