w
This commit is contained in:
parent
03ddaa09f2
commit
caa90775f9
89 changed files with 7126 additions and 0 deletions
206
AUDIT.md
Normal file
206
AUDIT.md
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
# 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で必要なときだけ有効化する方がよい。
|
||||||
48
BUILD_REPRODUCIBILITY.md
Normal file
48
BUILD_REPRODUCIBILITY.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 信頼境界
|
||||||
|
|
||||||
|
公式配布物のSHA-256を照合したclang 17.0.6とNode.js v22.18.0を作業領域内で使用し、
|
||||||
|
復元sourceのcompile、generated-WASM golden、SIMD/scalar同値性、画素中心契約を実行済みです。
|
||||||
|
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
|
||||||
|
```
|
||||||
53
COMPLETION_AUDIT.md
Normal file
53
COMPLETION_AUDIT.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Mandelbrot Deep Zoom v23 完了監査
|
||||||
|
|
||||||
|
## 判定
|
||||||
|
|
||||||
|
`IMPROVEMENT_PROPOSAL.md`のうち、計測結果に依存しないPhase 0〜3Aの実装項目はコード、build、非ブラウザ試験へ反映した。source/ABI/数値/pixel mapping/WASM Module clone/配布物のgateは合格している。
|
||||||
|
|
||||||
|
ただし、固定実機を必要とする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 runtime budget | pass。shallow約0.34〜0.91秒/MP、seahorse deep BLA約19.6〜36.6秒/MPのrunを観測し、deep標準を固定画素数から実測時間適応へ変更 |
|
||||||
|
| 全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`に記録した。
|
||||||
|
|
||||||
|
## 条件付き項目
|
||||||
|
|
||||||
|
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の実viewport・DPRで実行する。runnerはsceneごとのcanvas SHA-256、Preview/Covered/Refined、idle write、long task、managed memory、deep request、wheel settle、再着色、Export完了/取消、accessibilityを採取し、`acceptance`を出力する。合格JSONを`audit/v23-browser-baseline.json`へ保存した時点で、固定実機に対する最終受入を判定できる。
|
||||||
89
IMPLEMENTATION_REPORT.md
Normal file
89
IMPLEMENTATION_REPORT.md
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
# Mandelbrot Deep Zoom v23 実装報告
|
||||||
|
|
||||||
|
## 結論
|
||||||
|
|
||||||
|
`IMPROVEMENT_PROPOSAL.md`で定義したv23の実装、ローカル数値検証、WASM再現build、Hosted/Standalone成果物の生成まで完了した。実ブラウザでのみ測定できる性能・表示・操作・download検証は、今回の環境にin-app browser bindingがないため未実行である。したがって、実装と非ブラウザgateは合格、browser acceptance gateは保留として扱う。
|
||||||
|
|
||||||
|
## 実装済み
|
||||||
|
|
||||||
|
- 常時RAFを廃止し、invalidate時だけ動くevent-driven schedulerへ変更した。
|
||||||
|
- wheel/pinch/pan中は再投影だけを行い、settle後に計算を1回開始する。
|
||||||
|
- Preview時間予算を110msへ下げ、cold deep Previewは幅48px以下から開始する。以後は実測MPPでPreview寸法を決める。
|
||||||
|
- 標準deepのCoveredは実測MPPから約1.4秒のpixel budgetを算出する。未計測・再投影のみの場合は保守的MPPを使い、DPR下限は固定比率ではなく長辺64pxとするため4Kでも予算が効く。精細・精度優先ではこの時間適応を行わない。
|
||||||
|
- 処理モードの自動到達点を固定し、省電力はPreview、標準はCovered、精細はRefined、精度優先はValidatedで停止する。標準では境界AAと未確定追加反復を既定で実行しない。
|
||||||
|
- 精細の未確定追加反復は全未確定pixelではなくescape境界候補に限定し、1 viewあたりdeep 384点/shallow 4,096点を上限にする。
|
||||||
|
- `Reprojected → Preview → Covered → Refined → Validated/検証未完了`を分離した。
|
||||||
|
- Coveredはeffective DPRの表示Canvas全域をpixel中心`(x + 0.5, y + 0.5)`で計算する。
|
||||||
|
- escape field、分類、confidenceと彩色を分離し、palette変更時の反復再計算を廃止した。
|
||||||
|
- fieldへ各sampleの実escape iterationを保持し、再着色・未確定継続・Export監査で近似値ではなく同じ数値結果を使う。
|
||||||
|
- adaptive 2×/4× AA、linear-light resolve、byte-budget detail cacheを実装した。
|
||||||
|
- 深部Worker、deep/BLA/color WASM、reference資産を遅延生成し、Worker数を実測とmemory budgetで増減する。
|
||||||
|
- deep/BLA/colorはmainで各1回だけcompileし、compile済み`WebAssembly.Module`をWorkerへstructured cloneする。Worker sourceから埋込payload、palette LUT、RGBA生成の重複を除き、Workerは数値fieldだけを返す。
|
||||||
|
- 通常jobから学習したstrip時間とWorker数をkernel version・端末特性keyで永続化し、次回起動で再利用する。
|
||||||
|
- fixed-point direct、perturbation、rebase、BLA、局所guarded directを共通tile schedulerへ接続した。
|
||||||
|
- deep切替を固定zoom閾値ではなくpixel stepとULPの比、hysteresisで決める。
|
||||||
|
- deep切替とguard bitsへ直近orbitのglitch/未確定/補修率を加え、精度昇格時は低精度で作った基準軌道を破棄して再構築する。
|
||||||
|
- 高精度directを約6msでyieldする非同期処理へ変更し、取消可能にした。
|
||||||
|
- f64解析・収縮判定は`INTERIOR_LIKELY`のまま保持し、ValidatedではP/P+64双方の固定小数点整数不等式で主カージオイド/周期2球を証明できたsampleだけを`INTERIOR_PROVEN`へ昇格する。
|
||||||
|
- 精度優先のdeep描画では、基準軌道もP/P+64で独立再計算して疎なcheckpointとescape位置を照合し、不一致時はglobal reference全体を32 bit昇格して作り直す。
|
||||||
|
- 1×/2×/4×/custom、1×/2×2 AA、Balanced/Validated、進捗、取消に対応する独立tile exportを実装した。
|
||||||
|
- Export sidecarへViewSpec、precision/iteration policy、未確定sample数、backend、全kernel SHA-256、色空間、encoder、paletteを記録する。
|
||||||
|
- version付きURL、戻る/進む、座標・spanの正確値入力/copy、Undo/Redoを実装した。
|
||||||
|
- 初回UI、一般statusと診断status、mobile compact status、keyboard操作、live region、visible focus、44px target、reduced motion/transparencyを実装した。
|
||||||
|
- Hosted版はcontent-hashed外部WASMとstreaming compile、Standalone版は3ファイル直開きを維持した。
|
||||||
|
- cacheは件数ではなくmobile 96 MiB/desktop 192 MiBのlogical byte ledgerで管理する。
|
||||||
|
|
||||||
|
## 再現buildと配布
|
||||||
|
|
||||||
|
- `src/shallow_kernel.c`、`src/deep_kernel.c`、`src/bla_kernel_v18.c`、`src/color_kernel.c`、`src/abi.json`を配置した。
|
||||||
|
- `toolchain.lock.json`でclang 17.0.6、target、SIMD/scalar flagsを固定した。
|
||||||
|
- 公式SHA-256を照合したclang 17.0.6とNode.js v22.18.0で8 WASMを再buildした。
|
||||||
|
- shallow、deep、BLA、colorのgoldenとSIMD/scalar同値gateに合格後、payloadと`kernels.js`を昇格した。
|
||||||
|
- `scripts/build-kernels.ps1`は同一payloadだけを自動deduplicateし、異なるCOLOR variantsも正しく保持する。
|
||||||
|
- `scripts/build-hosted.ps1`は現manifest以外の古いcontent-hashed WASMを削除する。
|
||||||
|
|
||||||
|
生成先:
|
||||||
|
|
||||||
|
- Hosted: `dist/hosted/`と`dist/wasm/`
|
||||||
|
- Standalone: `dist/standalone/`
|
||||||
|
- 再現build: `build/wasm-v23/`
|
||||||
|
|
||||||
|
現在の非圧縮基準値は、Hosted first view 142,046 bytes、Standalone 170,593 bytesである。詳細なfile hashは`audit/v23-source-baseline.json`に記録した。
|
||||||
|
|
||||||
|
## 合格済みgate
|
||||||
|
|
||||||
|
- source contract: pass
|
||||||
|
- kernel source/ABI contract: pass
|
||||||
|
- fixed-point precision 256/320 bit: pass
|
||||||
|
- 基準軌道P/P+64 checkpoint一致と故意の不一致検出: pass
|
||||||
|
- analytic interior integer proof 256/320 bit: pass
|
||||||
|
- pixel mapping 851 samples、shallow/deep/BLA、2×/4× tile seam: pass
|
||||||
|
- 配布shallow WASM pixel contract 1,440 samples、mismatch 0: pass
|
||||||
|
- generated-WASM shallow/color/deep/BLA golden: pass
|
||||||
|
- SIMD/scalar結果比較: pass
|
||||||
|
- 本体、shallow Worker、deep WorkerのJavaScript構文: pass
|
||||||
|
- mock DOM初期化とv23 diagnostics: pass
|
||||||
|
- compile済みdeep/BLA/color Moduleのstructured clone・instantiate: pass
|
||||||
|
- Node固定scene性能基準と処理量契約: pass(`audit/v23-node-performance.json`)
|
||||||
|
- Hosted/Standalone buildと全payload SHA-256: pass
|
||||||
|
|
||||||
|
toolchainを利用できる環境では次で省略なしに再実行できる。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/test-all.ps1 -RequireToolchain
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser acceptance gate
|
||||||
|
|
||||||
|
`tests/browser-benchmark.html`は`tests/scenes.json`のmobile/desktop/4K profileと6 sceneを走査し、Preview、Covered、Refined、idle scheduler/write、long task、logical memory、canvas SHA-256をJSON化する。desktop/z0ではwheel連打、省電力がPreviewで停止すること、標準がCovered後にAA・追加反復を開始しないこと、即時再着色、64px Export、Validated Export取消、機械判定できるaccessibilityも実行し、結果へacceptance判定を付ける。今回の環境ではbrowser bindingがなかったため、`audit/v23-browser-baseline.json`は意図的に`not-run`のままである。
|
||||||
|
|
||||||
|
残る実機確認:
|
||||||
|
|
||||||
|
- mobile/desktop/4Kの表示画像とnative coverage
|
||||||
|
- 操作反映p95、Preview/Covered/Refined所要時間
|
||||||
|
- idle時の予約RAF/timer/background job/Canvas・DOM write 0
|
||||||
|
- browser/GPUを含むobserved peak memory
|
||||||
|
- Export寸法、完了前downloadなし、取消、PNG/JSON内容
|
||||||
|
- keyboard-only、focus、44px target、live status、page zoomとcanvas pinchの操作確認
|
||||||
|
|
||||||
|
このgateが合格するまで、v23の実装完了と数値gate合格は主張できるが、固定実機に対する性能目標の達成は主張しない。
|
||||||
545
IMPROVEMENT_PROPOSAL.md
Normal file
545
IMPROVEMENT_PROPOSAL.md
Normal file
|
|
@ -0,0 +1,545 @@
|
||||||
|
# マンデルブロ集合ビューワー 軽量・高精細化 改善提案
|
||||||
|
|
||||||
|
- 対象: Mandelbrot ∞ Zoom v22
|
||||||
|
- 作成日: 2026-08-22
|
||||||
|
- 対象ファイル: `index.html`, `script.js`, `kernels.js`, `src/bla_kernel_v18.c`
|
||||||
|
- 性格: 実装前の技術提案。効果値は、既存監査の引用を除き検証すべき目標または仮説である。
|
||||||
|
|
||||||
|
## 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する | target coverage、samples/pixel、継ぎ目 |
|
||||||
|
| 数値精度 | 座標・反復・BLA近似の誤差を検出し、未解決を黒と混同しない | 高精度referenceとの差、unresolved数、検証モード |
|
||||||
|
| 出力精細度 | 表示プレビューとは独立して、指定寸法を完了後に保存する | 出力寸法、AA、再現性、未完了pixel数 |
|
||||||
|
|
||||||
|
「Canvasが4Kサイズである」だけでは高精細とはみなさない。低解像度フレームを4K Canvasへ拡大した状態と、4K分のサンプルを実計算した状態を区別する。
|
||||||
|
|
||||||
|
## 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で再計測する必要がある。
|
||||||
|
|
||||||
|
### 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計算 | target coverage 100%、再投影pixel 0 | 全域描画 68% |
|
||||||
|
| Refined | Covered後のidle | 境界・不確実tileだけ2×/4× adaptive AA | 対象tile完了 | 境界AA完了 |
|
||||||
|
| Validated | 精度優先時またはExport | 宣言した有限反復・精度policyを局所照合 | checks合格かつunresolved 0。残る場合は別状態「検証未完了・未確定N」 | 検証完了 / 検証未完了 |
|
||||||
|
|
||||||
|
重要な契約は次の通り。
|
||||||
|
|
||||||
|
- Previewは粗くてよいが、新しい未解決画素を「内部」と断定して黒にしない。
|
||||||
|
- Coveredはズーム深度に関係なく、確定したScreen Canvas gridを新しいViewSpecの実sampleだけで全域計算する。再投影した旧frameは描画中のpresentationにだけ使い、coverageへ数えない。
|
||||||
|
- 標準モードは明示したeffective DPR、精細モードは原則native DPR、Exportは指定gridをtargetにする。DPRを画素予算で下げた場合は実解像度を表示する。
|
||||||
|
- Refinedは単なる2倍補間ではなく、複数の実サンプルをlinear-lightでresolveする。
|
||||||
|
- Validatedは数学的なマンデルブロ集合所属証明ではなく、宣言した有限反復・precision policyの照合完了を表す。全sampleへ誤差境界または証明手続きを適用する将来モードだけを`Certified`と呼ぶ。
|
||||||
|
- 「高精細」と「数値検証完了」を同じチェックボックスにしない。
|
||||||
|
- Exportは表示Canvasとは独立したjobであり、view、寸法、反復方針、AA、乱数seedを固定してから開始する。
|
||||||
|
|
||||||
|
処理モードと描画状態は分ける。
|
||||||
|
|
||||||
|
| 処理モード | 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。未確定追加反復は境界候補だけを上限付きで行う |
|
||||||
|
| 精度優先 / Export | 指定gridとprecision policy | Validated。将来はCertifiedを選択可 |
|
||||||
|
|
||||||
|
## 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に永続化する。
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
JS f64、shallow WASM、deep WASM、BLA、direct fallback、detail、exportについて、同一pixelが同一座標になるcross-backend testを追加する。整数格子式だけで現在の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として重ねられるが、それを新ViewSpecの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は高難度である。v22基準計測で、reference生成、glitch fallback、10^-280以深のどれが実際に支配的か確認してから実装する。
|
||||||
|
|
||||||
|
### 7.8 Exportを独立pipelineにする — P0/P1
|
||||||
|
|
||||||
|
`PNG`を次の2操作へ分ける。
|
||||||
|
|
||||||
|
- Quick snapshot: 現在見えているCanvasを保存。Previewである可能性を明示する。
|
||||||
|
- High-quality export: 1× / 2× / 4× / custom、AA、Balanced / Validated、進捗、取消を指定する。Certifiedは誤差境界経路を実装した後に追加する。
|
||||||
|
|
||||||
|
High-quality exportでは、固定したViewSpecをtile描画し、全tile完了後にだけencodeする。座標、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上限と未確定数を併記 |
|
||||||
|
| 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. 実装ロードマップ
|
||||||
|
|
||||||
|
### Phase 0 — v22基準化
|
||||||
|
|
||||||
|
- shallow/deep/colorを含む全kernel sourceを回収または復元し、固定toolchain、compile flags、生成script、payload checksumを揃える。
|
||||||
|
- z0 / z14 / z20 / z100 / period-3 interior / boundary / 10^-280付近を固定sceneにする。
|
||||||
|
- desktop、mobile相当、4Kでcold / warmを測る。
|
||||||
|
- first preview、Covered完了、Refined完了、idle job/draw、peak memory、long taskを記録する。
|
||||||
|
- 現行`exactDeepPixel`とは独立した、複数精度一致と誤差上限を持つ高精度reference実装で、分類、iteration、smooth valueを比較する。
|
||||||
|
- v18監査はarchiveと明記し、v22 baselineを別JSONにする。
|
||||||
|
|
||||||
|
Exit gate: 全kernelを再現buildでき、v22の速度・memory・数値baselineを同じcommandで再生成できる。
|
||||||
|
|
||||||
|
### 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: 同一sample座標のbackend差0、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と即時再着色を導入する。
|
||||||
|
- 処理モード別target coverage 100%を実装する。
|
||||||
|
- ULPベースのshallow/deep切替を導入する。
|
||||||
|
- `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を導入し、局所反復継続を実装する。
|
||||||
|
|
||||||
|
Exit gate: target 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. 受入基準
|
||||||
|
|
||||||
|
| 分野 | 基準案 |
|
||||||
|
|---|---|
|
||||||
|
| Idle | 最終更新2秒後に予約RAF/timer/background job 0、Canvas/DOM write 0回/秒。CPUは同環境blank baselineとの差を補助指標にする |
|
||||||
|
| 操作 | transform反映p95 < 16ms、main thread 50ms超long task 0、wheel中に新規full renderを開始しない |
|
||||||
|
| Preview | 基準端末・固定sceneでgesture settle後p95 < 120ms |
|
||||||
|
| Covered | target Screen Canvas coverage 100%、effective DPRを表示し、再投影pixelをcoverageへ数えない。scene別時間budgetはPhase 0後に確定 |
|
||||||
|
| Refined | 対象pixelは記録済み実sampleのlinear-light resolve。Canvas補間だけをAAと数えず、heuristicであることを明示 |
|
||||||
|
| 数値 | false `ESCAPED` 0、false `INTERIOR_PROVEN` 0、escape iteration差・smooth誤差・`INTERIOR_LIKELY` mismatch・UNRESOLVED数を別々に記録 |
|
||||||
|
| Pixel mapping | 全backendで同一pixelのworld座標が一致し、tile境界に1px/半pxの回帰がない |
|
||||||
|
| Export | 指定寸法どおり、全tile完了前はfinal fileを出さない、取消可能、宣言したbackend/encoder条件で許容差内再現 |
|
||||||
|
| 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 |
|
||||||
|
| 配布 | shallow first viewの取得bytes・compile timeをv22 baselineより増やさず、Hostedではdeep到達前のdeep request 0 |
|
||||||
|
| Accessibility | keyboard-only操作、visible focus、44px target、label/name、live status、page zoom、reduced motion/transparencyが合格 |
|
||||||
|
| Regression | v22 scene corpusをCIで実行し、速度・memory・分類・例外を履歴化する |
|
||||||
|
|
||||||
|
端末依存の時間値は、対象端末を固定して初めて合否に使う。絶対時間だけでなく、同一scene・同一pixel数の比率とcorrectnessを主要指標にする。
|
||||||
|
|
||||||
|
## 12. 推奨する最初の実装単位
|
||||||
|
|
||||||
|
最初の変更セットは、次の範囲に限定すると効果を測りやすい。
|
||||||
|
|
||||||
|
1. v22 baselineと全WASMの再現buildを用意する。
|
||||||
|
2. 常時RAFと毎frame DOM更新を廃止する。
|
||||||
|
3. wheelをgestureとしてデバウンスし、非表示時のjobを止める。
|
||||||
|
4. Screen Canvasへpixel budgetを設ける。
|
||||||
|
5. deep poolを必要時まで作らない。
|
||||||
|
6. 既存`q`のままPreview時間予算へ`renderPerf`を接続する。
|
||||||
|
7. v22用の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適用後に実際に使う比率 |
|
||||||
|
| target coverage | 選択したScreen Canvas gridのうち、新ViewSpecで実計算済みの割合 |
|
||||||
|
| 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 0で実機matrixを作って確定する。
|
||||||
245
audit/benchmarks.json
Normal file
245
audit/benchmarks.json
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
82
audit/final-browser-benchmark.json
Normal file
82
audit/final-browser-benchmark.json
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
168
audit/raw/audit_v17_algos_results.json
Normal file
168
audit/raw/audit_v17_algos_results.json
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
142
audit/raw/audit_v18_kernel.txt
Normal file
142
audit/raw/audit_v18_kernel.txt
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
|
||||||
|
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 }
|
||||||
132
audit/raw/dev-browser-benchmark.json
Normal file
132
audit/raw/dev-browser-benchmark.json
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
77
audit/raw/dev-verification-benchmark.json
Normal file
77
audit/raw/dev-verification-benchmark.json
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
44
audit/raw/dev-warm-cache-benchmark.json
Normal file
44
audit/raw/dev-warm-cache-benchmark.json
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
62
audit/raw/interior_safety_v18.json
Normal file
62
audit/raw/interior_safety_v18.json
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
98
audit/raw/mb_audit_v17_results.json
Normal file
98
audit/raw/mb_audit_v17_results.json
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
16
audit/raw/pilot_fill_audit.json
Normal file
16
audit/raw/pilot_fill_audit.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"full": [
|
||||||
|
128,
|
||||||
|
78
|
||||||
|
],
|
||||||
|
"pilot": [
|
||||||
|
56,
|
||||||
|
34
|
||||||
|
],
|
||||||
|
"fullMs": 1356.671812,
|
||||||
|
"pilotMs": 169.93951399999992,
|
||||||
|
"classMismatch": 451,
|
||||||
|
"classRate": 0.04517227564102564,
|
||||||
|
"escapeIterDiffGt8": 4364,
|
||||||
|
"diffRate": 0.437099358974359
|
||||||
|
}
|
||||||
59
audit/raw/reference_recenter_audit.json
Normal file
59
audit/raw/reference_recenter_audit.json
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
59
audit/raw/reference_recenter_audit_z20.json
Normal file
59
audit/raw/reference_recenter_audit_z20.json
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
82
audit/raw/v17-final-browser-benchmark.json
Normal file
82
audit/raw/v17-final-browser-benchmark.json
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
82
audit/raw/v18-final-browser-benchmark.json
Normal file
82
audit/raw/v18-final-browser-benchmark.json
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
12
audit/raw/workcap_truth_v17.json
Normal file
12
audit/raw/workcap_truth_v17.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"w": 48,
|
||||||
|
"h": 30,
|
||||||
|
"pixels": 1440,
|
||||||
|
"globalIter": 3490,
|
||||||
|
"capIter": 2870,
|
||||||
|
"capped": 1440,
|
||||||
|
"late": 1066,
|
||||||
|
"insideGlobal": 374,
|
||||||
|
"lateEscapeRateAmongCapped": 0.7402777777777778,
|
||||||
|
"elapsedMs": 6344.968723999999
|
||||||
|
}
|
||||||
19
audit/smoke-test.json
Normal file
19
audit/smoke-test.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
16
audit/v23-browser-baseline.json
Normal file
16
audit/v23-browser-baseline.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
69
audit/v23-node-performance.json
Normal file
69
audit/v23-node-performance.json
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
108
audit/v23-source-baseline.json
Normal file
108
audit/v23-source-baseline.json
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
{
|
||||||
|
"format": "mandelbrot-source-baseline-v23",
|
||||||
|
"generatedUtc": "2026-08-22T10:48:34.5532410Z",
|
||||||
|
"rendererVersion": 23,
|
||||||
|
"contract": {
|
||||||
|
"status": "pass",
|
||||||
|
"rendererVersion": 23,
|
||||||
|
"wasmPayloads": 8,
|
||||||
|
"scriptBytes": 129452,
|
||||||
|
"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": 129452,
|
||||||
|
"sha256": "97a1a3d897fa0f7bce3a30727a0e90a767799356b12899c074fdb04968506ace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm",
|
||||||
|
"bytes": 511,
|
||||||
|
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"firstViewUncompressedBytes": 143689,
|
||||||
|
"deepRequestsBeforeDeepView": 0,
|
||||||
|
"note": "Transfer compression, parse, compile, and runtime timings require the fixed browser benchmark."
|
||||||
|
},
|
||||||
|
"standalone": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"file": "dist/standalone/index.html",
|
||||||
|
"bytes": 11395,
|
||||||
|
"sha256": "b2017e3aac73d22d1178e2a28bb5cb1ea0035cbcae5a4ed422dbe4ff7eb951fa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "dist/standalone/script.js",
|
||||||
|
"bytes": 129452,
|
||||||
|
"sha256": "97a1a3d897fa0f7bce3a30727a0e90a767799356b12899c074fdb04968506ace"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file": "dist/standalone/kernels.js",
|
||||||
|
"bytes": 31389,
|
||||||
|
"sha256": "e97726c09af138da92b331c376766c1128c10aa3c361b83750356824262ea1a0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"uncompressedBytes": 172236
|
||||||
|
},
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
build/wasm-v23/bla-scalar.wasm
Normal file
BIN
build/wasm-v23/bla-scalar.wasm
Normal file
Binary file not shown.
BIN
build/wasm-v23/bla-simd.wasm
Normal file
BIN
build/wasm-v23/bla-simd.wasm
Normal file
Binary file not shown.
BIN
build/wasm-v23/color-scalar.wasm
Normal file
BIN
build/wasm-v23/color-scalar.wasm
Normal file
Binary file not shown.
BIN
build/wasm-v23/color-simd.wasm
Normal file
BIN
build/wasm-v23/color-simd.wasm
Normal file
Binary file not shown.
BIN
build/wasm-v23/deep-scalar.wasm
Normal file
BIN
build/wasm-v23/deep-scalar.wasm
Normal file
Binary file not shown.
BIN
build/wasm-v23/deep-simd.wasm
Normal file
BIN
build/wasm-v23/deep-simd.wasm
Normal file
Binary file not shown.
85
build/wasm-v23/manifest.json
Normal file
85
build/wasm-v23/manifest.json
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
build/wasm-v23/wasm-scalar.wasm
Normal file
BIN
build/wasm-v23/wasm-scalar.wasm
Normal file
Binary file not shown.
BIN
build/wasm-v23/wasm-simd.wasm
Normal file
BIN
build/wasm-v23/wasm-simd.wasm
Normal file
Binary file not shown.
11
dist/_headers
vendored
Normal file
11
dist/_headers
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
11
dist/hosted/_headers
vendored
Normal file
11
dist/hosted/_headers
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
36
dist/hosted/hosted-loader.js
vendored
Normal file
36
dist/hosted/hosted-loader.js
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
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);
|
||||||
69
dist/hosted/index.html
vendored
Normal file
69
dist/hosted/index.html
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||||
|
<meta name="theme-color" content="#050813">
|
||||||
|
<title>Mandelbrot Deep Zoom v23</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
|
||||||
|
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
|
||||||
|
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
|
||||||
|
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
|
||||||
|
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
|
||||||
|
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
|
||||||
|
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
|
||||||
|
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}#palette{color:#111;background:#f4f5f8}#palette option{color:#111;background:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
|
||||||
|
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
|
||||||
|
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.exact-grid{display:grid;grid-template-columns:54px 1fr;gap:6px;margin-top:8px}.exact-grid input{min-width:0;width:100%;min-height:44px;border:1px solid var(--line);border-radius:8px;background:#070c19;color:var(--text);padding:6px;font:11px ui-monospace,monospace}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
|
||||||
|
.diagnostics{margin-top:8px;color:var(--muted);font:10.5px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.exact-grid input{user-select:text;-webkit-user-select:text}
|
||||||
|
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
|
||||||
|
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
|
||||||
|
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}
|
||||||
|
#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
|
||||||
|
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
|
||||||
|
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
|
||||||
|
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
|
||||||
|
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord">…</span></div><div class="row"><span class="muted">倍率</span> <span id="zoom">…</span> <span class="muted">表示幅</span> <span id="span">…</span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render">…</span></div></div></div>
|
||||||
|
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
|
||||||
|
<div id="controls" class="panel">
|
||||||
|
<div class="toolbar"><button id="zin" aria-label="中心を拡大">+</button><button id="zout" aria-label="中心を縮小">−</button><button id="reset">リセット</button><button id="png">出力</button></div>
|
||||||
|
<div class="group">
|
||||||
|
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">精度優先</option></select><output></output></div>
|
||||||
|
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
|
||||||
|
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0.001" max="0.05" step="0.0005" value="0.008"><output id="cycleO" for="cycle">0.0080</output></div>
|
||||||
|
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
|
||||||
|
<details><summary>詳細設定・正確な座標</summary>
|
||||||
|
<div class="line"><label for="iters">基準反復</label><input id="iters" type="range" min="100" max="2500" step="25" value="350"><output id="itersO" for="iters">350</output></div>
|
||||||
|
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> 境界AAを追加</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></details>
|
||||||
|
</div>
|
||||||
|
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
|
||||||
|
<div class="hint">ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。</div>
|
||||||
|
</div>
|
||||||
|
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI−</button>
|
||||||
|
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||||
|
<dialog id="exportDialog" aria-labelledby="exportTitle">
|
||||||
|
<h2 id="exportTitle">高解像度 PNG 出力</h2>
|
||||||
|
<div class="export-grid">
|
||||||
|
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
|
||||||
|
<label for="exportWidth">幅(px)</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
|
||||||
|
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×(高速)</option><option value="2">2×2 AA</option></select>
|
||||||
|
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="validated">Validated direct</option></select>
|
||||||
|
</div>
|
||||||
|
<progress id="exportProgress" max="1" value="0" hidden></progress>
|
||||||
|
<div id="exportStatus" role="status" aria-live="polite"></div>
|
||||||
|
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
|
||||||
|
</dialog>
|
||||||
|
<script type="module" src="./hosted-loader.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
912
dist/hosted/script.js
vendored
Normal file
912
dist/hosted/script.js
vendored
Normal file
|
|
@ -0,0 +1,912 @@
|
||||||
|
(()=>{'use strict';
|
||||||
|
const K=globalThis.MANDEL_KERNELS;
|
||||||
|
if(!K)throw new Error('kernels.js が読み込まれていません');
|
||||||
|
const {WASM_SIMD_B64,WASM_SCALAR_B64,DEEP_SIMD_B64,DEEP_SCALAR_B64,BLA_SIMD_B64,BLA_SCALAR_B64,COLOR_SIMD_B64,COLOR_SCALAR_B64}=K;
|
||||||
|
const $=s=>document.querySelector(s);
|
||||||
|
const canvas=$('#view');
|
||||||
|
const ctx=canvas.getContext('2d',{alpha:false,desynchronized:true})||canvas.getContext('2d',{alpha:false});
|
||||||
|
if(!ctx){document.body.innerHTML='<div style="padding:30px;color:white">Canvas 2Dを利用できません。</div>';return;}
|
||||||
|
|
||||||
|
const INITIAL_BITS=256;
|
||||||
|
const MIN_SPAN_BITS=224;
|
||||||
|
const TARGET_SPAN_BITS=240;
|
||||||
|
const RATIO_DEN=4503599627370496n; // 2^52
|
||||||
|
const POW256=1.157920892373162e77;
|
||||||
|
const INV256=8.636168555094445e-78;
|
||||||
|
const HI128=3.402823669209385e38;
|
||||||
|
const LO128=2.938735877055719e-39;
|
||||||
|
const NEG_BUCKET=-1000000000;
|
||||||
|
const LEGACY_DEEP_ZOOM_THRESHOLD=11.5;
|
||||||
|
const RENDER_PASS=Object.freeze({PREVIEW:'preview',COVERED:'covered'});
|
||||||
|
const MODE_TARGET=Object.freeze({power:'PREVIEW',standard:'COVERED',fine:'REFINED',validate:'VALIDATED'});
|
||||||
|
function modeTarget(mode=state.processMode){return MODE_TARGET[mode]||MODE_TARGET.standard}
|
||||||
|
const RENDER_PROFILE=Object.freeze({
|
||||||
|
[RENDER_PASS.PREVIEW]:Object.freeze({id:RENDER_PASS.PREVIEW,covered:false,budgetMs:110,nominalScale:.42,minWidth:360,maxWidth:900,densityLimit:1.55,blaSteps:1700,ptbSteps:2500}),
|
||||||
|
[RENDER_PASS.COVERED]:Object.freeze({id:RENDER_PASS.COVERED,covered:true,budgetMs:1050,nominalScale:1,minWidth:0,maxWidth:Infinity,densityLimit:1,blaSteps:0,ptbSteps:0})
|
||||||
|
});
|
||||||
|
function renderProfile(pass){return RENDER_PROFILE[pass]||RENDER_PROFILE[RENDER_PASS.PREVIEW]}
|
||||||
|
let deepMode=false;
|
||||||
|
|
||||||
|
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,lastRender:0,lastEngine:'起動中',lastPass:null,lastInteraction:performance.now(),dirty:true,
|
||||||
|
uiHidden:false,frameView:null,fieldView:null,lastFrameDone:0,
|
||||||
|
detailGeneration:0,detailActive:false,detailQueued:0,detailDone:0,
|
||||||
|
drawState:'REPROJECTED',coverage:0,unresolved:0,
|
||||||
|
precisionPending:false,
|
||||||
|
pointerActive:false,wheelActive:false,panReuse:null,effectiveDpr:1,screenPixelBudget:0,adaptivePixelBudget:0,continuationProgress:0,focusX:.5,focusY:.5
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deep telemetry is collected from the actual BLA/perturbation
|
||||||
|
// kernel rather than from a synthetic JavaScript loop.
|
||||||
|
const deepTelemetry={
|
||||||
|
frames:0,kernelMPP:0,meanIter:0,blackRatio:0,badRatio:0,blaBuildEMA:0,
|
||||||
|
refDistance:0,interiorRatio:0,repairRatio:0,unresolvedRatio:0,
|
||||||
|
blaStepsPerPixel:0,ptbStepsPerPixel:0,rebasePerPixel:0,lastPilotMs:0,lastPilotMPP:0,lastKernelMs:0,lastVerifyMs:0,lastPixels:0
|
||||||
|
};
|
||||||
|
const refControl={buildEMA:18,lastBuildMs:0,lastRecenterAt:0,refId:0,baseMPP:0,lastMPP:0,lastPixels:0,cooldownMs:900};
|
||||||
|
|
||||||
|
// Adaptive quality controller: instead of tying a quality level to a fixed
|
||||||
|
// pixel width, learn the recent cost per pixel and spend a bounded amount of time.
|
||||||
|
const renderPerf={deepMPP:0,shallowMPP:0};
|
||||||
|
const runtimeMetrics={canvasWrites:0,domWrites:0,renderStarts:0,renderStartsDuringGesture:0,longTasks:0,maxLongTaskMs:0,pendingBackgroundJobs:0};
|
||||||
|
try{if('PerformanceObserver'in globalThis){const observer=new PerformanceObserver(list=>{for(const entry of list.getEntries()){runtimeMetrics.longTasks++;runtimeMetrics.maxLongTaskMs=Math.max(runtimeMetrics.maxLongTaskMs,entry.duration||0)}});observer.observe({type:'longtask',buffered:true})}}catch{}
|
||||||
|
function scheduleBackground(fn,delay=0){runtimeMetrics.pendingBackgroundJobs++;return setTimeout(()=>{runtimeMetrics.pendingBackgroundJobs=Math.max(0,runtimeMetrics.pendingBackgroundJobs-1);fn()},delay)}
|
||||||
|
function scheduleIdle(fn,timeout=350){runtimeMetrics.pendingBackgroundJobs++;const run=()=>{runtimeMetrics.pendingBackgroundJobs=Math.max(0,runtimeMetrics.pendingBackgroundJobs-1);fn()};return'requestIdleCallback'in window?requestIdleCallback(run,{timeout}):setTimeout(run,Math.min(80,timeout))}
|
||||||
|
const DETAIL_TILE_CACHE=new Map();
|
||||||
|
let detailCacheBytes=0;
|
||||||
|
const activeDetailTiles=[];
|
||||||
|
let detailPlan=null;
|
||||||
|
let schedulerRAF=0,schedulerTimer=0,schedulerDue=0,needsPaint=true,needsStats=true,wheelSettleTimer=0,pointerSettleTimer=0;
|
||||||
|
|
||||||
|
function requestScheduler(delay=0){
|
||||||
|
if(document.hidden)return;
|
||||||
|
if(delay>0){
|
||||||
|
const due=performance.now()+delay;
|
||||||
|
if(schedulerTimer&&schedulerDue<=due)return;
|
||||||
|
if(schedulerTimer)clearTimeout(schedulerTimer);
|
||||||
|
schedulerDue=due;
|
||||||
|
schedulerTimer=setTimeout(()=>{schedulerTimer=0;schedulerDue=0;requestScheduler()},Math.max(0,Math.ceil(delay)));
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(schedulerTimer){clearTimeout(schedulerTimer);schedulerTimer=0;schedulerDue=0}
|
||||||
|
if(!schedulerRAF)schedulerRAF=requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
function invalidateView(withStats=true){needsPaint=true;if(withStats)needsStats=true;requestScheduler()}
|
||||||
|
function invalidateStats(){needsStats=true;requestScheduler()}
|
||||||
|
|
||||||
|
// Device-specific runtime wisdom. It is populated by a small background CPU
|
||||||
|
// concurrency benchmark and then refined by real deep-render strip timings.
|
||||||
|
const deepWisdom={
|
||||||
|
ready:false,running:false,maxWorkers:1,workerCount:1,
|
||||||
|
targetStripMs:12,stripRows:24,rowMsEMA:0,
|
||||||
|
realBench:false
|
||||||
|
};
|
||||||
|
function deepWisdomStorageKey(){
|
||||||
|
const meta=globalThis.MANDEL_KERNEL_META||{},kernel=String(meta.BLA_SIMD_B64||meta.BLA_SCALAR_B64||'embedded').slice(0,16),hc=Math.max(1,navigator.hardwareConcurrency||1),dm=Number(navigator.deviceMemory||0);return'mandelbrot.wisdom.v23.'+[kernel,hc,dm].join('.')
|
||||||
|
}
|
||||||
|
function restoreDeepWisdom(){
|
||||||
|
try{const saved=JSON.parse(localStorage.getItem(deepWisdomStorageKey())||'null');if(!saved||saved.version!==23)return;const row=Number(saved.rowMsEMA),rows=Number(saved.stripRows),workers=Number(saved.workerCount);if(Number.isFinite(row)&&row>0&&row<1000)deepWisdom.rowMsEMA=row;if(Number.isInteger(rows)&&rows>=2&&rows<=128)deepWisdom.stripRows=rows;if(Number.isInteger(workers)&&workers>=1&&workers<=deepWorkerLimit())deepWisdom.workerCount=workers;deepWisdom.ready=deepWisdom.rowMsEMA>0}catch{}
|
||||||
|
}
|
||||||
|
function persistDeepWisdom(){
|
||||||
|
if(!deepWisdom.rowMsEMA)return;try{localStorage.setItem(deepWisdomStorageKey(),JSON.stringify({version:23,rowMsEMA:deepWisdom.rowMsEMA,stripRows:deepWisdom.stripRows,workerCount:deepWisdom.workerCount}))}catch{}
|
||||||
|
}
|
||||||
|
|
||||||
|
function one(bits=state.bits){return 1n<<BigInt(bits)}
|
||||||
|
function fromFrac(n,d=1n){return n*one()/d}
|
||||||
|
function fromDec(s){
|
||||||
|
s=String(s).trim();let neg=s[0]==='-';if(neg)s=s.slice(1);
|
||||||
|
const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0;
|
||||||
|
const a=mant.split('.'),i=a[0]||'0',f=a[1]||'';
|
||||||
|
let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',decPlaces=f.length-exp;
|
||||||
|
if(decPlaces<0){digits+='0'.repeat(-decPlaces);decPlaces=0}
|
||||||
|
const den=10n**BigInt(decPlaces),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,places=Math.max(0,f-e);return Math.max(64,Math.ceil(places*Math.log2(10))+32)}
|
||||||
|
function bitLen(n){n=n<0n?-n:n;return n===0n?0:n.toString(2).length}
|
||||||
|
function fixedNum(v,bits=state.bits){
|
||||||
|
if(v===0n)return 0;let 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;
|
||||||
|
}
|
||||||
|
// Hot-path conversion for orbit values known to stay O(1).
|
||||||
|
function fixedOrbitNum(v,bits){
|
||||||
|
if(v===0n)return 0;
|
||||||
|
const sh=bits-54;
|
||||||
|
if(sh>0)return Number(v>>BigInt(sh))*Math.pow(2,-54);
|
||||||
|
return Number(v)*Math.pow(2,-bits);
|
||||||
|
}
|
||||||
|
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 zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
|
||||||
|
function fixedRatio(a,b){
|
||||||
|
if(b===0n)return 0;if(a===0n)return 0;const neg=a<0n;if(neg)a=-a;
|
||||||
|
const q=(a<<52n)/b;const v=Number(q)/4503599627370496;return neg?-v:v;
|
||||||
|
}
|
||||||
|
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||||
|
function promoteState(shift){
|
||||||
|
// Precision promotion used to invalidate the reference orbit. Keep the same
|
||||||
|
// mathematical values by shifting their fixed-point representation instead.
|
||||||
|
// If a render is in flight, logically cancel it before mutating shared cache state.
|
||||||
|
if(state.rendering)cancelRender();
|
||||||
|
const oldBits=state.bits,s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;
|
||||||
|
if(state.frameView){state.frameView.re<<=s;state.frameView.im<<=s;state.frameView.span<<=s;state.frameView.bits+=shift}
|
||||||
|
state.bits+=shift;
|
||||||
|
promoteReferenceCache(shift,oldBits);
|
||||||
|
}
|
||||||
|
function ensurePrecision(){
|
||||||
|
const bl=bitLen(state.span);
|
||||||
|
if(bl>=MIN_SPAN_BITS)return false;
|
||||||
|
// Defer representation-only precision promotion until the current frame finishes.
|
||||||
|
if(state.rendering){state.precisionPending=true;return false}
|
||||||
|
state.precisionPending=false;promoteState(TARGET_SPAN_BITS-bl);return true
|
||||||
|
}
|
||||||
|
function flushPendingPrecision(){
|
||||||
|
if(state.rendering||!state.precisionPending)return false;state.precisionPending=false;
|
||||||
|
const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS){promoteState(TARGET_SPAN_BITS-bl);return true}return false
|
||||||
|
}
|
||||||
|
function mulRatio(v,factor){
|
||||||
|
const n=BigInt(Math.max(1,Math.round(factor*Number(RATIO_DEN))));return v*n/RATIO_DEN;
|
||||||
|
}
|
||||||
|
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 d=state.bits,q=v*(10n**BigInt(d))>>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 fmtSpan(){const l=log2Fixed(state.span);if(!Number.isFinite(l))return'0';const e=Math.floor(l/Math.log2(10)),m=Math.pow(2,l-e*Math.log2(10));return m.toFixed(4)+'e'+e}
|
||||||
|
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp();if(z<12)return Math.min(12000,Math.round(state.baseIter+22*Math.sqrt(z)*Math.log2(2+z)));return Math.min(140000,Math.round(state.baseIter+150*z+260*Math.log2(2+z)))}
|
||||||
|
function ensureOrbitPrecision(iter,width=Math.max(1,canvas.width)){const conditionBits=referenceCache.rr?Math.max(0,Math.min(256,Math.ceil(referenceCache.conditionLog2||0))):0,required=160+conditionBits,available=bitLen(state.span)-Math.ceil(Math.log2(width))-Math.ceil(Math.log2(Math.max(2,iter)));if(available>=required)return false;promoteState(required+32-available);invalidateReferenceOrbit();return true}
|
||||||
|
function f64Ulp(x){x=Math.abs(x);if(!Number.isFinite(x))return Infinity;if(x===0)return Number.MIN_VALUE;return Math.pow(2,Math.floor(Math.log2(x))-52)}
|
||||||
|
function deepResolutionRatio(snap=snapshot(),width=Math.max(1,canvas.width)){const step=Math.abs(fixedNum(snap.span,snap.bits))/Math.max(1,width),ulp=Math.max(f64Ulp(fixedNum(snap.re,snap.bits)),f64Ulp(fixedNum(snap.im,snap.bits)));return step===0||!Number.isFinite(step)?0:step/Math.max(Number.MIN_VALUE,ulp)}
|
||||||
|
function deepEngineNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){if(exportForcePrecision)return true;const ratio=deepResolutionRatio(snap,width),orbitRisk=ratio<=128&&(deepTelemetry.badRatio>1e-4||deepTelemetry.unresolvedRatio>2e-3||deepTelemetry.repairRatio>.08);deepMode=orbitRisk||(deepMode?ratio<64:ratio<=32);return deepMode}
|
||||||
|
function iterationPlan(profile,deep){
|
||||||
|
const colorIter=maxIter();return{colorIter,computeIter:colorIter};
|
||||||
|
}
|
||||||
|
function workProfile(profile){return{bla:profile.blaSteps,ptb:profile.ptbSteps}}
|
||||||
|
function cancelRender(){state.token++;state.rendering=false;cancelDeepPoolJob();flushPendingPrecision()}
|
||||||
|
function cancelDetailRefinement(clearCurrent=true){
|
||||||
|
state.detailGeneration++;state.detailActive=false;state.detailQueued=0;state.detailDone=0;detailPlan=null;
|
||||||
|
if(clearCurrent)activeDetailTiles.length=0;
|
||||||
|
}
|
||||||
|
function clearDetailCache(){DETAIL_TILE_CACHE.clear();detailCacheBytes=0;activeDetailTiles.length=0;cancelDetailRefinement(false)}
|
||||||
|
function setDirty(cancel=true){cancelUnknownContinuation();state.lastInteraction=performance.now();state.dirty=true;state.lastPass=null;state.drawState=state.frameView?'REPROJECTED':'PREVIEW';state.coverage=0;cancelDetailRefinement(true);if(cancel)cancelRender();invalidateView()}
|
||||||
|
function reset(){
|
||||||
|
clearDetailCache();clearPanReuse();state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);setDirty();saveHash(false)
|
||||||
|
}
|
||||||
|
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){
|
||||||
|
if(!state.panReuse)capturePanSource();
|
||||||
|
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);
|
||||||
|
const 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();setDirty(!(state.pointerActive||state.wheelActive));
|
||||||
|
}
|
||||||
|
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*100000))/BigInt(Math.round(w*100000));
|
||||||
|
const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));
|
||||||
|
state.im+=ys*BigInt(Math.round(dy*100000))/BigInt(Math.round(h*100000));setDirty(!(state.pointerActive||state.wheelActive));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function clearPanReuse(){state.panReuse=null}
|
||||||
|
function capturePanSource(){
|
||||||
|
if(!state.frameView||!frameCanvas.width||!frameCanvas.height)return false;
|
||||||
|
const fv=state.frameView;state.panReuse={canvas:frameCanvas,snap:{bits:fv.bits,re:fv.re,im:fv.im,span:fv.span},style:styleSignature(),pass:state.lastPass,iter:maxIter(),created:performance.now()};return true
|
||||||
|
}
|
||||||
|
function buildPanReuse(snap,w,h,iter,profile){
|
||||||
|
const src=state.panReuse;if(!src||!src.canvas||src.style!==styleSignature())return null;
|
||||||
|
// Final HQ must recompute after a real scale change. Interactive/base passes may
|
||||||
|
// reproject a much larger range so wheel/pinch zoom can reuse existing pixels.
|
||||||
|
const re=align(src.snap.re,src.snap.bits,snap.bits),im=align(src.snap.im,src.snap.bits,snap.bits),sp=align(src.snap.span,src.snap.bits,snap.bits),scale=fixedRatio(sp,snap.span);
|
||||||
|
if(!Number.isFinite(scale)||scale<.22||scale>4.5)return null;
|
||||||
|
const scaleChange=Math.abs(Math.log2(Math.max(1e-12,scale)));
|
||||||
|
// Covered is a sampling contract, not a presentation shortcut. Reprojection
|
||||||
|
// is allowed only for Preview; the target grid is always recomputed in full.
|
||||||
|
if(profile.covered)return null;
|
||||||
|
const dx=fixedRatio(re-snap.re,snap.span)*w,dy=-fixedRatio(im-snap.im,snap.span)*w,move=Math.hypot(dx/Math.max(1,w),dy/Math.max(1,w));if(!Number.isFinite(move)||move>1.35)return null;
|
||||||
|
const dw=w*scale,dh=w*scale*(src.canvas.height/Math.max(1,src.canvas.width)),density=dw/Math.max(1,src.canvas.width);
|
||||||
|
// A zoomed preview may stretch old pixels temporarily, but the idle HQ pass will
|
||||||
|
// recompute it. Keep the stretch bounded so interaction never becomes misleading.
|
||||||
|
if(density>profile.densityLimit)return null;
|
||||||
|
const x0=w*.5+dx-dw*.5,y0=h*.5+dy-dh*.5,x1=x0+dw,y1=y0+dh;
|
||||||
|
let ix0=Math.max(0,Math.ceil(x0)+2),iy0=Math.max(0,Math.ceil(y0)+2),ix1=Math.min(w,Math.floor(x1)-2),iy1=Math.min(h,Math.floor(y1)-2);
|
||||||
|
if(ix1<=ix0||iy1<=iy0)return null;
|
||||||
|
const overlap=(ix1-ix0)*(iy1-iy0),frac=overlap/Math.max(1,w*h);if(frac<.20)return null;
|
||||||
|
const c=document.createElement('canvas');c.width=w;c.height=h;const cc=c.getContext('2d',{alpha:false});if(!cc)return null;cc.fillStyle='#050813';cc.fillRect(0,0,w,h);cc.imageSmoothingEnabled=true;try{cc.imageSmoothingQuality='high'}catch{};cc.drawImage(src.canvas,x0,y0,dw,dh);
|
||||||
|
const out=new Uint8ClampedArray(cc.getImageData(0,0,w,h).data),rects=[];
|
||||||
|
const add=(x,y,rw,rh)=>{x=Math.max(0,x|0);y=Math.max(0,y|0);rw=Math.min(w-x,rw|0);rh=Math.min(h-y,rh|0);if(rw>0&&rh>0)rects.push({x0:x,y0:y,w:rw,h:rh})};
|
||||||
|
add(0,0,w,iy0);add(0,iy1,w,h-iy1);add(0,iy0,ix0,iy1-iy0);add(ix1,iy0,w-ix1,iy1-iy0);
|
||||||
|
const exposed=rects.reduce((a,r)=>a+r.w*r.h,0);if(exposed>w*h*.80)return null;
|
||||||
|
return{out,rects,reusedPixels:w*h-exposed,exposedPixels:exposed,scale};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Palette / smooth colouring ───────────────────────────────────────────
|
||||||
|
const PALETTE_CURRENT=0,PALETTE_RAINBOW=1,PALETTE_MONO=2;
|
||||||
|
const FIELD_ESCAPED=1,FIELD_INTERIOR_PROVEN=2,FIELD_INTERIOR_LIKELY=3,FIELD_UNKNOWN=4;
|
||||||
|
const stops=[[0,[4,10,27]],[.11,[12,53,79]],[.25,[31,156,184]],[.38,[91,226,234]],[.50,[66,53,151]],[.62,[139,49,170]],[.73,[232,72,145]],[.84,[255,137,64]],[.93,[255,211,99]],[1,[255,250,223]]];
|
||||||
|
const COLOR_PHASES=2048,COLOR_MIXES=64;
|
||||||
|
function hsvRgb(h,sat,val){h=((h%1)+1)%1;const x=h*6,i=Math.floor(x),f=x-i,p=val*(1-sat),q=val*(1-sat*f),s=val*(1-sat*(1-f));let r,g,b;switch(i%6){case 0:r=val;g=s;b=p;break;case 1:r=q;g=val;b=p;break;case 2:r=p;g=val;b=s;break;case 3:r=p;g=q;b=val;break;case 4:r=s;g=p;b=val;break;default:r=val;g=p;b=q}return[r*255,g*255,b*255]}
|
||||||
|
function basePalette(kind,t){
|
||||||
|
t=((t%1)+1)%1;
|
||||||
|
if(kind===PALETTE_RAINBOW)return hsvRgb(t,.92,1);
|
||||||
|
if(kind===PALETTE_MONO){const g=22+233*(.5-.5*Math.cos(Math.PI*2*t));return[g,g,g]}
|
||||||
|
let a=stops[0],b=stops[stops.length-1];for(let j=1;j<stops.length;j++){if(t<=stops[j][0]){a=stops[j-1];b=stops[j];break}}
|
||||||
|
let f=(t-a[0])/(b[0]-a[0]||1);f=f*f*(3-2*f);return[a[1][0]+(b[1][0]-a[1][0])*f,a[1][1]+(b[1][1]-a[1][1])*f,a[1][2]+(b[1][2]-a[1][2])*f]
|
||||||
|
}
|
||||||
|
function buildColorLut(kind){const lut=new Uint8Array(COLOR_PHASES*3);for(let pi=0;pi<COLOR_PHASES;pi++){const u=pi/(COLOR_PHASES-1),t=kind===PALETTE_CURRENT?(u<=.5?u*2:2-u*2):u,c=basePalette(kind,t),k=pi*3;lut[k]=c[0]|0;lut[k+1]=c[1]|0;lut[k+2]=c[2]|0}return lut}
|
||||||
|
const COLOR_LUTS=[buildColorLut(PALETTE_CURRENT),buildColorLut(PALETTE_RAINBOW),buildColorLut(PALETTE_MONO)];
|
||||||
|
const SMOOTH_U_MIN=2,SMOOTH_U_STEP=1/128,SMOOTH_U_N=4097,SMOOTH_CORR=new Float32Array(SMOOTH_U_N);
|
||||||
|
for(let i=0;i<SMOOTH_U_N;i++){const u=SMOOTH_U_MIN+i*SMOOTH_U_STEP;SMOOTH_CORR[i]=1-Math.log2(.5*u)}
|
||||||
|
const COLOR_CTX_CACHE=new Map();
|
||||||
|
function makeColorCtx(iter){let hit=COLOR_CTX_CACHE.get(iter);if(hit)return hit;const mixBin=new Uint8Array(iter+1),den=Math.log1p(Math.max(8,iter));for(let n=0;n<=iter;n++){const edge=Math.max(0,Math.min(1,Math.log1p(n)/den));mixBin[n]=Math.min(COLOR_MIXES-1,Math.round((COLOR_MIXES-1)*Math.pow(edge,.38)))}hit={mixBin};COLOR_CTX_CACHE.set(iter,hit);if(COLOR_CTX_CACHE.size>8)COLOR_CTX_CACHE.delete(COLOR_CTX_CACHE.keys().next().value);return hit}
|
||||||
|
function smoothEscape(n,m){const u=Math.log2(Math.max(4.0000001,m));let corr;const fi=(u-SMOOTH_U_MIN)/SMOOTH_U_STEP;if(fi>=0&&fi<SMOOTH_U_N-1){const i=fi|0,f=fi-i;corr=SMOOTH_CORR[i]+(SMOOTH_CORR[i+1]-SMOOTH_CORR[i])*f}else corr=1-Math.log2(.5*u);return n+corr}
|
||||||
|
function putPaletteColor(out,oi,sm,n,iter,colorCtx){let phase=state.shift+sm*state.cycle;phase-=Math.floor(phase);const pi=Math.min(COLOR_PHASES-1,(phase*COLOR_PHASES)|0),mi=colorCtx.mixBin[Math.min(iter,n)],mix=.34+.66*(mi/(COLOR_MIXES-1)),kind=state.palette,lut=COLOR_LUTS[kind]||COLOR_LUTS[0],k=pi*3,f0=kind===PALETTE_MONO?8:2,f1=kind===PALETTE_MONO?8:5,f2=kind===PALETTE_MONO?8:15;out[oi]=(f0+(lut[k]-f0)*mix)|0;out[oi+1]=(f1+(lut[k+1]-f1)*mix)|0;out[oi+2]=(f2+(lut[k+2]-f2)*mix)|0;out[oi+3]=255}
|
||||||
|
function putFastColor(out,oi,n,m,iter,colorCtx){putPaletteColor(out,oi,smoothEscape(n,m),n,iter,colorCtx)}
|
||||||
|
function makeField(size,iter){return{smooth:new Float32Array(size),iterations:new Uint32Array(size),classes:new Uint8Array(size),confidence:new Uint8Array(size),iter}}
|
||||||
|
function fieldConfidence(kind){return kind===FIELD_ESCAPED||kind===FIELD_INTERIOR_PROVEN?255:kind===FIELD_INTERIOR_LIKELY?192:0}
|
||||||
|
function putField(field,index,n,m,kind){field.classes[index]=kind;if(field.iterations)field.iterations[index]=Math.max(0,n)>>>0;if(field.confidence)field.confidence[index]=fieldConfidence(kind);if(kind===FIELD_ESCAPED)field.smooth[index]=smoothEscape(n,m);else field.smooth[index]=NaN}
|
||||||
|
function fillFieldConfidence(field){if(!field.confidence)field.confidence=new Uint8Array(field.classes.length);for(let i=0;i<field.classes.length;i++)field.confidence[i]=fieldConfidence(field.classes[i])}
|
||||||
|
function colorizeField(field){const out=new Uint8ClampedArray(field.classes.length*4),colorCtx=makeColorCtx(field.iter);for(let i=0,oi=0;i<field.classes.length;i++,oi+=4){const kind=field.classes[i];if(kind!==FIELD_ESCAPED){const neutral=kind===FIELD_UNKNOWN;out[oi]=neutral?20:0;out[oi+1]=neutral?22:0;out[oi+2]=neutral?30:0;out[oi+3]=255;continue}const sm=field.smooth[i],n=Math.max(0,Math.min(field.iter,Math.floor(sm)));putPaletteColor(out,oi,sm,n,field.iter,colorCtx)}return out}
|
||||||
|
|
||||||
|
// Embedded WebAssembly: SIMD-optimized kernel with a scalar fallback.
|
||||||
|
|
||||||
|
|
||||||
|
let wasm=null;
|
||||||
|
function instantiateWasm(b64){
|
||||||
|
const module=b64 instanceof WebAssembly.Module?b64:(()=>{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return new WebAssembly.Module(bytes)})(),inst=new WebAssembly.Instance(module,{}),ex=inst.exports;
|
||||||
|
return {module,ex,counts:()=>new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:()=>new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),refsR:()=>new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001),refsI:()=>new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001)};
|
||||||
|
}
|
||||||
|
try{wasm=instantiateWasm(WASM_SIMD_B64);wasm.simd=globalThis.MANDEL_HOSTED_SHALLOW_SIMD!==false}catch(e){try{wasm=instantiateWasm(WASM_SCALAR_B64);wasm.simd=false}catch(_e){wasm=null}}
|
||||||
|
let shallowWorker=null,shallowWorkerUrl=null,shallowWorkerBusy=false,shallowRecycle=null;
|
||||||
|
function shallowWorkerSource(){return `'use strict';let ex=null;function smooth(n,m){const u=Math.log2(Math.max(4.0000001,m));return n+1-Math.log2(.5*u)}function likely(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}self.onmessage=e=>{const d=e.data;if(d.type==='init'){ex=new WebAssembly.Instance(d.module,{}).exports;postMessage({type:'ready'});return}if(d.type!=='render'||!ex)return;try{const scale=d.sp/d.w,npx=ex.render_rows(d.cre+scale*.5,d.cim-scale*.5,d.sp,d.w,d.h,d.y,d.rows,d.iter),counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),field=d.fieldBuffer&&d.fieldBuffer.byteLength>=npx*4?new Float32Array(d.fieldBuffer,0,npx):new Float32Array(npx),iterations=d.iterationBuffer&&d.iterationBuffer.byteLength>=npx*4?new Uint32Array(d.iterationBuffer,0,npx):new Uint32Array(npx),classes=d.classBuffer&&d.classBuffer.byteLength>=npx?new Uint8Array(d.classBuffer,0,npx):new Uint8Array(npx);for(let i=0;i<npx;i++){const n=counts[i],x=i%d.w,y=d.y+((i/d.w)|0),cr=d.cre+(x+.5-d.w*.5)*scale,ci=d.cim+(d.h*.5-y-.5)*scale;iterations[i]=n;if(n<d.iter){classes[i]=1;field[i]=smooth(n,mags[i])}else{classes[i]=likely(cr,ci)?3:4;field[i]=NaN}}postMessage({type:'render',jobId:d.jobId,y:d.y,rows:d.rows,field:field.buffer,iterations:iterations.buffer,classes:classes.buffer},[field.buffer,iterations.buffer,classes.buffer])}catch(error){postMessage({type:'render',jobId:d.jobId,error:String(error&&error.message||error)})}}`}
|
||||||
|
function ensureShallowWorker(){if(shallowWorker||!wasm||typeof Worker==='undefined'||typeof Blob==='undefined')return!!shallowWorker;try{shallowWorkerUrl=URL.createObjectURL(new Blob([shallowWorkerSource()],{type:'text/javascript'}));shallowWorker=new Worker(shallowWorkerUrl);shallowWorker.postMessage({type:'init',module:wasm.module});return true}catch{shallowWorker=null;return false}}
|
||||||
|
function destroyShallowWorker(){if(shallowWorker){try{shallowWorker.terminate()}catch{}shallowWorker=null}if(shallowWorkerUrl){try{URL.revokeObjectURL(shallowWorkerUrl)}catch{}shallowWorkerUrl=null}shallowWorkerBusy=false;shallowRecycle=null}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function instantiateDeepWasm(b64){
|
||||||
|
const module=b64 instanceof WebAssembly.Module?b64:(()=>{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return new WebAssembly.Module(bytes)})();
|
||||||
|
const ex=new WebAssembly.Instance(module,{}).exports;
|
||||||
|
return {ex,counts:()=>new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:()=>new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),refsR:()=>new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001),refsI:()=>new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001)};
|
||||||
|
}
|
||||||
|
let deepWasm=null,deepWasmTried=false,deepModuleBundle=null,deepModulePromise=null;
|
||||||
|
async function compileKernelSource(source){if(source instanceof WebAssembly.Module)return source;if(/^https?:/i.test(source)){const response=await fetch(source,{cache:'force-cache'});if(!response.ok)throw new Error('WASM request failed '+response.status);if(WebAssembly.compileStreaming){try{return await WebAssembly.compileStreaming(Promise.resolve(response.clone()))}catch{}}return WebAssembly.compile(await response.arrayBuffer())}const raw=atob(source),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return WebAssembly.compile(bytes)}
|
||||||
|
async function compileKernelPair(simd,scalar){try{return{module:await compileKernelSource(simd),simd:true}}catch{return{module:await compileKernelSource(scalar),simd:false}}}
|
||||||
|
function prepareDeepModules(){if(deepModuleBundle)return Promise.resolve(deepModuleBundle);if(!deepModulePromise)deepModulePromise=Promise.all([compileKernelPair(DEEP_SIMD_B64,DEEP_SCALAR_B64),compileKernelPair(BLA_SIMD_B64,BLA_SCALAR_B64),compileKernelPair(COLOR_SIMD_B64,COLOR_SCALAR_B64)]).then(([deep,bla,color])=>deepModuleBundle={deep,bla,color}).catch(error=>{deepModulePromise=null;throw error});return deepModulePromise}
|
||||||
|
function ensureDeepWasm(){
|
||||||
|
if(deepWasm)return true;if(deepWasmTried)return false;deepWasmTried=true;if(deepModuleBundle){try{deepWasm=instantiateDeepWasm(deepModuleBundle.deep.module);deepWasm.simd=deepModuleBundle.deep.simd}catch{deepWasm=null}return!!deepWasm}if(/^https?:/i.test(DEEP_SIMD_B64)||/^https?:/i.test(DEEP_SCALAR_B64))return false;
|
||||||
|
try{deepWasm=instantiateDeepWasm(DEEP_SIMD_B64);deepWasm.simd=true}catch(e){try{deepWasm=instantiateDeepWasm(DEEP_SCALAR_B64);deepWasm.simd=false}catch(_e){deepWasm=null}}
|
||||||
|
return!!deepWasm
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Deep renderer worker pool. Each worker owns a WASM instance and performs both the
|
||||||
|
// perturbation kernel and colour mapping, so colourful frames no longer funnel all
|
||||||
|
// post-processing through the UI thread. Jobs are striped dynamically for load balance.
|
||||||
|
const deepPool={workers:[],url:null,activeToken:0,serial:0,failed:false,current:null,maxWorkers:0,benchTimer:0};
|
||||||
|
// ── Persistent deep-render workers / BLA kernel ──────────────────────────
|
||||||
|
function deepWorkerSource(){
|
||||||
|
return `'use strict';
|
||||||
|
let core=null,blaCore=null,colorCore=null,refKey='',refLoaded=0,RR=new Float64Array(150001),RI=new Float64Array(150001),blaRefKey='',blaRefLoaded=0,blaBuiltKey='',lastBlaBuildMs=0;
|
||||||
|
function ensure(){if(!core)throw new Error('deep module not initialized');return core}
|
||||||
|
function ensureBla(){if(!blaCore)throw new Error('BLA module not initialized');return blaCore}
|
||||||
|
function ensureColor(){if(!colorCore)throw new Error('color module not initialized');return colorCore}
|
||||||
|
function applyRef(d,ex){if(d.refKey!==refKey){refKey=d.refKey;refLoaded=0;blaRefKey='';blaRefLoaded=0;blaBuiltKey=''}if(d.rr){const ar=new Float64Array(d.rr),ai=new Float64Array(d.ri),st=d.rrStart|0;RR.set(ar,st);RI.set(ai,st);new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ar,st);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ai,st);refLoaded=Math.max(refLoaded,st+ar.length);if(blaCore&&blaRefKey===refKey){const bx=blaCore.ex;new Float64Array(bx.memory.buffer,bx.refs_r_ptr(),150001).set(ar,st);new Float64Array(bx.memory.buffer,bx.refs_i_ptr(),150001).set(ai,st);blaRefLoaded=Math.max(blaRefLoaded,st+ar.length);blaBuiltKey=''}}if(refLoaded<d.refLen+1)throw new Error('reference cache miss')}
|
||||||
|
function syncRefsToBla(d,key=d.blaKey,eps=d.blaEps){if(!d.useBla)return null;const b=ensureBla(),ex=b.ex;if(blaRefKey!==refKey){new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(RR.subarray(0,refLoaded));new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(RI.subarray(0,refLoaded));blaRefKey=refKey;blaRefLoaded=refLoaded;blaBuiltKey=''}else if(refLoaded>blaRefLoaded){new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(RR.subarray(blaRefLoaded,refLoaded),blaRefLoaded);blaRefLoaded=refLoaded;blaBuiltKey=''}lastBlaBuildMs=0;if(blaBuiltKey!==key){const t=performance.now(),levels=ex.build_bla(d.refLen,d.cMax,eps);lastBlaBuildMs=performance.now()-t;if(!levels)return null;blaBuiltKey=key}return b}
|
||||||
|
function smoothBatch(srcMags,npx){const c=ensureColor(),ex=c.ex,mi=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),co=new Float32Array(ex.memory.buffer,ex.corr_ptr(),65536);mi.set(srcMags.subarray(0,npx),0);ex.smooth_batch(npx);return co}
|
||||||
|
function strictOne(ex,x,y,d){const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536);ex.render_perturb_rebase_rect(d.spanMant,d.spanBucket,d.offR,d.offI,d.offBucket,d.refLen,d.w,d.h,d.cx,d.cy,x,y,1,1,d.iter,0,0,0,0,0,0,0);return[counts[0],mags[0]]}
|
||||||
|
function stats(bx){return{blaSteps:bx.stat_bla_steps?bx.stat_bla_steps():0,ptbSteps:bx.stat_ptb_steps?bx.stat_ptb_steps():0,rebases:bx.stat_rebases?bx.stat_rebases():0,interior:bx.stat_interior?bx.stat_interior():0,unresolved:bx.stat_unresolved?bx.stat_unresolved():0,fail:bx.stat_fail?bx.stat_fail():0}}
|
||||||
|
function renderBla(d,b,key,eps,bcap,pcap){const bx=b.ex;if(key!==d.blaKey||eps!==d.blaEps)b=syncRefsToBla(d,key,eps)||b;const t=performance.now(),npx=bx.render_bla_rect_v2?bx.render_bla_rect_v2(d.spanNormal,d.offRn,d.offIn,d.baseCr,d.baseCi,d.refLen,d.w,d.h,d.x0||0,d.y0,d.rectW||d.w,d.rows,d.iter,bcap||0,pcap||0,1):bx.render_bla_rect(d.spanNormal,d.offRn,d.offIn,d.refLen,d.w,d.h,d.x0||0,d.y0,d.rectW||d.w,d.rows,d.iter);return{npx,kernelMs:performance.now()-t,counts:new Uint32Array(bx.memory.buffer,bx.counts_ptr(),65536),mags:new Float64Array(bx.memory.buffer,bx.mags_ptr(),65536),stats:stats(bx)}}
|
||||||
|
function verifyAgainstSafe(d,result,rw,npx,b){const want=d.verifySamples||0;if(!want||!b)return{samples:0,mismatch:0};const counts=Uint32Array.from(result.counts.subarray(0,npx)),mags=Float64Array.from(result.mags.subarray(0,npx));result.counts=counts;result.mags=mags;const picks=[],seen=new Set(),add=i=>{if(picks.length>=want)return;i=Math.max(0,Math.min(npx-1,i|0));if(!seen.has(i)){seen.add(i);picks.push(i)}};for(let k=0;k<Math.max(2,want>>1);k++)add(((k+.37)*npx/Math.max(2,want>>1))|0);const stride=Math.max(1,Math.floor(npx/Math.max(16,want*10))),top=[];for(let i=stride;i<npx;i+=stride){const n=counts[i],p=counts[i-stride];if(n<0xfffffffe&&p<0xfffffffe&&((n>=d.iter)!==(p>=d.iter))){add(i);add(i-stride)}if(n<d.iter&&n<0xfffffffe){top.push([n,i]);top.sort((a,b)=>b[0]-a[0]);if(top.length>want)top.length=want}}for(const x of top)add(x[1]);for(let k=0;picks.length<want&&k<want*2;k++)add(((k+.73)*npx/want)|0);let mismatch=0;const bad=[];for(const i of picks){const bn=counts[i];if(bn>=0xfffffffe)continue;const x=(d.x0||0)+(i%rw),y=d.y0+((i/rw)|0),sr=renderBla({...d,x0:x,y0:y,rectW:1,rows:1},b,d.safeBlaKey||d.blaKey,d.safeBlaEps||d.blaEps,0,0),sn=sr.counts[0];if(sn>=0xfffffffe)continue;if((bn>=d.iter)!==(sn>=d.iter)||Math.abs((bn|0)-(sn|0))>(d.verifyDelta||64)){mismatch++;bad.push(i)}}return{samples:picks.length,mismatch,bad}}
|
||||||
|
self.onmessage=async e=>{const d=e.data;if(!d)return;if(d.type==='init'){try{core={ex:new WebAssembly.Instance(d.modules.deep.module,{}).exports,simd:!!d.modules.deep.simd};blaCore={ex:new WebAssembly.Instance(d.modules.bla.module,{}).exports,simd:!!d.modules.bla.simd};colorCore={ex:new WebAssembly.Instance(d.modules.color.module,{}).exports,simd:!!d.modules.color.simd};postMessage({type:'ready'})}catch(error){postMessage({type:'ready',error:String(error&&error.message||error)})}return}if(d.type!=='render'&&d.type!=='pilot'&&d.type!=='realBench')return;try{const c=ensure(),ex=c.ex;applyRef(d,ex);const b=syncRefsToBla(d);if((d.type==='pilot'||d.type==='realBench')&&!b)throw new Error('BLA unavailable');if(d.type==='pilot'||d.type==='realBench'){const reps=d.type==='realBench'?Math.max(1,d.repeats|0):1;let rr=null,total=0;for(let k=0;k<reps;k++){rr=renderBla({...d,x0:0,y0:0,rectW:d.w,rows:d.h},b,d.blaKey,d.blaEps,0,0);total+=rr.kernelMs}if(d.type==='realBench'){postMessage({type:'realBench',benchId:d.benchId,kernelMs:total,pixels:rr.npx*reps,blaBuildMs:lastBlaBuildMs,simd:b.simd});return}const co=rr.counts;let black=0,bad=0,sum=0;for(let i=0;i<rr.npx;i++){const n=co[i];if(n>=0xfffffffe){bad++;continue}sum+=Math.min(d.iter,n);if(n>=d.iter)black++}postMessage({type:'pilot',pilotId:d.pilotId,kernelMs:total,pixels:rr.npx,blaBuildMs:lastBlaBuildMs,blackRatio:black/Math.max(1,rr.npx),badRatio:bad/Math.max(1,rr.npx),meanIter:sum/Math.max(1,rr.npx-bad),stats:rr.stats,simd:b.simd});return}
|
||||||
|
const rx=d.x0||0,rw=d.rectW||d.w,field=d.fieldBuffer&&d.fieldBuffer.byteLength>=rw*d.rows*4?new Float32Array(d.fieldBuffer,0,rw*d.rows):new Float32Array(rw*d.rows),iterations=d.iterationBuffer&&d.iterationBuffer.byteLength>=rw*d.rows*4?new Uint32Array(d.iterationBuffer,0,rw*d.rows):new Uint32Array(rw*d.rows),classes=d.classBuffer&&d.classBuffer.byteLength>=rw*d.rows?new Uint8Array(d.classBuffer,0,rw*d.rows):new Uint8Array(rw*d.rows),bad=[];let result,repaired=0,verifyMs=0;if(b){result=renderBla(d,b,d.blaKey,d.blaEps,d.maxBlaSteps||0,d.maxPtbSteps||0);let unresolved=result.stats.unresolved||0;if(unresolved&&d.covered){const co=result.counts,ma=result.mags;for(let i=0;i<result.npx;i++)if(co[i]===0xfffffffe){const x=rx+(i%rw),y=d.y0+((i/rw)|0),r=strictOne(ex,x,y,d);co[i]=r[0];ma[i]=r[1]}}
|
||||||
|
const tv=performance.now();let v=verifyAgainstSafe(d,result,rw,result.npx,b);verifyMs=performance.now()-tv;if(v.mismatch){const safeKey=d.safeBlaKey||d.blaKey,safeEps=d.safeBlaEps||d.blaEps;result=renderBla(d,b,safeKey,safeEps,0,0);repaired=1}
|
||||||
|
}else{const t=performance.now(),npx=ex.render_perturb_rebase_rect(d.spanMant,d.spanBucket,d.offR,d.offI,d.offBucket,d.refLen,d.w,d.h,d.cx,d.cy,rx,d.y0,rw,d.rows,d.iter,d.skip||0,d.Ar||0,d.Ai||0,d.Ab||0,d.Br||0,d.Bi||0,d.Bb||0);result={npx,kernelMs:performance.now()-t,counts:new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),stats:{blaSteps:0,ptbSteps:0,rebases:0,interior:0,unresolved:0,fail:0}}}
|
||||||
|
const corr=smoothBatch(result.mags,result.npx);let blackCount=0,sumIter=0;for(let i=0;i<result.npx;i++){let n=result.counts[i],m=result.mags[i];const x=rx+(i%rw),y=d.y0+((i/rw)|0);if(n>=0xfffffffe){if(n===0xffffffff){const r=strictOne(ex,x,y,d);n=r[0];m=r[1]}else{n=d.iter;m=0}}iterations[i]=n;if(n===0xffffffff){classes[i]=4;field[i]=NaN;bad.push(i);continue}sumIter+=Math.min(d.iter,n);if(n>=d.iter){classes[i]=4;field[i]=NaN;blackCount++;continue}classes[i]=1;field[i]=n+corr[i]}
|
||||||
|
postMessage({type:'render',jobId:d.jobId,x0:rx,rectW:rw,y0:d.y0,rows:d.rows,field:field.buffer,iterations:iterations.buffer,classes:classes.buffer,bad,simd:c.simd,bla:!!b,blaSimd:b?b.simd:false,colorSimd:colorCore?colorCore.simd:false,kernelMs:result.kernelMs,verifyMs,blaBuildMs:lastBlaBuildMs,pixels:result.npx,blackCount,sumIter,stats:result.stats,verifySamples:d.verifySamples||0,repaired},[field.buffer,iterations.buffer,classes.buffer])}catch(err){postMessage({type:'render',jobId:d.jobId,error:String(err&&err.message||err)})}};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
function destroyDeepPool(){
|
||||||
|
if(deepPool.current)deepPool.current.cancelled=true;deepPool.current=null;
|
||||||
|
for(const w of deepPool.workers){try{w.terminate()}catch{}}
|
||||||
|
deepPool.workers.length=0;if(deepPool.url){try{URL.revokeObjectURL(deepPool.url)}catch{}deepPool.url=null}
|
||||||
|
deepPool.activeToken=0;deepPool.maxWorkers=0
|
||||||
|
}
|
||||||
|
function retireDeepAssets(){destroyDeepPool();deepPool.failed=false;deepWasm=null;deepWasmTried=false;deepModuleBundle=null;deepModulePromise=null;referenceCache.rr=null;referenceCache.ri=null;referenceCache.series=null;referenceCache.n=0;referenceCache.escape=0;referenceCache.loadedLen=0;referenceCache.conditionLog2=0;referenceCache.derivative=[0,0,NEG_BUCKET]}
|
||||||
|
function cancelDeepPoolJob(){
|
||||||
|
// Logical cancellation: keep Worker, WASM, reference and BLA caches alive.
|
||||||
|
deepPool.activeToken=0;
|
||||||
|
if(deepPool.current){deepPool.current.cancelled=true;deepPool.current=null}
|
||||||
|
}
|
||||||
|
function handleDeepWorkerMessage(worker,d){
|
||||||
|
const job=worker._job;worker._job=null;worker._busy=false;
|
||||||
|
if(d&&d.type==='ready'){if(d.error){worker._ready=false;deepPool.failed=true;destroyDeepPool();return}worker._ready=true;kickDeepWorker(worker);return}
|
||||||
|
if(!job){kickDeepWorker(worker);return}
|
||||||
|
if(job.type==='bench'){if(d&&d.type==='bench'&&d.benchId===job.benchId)job.resolve(d);else job.reject(new Error('benchmark reply mismatch'));kickDeepWorker(worker);return}
|
||||||
|
if(job.type==='pilot'||job.type==='realBench'){if(d&&((job.type==='pilot'&&d.type==='pilot'&&d.pilotId===job.id)||(job.type==='realBench'&&d.type==='realBench'&&d.benchId===job.id)))job.resolve(d);else job.reject(new Error(job.type+' reply mismatch'));kickDeepWorker(worker);return}
|
||||||
|
if(job.type==='render')job.runner.onResult(worker,d,job);
|
||||||
|
kickDeepWorker(worker)
|
||||||
|
}
|
||||||
|
function handleDeepWorkerError(worker,e){
|
||||||
|
const job=worker._job;worker._job=null;worker._busy=false;
|
||||||
|
if(job&&(job.type==='bench'||job.type==='pilot'||job.type==='realBench'))job.reject(e instanceof Error?e:new Error('worker job failed'));
|
||||||
|
else if(job&&job.type==='render')job.runner.fail(e);
|
||||||
|
else{deepPool.failed=true;destroyDeepPool()}
|
||||||
|
}
|
||||||
|
function deepWorkerLimit(){const hc=Math.max(1,navigator.hardwareConcurrency||4),memoryLimited=Number(navigator.deviceMemory||8)<=4?1:2;return Math.max(1,Math.min(memoryLimited,hc>2?hc-1:1))}
|
||||||
|
function addDeepWorker(){if(!deepModuleBundle)throw new Error('deep modules are not ready');const i=deepPool.workers.length,w=new Worker(deepPool.url);w._index=i;w._refKey='';w._refLoaded=0;w._busy=false;w._ready=false;w._job=null;w._recycle=null;w.onmessage=e=>handleDeepWorkerMessage(w,e.data);w.onerror=e=>handleDeepWorkerError(w,e);deepPool.workers.push(w);w.postMessage({type:'init',modules:deepModuleBundle});return w}
|
||||||
|
function ensureDeepPool(){
|
||||||
|
if(deepPool.failed||!deepModuleBundle||typeof Worker==='undefined'||typeof Blob==='undefined')return false;
|
||||||
|
if(deepPool.workers.length)return true;
|
||||||
|
try{
|
||||||
|
const count=deepWorkerLimit();
|
||||||
|
deepPool.url=URL.createObjectURL(new Blob([deepWorkerSource()],{type:'text/javascript'}));
|
||||||
|
deepPool.maxWorkers=count;deepWisdom.maxWorkers=count;deepWisdom.workerCount=1;addDeepWorker();
|
||||||
|
return true
|
||||||
|
}catch(e){deepPool.failed=true;destroyDeepPool();return false}
|
||||||
|
}
|
||||||
|
function prewarmDeepAssets(){prepareDeepModules().then(()=>{if(deepResolutionRatio()<=128)ensureDeepPool()}).catch(()=>{})}
|
||||||
|
function kickDeepWorker(worker){
|
||||||
|
if(worker._busy||!worker._ready)return;
|
||||||
|
const r=deepPool.current;if(!r||r.cancelled||r.token!==state.token)return;
|
||||||
|
const active=Math.max(1,Math.min(deepWisdom.ready?deepWisdom.workerCount:Math.min(2,deepPool.workers.length),deepPool.workers.length));
|
||||||
|
if(worker._index>=active)return;
|
||||||
|
r.dispatch(worker)
|
||||||
|
}
|
||||||
|
function attachReference(worker,msg,ref,refLen,refKey,transfer){
|
||||||
|
let start=worker._refKey===refKey?worker._refLoaded:0;start=Math.max(0,Math.min(start,refLen+1));
|
||||||
|
if(start<refLen+1){const rr=ref.rr.slice(start,refLen+1),ri=ref.ri.slice(start,refLen+1);msg.rr=rr.buffer;msg.ri=ri.buffer;msg.rrStart=start;transfer.push(rr.buffer,ri.buffer);worker._refKey=refKey;worker._refLoaded=refLen+1}
|
||||||
|
}
|
||||||
|
function blaProfile(profile){
|
||||||
|
const z=zoomExp(),exp=profile.covered?32:(z<16?28:23);
|
||||||
|
// Fast pass + local verification/repair is cheaper than making the entire frame
|
||||||
|
// conservative. Safe strips are rebuilt at e-48 only when a probe disagrees.
|
||||||
|
const safeExp=z<16?48:Math.min(48,Math.max(32,exp+8));
|
||||||
|
return{exp,eps:Math.pow(2,-exp),safeExp}
|
||||||
|
}
|
||||||
|
function runDeepPool(profile,snap,w,h,iter,colorIter,token,t0,out,sb,refC,off,ref,refLen,series,onFallback,rects=null){
|
||||||
|
if(!ensureDeepPool())return false;if(deepPool.current)deepPool.current.cancelled=true;
|
||||||
|
const centered=pixelCenteredOffset(snap,refC,w),workers=deepPool.workers,refKey=String(ref.id),bad=[],field=makeField(w*h,colorIter),spanNormal=fixedNum(snap.span,snap.bits),offRn=fixedNum(centered.r,snap.bits),offIn=fixedNum(centered.i,snap.bits),baseCr=fixedNum(refC.re,snap.bits),baseCi=fixedNum(refC.im,snap.bits);
|
||||||
|
const cMaxRaw=Math.hypot(offRn,offIn)+Math.abs(spanNormal)*Math.hypot(.5,h/(2*Math.max(1,w))),cBucket=cMaxRaw>0&&Number.isFinite(cMaxRaw)?Math.ceil(Math.log2(cMaxRaw)*8):0,cMaxSafe=cMaxRaw>0?Math.pow(2,cBucket/8):0,bp=blaProfile(profile),useBla=Number.isFinite(spanNormal)&&Math.abs(spanNormal)>=1e-280&&refLen>8,wp=workProfile(profile),colorCtx=makeColorCtx(colorIter);
|
||||||
|
const regions=(rects?rects:[{x0:0,y0:0,w,h}]).map(r=>({x0:Math.max(0,r.x0|0),y0:Math.max(0,r.y0|0),w:Math.max(0,Math.min(w-(r.x0|0),r.w|0)),h:Math.max(0,Math.min(h-(r.y0|0),r.h|0))})).filter(r=>r.w>0&&r.h>0),chunks=[];let preferredRows=deepWisdom.stripRows||16;if(deepWisdom.rowMsEMA>0)preferredRows=Math.round(deepWisdom.targetStripMs/deepWisdom.rowMsEMA);for(const r of regions){const rows=Math.max(2,Math.min(Math.max(1,Math.floor(65536/Math.max(1,r.w))),profile.covered?Math.max(4,preferredRows):Math.min(36,preferredRows)));for(let y=r.y0;y<r.y0+r.h;y+=rows)chunks.push({x0:r.x0,y0:y,w:r.w,rows:Math.min(rows,r.y0+r.h-y)})}chunks.sort((a,b)=>{const ad=Math.hypot((a.x0+a.w*.5)/w-state.focusX,(a.y0+a.rows*.5)/h-state.focusY),bd=Math.hypot((b.x0+b.w*.5)/w-state.focusX,(b.y0+b.rows*.5)/h-state.focusY);return ad-bd});
|
||||||
|
const totalPixels=regions.reduce((a,r)=>a+r.w*r.h,0),verifyJobs=profile.covered?8:3;
|
||||||
|
const runner={token,cancelled:false,chunkIndex:0,donePixels:0,totalPixels,failed:false,finishing:false,simd:true,blaUsed:false,kernelMs:0,verifyMs:0,blaBuildMs:0,sumIter:0,blackCount:0,badCount:0,blaSteps:0,ptbSteps:0,rebases:0,interior:0,unresolved:0,repaired:0,verifiedBuckets:new Set(),
|
||||||
|
fail(err){if(this.failed||this.cancelled)return;this.failed=true;deepPool.failed=true;destroyDeepPool();if(token===state.token)onFallback()},
|
||||||
|
nextChunk(){return this.chunkIndex<chunks.length?chunks[this.chunkIndex++]:null},
|
||||||
|
dispatch(worker){
|
||||||
|
if(this.cancelled||this.failed||token!==state.token)return false;const ch=this.nextChunk();if(!ch){this.maybeFinish();return false}
|
||||||
|
const jobId=token+':'+(++deepPool.serial),vb=Math.min(verifyJobs-1,Math.max(0,Math.floor((ch.y0+ch.rows*.5)*verifyJobs/Math.max(1,h)))),verifySamples=useBla&&!this.verifiedBuckets.has(vb)?(this.verifiedBuckets.add(vb),profile.covered?4:3):0,blaKey=refKey+':'+ref.version+':'+refLen+':'+cBucket+':e'+bp.exp,safeBlaKey=refKey+':'+ref.version+':'+refLen+':'+cBucket+':e'+bp.safeExp;
|
||||||
|
const msg={type:'render',jobId,refKey,refLen,w,h,x0:ch.x0,rectW:ch.w,y0:ch.y0,rows:ch.rows,iter,colorIter,covered:profile.covered,spanMant:sb.mant,spanBucket:sb.bucket,offR:off[0],offI:off[1],offBucket:off[2],cx:w*.5,cy:h*.5,skip:series.skip,Ar:series.Ar,Ai:series.Ai,Ab:series.Ab,Br:series.Br,Bi:series.Bi,Bb:series.Bb,shift:state.shift,cycle:state.cycle,palette:state.palette,useBla,blaEps:bp.eps,blaKey,safeBlaEps:Math.pow(2,-bp.safeExp),safeBlaKey,cMax:cMaxSafe,spanNormal,offRn,offIn,baseCr,baseCi,maxBlaSteps:wp.bla,maxPtbSteps:wp.ptb,verifySamples,verifyDelta:profile.covered?8:64},transfer=[];
|
||||||
|
attachReference(worker,msg,ref,refLen,refKey,transfer);if(worker._recycle){msg.fieldBuffer=worker._recycle.field;msg.iterationBuffer=worker._recycle.iterations;msg.classBuffer=worker._recycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);worker._recycle=null}worker._busy=true;worker._job={type:'render',runner:this,jobId,x0:ch.x0,y0:ch.y0,rectW:ch.w,rows:ch.rows,pixels:ch.w*ch.rows,started:performance.now()};try{worker.postMessage(msg,transfer)}catch(e){worker._busy=false;worker._job=null;this.fail(e);return false}return true
|
||||||
|
},
|
||||||
|
onResult(worker,d,job){
|
||||||
|
const elapsed=Math.max(.05,performance.now()-job.started),mpr=elapsed/Math.max(1,job.rows);deepWisdom.rowMsEMA=deepWisdom.rowMsEMA?deepWisdom.rowMsEMA*.86+mpr*.14:mpr;deepWisdom.stripRows=Math.round((deepWisdom.stripRows||12)*.75+Math.max(3,Math.min(128,Math.round(deepWisdom.targetStripMs/Math.max(.001,deepWisdom.rowMsEMA))))*.25);
|
||||||
|
if(this.cancelled||this.failed||token!==state.token)return;if(!d||d.jobId!==job.jobId)return;if(d.error){this.fail(new Error(d.error));return}
|
||||||
|
this.simd=this.simd&&!!d.simd;this.blaUsed=this.blaUsed||!!d.bla;this.kernelMs+=d.kernelMs||0;this.verifyMs+=d.verifyMs||0;this.blaBuildMs+=d.blaBuildMs||0;this.sumIter+=d.sumIter||0;this.blackCount+=d.blackCount||0;this.badCount+=(d.bad||[]).length;const st=d.stats||{};this.blaSteps+=st.blaSteps||0;this.ptbSteps+=st.ptbSteps||0;this.rebases+=st.rebases||0;this.interior+=st.interior||0;this.unresolved+=st.unresolved||0;this.repaired+=d.repaired||0;
|
||||||
|
const rw=d.rectW||job.rectW,rx=d.x0==null?job.x0:d.x0,sf=d.field?new Float32Array(d.field):null,it=d.iterations?new Uint32Array(d.iterations):null,cl=d.classes?new Uint8Array(d.classes):null,ro=sf&&cl?colorizeField({smooth:sf,classes:cl,iter:colorIter}):new Uint8ClampedArray(d.out);for(let yy=0;yy<d.rows;yy++){const dst=(d.y0+yy)*w+rx;out.set(ro.subarray(yy*rw*4,(yy+1)*rw*4),dst*4);if(sf)field.smooth.set(sf.subarray(yy*rw,(yy+1)*rw),dst);if(it)field.iterations.set(it.subarray(yy*rw,(yy+1)*rw),dst);if(cl)field.classes.set(cl.subarray(yy*rw,(yy+1)*rw),dst)}presentPartialStripe(ro,rx,d.y0,rw,d.rows,w,h,snap);if(d.field&&d.iterations&&d.classes)worker._recycle={field:d.field,iterations:d.iterations,classes:d.classes};if(d.bad)for(const local of d.bad){const gx=rx+(local%rw),gy=d.y0+((local/rw)|0);bad.push(gy*w+gx)}this.donePixels+=job.pixels;if(profile.covered&&workers.length<deepPool.maxWorkers&&memoryLedger().managedBytes+24*1048576<rendererMemoryBudget()&&this.totalPixels-this.donePixels>job.pixels*4&&elapsed>deepWisdom.targetStripMs){const added=addDeepWorker();deepWisdom.workerCount=workers.length;deepWisdom.ready=true;kickDeepWorker(added)}this.maybeFinish()
|
||||||
|
},
|
||||||
|
maybeFinish(){
|
||||||
|
if(this.cancelled||this.failed||token!==state.token||this.finishing||this.donePixels<this.totalPixels)return;this.finishing=true;if(deepPool.current===this)deepPool.current=null;deepPool.activeToken=0;const px=Math.max(1,this.totalPixels),kmpp=this.kernelMs/px;deepTelemetry.frames++;deepTelemetry.kernelMPP=deepTelemetry.kernelMPP?deepTelemetry.kernelMPP*.78+kmpp*.22:kmpp;deepTelemetry.meanIter=this.sumIter/px;deepTelemetry.blackRatio=this.blackCount/px;deepTelemetry.badRatio=this.badCount/px;deepTelemetry.blaBuildEMA=deepTelemetry.blaBuildEMA?deepTelemetry.blaBuildEMA*.85+this.blaBuildMs*.15:this.blaBuildMs;deepTelemetry.interiorRatio=this.interior/px;deepTelemetry.repairRatio=this.repaired/Math.max(1,regions.length);deepTelemetry.unresolvedRatio=this.unresolved/px;deepTelemetry.blaStepsPerPixel=this.blaSteps/px;deepTelemetry.ptbStepsPerPixel=this.ptbSteps/px;deepTelemetry.rebasePerPixel=this.rebases/px;deepTelemetry.lastKernelMs=this.kernelMs;deepTelemetry.lastVerifyMs=this.verifyMs;deepTelemetry.lastPixels=px;deepTelemetry.refDistance=refC.dist||0;refControl.lastMPP=kmpp;refControl.lastPixels=px;if(refControl.refId!==ref.id){refControl.refId=ref.id;refControl.baseMPP=kmpp}else if((refC.dist||0)<.2)refControl.baseMPP=refControl.baseMPP?refControl.baseMPP*.8+kmpp*.2:kmpp;persistDeepWisdom();
|
||||||
|
const label='WASM '+(this.simd?'SIMD':'scalar')+' ×'+Math.max(1,Math.min(deepWisdom.workerCount,workers.length))+' · '+(this.blaUsed?'BLA e-'+bp.exp+' + ':'')+'rebase'+(this.interior?' · 内部早期終了 '+Math.round(100*this.interior/px)+'%':'')+(this.repaired?' · 局所補修 '+this.repaired:'')+(this.totalPixels<w*h?' · 既存画像再利用 '+Math.round(100*(1-this.totalPixels/(w*h)))+'%':'');
|
||||||
|
const completeField=this.totalPixels===w*h?field:null;if(!bad.length){finishRender(token,t0,profile,snap,w,h,out,label,this.totalPixels,completeField);return}let k=0;const residual=async()=>{if(token!==state.token||this.cancelled)return;const deadline=performance.now()+5;while(k<bad.length&&performance.now()<deadline){const idx=bad[k++],x=idx%w,py=(idx/w)|0,result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,py,false,()=>token!==state.token||this.cancelled);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}if(k<bad.length)requestAnimationFrame(residual);else finishRender(token,t0,profile,snap,w,h,out,label+' · 残差 '+bad.length,this.totalPixels,completeField)};requestAnimationFrame(residual)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
deepPool.current=runner;deepPool.activeToken=token;if(totalPixels===0){queueMicrotask(()=>runner.maybeFinish());return true}const active=Math.max(1,Math.min(deepWisdom.ready?deepWisdom.workerCount:Math.min(2,workers.length),workers.length));for(let i=0;i<active;i++)kickDeepWorker(workers[i]);return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Frame reprojection / world-space detail cache ───────────────────────
|
||||||
|
const frameCanvas=document.createElement('canvas'),frameCtx=frameCanvas.getContext('2d',{alpha:false});
|
||||||
|
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
|
||||||
|
function snapshotToCurrent(s){if(s.bits===state.bits)return s;return{bits:state.bits,re:align(s.re,s.bits,state.bits),im:align(s.im,s.bits,state.bits),span:align(s.span,s.bits,state.bits)}}
|
||||||
|
function styleSignature(){return state.palette+':'+state.cycle.toFixed(6)+':'+state.shift.toFixed(5)}
|
||||||
|
function commitImage(data,w,h,snap,field=null){
|
||||||
|
frameCanvas.width=w;frameCanvas.height=h;const id=frameCtx.createImageData(w,h);id.data.set(data);frameCtx.putImageData(id,0,0);runtimeMetrics.canvasWrites++;
|
||||||
|
state.frameView=snapshotToCurrent(snap);state.fieldView=field?{field,w,h,snap:state.frameView}:null;invalidateView();
|
||||||
|
}
|
||||||
|
function presentPartialStripe(data,x,y,w,h,frameW,frameH,snap){if(frameCanvas.width!==frameW||frameCanvas.height!==frameH||!sameSnapshot(state.frameView,snap)){frameCanvas.width=frameW;frameCanvas.height=frameH;frameCtx.fillStyle='#050813';frameCtx.fillRect(0,0,frameW,frameH);state.frameView=snapshotToCurrent(snap);state.fieldView=null}const id=frameCtx.createImageData(w,h);id.data.set(data);frameCtx.putImageData(id,x,y);runtimeMetrics.canvasWrites++;invalidateView(false)}
|
||||||
|
function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span}
|
||||||
|
function recolorCurrentField(){const fv=state.fieldView;if(!fv||state.dirty||state.rendering||!sameSnapshot(fv.snap,state.frameView))return false;const out=colorizeField(fv.field);commitImage(out,fv.w,fv.h,fv.snap,fv.field);state.lastEngine='フィールド再彩色';state.lastRender=0;recolorCachedDetails();invalidateView();return true}
|
||||||
|
const validationJob={active:false,key:''};
|
||||||
|
const unknownContinuationJob={active:false,key:''};let unknownContinuationTimer=0;
|
||||||
|
function cancelUnknownContinuation(){if(unknownContinuationTimer){clearTimeout(unknownContinuationTimer);unknownContinuationTimer=0}unknownContinuationJob.active=false;state.continuationProgress=0}
|
||||||
|
function scheduleUnknownContinuation(delay=120){
|
||||||
|
const fv=state.fieldView;if(state.processMode!=='fine'||state.dirty||state.rendering||!fv||!state.unresolved)return;
|
||||||
|
const key=viewSpecKey(currentViewSpec())+':'+fv.w+'x'+fv.h+':'+fv.field.iter;
|
||||||
|
if(unknownContinuationJob.key===key||unknownContinuationTimer)return;
|
||||||
|
unknownContinuationTimer=setTimeout(()=>{unknownContinuationTimer=0;startUnknownContinuation(key)},delay)
|
||||||
|
}
|
||||||
|
function startUnknownContinuation(key){
|
||||||
|
const fv=state.fieldView;if(unknownContinuationJob.active||state.dirty||state.rendering||!fv||!sameSnapshot(fv.snap,state.frameView))return;
|
||||||
|
const field=fv.field,ranked=[];for(let i=0;i<field.classes.length;i++){if(field.classes[i]!==FIELD_UNKNOWN)continue;const x=i%fv.w,y=(i/fv.w)|0;let boundary=false;for(let yy=Math.max(0,y-1);yy<=Math.min(fv.h-1,y+1)&&!boundary;yy++)for(let xx=Math.max(0,x-1);xx<=Math.min(fv.w-1,x+1);xx++)if(field.classes[yy*fv.w+xx]===FIELD_ESCAPED){boundary=true;break}if(boundary)ranked.push({i,d:Math.hypot(x/fv.w-state.focusX,y/fv.h-state.focusY)})}if(!ranked.length)return;
|
||||||
|
const deep=deepEngineNeeded(fv.snap,fv.w),cap=Math.min(ranked.length,deep?384:4096);ranked.sort((a,b)=>a.d-b.d);const indices=ranked.slice(0,cap).map(v=>v.i);
|
||||||
|
unknownContinuationJob.active=true;unknownContinuationJob.key=key;const token=state.token,snap=fv.snap,baseIter=field.iter,extendedIter=Math.min(280000,Math.max(baseIter+256,baseIter*2)),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=fixedNum(snap.span,snap.bits)/fv.w;let p=0;
|
||||||
|
state.drawState='RESOLVING';state.continuationProgress=0;invalidateStats();
|
||||||
|
async function slice(){
|
||||||
|
if(token!==state.token||state.dirty){unknownContinuationJob.active=false;return}
|
||||||
|
const deadline=performance.now()+7;while(p<indices.length&&performance.now()<deadline){const i=indices[p++],x=i%fv.w,y=(i/fv.w)|0,result=deep?await highPrecisionDirectPixelAsync(snap,fv.w,fv.h,extendedIter,x,y,false,()=>token!==state.token||state.dirty):exportSampleShallow(cre,cim,scale,fv.w,fv.h,extendedIter,x,y);if(!result){unknownContinuationJob.active=false;return}const[n,m]=result;putField(field,i,n,m,n<extendedIter?FIELD_ESCAPED:FIELD_UNKNOWN)}
|
||||||
|
state.continuationProgress=p/indices.length;if(p<indices.length){invalidateStats();requestAnimationFrame(slice);return}
|
||||||
|
state.unresolved=field.classes.reduce((n,c)=>n+(c===FIELD_UNKNOWN),0);unknownContinuationJob.active=false;state.continuationProgress=1;state.drawState='COVERED';commitImage(colorizeField(field),fv.w,fv.h,snap,field);state.lastEngine+=' · 境界 '+indices.length+'点を追加反復';invalidateStats()
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function scheduleValidation(delay=350){if(state.processMode!=='validate'||state.dirty||state.rendering)return;scheduleBackground(()=>{if(state.processMode==='validate'&&!state.detailActive&&!unknownContinuationJob.active)startValidation();else if(state.processMode==='validate')scheduleValidation(300)},delay)}
|
||||||
|
function startValidation(){
|
||||||
|
const fv=state.fieldView;if(validationJob.active||!fv||state.dirty||state.rendering||!sameSnapshot(fv.snap,state.frameView))return;
|
||||||
|
const key=viewSpecKey(currentViewSpec())+':'+fv.w+'x'+fv.h+':'+fv.field.iter;if(validationJob.key===key)return;
|
||||||
|
validationJob.active=true;validationJob.key=key;
|
||||||
|
const token=state.token,snap=fv.snap,field=fv.field,baseIter=field.iter,extendedIter=Math.min(280000,Math.max(baseIter+256,baseIter*2));let i=0,lastStatus=0;
|
||||||
|
state.drawState='VALIDATING';state.coverage=0;invalidateStats();
|
||||||
|
async function slice(now){
|
||||||
|
if(token!==state.token||state.processMode!=='validate'){validationJob.active=false;return}
|
||||||
|
const deadline=performance.now()+9;
|
||||||
|
while(i<field.classes.length&&performance.now()<deadline){
|
||||||
|
const old=field.classes[i],x=i%fv.w,y=(i/fv.w)|0,limit=old===FIELD_ESCAPED?baseIter:extendedIter;
|
||||||
|
if(fixedAnalyticPixelProven(snap,fv.w,fv.h,x,y)){putField(field,i,limit,0,FIELD_INTERIOR_PROVEN);i++;continue}
|
||||||
|
const result=await highPrecisionDirectPixelAsync(snap,fv.w,fv.h,limit,x,y,true,()=>token!==state.token||state.processMode!=='validate');
|
||||||
|
if(!result){validationJob.active=false;return}
|
||||||
|
const[n,m]=result;if(n<limit)putField(field,i,n,m,FIELD_ESCAPED);else putField(field,i,n,0,FIELD_UNKNOWN);i++
|
||||||
|
}
|
||||||
|
if(now-lastStatus>180){lastStatus=now;state.coverage=i/field.classes.length;invalidateStats()}
|
||||||
|
if(i<field.classes.length){requestAnimationFrame(slice);return}
|
||||||
|
field.iter=extendedIter;state.unresolved=field.classes.reduce((n,c)=>n+(c!==FIELD_ESCAPED&&c!==FIELD_INTERIOR_PROVEN),0);state.drawState=state.unresolved?'VALIDATION_INCOMPLETE':'VALIDATED';state.coverage=1;validationJob.active=false;const out=colorizeField(field);commitImage(out,fv.w,fv.h,snap,field);state.lastEngine='高精度direct照合';invalidateStats()
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function drawWorldDetail(entry){
|
||||||
|
if(!entry||!entry.complete||entry.style!==styleSignature()||!entry.canvas)return;if(entry.iter){const rr=entry.iter/Math.max(1,maxIter());if(rr<.82||rr>1.22)return}
|
||||||
|
const re=align(entry.re,entry.bits,state.bits),im=align(entry.im,entry.bits,state.bits),sxv=align(entry.spanX,entry.bits,state.bits),syv=align(entry.spanY,entry.bits,state.bits);
|
||||||
|
const x=canvas.width*.5+fixedRatio(re-state.re,state.span)*canvas.width,y=canvas.height*.5-fixedRatio(im-state.im,state.span)*canvas.width;
|
||||||
|
const dw=Math.abs(fixedRatio(sxv,state.span)*canvas.width),dh=Math.abs(fixedRatio(syv,state.span)*canvas.width);
|
||||||
|
if(!Number.isFinite(x+y+dw+dh)||dw<5||dh<5||x+dw*.5<0||x-dw*.5>canvas.width||y+dh*.5<0||y-dh*.5>canvas.height)return;
|
||||||
|
// Do not magnify a cached tile beyond ~1.7 display pixels per source pixel.
|
||||||
|
if(dw/Math.max(1,entry.canvas.width)>1.7)return;
|
||||||
|
entry.lastUsed=performance.now();
|
||||||
|
ctx.imageSmoothingEnabled=true;try{ctx.imageSmoothingQuality='high'}catch{}
|
||||||
|
ctx.drawImage(entry.canvas,x-dw*.5,y-dh*.5,dw,dh);
|
||||||
|
}
|
||||||
|
function paintFrame(){
|
||||||
|
if(!state.frameView||!frameCanvas.width)return;
|
||||||
|
runtimeMetrics.canvasWrites++;
|
||||||
|
const fv=state.frameView;
|
||||||
|
const a=fixedRatio(fv.span,state.span);if(!Number.isFinite(a)||a<=0)return;
|
||||||
|
const dx=fixedRatio(fv.re-state.re,state.span)*canvas.width;
|
||||||
|
const dy=-fixedRatio(fv.im-state.im,state.span)*canvas.width;
|
||||||
|
ctx.save();ctx.setTransform(1,0,0,1,0,0);ctx.fillStyle='#050813';ctx.fillRect(0,0,canvas.width,canvas.height);
|
||||||
|
ctx.translate(canvas.width*.5+dx,canvas.height*.5+dy);ctx.scale(a,a);ctx.imageSmoothingEnabled=true;try{ctx.imageSmoothingQuality=state.lastPass===RENDER_PASS.COVERED?'high':'medium'}catch{}
|
||||||
|
const fw=canvas.width,fh=fw*(frameCanvas.height/Math.max(1,frameCanvas.width));ctx.drawImage(frameCanvas,-fw*.5,-fh*.5,fw,fh);ctx.restore();
|
||||||
|
// Only completed, world-validated tiles are composited. Partial tiles remain offscreen.
|
||||||
|
ctx.save();ctx.setTransform(1,0,0,1,0,0);for(const e of DETAIL_TILE_CACHE.values())drawWorldDetail(e);ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBudget(profile,deep){return profile.budgetMs}
|
||||||
|
function targetSize(profile,deep){
|
||||||
|
// Resolution is intentionally independent of zoom depth. 10^20, 10^100 and
|
||||||
|
// beyond use the same quality targets as low zoom; only the numerical engine changes.
|
||||||
|
const cssW=Math.max(1,canvas.clientWidth),cssH=Math.max(1,canvas.clientHeight),aspect=cssH/cssW,dpr=Math.max(.25,state.effectiveDpr||1);
|
||||||
|
if(profile.covered)return[canvas.width,canvas.height];
|
||||||
|
let nominal=Math.max(profile.minWidth,cssW*dpr*profile.nominalScale),minW=profile.minWidth,maxW=profile.maxWidth;
|
||||||
|
const measured=renderPerf[deep?'deepMPP':'shallowMPP'];
|
||||||
|
if(deep&&!profile.covered&&measured<=0){nominal=Math.min(nominal,48);minW=32}
|
||||||
|
if(measured>0){
|
||||||
|
const timedW=Math.sqrt(renderBudget(profile,deep)/Math.max(1e-7,measured*aspect));
|
||||||
|
nominal=Math.min(nominal,timedW);minW=deep?32:240
|
||||||
|
}
|
||||||
|
let w=Math.max(minW,Math.min(maxW,Math.round(nominal))),h=Math.max(deep&&!profile.covered?32:96,Math.round(w*aspect));
|
||||||
|
if(h>2200){const sc=2200/h;h=2200;w=Math.round(w*sc)}return[w,h]
|
||||||
|
}
|
||||||
|
function analyzeDetailTiles(data,w,h,baseMs,deep){
|
||||||
|
const field=state.fieldView&&state.fieldView.w===w&&state.fieldView.h===h?state.fieldView.field:null;if(!field)return[];
|
||||||
|
const leaves=[],minTile=32,rootTile=256;
|
||||||
|
function scoreRect(x0,y0,tw,th){
|
||||||
|
const step=Math.max(1,Math.floor(Math.min(tw,th)/14));let grad=0,edges=0,uncertain=0,samples=0;
|
||||||
|
for(let y=y0;y<y0+th;y+=step)for(let x=x0;x<x0+tw;x+=step){const i=y*w+x,c=field.classes[i],s=field.smooth[i];samples++;uncertain+=(255-(field.confidence?field.confidence[i]:fieldConfidence(c)))/255;if(x+step<x0+tw){const j=i+step,c2=field.classes[j];if((c===FIELD_ESCAPED)!==(c2===FIELD_ESCAPED))edges++;else if(c===FIELD_ESCAPED&&c2===FIELD_ESCAPED)grad+=Math.min(12,Math.abs(s-field.smooth[j]))}if(y+step<y0+th){const j=i+step*w,c2=field.classes[j];if((c===FIELD_ESCAPED)!==(c2===FIELD_ESCAPED))edges++;else if(c===FIELD_ESCAPED&&c2===FIELD_ESCAPED)grad+=Math.min(12,Math.abs(s-field.smooth[j]))}}
|
||||||
|
return edges/Math.max(1,samples)*4.4+grad/Math.max(1,samples*12)*1.5+uncertain/Math.max(1,samples)*.9
|
||||||
|
}
|
||||||
|
function visit(x,y,tw,th){const score=scoreRect(x,y,tw,th);if(score<.055)return;if((tw>minTile||th>minTile)&&score>.11){const aw=Math.max(1,tw>>1),bw=tw-aw,ah=Math.max(1,th>>1),bh=th-ah;visit(x,y,aw,ah);if(bw)visit(x+aw,y,bw,ah);if(bh)visit(x,y+ah,aw,bh);if(bw&&bh)visit(x+aw,y+ah,bw,bh);return}leaves.push({x,y,w:tw,h:th,score,area:tw*th})}
|
||||||
|
for(let y=0;y<h;y+=rootTile)for(let x=0;x<w;x+=rootTile)visit(x,y,Math.min(rootTile,w-x),Math.min(rootTile,h-y));
|
||||||
|
leaves.sort((a,b)=>{const ad=Math.hypot((a.x+a.w*.5)/w-state.focusX,(a.y+a.h*.5)/h-state.focusY),bd=Math.hypot((b.x+b.w*.5)/w-state.focusX,(b.y+b.h*.5)/h-state.focusY);return(b.score+.08/(.08+bd))-(a.score+.08/(.08+ad))});const byteCap=Math.max(0,detailCacheBudget()-detailCacheBytes),baseBudget=renderBudget(RENDER_PROFILE[RENDER_PASS.COVERED],deep),spare=Math.max(0,baseBudget*1.7-baseMs),sampleCap=Math.floor(w*h*Math.min(2.6,spare/Math.max(1,baseMs)));let bytes=0,sampleArea=0,n=0;while(n<leaves.length){const tile=leaves[n],sampleScale=tile.score>=1.15?4:2,costBytes=tile.area*(sampleScale*sampleScale*10+4),costSamples=tile.area*sampleScale*sampleScale;if(bytes+costBytes>byteCap||sampleArea+costSamples>sampleCap)break;bytes+=costBytes;sampleArea+=costSamples;n++}return leaves.slice(0,n)
|
||||||
|
}
|
||||||
|
function detailGeometry(tile,plan){
|
||||||
|
const s=plan.snap,den=BigInt(2*plan.w),re=s.re+s.span*BigInt(2*tile.x+tile.w-plan.w)/den,im=s.im+s.span*BigInt(plan.h-2*tile.y-tile.h)/den;
|
||||||
|
const spanX=s.span*BigInt(tile.w)/BigInt(plan.w),spanY=s.span*BigInt(tile.h)/BigInt(plan.w);
|
||||||
|
return{bits:s.bits,re,im,spanX,spanY}
|
||||||
|
}
|
||||||
|
function detailKey(tile,plan,sampleScale){const g=detailGeometry(tile,plan);return [g.bits,g.re,g.im,g.spanX,g.spanY,plan.iter,'aa'+sampleScale,'centered-v23'].join(':')}
|
||||||
|
function rendererMemoryBudget(){return(Number(navigator.deviceMemory||8)<=4||matchMedia('(max-width:700px)').matches?96:192)*1048576}
|
||||||
|
function wasmBytes(core){try{return core&&core.ex&&core.ex.memory?core.ex.memory.buffer.byteLength:0}catch{return 0}}
|
||||||
|
function memoryLedger(includeDetail=true){
|
||||||
|
const screenCanvasBytes=canvas.width*canvas.height*4,frameCanvasBytes=frameCanvas.width*frameCanvas.height*4;
|
||||||
|
const fieldBytes=state.fieldView?state.fieldView.w*state.fieldView.h*10:0;
|
||||||
|
const referenceBytes=(referenceCache.rr?.byteLength||0)+(referenceCache.ri?.byteLength||0);
|
||||||
|
const mainWasmBytes=wasmBytes(wasm)+wasmBytes(deepWasm);
|
||||||
|
// Worker memories are isolated, so browsers do not expose their byteLength.
|
||||||
|
// Account them conservatively: reference copies + kernel scratch/linear memory.
|
||||||
|
const workerEstimateBytes=(shallowWorker?2*1048576:0)+deepPool.workers.length*24*1048576;
|
||||||
|
const renderBytes=state.rendering?(()=>{const size=targetSize(renderProfile(state.lastPass),deepMode),pixels=size[0]*size[1];return pixels*18})():0;
|
||||||
|
const exportBytes=exportJob&&exportJob.active?exportJob.bytes||0:0,activeDetailBytes=activeDetailTiles.reduce((sum,e)=>sum+(e.transientBytes||0),0);
|
||||||
|
const cacheBytes=includeDetail?detailCacheBytes:0;
|
||||||
|
const managedBytes=fieldBytes+referenceBytes+mainWasmBytes+workerEstimateBytes+renderBytes+exportBytes+activeDetailBytes+cacheBytes;
|
||||||
|
const canvasBytes=screenCanvasBytes+frameCanvasBytes;
|
||||||
|
return{managedBytes,logicalBytes:managedBytes+canvasBytes,canvasBytes,screenCanvasBytes,frameCanvasBytes,fieldBytes,referenceBytes,mainWasmBytes,workerEstimateBytes,renderBytes,exportBytes,activeDetailBytes,detailCacheBytes:cacheBytes,budget:rendererMemoryBudget()}
|
||||||
|
}
|
||||||
|
function detailCacheBudget(){const base=memoryLedger(false).managedBytes,reserve=16*1048576;return Math.max(0,Math.floor(Math.min(rendererMemoryBudget()*.18,rendererMemoryBudget()-base-reserve)))}
|
||||||
|
function detailEntryBytes(entry){return entry&&entry.canvas?entry.canvas.width*entry.canvas.height*4+(entry.field?entry.field.classes.length*10:0):0}
|
||||||
|
function detailDistance(entry){try{const re=align(entry.re,entry.bits,state.bits),im=align(entry.im,entry.bits,state.bits);return Math.hypot(fixedRatio(re-state.re,state.span),fixedRatio(im-state.im,state.span))}catch{return Infinity}}
|
||||||
|
function trimDetailCache(){const budget=detailCacheBudget();while(detailCacheBytes>budget&&DETAIL_TILE_CACHE.size){let victim=null,rank=-Infinity;const now=performance.now();for(const [key,entry]of DETAIL_TILE_CACHE){const age=Math.max(0,now-(entry.lastUsed||0))/60000,distance=detailDistance(entry),score=(Number.isFinite(distance)?distance:1000)*4+age;if(score>rank){rank=score;victim=[key,entry]}}if(!victim)break;DETAIL_TILE_CACHE.delete(victim[0]);detailCacheBytes=Math.max(0,detailCacheBytes-detailEntryBytes(victim[1]))}}
|
||||||
|
function linearChannel(v){v/=255;return v<=.04045?v/12.92:Math.pow((v+.055)/1.055,2.4)}
|
||||||
|
function srgbChannel(v){v=Math.max(0,Math.min(1,v));return Math.round(255*(v<=.0031308?12.92*v:1.055*Math.pow(v,1/2.4)-.055))}
|
||||||
|
function resolveSubsampleField(field,w,h,scale=2){const hi=colorizeField(field),c=document.createElement('canvas');c.width=w;c.height=h;const cc=c.getContext('2d',{alpha:false}),id=cc.createImageData(w,h),out=id.data,samples=scale*scale;for(let y=0;y<h;y++)for(let x=0;x<w;x++){let r=0,g=0,b=0;for(let sy=0;sy<scale;sy++)for(let sx=0;sx<scale;sx++){const i=(((y*scale+sy)*w*scale)+(x*scale+sx))*4;r+=linearChannel(hi[i]);g+=linearChannel(hi[i+1]);b+=linearChannel(hi[i+2])}const oi=(y*w+x)*4;out[oi]=srgbChannel(r/samples);out[oi+1]=srgbChannel(g/samples);out[oi+2]=srgbChannel(b/samples);out[oi+3]=255}cc.putImageData(id,0,0);return c}
|
||||||
|
function recolorDetailEntry(entry){if(!entry||!entry.field)return;entry.canvas=resolveSubsampleField(entry.field,entry.baseW,entry.baseH,entry.sampleScale||2);entry.style=styleSignature()}
|
||||||
|
function recolorCachedDetails(){cancelDetailRefinement(true);for(const entry of DETAIL_TILE_CACHE.values())recolorDetailEntry(entry)}
|
||||||
|
function makeDetailTask(tile,plan){
|
||||||
|
const sampleScale=tile.score>=1.15?4:2,key=detailKey(tile,plan,sampleScale),cached=DETAIL_TILE_CACHE.get(key);if(cached){DETAIL_TILE_CACHE.delete(key);DETAIL_TILE_CACHE.set(key,cached);cached.lastUsed=performance.now();if(cached.style!==styleSignature())recolorDetailEntry(cached);return{cached:true,entry:cached}}
|
||||||
|
const c=document.createElement('canvas');c.width=Math.max(2,tile.w*sampleScale);c.height=Math.max(2,tile.h*sampleScale);const dc=c.getContext('2d',{alpha:false});
|
||||||
|
dc.imageSmoothingEnabled=true;try{dc.imageSmoothingQuality='high'}catch{};dc.drawImage(frameCanvas,tile.x,tile.y,tile.w,tile.h,0,0,c.width,c.height);
|
||||||
|
const g=detailGeometry(tile,plan),entry={...g,canvas:c,field:null,baseW:tile.w,baseH:tile.h,sampleScale,style:styleSignature(),key,score:tile.score,iter:plan.iter,complete:false,lastUsed:performance.now(),transientBytes:c.width*c.height*18};
|
||||||
|
const task={tile,canvas:c,ctx:dc,field:makeField(c.width*c.height,plan.iter),entry,phases:0,plan};activeDetailTiles.push(entry);return task
|
||||||
|
}
|
||||||
|
function validateDetailTask(task){
|
||||||
|
const plan=task&&task.plan,t=task&&task.tile,base=plan&&plan.baseData;if(!plan||!t||!base)return false;
|
||||||
|
let samples=0,blackMismatch=0,rgbDiff=0,colorSamples=0;const sampleScale=task.entry.sampleScale||2,step=Math.max(3,Math.floor(Math.min(t.w,t.h)/7)),pix=task.ctx.getImageData(0,0,task.canvas.width,task.canvas.height).data;
|
||||||
|
for(let by=t.y+1;by<t.y+t.h-1;by+=step)for(let bx=t.x+1;bx<t.x+t.w-1;bx+=step){
|
||||||
|
const bi=(by*plan.w+bx)*4,lx=Math.min(task.canvas.width-1,Math.max(0,sampleScale*(bx-t.x)+(sampleScale>>1))),ly=Math.min(task.canvas.height-1,Math.max(0,sampleScale*(by-t.y)+(sampleScale>>1))),hi=(ly*task.canvas.width+lx)*4;
|
||||||
|
const bb=(base[bi]|base[bi+1]|base[bi+2])===0,hb=(pix[hi]|pix[hi+1]|pix[hi+2])===0;samples++;if(bb!==hb)blackMismatch++;else if(!bb){rgbDiff+=Math.abs(base[bi]-pix[hi])+Math.abs(base[bi+1]-pix[hi+1])+Math.abs(base[bi+2]-pix[hi+2]);colorSamples++}
|
||||||
|
}
|
||||||
|
if(samples<4)return false;const classRate=blackMismatch/samples,meanDiff=colorSamples?rgbDiff/(3*colorSamples):0;return classRate<=.035&&meanDiff<=48
|
||||||
|
}
|
||||||
|
function cacheDetailTask(task){
|
||||||
|
if(!task||task.cached)return;const i=activeDetailTiles.indexOf(task.entry);if(i>=0)activeDetailTiles.splice(i,1);
|
||||||
|
if(!validateDetailTask(task))return;fillFieldConfidence(task.field);task.entry.field=task.field;task.entry.canvas=resolveSubsampleField(task.field,task.tile.w,task.tile.h,task.entry.sampleScale);task.entry.style=styleSignature();task.entry.complete=true;task.entry.lastUsed=performance.now();task.entry.transientBytes=0;const old=DETAIL_TILE_CACHE.get(task.entry.key);if(old)detailCacheBytes-=detailEntryBytes(old);DETAIL_TILE_CACHE.delete(task.entry.key);DETAIL_TILE_CACHE.set(task.entry.key,task.entry);detailCacheBytes+=detailEntryBytes(task.entry);
|
||||||
|
trimDetailCache()
|
||||||
|
}
|
||||||
|
function phaseRect(task,phase){
|
||||||
|
const t=task.tile,scale=task.entry.sampleScale,left=(phase%scale)*t.w,top=((phase/scale)|0)*t.h;
|
||||||
|
return{x0:t.x*scale+left,y0:t.y*scale+top,rw:t.w,rows:t.h,dx:left,dy:top}
|
||||||
|
}
|
||||||
|
function prepareDeepTileContext(plan,iter,sampleScale,done){
|
||||||
|
const cacheKey='i'+iter+':s'+sampleScale;if(plan.deepCtx&&plan.deepCtx[cacheKey]){done(plan.deepCtx[cacheKey]);return}if(!plan.deepCtx)plan.deepCtx={};
|
||||||
|
const snap=plan.snap,refC=chooseReference(snap),W=plan.w*sampleScale,H=plan.h*sampleScale,refX=W*.5-.5+fixedRatio(refC.re-snap.re,snap.span)*W,refY=H*.5-.5-fixedRatio(refC.im-snap.im,snap.span)*W;
|
||||||
|
const cornerR=Math.max(Math.hypot(refX,refY),Math.hypot(W-refX,refY),Math.hypot(refX,H-refY),Math.hypot(W-refX,H-refY))/Math.max(1,W),logMaxDc=log2FixedAt(snap.span,snap.bits)+Math.log2(Math.max(1e-300,cornerR));
|
||||||
|
buildReferenceAt(snap.bits,refC.re,refC.im,iter,state.token,(ref,refLen)=>{
|
||||||
|
if(plan.gen!==state.detailGeneration)return;const series=computeSeries(ref,refLen,logMaxDc),centered=pixelCenteredOffset(snap,refC,W),sb=spanMantBucket(snap.span,snap.bits),off=fixedComplexScaled(centered.r,centered.i,snap.bits),spanNormal=fixedNum(snap.span,snap.bits),offRn=fixedNum(centered.r,snap.bits),offIn=fixedNum(centered.i,snap.bits);
|
||||||
|
const cMaxRaw=Math.hypot(offRn,offIn)+Math.abs(spanNormal)*Math.hypot(.5,H/(2*Math.max(1,W))),cBucket=cMaxRaw>0&&Number.isFinite(cMaxRaw)?Math.ceil(Math.log2(cMaxRaw)*8):0,cMaxSafe=cMaxRaw>0?Math.pow(2,cBucket/8):0;
|
||||||
|
const ctx={snap,refC,ref,refLen,series,sb,off,spanNormal,offRn,offIn,cBucket,cMaxSafe,W,H,iter};plan.deepCtx[cacheKey]=ctx;done(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function renderGuardedDeepDetailRect(task,phase,iter,done){
|
||||||
|
const plan=task.plan,r=phaseRect(task,phase),W=plan.w*task.entry.sampleScale,H=plan.h*task.entry.sampleScale,snap=plan.snap;let yy=0;
|
||||||
|
async function slice(){if(plan.gen!==state.detailGeneration){done(false);return}const deadline=performance.now()+6;while(yy<r.rows&&performance.now()<deadline){const gy=r.y0+yy;for(let xx=0;xx<r.rw;xx++){const gx=r.x0+xx,result=await highPrecisionDirectPixelAsync(snap,W,H,iter,gx,gy,true,()=>plan.gen!==state.detailGeneration);if(!result){done(false);return}const[n,m]=result,fi=(r.dy+yy)*task.canvas.width+r.dx+xx;putField(task.field,fi,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN)}yy++}if(yy<r.rows){requestAnimationFrame(slice);return}const stripe=makeField(r.rw*r.rows,iter);for(let y=0;y<r.rows;y++){const src=(r.dy+y)*task.canvas.width+r.dx,dst=y*r.rw;stripe.smooth.set(task.field.smooth.subarray(src,src+r.rw),dst);stripe.iterations.set(task.field.iterations.subarray(src,src+r.rw),dst);stripe.classes.set(task.field.classes.subarray(src,src+r.rw),dst)}const id=task.ctx.createImageData(r.rw,r.rows);id.data.set(colorizeField(stripe));task.ctx.putImageData(id,r.dx,r.dy);task.phases|=(1<<phase);invalidateView(false);done(true)}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function sendDeepDetailRect(task,phase,iter,colorIter,done){
|
||||||
|
const plan=task.plan,gen=plan.gen;if(gen!==state.detailGeneration){done(false);return}if(state.processMode==='validate'||task.tile.score>1.35){renderGuardedDeepDetailRect(task,phase,iter,done);return}if(!ensureDeepPool()){done(false);return}
|
||||||
|
prepareDeepTileContext(plan,iter,task.entry.sampleScale,dc=>{
|
||||||
|
if(gen!==state.detailGeneration){done(false);return}const worker=deepPool.workers.find(w=>!w._busy);if(!worker){scheduleBackground(()=>sendDeepDetailRect(task,phase,iter,colorIter,done),18);return}
|
||||||
|
const r=phaseRect(task,phase),ref=dc.ref,refKey=String(ref.id),bpExp=32,useBla=Number.isFinite(dc.spanNormal)&&Math.abs(dc.spanNormal)>=1e-280&&dc.refLen>8,blaEps=Math.pow(2,-bpExp),blaKey=refKey+':'+ref.version+':'+dc.refLen+':'+dc.cBucket+':e'+bpExp;
|
||||||
|
const jobId='detail:'+gen+':'+Date.now()+':'+Math.random(),msg={type:'render',jobId,refKey,refLen:dc.refLen,w:dc.W,h:dc.H,x0:r.x0,y0:r.y0,rectW:r.rw,rows:r.rows,iter,colorIter,spanMant:dc.sb.mant,spanBucket:dc.sb.bucket,offR:dc.off[0],offI:dc.off[1],offBucket:dc.off[2],cx:dc.W*.5,cy:dc.H*.5,skip:dc.series.skip,Ar:dc.series.Ar,Ai:dc.series.Ai,Ab:dc.series.Ab,Br:dc.series.Br,Bi:dc.series.Bi,Bb:dc.series.Bb,shift:state.shift,cycle:state.cycle,palette:state.palette,useBla,blaEps,blaKey,cMax:dc.cMaxSafe,spanNormal:dc.spanNormal,offRn:dc.offRn,offIn:dc.offIn,baseCr:fixedNum(dc.refC.re,dc.snap.bits),baseCi:fixedNum(dc.refC.im,dc.snap.bits),maxBlaSteps:0,maxPtbSteps:0,verifySamples:3,verifyDelta:6,safeBlaEps:Math.pow(2,-48),safeBlaKey:blaKey+':safe48'},transfer=[];
|
||||||
|
let start=worker._refKey===refKey?worker._refLoaded:0;start=Math.max(0,Math.min(start,dc.refLen+1));if(start<dc.refLen+1){const rr=ref.rr.slice(start,dc.refLen+1),ri=ref.ri.slice(start,dc.refLen+1);msg.rr=rr.buffer;msg.ri=ri.buffer;msg.rrStart=start;transfer.push(rr.buffer,ri.buffer);worker._refKey=refKey;worker._refLoaded=dc.refLen+1}
|
||||||
|
const runner={token:state.token,cancelled:false,fail(){done(false)},onResult(w,d){if(gen!==state.detailGeneration||!d||d.error){done(false);return}const sf=d.field?new Float32Array(d.field):null,it=d.iterations?new Uint32Array(d.iterations):null,cl=d.classes?new Uint8Array(d.classes):null,arr=sf&&cl?colorizeField({smooth:sf,classes:cl,iter:colorIter}):new Uint8ClampedArray(d.out),id=task.ctx.createImageData(r.rw,r.rows);id.data.set(arr);task.ctx.putImageData(id,r.dx,r.dy);if(sf&&it&&cl)for(let yy=0;yy<r.rows;yy++){const dst=(r.dy+yy)*task.canvas.width+r.dx;task.field.smooth.set(sf.subarray(yy*r.rw,(yy+1)*r.rw),dst);task.field.iterations.set(it.subarray(yy*r.rw,(yy+1)*r.rw),dst);task.field.classes.set(cl.subarray(yy*r.rw,(yy+1)*r.rw),dst)}if(d.field&&d.iterations&&d.classes)w._recycle={field:d.field,iterations:d.iterations,classes:d.classes};task.phases|=(1<<phase);task.entry.canvas=task.canvas;invalidateView(false);done(true)}};
|
||||||
|
if(worker._recycle){msg.fieldBuffer=worker._recycle.field;msg.iterationBuffer=worker._recycle.iterations;msg.classBuffer=worker._recycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);worker._recycle=null}worker._busy=true;worker._job={type:'render',runner,jobId,y0:r.y0,rows:r.rows,started:performance.now()};try{worker.postMessage(msg,transfer)}catch(e){worker._busy=false;worker._job=null;done(false)}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function renderShallowDetailRect(task,phase,done){
|
||||||
|
const plan=task.plan,r=phaseRect(task,phase),sampleScale=task.entry.sampleScale,W=plan.w*sampleScale,H=plan.h*sampleScale,iter=plan.iter,snap=plan.snap,sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/W,colorCtx=makeColorCtx(iter),out=new Uint8ClampedArray(r.rw*r.rows*4);let yy=0;
|
||||||
|
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
function slice(){if(plan.gen!==state.detailGeneration){done(false);return}const deadline=performance.now()+5;while(yy<r.rows&&performance.now()<deadline){const gy=r.y0+yy,ci=cim+(H*.5-gy-.5)*scale;let oi=yy*r.rw*4;for(let xx=0;xx<r.rw;xx++){const gx=r.x0+xx,cr=cre+(gx+.5-W*.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0,inside=inBulbs(cr,ci);if(inside)n=iter;else while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}const m=n<iter?Math.max(4.0000001,zr2+zi2):0,fi=(r.dy+yy)*task.canvas.width+r.dx+xx;putField(task.field,fi,n,m,n<iter?FIELD_ESCAPED:(inside?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN));if(n>=iter){out[oi]=out[oi+1]=out[oi+2]=0;out[oi+3]=255}else putFastColor(out,oi,n,m,iter,colorCtx);oi+=4}yy++}if(yy<r.rows)requestAnimationFrame(slice);else{const id=task.ctx.createImageData(r.rw,r.rows),data=id.data;data.set(out);task.ctx.putImageData(id,r.dx,r.dy);task.phases|=(1<<phase);invalidateView(false);done(true)}}requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function pumpDetailRefinement(){
|
||||||
|
const plan=detailPlan;if(!plan||plan.gen!==state.detailGeneration||!state.hq){state.detailActive=false;return}if(state.rendering||unknownContinuationJob.active||performance.now()-state.lastInteraction<720){scheduleBackground(pumpDetailRefinement,90);return}
|
||||||
|
while(plan.index<plan.tasks.length&&plan.tasks[plan.index].cached)plan.index++;
|
||||||
|
if(plan.index>=plan.tasks.length){state.detailActive=false;state.detailDone=plan.tasks.length;state.drawState='REFINED';updateStats();scheduleValidation();return}
|
||||||
|
const task=plan.tasks[plan.index],phase=task.nextPhase||0,cb=ok=>{if(!ok){const i=activeDetailTiles.indexOf(task.entry);if(i>=0)activeDetailTiles.splice(i,1);plan.index++;scheduleBackground(pumpDetailRefinement,10);return}task.nextPhase=phase+1;if(task.nextPhase>=task.entry.sampleScale*task.entry.sampleScale){cacheDetailTask(task);plan.index++}state.detailDone=plan.tasks.reduce((n,t)=>n+(t.cached||t.entry.complete?1:0),0);updateStats();scheduleBackground(pumpDetailRefinement,0)};
|
||||||
|
if(plan.deep)sendDeepDetailRect(task,phase,plan.iter,plan.iter,cb);else renderShallowDetailRect(task,phase,cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Idle-only adaptive detail refinement ────────────────────────────────
|
||||||
|
function scheduleDetailRefinement(data,w,h,snap,iter,baseMs,deep){
|
||||||
|
if(!state.hq)return;cancelDetailRefinement(true);const gen=state.detailGeneration,tiles=analyzeDetailTiles(data,w,h,baseMs,deep);if(!tiles.length){state.drawState='REFINED';invalidateStats();scheduleValidation();return}
|
||||||
|
const plan={gen,snap,w,h,iter,deep,tiles,tasks:[],index:0,deepCtx:null,baseData:data};for(const t of tiles)plan.tasks.push(makeDetailTask(t,plan));detailPlan=plan;state.detailActive=true;state.drawState='REFINING';state.detailQueued=plan.tasks.length;state.detailDone=plan.tasks.filter(t=>t.cached).length;scheduleBackground(pumpDetailRefinement,160)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishRender(token,t0,profile,snap,w,h,out,engine,computedPixels=w*h,field=null){
|
||||||
|
if(token!==state.token)return;const elapsed=Math.max(.1,performance.now()-t0),deep=deepEngineNeeded(snap,Math.max(1,canvas.width));
|
||||||
|
// Reproject-only frames must not poison performance estimates with near-zero work.
|
||||||
|
if(computedPixels>Math.max(512,w*h*.01)){const mpp=elapsed/computedPixels,key=deep?'deepMPP':'shallowMPP';renderPerf[key]=renderPerf[key]?renderPerf[key]*.72+mpp*.28:mpp}
|
||||||
|
if(field){fillFieldConfidence(field);out=colorizeField(field)}commitImage(out,w,h,snap,field);state.rendering=false;state.lastRender=elapsed;state.lastPass=profile.id;state.dirty=false;state.lastEngine=engine;state.lastFrameDone=performance.now();state.coverage=Math.min(1,w*h/Math.max(1,canvas.width*canvas.height));state.drawState=profile.covered&&state.coverage>=.999?'COVERED':'PREVIEW';state.unresolved=field?field.classes.reduce((n,c)=>n+(c===FIELD_UNKNOWN),0):0;flushPendingPrecision();
|
||||||
|
if(!profile.covered&&adaptStandardDeepBudget(deep)){updateStats();return}
|
||||||
|
if(profile.covered&&state.unresolved)scheduleUnknownContinuation();
|
||||||
|
if(profile.covered&&state.hq){const gen=state.detailGeneration;const schedule=()=>{if(gen!==state.detailGeneration||state.rendering)return;if(unknownContinuationJob.active||unknownContinuationTimer){scheduleBackground(schedule,90);return}const fv=state.fieldView,base=fv&&sameSnapshot(fv.snap,snap)?colorizeField(fv.field):out;scheduleDetailRefinement(base,w,h,snap,fv?fv.field.iter:iterationPlan(profile,deep).colorIter,elapsed,deep)};scheduleIdle(schedule,350);clearPanReuse()}
|
||||||
|
if(profile.covered&&state.processMode==='validate')scheduleValidation();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Shallow renderers ───────────────────────────────────────────────────
|
||||||
|
function renderShallowReuse(profile,snap,w,h,iter,token,t0,reuse){
|
||||||
|
const out=reuse.out,colorCtx=makeColorCtx(iter),sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/w,rects=reuse.rects;let ri=0,yy=0;
|
||||||
|
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,qq=x*x+y2;if(qq*(qq+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
function slice(){if(token!==state.token)return;const deadline=performance.now()+7;while(ri<rects.length&&performance.now()<deadline){const r=rects[ri];while(yy<r.h&&performance.now()<deadline){const gy=r.y0+yy,ci=cim+(h*.5-gy-.5)*scale;for(let xx=0;xx<r.w;xx++){const gx=r.x0+xx,cr=cre+(gx+.5-w*.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0;if(inBulbs(cr,ci))n=iter;else while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}const oi=(gy*w+gx)*4;if(n>=iter){out[oi]=out[oi+1]=out[oi+2]=0;out[oi+3]=255}else putFastColor(out,oi,n,Math.max(4.0000001,zr2+zi2),iter,colorCtx)}yy++}if(yy>=r.h){ri++;yy=0}}
|
||||||
|
if(ri<rects.length)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'JavaScript f64 · 既存画像再利用 '+Math.round(100*reuse.reusedPixels/(w*h))+'%',reuse.exposedPixels)
|
||||||
|
}requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function renderWasmMain(profile,snap,w,h,iter,token,t0){
|
||||||
|
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),colorCtx=makeColorCtx(iter);
|
||||||
|
const cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;
|
||||||
|
function proven(cr,ci){const y2=ci*ci,x=cr-.25,q0=x*x+y2;if(q0*(q0+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
let y=0;const counts=wasm.counts(),mags=wasm.mags();
|
||||||
|
function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+(profile.covered?11:8);
|
||||||
|
while(y<h&&performance.now()<deadline){
|
||||||
|
const rows=Math.min(24,h-y),y0=y,npx=wasm.ex.render_rows(cre+scale*.5,cim-scale*.5,sp,w,h,y,rows,iter);let oi=y*w*4;
|
||||||
|
for(let i=0;i<npx;i++){
|
||||||
|
const n=counts[i],m=mags[i],px=i%w,py=y0+((i/w)|0);
|
||||||
|
const cr=cre+(px+.5-w*.5)*scale,ci=cim+(h*.5-py-.5)*scale,kind=n<iter?FIELD_ESCAPED:(proven(cr,ci)?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN);putField(field,py*w+px,n,m,kind);
|
||||||
|
if(n>=iter){out[oi++]=0;out[oi++]=0;out[oi++]=0;out[oi++]=255}
|
||||||
|
else{
|
||||||
|
putFastColor(out,oi,n,Math.max(4.0000001,m),iter,colorCtx);oi+=4;
|
||||||
|
}
|
||||||
|
}y+=rows;
|
||||||
|
}
|
||||||
|
if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'WebAssembly f64',w*h,field);
|
||||||
|
}requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
function renderWasm(profile,snap,w,h,iter,token,t0){
|
||||||
|
if(!ensureShallowWorker()||shallowWorkerBusy){renderWasmMain(profile,snap,w,h,iter,token,t0);return}
|
||||||
|
const worker=shallowWorker,out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),jobId='shallow:'+token+':'+Date.now(),rowsPerChunk=Math.min(h,Math.max(4,Math.floor(32768/Math.max(1,w)))),chunks=[];for(let y=0;y<h;y+=rowsPerChunk)chunks.push({y,rows:Math.min(rowsPerChunk,h-y)});chunks.sort((a,b)=>Math.abs((a.y+a.rows*.5)/h-state.focusY)-Math.abs((b.y+b.rows*.5)/h-state.focusY));let next=0;shallowWorkerBusy=true;
|
||||||
|
const dispatch=()=>{if(token!==state.token){shallowWorkerBusy=false;return}if(next>=chunks.length){shallowWorkerBusy=false;finishRender(token,t0,profile,snap,w,h,out,'WebAssembly '+(wasm.simd?'SIMD':'scalar')+' Worker',w*h,field);return}const chunk=chunks[next++],msg={type:'render',jobId,cre,cim,sp,w,h,y:chunk.y,rows:chunk.rows,iter},transfer=[];if(shallowRecycle){msg.fieldBuffer=shallowRecycle.field;msg.iterationBuffer=shallowRecycle.iterations;msg.classBuffer=shallowRecycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);shallowRecycle=null}worker.postMessage(msg,transfer)};
|
||||||
|
worker.onmessage=e=>{const d=e.data;if(!d||d.type==='ready')return;if(d.jobId!==jobId)return;if(d.error){shallowWorkerBusy=false;renderWasmMain(profile,snap,w,h,iter,token,t0);return}const sf=new Float32Array(d.field),it=new Uint32Array(d.iterations),cl=new Uint8Array(d.classes);if(token!==state.token){shallowRecycle={field:d.field,iterations:d.iterations,classes:d.classes};shallowWorkerBusy=false;return}const stripe=colorizeField({smooth:sf,classes:cl,iter}),dst=d.y*w;field.smooth.set(sf,dst);field.iterations.set(it,dst);field.classes.set(cl,dst);out.set(stripe,dst*4);presentPartialStripe(stripe,0,d.y,w,d.rows,w,h,snap);shallowRecycle={field:d.field,iterations:d.iterations,classes:d.classes};dispatch()};worker.onerror=()=>{shallowWorkerBusy=false;destroyShallowWorker();if(token===state.token)renderWasmMain(profile,snap,w,h,iter,token,t0)};dispatch()
|
||||||
|
}
|
||||||
|
function renderJsDouble(profile,snap,w,h,iter,token,t0){
|
||||||
|
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),colorCtx=makeColorCtx(iter),sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/w;let y=0;
|
||||||
|
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,qq=x*x+y2;if(qq*(qq+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
function slice(){if(token!==state.token)return;const deadline=performance.now()+(profile.covered?10:7);while(y<h&&performance.now()<deadline){let oi=y*w*4,ci=cim+(h*.5-y-.5)*scale,cr=cre+(.5-w*.5)*scale;for(let x=0;x<w;x++,cr+=scale){let zr=0,zi=0,zr2=0,zi2=0,n=0,inside=inBulbs(cr,ci);if(inside)n=iter;else{let oldr=0,oldi=0;while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++;if((n&63)===0){if(Math.abs(zr-oldr)+Math.abs(zi-oldi)<1e-15){n=iter;break}oldr=zr;oldi=zi}}}const mm=n>=iter?0:Math.max(4.0000001,zr2+zi2),kind=n<iter?FIELD_ESCAPED:(inside?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN);putField(field,y*w+x,n,mm,kind);if(n>=iter){out[oi++]=0;out[oi++]=0;out[oi++]=0;out[oi++]=255}else{putFastColor(out,oi,n,mm,iter,colorCtx);oi+=4}}y++}if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'JavaScript f64',w*h,field);}requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
function spanMantBucket(span,bits){
|
||||||
|
const l=log2FixedAt(span,bits);let bucket=Math.round(l/256),mant=Math.pow(2,l-bucket*256);return{mant,bucket};
|
||||||
|
}
|
||||||
|
function normalizeBucket(r,i,b){
|
||||||
|
let m=Math.max(Math.abs(r),Math.abs(i));if(!m)return[0,0,NEG_BUCKET];
|
||||||
|
while(m>HI128){r*=INV256;i*=INV256;b++;m*=INV256}
|
||||||
|
while(m<LO128){r*=POW256;i*=POW256;b--;m*=POW256}
|
||||||
|
return[r,i,b];
|
||||||
|
}
|
||||||
|
function scAdd(a,b){
|
||||||
|
if(a[2]===NEG_BUCKET)return b;if(b[2]===NEG_BUCKET)return a;
|
||||||
|
const eb=Math.max(a[2],b[2]);let r=0,i=0;
|
||||||
|
if(a[2]===eb){r+=a[0];i+=a[1]}else if(a[2]===eb-1){r+=a[0]*INV256;i+=a[1]*INV256}
|
||||||
|
if(b[2]===eb){r+=b[0];i+=b[1]}else if(b[2]===eb-1){r+=b[0]*INV256;i+=b[1]*INV256}
|
||||||
|
return normalizeBucket(r,i,eb);
|
||||||
|
}
|
||||||
|
function scLog2(a){return a[2]===NEG_BUCKET?-Infinity:Math.log2(Math.hypot(a[0],a[1]))+256*a[2]}
|
||||||
|
function fixedMantAtBucket(v,bits,bucket){
|
||||||
|
if(v===0n)return 0;const neg=v<0n;let a=neg?-v:v,bl=bitLen(a),take=Math.min(53,bl),sh=bl-take,top=Number(a>>BigInt(sh));
|
||||||
|
const exp=sh-bits-256*bucket;const n=top*Math.pow(2,exp);return neg?-n:n;
|
||||||
|
}
|
||||||
|
function fixedComplexScaled(r,i,bits){
|
||||||
|
if(r===0n&&i===0n)return[0,0,NEG_BUCKET];
|
||||||
|
const lr=r===0n?-Infinity:log2FixedAt(r,bits),li=i===0n?-Infinity:log2FixedAt(i,bits),bucket=Math.round(Math.max(lr,li)/256);
|
||||||
|
return normalizeBucket(fixedMantAtBucket(r,bits,bucket),fixedMantAtBucket(i,bits,bucket),bucket);
|
||||||
|
}
|
||||||
|
function pixelCenteredOffset(snap,refC,w){const half=roundDiv(snap.span,BigInt(2*Math.max(1,w)));return{r:snap.re-refC.re+half,i:snap.im-refC.im-half}}
|
||||||
|
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||||
|
function fixedPixelPoint(snap,w,h,x,y,bits=snap.bits){const den=BigInt(2*w),re=align(snap.re,snap.bits,bits),im=align(snap.im,snap.bits,bits),span=align(snap.span,snap.bits,bits);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||||
|
function fixedAnalyticInterior(cr,ci,bits){const S=1n<<BigInt(bits),X=cr-(S>>2n),Y=ci,Q=X*X+Y*Y;if(4n*Q*(Q+X*S)<=Y*Y*S*S)return true;const D=cr+S;return 16n*(D*D+Y*Y)<=S*S}
|
||||||
|
function fixedAnalyticPixelProven(snap,w,h,x,y){const p=fixedPixelPoint(snap,w,h,x,y),g=fixedPixelPoint(snap,w,h,x,y,snap.bits+64);return fixedAnalyticInterior(p[0],p[1],snap.bits)&&fixedAnalyticInterior(g[0],g[1],snap.bits+64)}
|
||||||
|
function roundShift(v,bits){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(bits)-1n),q=(a+half)>>BigInt(bits);return neg?-q:q}
|
||||||
|
// ── Arbitrary-precision reference orbit / perturbation setup ────────────
|
||||||
|
const referenceCache={id:0,bits:0,re:0n,im:0n,n:0,escape:0,zr:0n,zi:0n,rr:null,ri:null,series:null,version:0,loadedLen:0,derivative:[0,0,NEG_BUCKET],conditionLog2:0,checkpointVersion:0,checkpointBits:0,checkpointCount:0,checkpointMismatch:false};
|
||||||
|
function promoteReferenceCache(shift,oldBits){
|
||||||
|
const c=referenceCache;if(!c.rr||c.bits!==oldBits)return;
|
||||||
|
const sh=BigInt(shift);c.re<<=sh;c.im<<=sh;c.zr<<=sh;c.zi<<=sh;c.bits+=shift;
|
||||||
|
// rr/ri are normalized Float64 values, so neither they nor Worker-side copies
|
||||||
|
// need to change when only the fixed-point radix moves.
|
||||||
|
}
|
||||||
|
function sameReference(bits,re,im){
|
||||||
|
return referenceCache.rr&&referenceCache.bits===bits&&referenceCache.re===re&&referenceCache.im===im;
|
||||||
|
}
|
||||||
|
function resetReference(bits,re,im){
|
||||||
|
const c=referenceCache;c.id++;c.bits=bits;c.re=re;c.im=im;c.n=0;c.escape=0;c.zr=0n;c.zi=0n;c.derivative=[0,0,NEG_BUCKET];c.conditionLog2=0;
|
||||||
|
if(!c.rr){c.rr=new Float64Array(150001);c.ri=new Float64Array(150001)}
|
||||||
|
c.series=null;c.version=1;c.loadedLen=0;c.checkpointVersion=0;c.checkpointBits=0;c.checkpointCount=0;c.checkpointMismatch=false;
|
||||||
|
}
|
||||||
|
function invalidateReferenceOrbit(){const c=referenceCache;c.id++;c.bits=0;c.re=0n;c.im=0n;c.n=0;c.escape=0;c.zr=0n;c.zi=0n;c.rr=null;c.ri=null;c.series=null;c.version=0;c.loadedLen=0;c.derivative=[0,0,NEG_BUCKET];c.conditionLog2=0;c.checkpointVersion=0;c.checkpointBits=0;c.checkpointCount=0;c.checkpointMismatch=false;for(const worker of deepPool.workers){worker._refKey='';worker._refLoaded=0}}
|
||||||
|
function buildReferenceAt(bits,cRe,cIm,iter,token,done){
|
||||||
|
if(!sameReference(bits,cRe,cIm))resetReference(bits,cRe,cIm);
|
||||||
|
const c=referenceCache,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,startN=c.n,refT0=performance.now();
|
||||||
|
if((c.escape&&c.escape<=iter)||c.n>=iter){
|
||||||
|
const refLen=c.escape&&c.escape<=iter?c.escape:iter;done(c,refLen);return
|
||||||
|
}
|
||||||
|
function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+6.5;
|
||||||
|
while(c.n<iter&&!c.escape&&performance.now()<deadline){
|
||||||
|
c.rr[c.n]=fixedOrbitNum(c.zr,bits);c.ri[c.n]=fixedOrbitNum(c.zi,bits);const Rr=c.rr[c.n],Ri=c.ri[c.n],d=c.derivative,term=d[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(Rr*d[0]-Ri*d[1]),2*(Rr*d[1]+Ri*d[0]),d[2]);c.derivative=scAdd(term,[1,0,0]);c.conditionLog2=Math.max(c.conditionLog2,scLog2(c.derivative));
|
||||||
|
const zr2=roundShift(c.zr*c.zr,bits),zi2=roundShift(c.zi*c.zi,bits);c.zi=roundShift(2n*c.zr*c.zi,bits)+cIm;c.zr=zr2-zi2+cRe;c.n++;
|
||||||
|
const mag=roundShift(c.zr*c.zr,bits)+roundShift(c.zi*c.zi,bits);if(mag>BAIL)c.escape=c.n;
|
||||||
|
}
|
||||||
|
if(c.escape||c.n>=iter){
|
||||||
|
c.rr[c.n]=fixedOrbitNum(c.zr,bits);c.ri[c.n]=fixedOrbitNum(c.zi,bits);c.version++;if(c.n>startN){const ms=performance.now()-refT0;refControl.lastBuildMs=ms;refControl.buildEMA=refControl.buildEMA?refControl.buildEMA*.82+ms*.18:ms}
|
||||||
|
const refLen=c.escape&&c.escape<=iter?c.escape:iter;done(c,refLen)
|
||||||
|
}else requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
function verifyReferenceCheckpoints(ref,refLen,token,done){
|
||||||
|
const guardBits=ref.bits+64;
|
||||||
|
if(ref.checkpointVersion===ref.version&&ref.checkpointBits===guardBits&&!ref.checkpointMismatch){done(true);return}
|
||||||
|
const cRe=align(ref.re,ref.bits,guardBits),cIm=align(ref.im,ref.bits,guardBits),ONE=1n<<BigInt(guardBits),BAIL=16n*ONE;
|
||||||
|
const 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,n=0,escape=0,checked=0,mismatch=false;
|
||||||
|
function compare(){if(!targets.has(n)||n>refLen)return;checked++;const rr=fixedOrbitNum(zr,guardBits),ri=fixedOrbitNum(zi,guardBits);if(!Object.is(rr,ref.rr[n])||!Object.is(ri,ref.ri[n]))mismatch=true}
|
||||||
|
function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+6;compare();
|
||||||
|
while(n<refLen&&!escape&&!mismatch&&performance.now()<deadline){const zr2=roundShift(zr*zr,guardBits),zi2=roundShift(zi*zi,guardBits);zi=roundShift(2n*zr*zi,guardBits)+cIm;zr=zr2-zi2+cRe;n++;const mag=roundShift(zr*zr,guardBits)+roundShift(zi*zi,guardBits);if(mag>BAIL)escape=n;compare()}
|
||||||
|
if(mismatch||escape||n>=refLen){if((ref.escape||0)!==escape&&((ref.escape||0)<=refLen||escape<=refLen))mismatch=true;ref.checkpointVersion=ref.version;ref.checkpointBits=guardBits;ref.checkpointCount=checked;ref.checkpointMismatch=mismatch;done(!mismatch)}else requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function computeSeries(ref,refLen,logMaxDc){
|
||||||
|
// A series valid for a wider image remains valid after zooming further in.
|
||||||
|
if(ref.series&&logMaxDc<=ref.series.logMaxDc+.02&&ref.series.skip<refLen)return ref.series;
|
||||||
|
let A=[0,0,NEG_BUCKET],Bc=[0,0,NEG_BUCKET],bestSkip=0,bestA=[0,0,NEG_BUCKET],bestB=[0,0,NEG_BUCKET];
|
||||||
|
const LOG_LIMIT=Math.log2(2.2e-4),LOG_RATIO=Math.log2(.10);
|
||||||
|
for(let n=0;n<refLen;n++){
|
||||||
|
const R=ref.rr[n],I=ref.ri[n],oldA=A;
|
||||||
|
const at=oldA[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(R*oldA[0]-I*oldA[1]),2*(R*oldA[1]+I*oldA[0]),oldA[2]);
|
||||||
|
A=scAdd(at,[1,0,0]);
|
||||||
|
const bt=Bc[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(R*Bc[0]-I*Bc[1]),2*(R*Bc[1]+I*Bc[0]),Bc[2]);
|
||||||
|
const a2=oldA[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(oldA[0]*oldA[0]-oldA[1]*oldA[1],2*oldA[0]*oldA[1],2*oldA[2]);
|
||||||
|
Bc=scAdd(bt,a2);
|
||||||
|
const l1=scLog2(A)+logMaxDc,l2=scLog2(Bc)+2*logMaxDc;
|
||||||
|
if(Number.isFinite(l1)&&l1<LOG_LIMIT&&(!Number.isFinite(l2)||(l2<LOG_LIMIT&&l2<l1+LOG_RATIO))){bestSkip=n+1;bestA=A.slice();bestB=Bc.slice()}
|
||||||
|
}
|
||||||
|
if(bestSkip>=refLen)bestSkip=Math.max(0,refLen-1);
|
||||||
|
ref.series={logMaxDc,skip:bestSkip,Ar:bestA[0],Ai:bestA[1],Ab:bestA[2],Br:bestB[0],Bi:bestB[1],Bb:bestB[2]};
|
||||||
|
return ref.series;
|
||||||
|
}
|
||||||
|
function chooseReference(snap){
|
||||||
|
const c=referenceCache;if(c.rr&&c.n>8){const rr=align(c.re,c.bits,snap.bits),ri=align(c.im,c.bits,snap.bits),dx=fixedRatio(rr-snap.re,snap.span),dy=fixedRatio(ri-snap.im,snap.span),dist=Math.hypot(dx,dy);if(Number.isFinite(dist)){const hard=2.25;if(dist<=hard){let keep=true;if(dist>.45&&refControl.refId===c.id&&refControl.baseMPP>0&&refControl.lastMPP>refControl.baseMPP*1.34&&performance.now()-refControl.lastRecenterAt>refControl.cooldownMs){const extra=(refControl.lastMPP-refControl.baseMPP)*Math.max(1,refControl.lastPixels),build=Math.max(5,refControl.buildEMA||18);if(extra>build*1.65)keep=false}if(keep)return{re:rr,im:ri,reused:true,dist};refControl.lastRecenterAt=performance.now()}}}return{re:snap.re,im:snap.im,reused:false,dist:0}
|
||||||
|
}
|
||||||
|
function putDeepPixel(out,w,computeIter,colorIter,x,y,n,m,colorCtx){
|
||||||
|
const oi=(y*w+x)*4,mm=n>=computeIter?0:Math.max(4.0000001,m);
|
||||||
|
|
||||||
|
if(n>=computeIter){out[oi]=0;out[oi+1]=0;out[oi+2]=0;out[oi+3]=255;return}
|
||||||
|
putFastColor(out,oi,n,mm,colorIter,colorCtx);
|
||||||
|
}
|
||||||
|
function directOrbitState(cr,ci,bits,iter){return{cr,ci,bits,iter,zr:0n,zi:0n,mag:0n,n:0,done:false,four:4n*(1n<<BigInt(bits))}}
|
||||||
|
function stepDirectOrbit(orbit,deadline){let batch=0;while(!orbit.done){const zr2=roundShift(orbit.zr*orbit.zr,orbit.bits),zi2=roundShift(orbit.zi*orbit.zi,orbit.bits);orbit.mag=zr2+zi2;if(orbit.mag>orbit.four||orbit.n>=orbit.iter){orbit.done=true;break}orbit.zi=roundShift(2n*orbit.zr*orbit.zi,orbit.bits)+orbit.ci;orbit.zr=zr2-zi2+orbit.cr;orbit.n++;if((++batch&31)===0&&performance.now()>=deadline)break}if(orbit.n>=orbit.iter)orbit.done=true}
|
||||||
|
async function highPrecisionDirectPixelAsync(snap,w,h,iter,x,y,guarded=true,cancelled=()=>false){const p=fixedPixelPoint(snap,w,h,x,y),base=directOrbitState(p[0],p[1],snap.bits,iter);let guard=null;if(guarded){const bits=snap.bits+64,g=fixedPixelPoint(snap,w,h,x,y,bits);guard=directOrbitState(g[0],g[1],bits,iter)}while(!base.done||(guard&&!guard.done)){if(cancelled())return null;const deadline=performance.now()+6;stepDirectOrbit(base,deadline);if(guard)stepDirectOrbit(guard,deadline);if(!base.done||(guard&&!guard.done))await new Promise(requestAnimationFrame)}if(guard&&(guard.n!==base.n||((guard.n<iter)!==(base.n<iter))))return[iter,0];const chosen=guard||base;return[chosen.n,chosen.n<iter?Math.max(4.0000001,fixedOrbitNum(chosen.mag,chosen.bits)):0]}
|
||||||
|
function renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0){
|
||||||
|
const cap=profile.covered?w:390,scale=Math.min(1,cap/w);if(scale<1){h=Math.max(100,Math.round(h*scale));w=Math.max(160,Math.round(w*scale))}
|
||||||
|
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,colorIter),colorCtx=makeColorCtx(colorIter);let y=0;
|
||||||
|
state.lastEngine='BigInt direct fallback';
|
||||||
|
async function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+7;
|
||||||
|
while(y<h&&performance.now()<deadline){const y0=y;for(let x=0;x<w;x++){const result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,y,false,()=>token!==state.token);if(!result)return;const[n,m]=result;putField(field,y*w+x,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,y,n,m,colorCtx)}presentPartialStripe(out.subarray(y0*w*4,(y0+1)*w*4),0,y0,w,1,w,h,snap);y++}
|
||||||
|
if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'BigInt direct fallback',w*h,field);
|
||||||
|
}requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
function prepareDeepContext(snap,refC,iter,token,done){
|
||||||
|
const probeW=64,probeH=Math.max(28,Math.round(probeW*canvas.clientHeight/Math.max(1,canvas.clientWidth))),refX=probeW*.5-.5+fixedRatio(refC.re-snap.re,snap.span)*probeW,refY=probeH*.5-.5-fixedRatio(refC.im-snap.im,snap.span)*probeW,cornerR=Math.max(Math.hypot(refX,refY),Math.hypot(probeW-refX,refY),Math.hypot(refX,probeH-refY),Math.hypot(probeW-refX,probeH-refY))/Math.max(1,probeW),logMaxDc=log2FixedAt(snap.span,snap.bits)+Math.log2(Math.max(1e-300,cornerR));
|
||||||
|
buildReferenceAt(snap.bits,refC.re,refC.im,iter,token,(ref,refLen)=>{if(token!==state.token)return;if(ensureOrbitPrecision(iter,Math.max(1,canvas.width))){state.dirty=true;invalidateView();return}const finish=()=>done({ref,refLen,series:computeSeries(ref,refLen,logMaxDc)});if(state.processMode!=='validate'){finish();return}verifyReferenceCheckpoints(ref,refLen,token,ok=>{if(token!==state.token)return;if(ok){finish();return}promoteState(32);invalidateReferenceOrbit();state.dirty=true;invalidateView()})})
|
||||||
|
}
|
||||||
|
function renderPerturbPrepared(profile,snap,w,h,iter,colorIter,token,t0,refC,ref,refLen,series,reuse=null){
|
||||||
|
const workerReady=ensureDeepPool();if(!workerReady&&(!ensureDeepWasm()||!deepWasm.ex.render_perturb_rebase_rect)){renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0);return}
|
||||||
|
const centered=pixelCenteredOffset(snap,refC,w),out=reuse?reuse.out:new Uint8ClampedArray(w*h*4),field=makeField(w*h,colorIter),colorCtx=makeColorCtx(colorIter),sb=spanMantBucket(snap.span,snap.bits),off=fixedComplexScaled(centered.r,centered.i,snap.bits);
|
||||||
|
const fallback=(strict=false)=>{if(token!==state.token)return;if(!ensureDeepWasm()||!deepWasm.ex.render_perturb_rebase_rect){renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0);return}if(ref.loadedLen<refLen+1){deepWasm.refsR().set(ref.rr.subarray(ref.loadedLen,refLen+1),ref.loadedLen);deepWasm.refsI().set(ref.ri.subarray(ref.loadedLen,refLen+1),ref.loadedLen);ref.loadedLen=refLen+1}const counts=deepWasm.counts(),mags=deepWasm.mags(),bad=[];let y=0;function rows(){if(token!==state.token)return;const deadline=performance.now()+(profile.covered?11:8);while(y<h&&performance.now()<deadline){const rows=Math.min(Math.max(1,Math.floor(65536/Math.max(1,w))),h-y),y0=y,npx=deepWasm.ex.render_perturb_rebase_rect(sb.mant,sb.bucket,off[0],off[1],off[2],refLen,w,h,w*.5,h*.5,0,y,w,rows,iter,strict?0:series.skip,strict?0:series.Ar,strict?0:series.Ai,strict?0:series.Ab,strict?0:series.Br,strict?0:series.Bi,strict?0:series.Bb);for(let i=0;i<npx;i++){const x=i%w,py=y0+((i/w)|0),n=counts[i],m=mags[i];if(n===0xffffffff){bad.push(py*w+x);continue}putField(field,py*w+x,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}y+=rows}if(y<h)requestAnimationFrame(rows);else if(bad.length)runResidual();else finish()}function runResidual(){let k=0;async function slice(){if(token!==state.token)return;const deadline=performance.now()+6;while(k<bad.length&&performance.now()<deadline){const idx=bad[k++],x=idx%w,py=(idx/w)|0,result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,py,false,()=>token!==state.token);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}if(k<bad.length)requestAnimationFrame(slice);else finish()}requestAnimationFrame(slice)}function finish(){state.lastEngine='WASM '+(deepWasm.simd?'SIMD':'scalar')+' · '+(strict?'strict ':'')+'main-thread rebase · skip '+(strict?0:series.skip);finishRender(token,t0,profile,snap,w,h,out,state.lastEngine,w*h,field)}requestAnimationFrame(rows)};
|
||||||
|
// Never abort and restart an in-flight frame because a budget estimate was wrong.
|
||||||
|
// Finish it once and feed the measured cost into the next frame.
|
||||||
|
if(!workerReady||!runDeepPool(profile,snap,w,h,iter,colorIter,token,t0,out,sb,refC,off,ref,refLen,series,fallback,reuse?reuse.rects:null))fallback();
|
||||||
|
}
|
||||||
|
async function renderDeepAdaptive(profile,snap,plan,token,t0){
|
||||||
|
try{await prepareDeepModules()}catch{if(token===state.token){const [fallbackW,fallbackH]=targetSize(profile,true);renderDeepDirect(profile,snap,fallbackW,fallbackH,plan.colorIter,plan.colorIter,token,t0)}return}if(token!==state.token)return;
|
||||||
|
const iter=plan.colorIter,colorIter=plan.colorIter,[w,h]=targetSize(profile,true),reuse=buildPanReuse(snap,w,h,iter,profile);
|
||||||
|
// A zoom-in can be represented entirely by the previous frame. Commit that preview
|
||||||
|
// immediately; the normal idle HQ pass performs the exact full-resolution render.
|
||||||
|
if(reuse&&reuse.rects.length===0&&!profile.covered){finishRender(token,t0,profile,snap,w,h,reuse.out,'既存画像再利用 100%',0);return}
|
||||||
|
const refC=chooseReference(snap);state.lastEngine='参照軌道を準備中…';prepareDeepContext(snap,refC,iter,token,ctx=>{if(token!==state.token)return;renderPerturbPrepared(profile,snap,w,h,iter,colorIter,token,t0,refC,ctx.ref,ctx.refLen,ctx.series,reuse)})
|
||||||
|
}
|
||||||
|
function render(pass=RENDER_PASS.PREVIEW){
|
||||||
|
const profile=renderProfile(pass);
|
||||||
|
if(pass===RENDER_PASS.COVERED){state.fieldView=null;trimDetailCache()}
|
||||||
|
runtimeMetrics.renderStarts++;if(state.pointerActive||state.wheelActive)runtimeMetrics.renderStartsDuringGesture++;
|
||||||
|
let snap=snapshot(),deep=deepEngineNeeded(snap);if(!deep&&state.adaptivePixelBudget){state.adaptivePixelBudget=0;resize()}if(deep&&ensureOrbitPrecision(maxIter()))snap=snapshot();const plan=iterationPlan(profile,deep),token=++state.token,t0=performance.now();state.rendering=true;state.drawState=profile.covered?'COVERING':'PREVIEW';state.lastPass=profile.id;state.lastEngine=deep?'参照軌道を準備中…':(wasm?('WebAssembly '+(wasm.simd?'SIMD':'scalar')):'JavaScript f64');
|
||||||
|
if(!deep){const ratio=deepResolutionRatio(snap);if(ratio<=128)prewarmDeepAssets();else if(deepPool.workers.length||deepWasm||deepModuleBundle||referenceCache.rr)retireDeepAssets()}
|
||||||
|
invalidateStats();
|
||||||
|
if(deep)renderDeepAdaptive(profile,snap,plan,token,t0);else{const [w,h]=targetSize(profile,false),reuse=buildPanReuse(snap,w,h,plan.computeIter,profile);if(reuse)renderShallowReuse(profile,snap,w,h,plan.computeIter,token,t0,reuse);else if(wasm)renderWasm(profile,snap,w,h,plan.computeIter,token,t0);else renderJsDouble(profile,snap,w,h,plan.computeIter,token,t0)}
|
||||||
|
}
|
||||||
|
function screenPixelBudget(){
|
||||||
|
const lowMemory=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(state.processMode==='power')return 1*1048576;if(state.processMode==='fine'||state.processMode==='validate')return(lowMemory||small?4:8)*1048576;
|
||||||
|
const base=(lowMemory||small?2:4)*1048576;return state.adaptivePixelBudget?Math.min(base,state.adaptivePixelBudget):base
|
||||||
|
}
|
||||||
|
function adaptStandardDeepBudget(deep){if(state.processMode!=='standard'||!deep)return false;const measuredMPP=renderPerf.deepMPP||.03,target=Math.max(32768,Math.min(4*1048576,Math.round(1400/measuredMPP))),oldBudget=state.adaptivePixelBudget||screenPixelBudget();if(target/oldBudget>=.8&&target/oldBudget<=1.25)return false;const oldPixels=canvas.width*canvas.height;state.adaptivePixelBudget=target;resize();return canvas.width*canvas.height!==oldPixels}
|
||||||
|
function resize(){
|
||||||
|
const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=screenPixelBudget(),nativeDpr=Math.max(1,window.devicePixelRatio||1),budgetDpr=Math.sqrt(budget/Math.max(1,cssW*cssH)),minDpr=Math.min(1,64/Math.max(cssW,cssH)),dpr=Math.max(minDpr,Math.min(nativeDpr,budgetDpr)),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){clearPanReuse();canvas.width=w;canvas.height=h;trimDetailCache();state.dirty=true;invalidateView()}
|
||||||
|
}
|
||||||
|
function jaEngine(s){return String(s||'').replace('preparing cached rebase reference…','参照軌道を準備中…').replace('persistent workers','常駐Worker').replace('main-thread','メインスレッド').replace('strict','厳密').replace('fallback','フォールバック').replace('rebase','リベース').replace('skip','スキップ').replace('residual','残差').replace('starting','起動中')}
|
||||||
|
function updateStats(){
|
||||||
|
runtimeMetrics.domWrites++;
|
||||||
|
const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';
|
||||||
|
$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();const deep=deepEngineNeeded(),diagnosticProfile=RENDER_PROFILE[RENDER_PASS.COVERED],ip=iterationPlan(diagnosticProfile,deep);$('#engine').textContent=deep?'高精度深部':'標準精度';$('#render').textContent=state.rendering?'描画中…':(state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了');let status;if(state.drawState==='REPROJECTED')status='再投影';else if(state.drawState==='PREVIEW')status=state.rendering?'プレビュー描画中':'プレビュー';else if(state.drawState==='COVERING')status='全域描画中';else if(state.drawState==='RESOLVING')status='未確定を追加計算 '+Math.round(state.continuationProgress*100)+'%';else if(state.drawState==='REFINING')status='境界AA '+state.detailDone+'/'+state.detailQueued;else if(state.drawState==='REFINED')status='境界AA 完了';else if(state.drawState==='VALIDATING')status='精度照合 '+Math.round(state.coverage*100)+'%';else if(state.drawState==='VALIDATION_INCOMPLETE')status='検証未完了';else if(state.drawState==='VALIDATED')status='検証完了';else status='全域描画 完了';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.effectiveDpr<(devicePixelRatio||1)*.99)status+=' · '+canvas.width+'×'+canvas.height;$('#badge').textContent=status;$('#compactStatus').textContent=status;const ledger=memoryLedger(),bp=blaProfile(diagnosticProfile);$('#diagEngine').textContent='engine: '+jaEngine(state.lastEngine)+' | workers '+deepPool.workers.length+' | '+(wasm&&wasm.simd?'SIMD':'scalar/JS');$('#diagNumeric').textContent='numeric: '+state.bits+' bit | iter '+ip.colorIter+' | BLA ε 2^-'+bp.exp+' | condition '+Math.round(referenceCache.conditionLog2||0)+' bit';$('#diagMemory').textContent='memory: managed '+(ledger.managedBytes/1048576).toFixed(1)+' / '+(ledger.budget/1048576).toFixed(0)+' MiB | canvas est. '+(ledger.canvasBytes/1048576).toFixed(1)+' MiB';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Scheduler / interaction loop ────────────────────────────────────────
|
||||||
|
function nextQualityDue(deep){
|
||||||
|
if(state.processMode==='power'||state.dirty||state.rendering||state.pointerActive||state.wheelActive)return Infinity;
|
||||||
|
if(deep&&state.lastPass!==RENDER_PASS.COVERED)return Math.max(state.lastInteraction+680,state.lastFrameDone+260);
|
||||||
|
if(!deep&&state.lastPass!==RENDER_PASS.COVERED)return Math.max(state.lastInteraction+520,state.lastFrameDone+280);
|
||||||
|
return Infinity
|
||||||
|
}
|
||||||
|
function loop(now){
|
||||||
|
schedulerRAF=0;if(document.hidden)return;
|
||||||
|
const deep=deepEngineNeeded();
|
||||||
|
if(!state.pointerActive&&!state.wheelActive&&!state.rendering){
|
||||||
|
if(state.dirty)render(RENDER_PASS.PREVIEW);
|
||||||
|
else{
|
||||||
|
const due=nextQualityDue(deep);
|
||||||
|
if(due<=now)render(RENDER_PASS.COVERED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(needsPaint){needsPaint=false;paintFrame()}
|
||||||
|
if(needsStats){needsStats=false;updateStats()}
|
||||||
|
if(!state.rendering&&!state.pointerActive&&!state.wheelActive&&!state.dirty){
|
||||||
|
const due=nextQualityDue(deep);if(Number.isFinite(due))requestScheduler(Math.max(1,due-performance.now()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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){state.wheelActive=true;cancelDetailRefinement(true);cancelRender();capturePanSource()}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(wheelSettleTimer);wheelSettleTimer=setTimeout(()=>{wheelSettleTimer=0;state.wheelActive=false;state.lastInteraction=performance.now();state.dirty=true;recordView();saveHash(false);invalidateView()},110)},{passive:false});
|
||||||
|
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(pts.size===0){clearTimeout(pointerSettleTimer);pointerSettleTimer=0;clearTimeout(wheelSettleTimer);wheelSettleTimer=0;state.wheelActive=false;state.pointerActive=true;cancelDetailRefinement(true);cancelRender();capturePanSource()}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else if(pts.size===2){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 end(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(pointerSettleTimer);pointerSettleTimer=setTimeout(()=>{pointerSettleTimer=0;state.pointerActive=false;state.lastInteraction=performance.now();state.dirty=true;recordView();saveHash(false);invalidateView()},90)}canvas.addEventListener('pointerup',end);canvas.addEventListener('pointercancel',end);
|
||||||
|
function applyUiVisibility(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',String(!state.uiHidden))}
|
||||||
|
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;applyUiVisibility();try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}};
|
||||||
|
$('#zin').onclick=()=>{capturePanSource();zoomAt(innerWidth/2,innerHeight/2,.5);recordView();saveHash(false)};$('#zout').onclick=()=>{capturePanSource();zoomAt(innerWidth/2,innerHeight/2,2);recordView();saveHash(false)};$('#reset').onclick=()=>{reset();recordView()};
|
||||||
|
const exportJob={active:false,cancelled:false,bytes:0};let exportForcePrecision=false;
|
||||||
|
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1500)}
|
||||||
|
function exportLedgerBytes(){return memoryLedger().logicalBytes}
|
||||||
|
function exportSampleShallow(cre,cim,scale,W,H,iter,x,y){const cr=cre+(x+.5-W*.5)*scale,ci=cim+(H*.5-y-.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0;while(n<iter&&zr2+zi2<=4){zi=2*zr*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}return[n,n<iter?Math.max(4.0000001,zr2+zi2):0]}
|
||||||
|
async function runExport(){if(exportJob.active)return;const scaleChoice=Number($('#exportScale').value),w=Math.max(64,Math.min(16384,Math.round(scaleChoice?canvas.width*scaleChoice:Number($('#exportWidth').value)||canvas.width))),h=Math.max(1,Math.round(w*canvas.height/Math.max(1,canvas.width))),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),needed=w*h*8,budget=rendererMemoryBudget(),available=Math.max(0,budget-exportLedgerBytes());if(w>16384||h>16384){$('#exportStatus').textContent='辺の長さは 16384px 以下にしてください。';return}if(needed>available){$('#exportStatus').textContent='メモリ予算を超えます。幅または倍率を下げてください(必要 '+Math.ceil(needed/1048576)+' MiB / 空き '+Math.floor(available/1048576)+' MiB)。';return}const outCanvas=document.createElement('canvas');outCanvas.width=w;outCanvas.height=h;const oc=outCanvas.getContext('2d',{alpha:false});if(!oc){$('#exportStatus').textContent='出力 Canvas を作成できません。';return}const snap=snapshot(),iter=maxIter(),deep=deepEngineNeeded(snap,w),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),W=w*ss,H=h*ss,pixelScale=sp/W,colorCtx=makeColorCtx(iter),tmp=new Uint8ClampedArray(4),tiles=[],tileW=exportForcePrecision?4:(deep?16:128),tileH=exportForcePrecision?1:(deep?4:24);for(let y=0;y<h;y+=tileH)for(let x=0;x<w;x+=tileW)tiles.push({x,y,w:Math.min(tileW,w-x),h:Math.min(tileH,h-y)});exportJob.active=true;exportJob.cancelled=false;exportJob.bytes=needed;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;$('#exportStatus').textContent='タイル描画中…';let done=0,unresolvedSamples=0;try{for(const tile of tiles){if(exportJob.cancelled)throw new Error('cancelled');const id=oc.createImageData(tile.w,tile.h),data=id.data;for(let yy=0;yy<tile.h;yy++)for(let xx=0;xx<tile.w;xx++){let ar=0,ag=0,ab=0;for(let sy=0;sy<ss;sy++)for(let sx=0;sx<ss;sx++){const gx=(tile.x+xx)*ss+sx,gy=(tile.y+yy)*ss+sy,sample=deep?await highPrecisionDirectPixelAsync(snap,W,H,iter,gx,gy,exportForcePrecision,()=>exportJob.cancelled):exportSampleShallow(cre,cim,pixelScale,W,H,iter,gx,gy);if(!sample)throw new Error('cancelled');const[n,m]=sample;if(n<iter){putFastColor(tmp,0,n,m,iter,colorCtx);ar+=linearChannel(tmp[0]);ag+=linearChannel(tmp[1]);ab+=linearChannel(tmp[2])}else{ar+=linearChannel(20);ag+=linearChannel(22);ab+=linearChannel(30);unresolvedSamples++}}const samples=ss*ss,oi=(yy*tile.w+xx)*4;data[oi]=srgbChannel(ar/samples);data[oi+1]=srgbChannel(ag/samples);data[oi+2]=srgbChannel(ab/samples);data[oi+3]=255}oc.putImageData(id,tile.x,tile.y);done++;$('#exportProgress').value=done/tiles.length;$('#exportStatus').textContent='生成中 '+Math.round(done/tiles.length*100)+'%';await new Promise(requestAnimationFrame)}if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGを圧縮中…';const blob=await new Promise(resolve=>outCanvas.toBlob(resolve,'image/png'));if(!blob)throw new Error('PNG encode failed');const stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v23',rendererVersion:23,pixelContract:'centered',width:w,height:h,supersampling:ss,sampleCount:w*h*ss*ss,unresolvedSamples,membershipCertified:false,precisionPolicy:{mode:exportForcePrecision?'validated-direct':'balanced',baseBits:snap.bits,agreementGuardBits:exportForcePrecision?64:0},iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},numericEngine:deep?'bigint-fixed-direct':'javascript-f64-direct',kernelSha256:globalThis.MANDEL_KERNEL_META||null,colorSpace:'sRGB with linear-light sample resolve',encoder:{mime:'image/png',api:'HTMLCanvasElement.toBlob'},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},determinism:{randomSeed:null,tiled:true,allTilesCompleted:true}};downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');$('#exportStatus').textContent='PNG と座標メタデータを保存しました。'}catch(e){$('#exportStatus').textContent=String(e&&e.message)==='cancelled'?'出力を中止しました。':'出力に失敗しました: '+String(e&&e.message||e)}finally{exportJob.active=false;exportJob.bytes=0;$('#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','')};
|
||||||
|
$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(!blob)return;downloadBlob(blob,'mandelbrot-'+state.drawState.toLowerCase()+'-'+Date.now()+'.png');$('#exportStatus').textContent='現在の表示('+state.drawState+')を保存しました。'},'image/png');
|
||||||
|
$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(Math.min(16384,canvas.width*s))};$('#exportStart').onclick=async()=>{exportForcePrecision=$('#exportPrecision').value==='validated';try{await runExport()}finally{exportForcePrecision=false}};$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};
|
||||||
|
function toast(s){const t=$('#toast');t.textContent=s;t.classList.add('show');clearTimeout(toast._t);toast._t=setTimeout(()=>t.classList.remove('show'),1500)}
|
||||||
|
let lastWrittenHash='',navigationHash='';
|
||||||
|
const viewHistory=[];let viewHistoryIndex=-1;
|
||||||
|
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(),key=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===key)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;state.bits=v.bits;state.re=v.re;state.im=v.im;state.span=v.span;state.palette=v.palette;state.cycle=v.cycle;state.shift=v.shift;state.baseIter=v.baseIter;state.adaptive=v.adaptive;clearDetailCache();clearPanReuse();ensurePrecision();syncControls();saveHash(false);setDirty()}
|
||||||
|
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
|
||||||
|
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
|
||||||
|
function saveHash(push){const p=new URLSearchParams();p.set('v','23');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{if(location.protocol==='file:'||location.origin==='null'){if(location.hash!==h)location.hash=h}else{push?history.pushState(null,'',h):history.replaceState(null,'',h)}}catch{}}
|
||||||
|
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 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(Math.round(state.baseIter));$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
|
||||||
|
function applyHashNavigation(){const h=location.hash;if(h===lastWrittenHash){lastWrittenHash='';return}if(h===navigationHash)return;navigationHash=h;setTimeout(()=>{navigationHash=''},0);if(loadHash()){clearDetailCache();clearPanReuse();recordView();syncControls();setDirty()}}
|
||||||
|
$('#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);setDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
|
||||||
|
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:23,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()}};
|
||||||
|
function bind(id,key,out,fmt,flush=false){const el=$(id),o=$(out),apply=()=>{state[key]=Number(el.value);o.textContent=fmt(Number(el.value));if(flush)clearDetailCache();setDirty()};el.addEventListener('input',apply);apply()}
|
||||||
|
function bindColor(id,key,out,fmt){const el=$(id),o=$(out),apply=()=>{state[key]=Number(el.value);o.textContent=fmt(Number(el.value));if(!recolorCurrentField())setDirty()};el.addEventListener('input',apply);apply()}
|
||||||
|
bind('#iters','baseIter','#itersO',x=>String(Math.round(x)),true);bindColor('#cycle','cycle','#cycleO',x=>x.toFixed(4));bindColor('#shift','shift','#shiftO',x=>x.toFixed(2));
|
||||||
|
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));if(!recolorCurrentField())setDirty()};
|
||||||
|
$('#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';state.adaptivePixelBudget=0;$('#hq').checked=state.hq;try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}resize();setDirty()};
|
||||||
|
$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;clearDetailCache();setDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;if(!state.hq)cancelDetailRefinement(true);setDirty()};
|
||||||
|
addEventListener('resize',()=>{resize();setDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let handled=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'){capturePanSource();pan(innerWidth*.08,0)}else if(e.key==='ArrowRight'){capturePanSource();pan(-innerWidth*.08,0)}else if(e.key==='ArrowUp'){capturePanSource();pan(0,innerHeight*.08)}else if(e.key==='ArrowDown'){capturePanSource();pan(0,-innerHeight*.08)}else handled=false;if(handled){e.preventDefault();recordView();saveHash(false)}});addEventListener('popstate',applyHashNavigation);addEventListener('hashchange',applyHashNavigation);
|
||||||
|
function stopSchedulers(){if(schedulerRAF){cancelAnimationFrame(schedulerRAF);schedulerRAF=0}if(schedulerTimer){clearTimeout(schedulerTimer);schedulerTimer=0;schedulerDue=0}if(wheelSettleTimer){clearTimeout(wheelSettleTimer);wheelSettleTimer=0}if(pointerSettleTimer){clearTimeout(pointerSettleTimer);pointerSettleTimer=0}cancelUnknownContinuation()}
|
||||||
|
addEventListener('visibilitychange',()=>{if(document.hidden){exportJob.cancelled=true;stopSchedulers();state.wheelActive=false;cancelRender();cancelDetailRefinement(true)}else{state.lastInteraction=performance.now();state.dirty=true;invalidateView()}});
|
||||||
|
globalThis.__MANDEL_DIAG__={snapshot:()=>{const deep=deepEngineNeeded(),ledger=memoryLedger(),coveredProfile=RENDER_PROFILE[RENDER_PASS.COVERED];return{rendererVersion:23,pixelContract:'centered',processMode:state.processMode,automaticTarget:modeTarget(),drawState:state.drawState,coverage:state.coverage,unresolved:state.unresolved,zoom:zoomExp(),deep,legacyDeepThreshold:LEGACY_DEEP_ZOOM_THRESHOLD,bits:state.bits,palette:state.palette,rendering:state.rendering,validating:validationJob.active,exporting:exportJob.active,lastRender:state.lastRender,lastPass:state.lastPass,transformReuse:!!state.panReuse,detailCache:DETAIL_TILE_CACHE.size,detailCacheBytes,detailCacheBudget:detailCacheBudget(),detailActive:state.detailActive,frame:frameCanvas.width+'x'+frameCanvas.height,frameCoverage:frameCanvas.width/Math.max(1,canvas.width),hqTarget:targetSize(coveredProfile,deep).join('x'),scheduler:{raf:!!schedulerRAF,timer:!!schedulerTimer,needsPaint,needsStats,wheelActive:state.wheelActive,pointerSettle:!!pointerSettleTimer,unknownTimer:!!unknownContinuationTimer,backgroundJobs:runtimeMetrics.pendingBackgroundJobs},screen:{effectiveDpr:state.effectiveDpr,deviceDpr:window.devicePixelRatio||1,pixelBudget:state.screenPixelBudget,pixels:canvas.width*canvas.height},memory:ledger,shallowAssets:{wasmReady:!!wasm,worker:!!shallowWorker,workerBusy:shallowWorkerBusy},deepAssets:{wasmReady:!!deepWasm,workers:deepPool.workers.length},runtimeMetrics:{...runtimeMetrics},telemetry:{...deepTelemetry},renderPerf:{...renderPerf},wisdom:{workerCount:deepWisdom.workerCount,rowMsEMA:deepWisdom.rowMsEMA,stripRows:deepWisdom.stripRows,realBench:deepWisdom.realBench},reference:{id:referenceCache.id,bits:referenceCache.bits,n:referenceCache.n,escape:referenceCache.escape,buildMs:refControl.lastBuildMs,buildEMA:refControl.buildEMA,baseMPP:refControl.baseMPP,lastMPP:refControl.lastMPP,conditionLog2:referenceCache.conditionLog2,checkpointBits:referenceCache.checkpointBits,checkpointCount:referenceCache.checkpointCount,checkpointMismatch:referenceCache.checkpointMismatch}}},memoryLedger:()=>memoryLedger()};
|
||||||
|
addEventListener('pagehide',()=>{stopSchedulers();destroyShallowWorker();destroyDeepPool()},{once:true});
|
||||||
|
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const savedMode=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(savedMode)){state.processMode=savedMode;state.hq=savedMode==='fine'||savedMode==='validate'}}catch{}restoreDeepWisdom();applyUiVisibility();resize();if(!loadHash())reset();else setDirty();recordView();syncControls();ctx.fillStyle='#050813';ctx.fillRect(0,0,canvas.width,canvas.height);render(RENDER_PASS.PREVIEW);requestScheduler();
|
||||||
|
})();
|
||||||
69
dist/standalone/index.html
vendored
Normal file
69
dist/standalone/index.html
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||||
|
<meta name="theme-color" content="#050813">
|
||||||
|
<title>Mandelbrot Deep Zoom v23</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
|
||||||
|
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
|
||||||
|
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
|
||||||
|
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
|
||||||
|
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
|
||||||
|
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
|
||||||
|
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
|
||||||
|
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}#palette{color:#111;background:#f4f5f8}#palette option{color:#111;background:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
|
||||||
|
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
|
||||||
|
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.exact-grid{display:grid;grid-template-columns:54px 1fr;gap:6px;margin-top:8px}.exact-grid input{min-width:0;width:100%;min-height:44px;border:1px solid var(--line);border-radius:8px;background:#070c19;color:var(--text);padding:6px;font:11px ui-monospace,monospace}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
|
||||||
|
.diagnostics{margin-top:8px;color:var(--muted);font:10.5px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.exact-grid input{user-select:text;-webkit-user-select:text}
|
||||||
|
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
|
||||||
|
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
|
||||||
|
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}
|
||||||
|
#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
|
||||||
|
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
|
||||||
|
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
|
||||||
|
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
|
||||||
|
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord">…</span></div><div class="row"><span class="muted">倍率</span> <span id="zoom">…</span> <span class="muted">表示幅</span> <span id="span">…</span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render">…</span></div></div></div>
|
||||||
|
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
|
||||||
|
<div id="controls" class="panel">
|
||||||
|
<div class="toolbar"><button id="zin" aria-label="中心を拡大">+</button><button id="zout" aria-label="中心を縮小">−</button><button id="reset">リセット</button><button id="png">出力</button></div>
|
||||||
|
<div class="group">
|
||||||
|
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">精度優先</option></select><output></output></div>
|
||||||
|
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
|
||||||
|
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0.001" max="0.05" step="0.0005" value="0.008"><output id="cycleO" for="cycle">0.0080</output></div>
|
||||||
|
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
|
||||||
|
<details><summary>詳細設定・正確な座標</summary>
|
||||||
|
<div class="line"><label for="iters">基準反復</label><input id="iters" type="range" min="100" max="2500" step="25" value="350"><output id="itersO" for="iters">350</output></div>
|
||||||
|
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> 境界AAを追加</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></details>
|
||||||
|
</div>
|
||||||
|
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
|
||||||
|
<div class="hint">ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。</div>
|
||||||
|
</div>
|
||||||
|
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI−</button>
|
||||||
|
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||||
|
<dialog id="exportDialog" aria-labelledby="exportTitle">
|
||||||
|
<h2 id="exportTitle">高解像度 PNG 出力</h2>
|
||||||
|
<div class="export-grid">
|
||||||
|
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
|
||||||
|
<label for="exportWidth">幅(px)</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
|
||||||
|
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×(高速)</option><option value="2">2×2 AA</option></select>
|
||||||
|
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="validated">Validated direct</option></select>
|
||||||
|
</div>
|
||||||
|
<progress id="exportProgress" max="1" value="0" hidden></progress>
|
||||||
|
<div id="exportStatus" role="status" aria-live="polite"></div>
|
||||||
|
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
|
||||||
|
</dialog>
|
||||||
|
<script src="kernels.js"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
dist/standalone/kernels.js
vendored
Normal file
22
dist/standalone/kernels.js
vendored
Normal file
File diff suppressed because one or more lines are too long
912
dist/standalone/script.js
vendored
Normal file
912
dist/standalone/script.js
vendored
Normal file
|
|
@ -0,0 +1,912 @@
|
||||||
|
(()=>{'use strict';
|
||||||
|
const K=globalThis.MANDEL_KERNELS;
|
||||||
|
if(!K)throw new Error('kernels.js が読み込まれていません');
|
||||||
|
const {WASM_SIMD_B64,WASM_SCALAR_B64,DEEP_SIMD_B64,DEEP_SCALAR_B64,BLA_SIMD_B64,BLA_SCALAR_B64,COLOR_SIMD_B64,COLOR_SCALAR_B64}=K;
|
||||||
|
const $=s=>document.querySelector(s);
|
||||||
|
const canvas=$('#view');
|
||||||
|
const ctx=canvas.getContext('2d',{alpha:false,desynchronized:true})||canvas.getContext('2d',{alpha:false});
|
||||||
|
if(!ctx){document.body.innerHTML='<div style="padding:30px;color:white">Canvas 2Dを利用できません。</div>';return;}
|
||||||
|
|
||||||
|
const INITIAL_BITS=256;
|
||||||
|
const MIN_SPAN_BITS=224;
|
||||||
|
const TARGET_SPAN_BITS=240;
|
||||||
|
const RATIO_DEN=4503599627370496n; // 2^52
|
||||||
|
const POW256=1.157920892373162e77;
|
||||||
|
const INV256=8.636168555094445e-78;
|
||||||
|
const HI128=3.402823669209385e38;
|
||||||
|
const LO128=2.938735877055719e-39;
|
||||||
|
const NEG_BUCKET=-1000000000;
|
||||||
|
const LEGACY_DEEP_ZOOM_THRESHOLD=11.5;
|
||||||
|
const RENDER_PASS=Object.freeze({PREVIEW:'preview',COVERED:'covered'});
|
||||||
|
const MODE_TARGET=Object.freeze({power:'PREVIEW',standard:'COVERED',fine:'REFINED',validate:'VALIDATED'});
|
||||||
|
function modeTarget(mode=state.processMode){return MODE_TARGET[mode]||MODE_TARGET.standard}
|
||||||
|
const RENDER_PROFILE=Object.freeze({
|
||||||
|
[RENDER_PASS.PREVIEW]:Object.freeze({id:RENDER_PASS.PREVIEW,covered:false,budgetMs:110,nominalScale:.42,minWidth:360,maxWidth:900,densityLimit:1.55,blaSteps:1700,ptbSteps:2500}),
|
||||||
|
[RENDER_PASS.COVERED]:Object.freeze({id:RENDER_PASS.COVERED,covered:true,budgetMs:1050,nominalScale:1,minWidth:0,maxWidth:Infinity,densityLimit:1,blaSteps:0,ptbSteps:0})
|
||||||
|
});
|
||||||
|
function renderProfile(pass){return RENDER_PROFILE[pass]||RENDER_PROFILE[RENDER_PASS.PREVIEW]}
|
||||||
|
let deepMode=false;
|
||||||
|
|
||||||
|
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,lastRender:0,lastEngine:'起動中',lastPass:null,lastInteraction:performance.now(),dirty:true,
|
||||||
|
uiHidden:false,frameView:null,fieldView:null,lastFrameDone:0,
|
||||||
|
detailGeneration:0,detailActive:false,detailQueued:0,detailDone:0,
|
||||||
|
drawState:'REPROJECTED',coverage:0,unresolved:0,
|
||||||
|
precisionPending:false,
|
||||||
|
pointerActive:false,wheelActive:false,panReuse:null,effectiveDpr:1,screenPixelBudget:0,adaptivePixelBudget:0,continuationProgress:0,focusX:.5,focusY:.5
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deep telemetry is collected from the actual BLA/perturbation
|
||||||
|
// kernel rather than from a synthetic JavaScript loop.
|
||||||
|
const deepTelemetry={
|
||||||
|
frames:0,kernelMPP:0,meanIter:0,blackRatio:0,badRatio:0,blaBuildEMA:0,
|
||||||
|
refDistance:0,interiorRatio:0,repairRatio:0,unresolvedRatio:0,
|
||||||
|
blaStepsPerPixel:0,ptbStepsPerPixel:0,rebasePerPixel:0,lastPilotMs:0,lastPilotMPP:0,lastKernelMs:0,lastVerifyMs:0,lastPixels:0
|
||||||
|
};
|
||||||
|
const refControl={buildEMA:18,lastBuildMs:0,lastRecenterAt:0,refId:0,baseMPP:0,lastMPP:0,lastPixels:0,cooldownMs:900};
|
||||||
|
|
||||||
|
// Adaptive quality controller: instead of tying a quality level to a fixed
|
||||||
|
// pixel width, learn the recent cost per pixel and spend a bounded amount of time.
|
||||||
|
const renderPerf={deepMPP:0,shallowMPP:0};
|
||||||
|
const runtimeMetrics={canvasWrites:0,domWrites:0,renderStarts:0,renderStartsDuringGesture:0,longTasks:0,maxLongTaskMs:0,pendingBackgroundJobs:0};
|
||||||
|
try{if('PerformanceObserver'in globalThis){const observer=new PerformanceObserver(list=>{for(const entry of list.getEntries()){runtimeMetrics.longTasks++;runtimeMetrics.maxLongTaskMs=Math.max(runtimeMetrics.maxLongTaskMs,entry.duration||0)}});observer.observe({type:'longtask',buffered:true})}}catch{}
|
||||||
|
function scheduleBackground(fn,delay=0){runtimeMetrics.pendingBackgroundJobs++;return setTimeout(()=>{runtimeMetrics.pendingBackgroundJobs=Math.max(0,runtimeMetrics.pendingBackgroundJobs-1);fn()},delay)}
|
||||||
|
function scheduleIdle(fn,timeout=350){runtimeMetrics.pendingBackgroundJobs++;const run=()=>{runtimeMetrics.pendingBackgroundJobs=Math.max(0,runtimeMetrics.pendingBackgroundJobs-1);fn()};return'requestIdleCallback'in window?requestIdleCallback(run,{timeout}):setTimeout(run,Math.min(80,timeout))}
|
||||||
|
const DETAIL_TILE_CACHE=new Map();
|
||||||
|
let detailCacheBytes=0;
|
||||||
|
const activeDetailTiles=[];
|
||||||
|
let detailPlan=null;
|
||||||
|
let schedulerRAF=0,schedulerTimer=0,schedulerDue=0,needsPaint=true,needsStats=true,wheelSettleTimer=0,pointerSettleTimer=0;
|
||||||
|
|
||||||
|
function requestScheduler(delay=0){
|
||||||
|
if(document.hidden)return;
|
||||||
|
if(delay>0){
|
||||||
|
const due=performance.now()+delay;
|
||||||
|
if(schedulerTimer&&schedulerDue<=due)return;
|
||||||
|
if(schedulerTimer)clearTimeout(schedulerTimer);
|
||||||
|
schedulerDue=due;
|
||||||
|
schedulerTimer=setTimeout(()=>{schedulerTimer=0;schedulerDue=0;requestScheduler()},Math.max(0,Math.ceil(delay)));
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(schedulerTimer){clearTimeout(schedulerTimer);schedulerTimer=0;schedulerDue=0}
|
||||||
|
if(!schedulerRAF)schedulerRAF=requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
function invalidateView(withStats=true){needsPaint=true;if(withStats)needsStats=true;requestScheduler()}
|
||||||
|
function invalidateStats(){needsStats=true;requestScheduler()}
|
||||||
|
|
||||||
|
// Device-specific runtime wisdom. It is populated by a small background CPU
|
||||||
|
// concurrency benchmark and then refined by real deep-render strip timings.
|
||||||
|
const deepWisdom={
|
||||||
|
ready:false,running:false,maxWorkers:1,workerCount:1,
|
||||||
|
targetStripMs:12,stripRows:24,rowMsEMA:0,
|
||||||
|
realBench:false
|
||||||
|
};
|
||||||
|
function deepWisdomStorageKey(){
|
||||||
|
const meta=globalThis.MANDEL_KERNEL_META||{},kernel=String(meta.BLA_SIMD_B64||meta.BLA_SCALAR_B64||'embedded').slice(0,16),hc=Math.max(1,navigator.hardwareConcurrency||1),dm=Number(navigator.deviceMemory||0);return'mandelbrot.wisdom.v23.'+[kernel,hc,dm].join('.')
|
||||||
|
}
|
||||||
|
function restoreDeepWisdom(){
|
||||||
|
try{const saved=JSON.parse(localStorage.getItem(deepWisdomStorageKey())||'null');if(!saved||saved.version!==23)return;const row=Number(saved.rowMsEMA),rows=Number(saved.stripRows),workers=Number(saved.workerCount);if(Number.isFinite(row)&&row>0&&row<1000)deepWisdom.rowMsEMA=row;if(Number.isInteger(rows)&&rows>=2&&rows<=128)deepWisdom.stripRows=rows;if(Number.isInteger(workers)&&workers>=1&&workers<=deepWorkerLimit())deepWisdom.workerCount=workers;deepWisdom.ready=deepWisdom.rowMsEMA>0}catch{}
|
||||||
|
}
|
||||||
|
function persistDeepWisdom(){
|
||||||
|
if(!deepWisdom.rowMsEMA)return;try{localStorage.setItem(deepWisdomStorageKey(),JSON.stringify({version:23,rowMsEMA:deepWisdom.rowMsEMA,stripRows:deepWisdom.stripRows,workerCount:deepWisdom.workerCount}))}catch{}
|
||||||
|
}
|
||||||
|
|
||||||
|
function one(bits=state.bits){return 1n<<BigInt(bits)}
|
||||||
|
function fromFrac(n,d=1n){return n*one()/d}
|
||||||
|
function fromDec(s){
|
||||||
|
s=String(s).trim();let neg=s[0]==='-';if(neg)s=s.slice(1);
|
||||||
|
const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0;
|
||||||
|
const a=mant.split('.'),i=a[0]||'0',f=a[1]||'';
|
||||||
|
let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',decPlaces=f.length-exp;
|
||||||
|
if(decPlaces<0){digits+='0'.repeat(-decPlaces);decPlaces=0}
|
||||||
|
const den=10n**BigInt(decPlaces),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,places=Math.max(0,f-e);return Math.max(64,Math.ceil(places*Math.log2(10))+32)}
|
||||||
|
function bitLen(n){n=n<0n?-n:n;return n===0n?0:n.toString(2).length}
|
||||||
|
function fixedNum(v,bits=state.bits){
|
||||||
|
if(v===0n)return 0;let 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;
|
||||||
|
}
|
||||||
|
// Hot-path conversion for orbit values known to stay O(1).
|
||||||
|
function fixedOrbitNum(v,bits){
|
||||||
|
if(v===0n)return 0;
|
||||||
|
const sh=bits-54;
|
||||||
|
if(sh>0)return Number(v>>BigInt(sh))*Math.pow(2,-54);
|
||||||
|
return Number(v)*Math.pow(2,-bits);
|
||||||
|
}
|
||||||
|
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 zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
|
||||||
|
function fixedRatio(a,b){
|
||||||
|
if(b===0n)return 0;if(a===0n)return 0;const neg=a<0n;if(neg)a=-a;
|
||||||
|
const q=(a<<52n)/b;const v=Number(q)/4503599627370496;return neg?-v:v;
|
||||||
|
}
|
||||||
|
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||||
|
function promoteState(shift){
|
||||||
|
// Precision promotion used to invalidate the reference orbit. Keep the same
|
||||||
|
// mathematical values by shifting their fixed-point representation instead.
|
||||||
|
// If a render is in flight, logically cancel it before mutating shared cache state.
|
||||||
|
if(state.rendering)cancelRender();
|
||||||
|
const oldBits=state.bits,s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;
|
||||||
|
if(state.frameView){state.frameView.re<<=s;state.frameView.im<<=s;state.frameView.span<<=s;state.frameView.bits+=shift}
|
||||||
|
state.bits+=shift;
|
||||||
|
promoteReferenceCache(shift,oldBits);
|
||||||
|
}
|
||||||
|
function ensurePrecision(){
|
||||||
|
const bl=bitLen(state.span);
|
||||||
|
if(bl>=MIN_SPAN_BITS)return false;
|
||||||
|
// Defer representation-only precision promotion until the current frame finishes.
|
||||||
|
if(state.rendering){state.precisionPending=true;return false}
|
||||||
|
state.precisionPending=false;promoteState(TARGET_SPAN_BITS-bl);return true
|
||||||
|
}
|
||||||
|
function flushPendingPrecision(){
|
||||||
|
if(state.rendering||!state.precisionPending)return false;state.precisionPending=false;
|
||||||
|
const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS){promoteState(TARGET_SPAN_BITS-bl);return true}return false
|
||||||
|
}
|
||||||
|
function mulRatio(v,factor){
|
||||||
|
const n=BigInt(Math.max(1,Math.round(factor*Number(RATIO_DEN))));return v*n/RATIO_DEN;
|
||||||
|
}
|
||||||
|
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 d=state.bits,q=v*(10n**BigInt(d))>>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 fmtSpan(){const l=log2Fixed(state.span);if(!Number.isFinite(l))return'0';const e=Math.floor(l/Math.log2(10)),m=Math.pow(2,l-e*Math.log2(10));return m.toFixed(4)+'e'+e}
|
||||||
|
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp();if(z<12)return Math.min(12000,Math.round(state.baseIter+22*Math.sqrt(z)*Math.log2(2+z)));return Math.min(140000,Math.round(state.baseIter+150*z+260*Math.log2(2+z)))}
|
||||||
|
function ensureOrbitPrecision(iter,width=Math.max(1,canvas.width)){const conditionBits=referenceCache.rr?Math.max(0,Math.min(256,Math.ceil(referenceCache.conditionLog2||0))):0,required=160+conditionBits,available=bitLen(state.span)-Math.ceil(Math.log2(width))-Math.ceil(Math.log2(Math.max(2,iter)));if(available>=required)return false;promoteState(required+32-available);invalidateReferenceOrbit();return true}
|
||||||
|
function f64Ulp(x){x=Math.abs(x);if(!Number.isFinite(x))return Infinity;if(x===0)return Number.MIN_VALUE;return Math.pow(2,Math.floor(Math.log2(x))-52)}
|
||||||
|
function deepResolutionRatio(snap=snapshot(),width=Math.max(1,canvas.width)){const step=Math.abs(fixedNum(snap.span,snap.bits))/Math.max(1,width),ulp=Math.max(f64Ulp(fixedNum(snap.re,snap.bits)),f64Ulp(fixedNum(snap.im,snap.bits)));return step===0||!Number.isFinite(step)?0:step/Math.max(Number.MIN_VALUE,ulp)}
|
||||||
|
function deepEngineNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){if(exportForcePrecision)return true;const ratio=deepResolutionRatio(snap,width),orbitRisk=ratio<=128&&(deepTelemetry.badRatio>1e-4||deepTelemetry.unresolvedRatio>2e-3||deepTelemetry.repairRatio>.08);deepMode=orbitRisk||(deepMode?ratio<64:ratio<=32);return deepMode}
|
||||||
|
function iterationPlan(profile,deep){
|
||||||
|
const colorIter=maxIter();return{colorIter,computeIter:colorIter};
|
||||||
|
}
|
||||||
|
function workProfile(profile){return{bla:profile.blaSteps,ptb:profile.ptbSteps}}
|
||||||
|
function cancelRender(){state.token++;state.rendering=false;cancelDeepPoolJob();flushPendingPrecision()}
|
||||||
|
function cancelDetailRefinement(clearCurrent=true){
|
||||||
|
state.detailGeneration++;state.detailActive=false;state.detailQueued=0;state.detailDone=0;detailPlan=null;
|
||||||
|
if(clearCurrent)activeDetailTiles.length=0;
|
||||||
|
}
|
||||||
|
function clearDetailCache(){DETAIL_TILE_CACHE.clear();detailCacheBytes=0;activeDetailTiles.length=0;cancelDetailRefinement(false)}
|
||||||
|
function setDirty(cancel=true){cancelUnknownContinuation();state.lastInteraction=performance.now();state.dirty=true;state.lastPass=null;state.drawState=state.frameView?'REPROJECTED':'PREVIEW';state.coverage=0;cancelDetailRefinement(true);if(cancel)cancelRender();invalidateView()}
|
||||||
|
function reset(){
|
||||||
|
clearDetailCache();clearPanReuse();state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);setDirty();saveHash(false)
|
||||||
|
}
|
||||||
|
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){
|
||||||
|
if(!state.panReuse)capturePanSource();
|
||||||
|
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);
|
||||||
|
const 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();setDirty(!(state.pointerActive||state.wheelActive));
|
||||||
|
}
|
||||||
|
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*100000))/BigInt(Math.round(w*100000));
|
||||||
|
const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));
|
||||||
|
state.im+=ys*BigInt(Math.round(dy*100000))/BigInt(Math.round(h*100000));setDirty(!(state.pointerActive||state.wheelActive));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function clearPanReuse(){state.panReuse=null}
|
||||||
|
function capturePanSource(){
|
||||||
|
if(!state.frameView||!frameCanvas.width||!frameCanvas.height)return false;
|
||||||
|
const fv=state.frameView;state.panReuse={canvas:frameCanvas,snap:{bits:fv.bits,re:fv.re,im:fv.im,span:fv.span},style:styleSignature(),pass:state.lastPass,iter:maxIter(),created:performance.now()};return true
|
||||||
|
}
|
||||||
|
function buildPanReuse(snap,w,h,iter,profile){
|
||||||
|
const src=state.panReuse;if(!src||!src.canvas||src.style!==styleSignature())return null;
|
||||||
|
// Final HQ must recompute after a real scale change. Interactive/base passes may
|
||||||
|
// reproject a much larger range so wheel/pinch zoom can reuse existing pixels.
|
||||||
|
const re=align(src.snap.re,src.snap.bits,snap.bits),im=align(src.snap.im,src.snap.bits,snap.bits),sp=align(src.snap.span,src.snap.bits,snap.bits),scale=fixedRatio(sp,snap.span);
|
||||||
|
if(!Number.isFinite(scale)||scale<.22||scale>4.5)return null;
|
||||||
|
const scaleChange=Math.abs(Math.log2(Math.max(1e-12,scale)));
|
||||||
|
// Covered is a sampling contract, not a presentation shortcut. Reprojection
|
||||||
|
// is allowed only for Preview; the target grid is always recomputed in full.
|
||||||
|
if(profile.covered)return null;
|
||||||
|
const dx=fixedRatio(re-snap.re,snap.span)*w,dy=-fixedRatio(im-snap.im,snap.span)*w,move=Math.hypot(dx/Math.max(1,w),dy/Math.max(1,w));if(!Number.isFinite(move)||move>1.35)return null;
|
||||||
|
const dw=w*scale,dh=w*scale*(src.canvas.height/Math.max(1,src.canvas.width)),density=dw/Math.max(1,src.canvas.width);
|
||||||
|
// A zoomed preview may stretch old pixels temporarily, but the idle HQ pass will
|
||||||
|
// recompute it. Keep the stretch bounded so interaction never becomes misleading.
|
||||||
|
if(density>profile.densityLimit)return null;
|
||||||
|
const x0=w*.5+dx-dw*.5,y0=h*.5+dy-dh*.5,x1=x0+dw,y1=y0+dh;
|
||||||
|
let ix0=Math.max(0,Math.ceil(x0)+2),iy0=Math.max(0,Math.ceil(y0)+2),ix1=Math.min(w,Math.floor(x1)-2),iy1=Math.min(h,Math.floor(y1)-2);
|
||||||
|
if(ix1<=ix0||iy1<=iy0)return null;
|
||||||
|
const overlap=(ix1-ix0)*(iy1-iy0),frac=overlap/Math.max(1,w*h);if(frac<.20)return null;
|
||||||
|
const c=document.createElement('canvas');c.width=w;c.height=h;const cc=c.getContext('2d',{alpha:false});if(!cc)return null;cc.fillStyle='#050813';cc.fillRect(0,0,w,h);cc.imageSmoothingEnabled=true;try{cc.imageSmoothingQuality='high'}catch{};cc.drawImage(src.canvas,x0,y0,dw,dh);
|
||||||
|
const out=new Uint8ClampedArray(cc.getImageData(0,0,w,h).data),rects=[];
|
||||||
|
const add=(x,y,rw,rh)=>{x=Math.max(0,x|0);y=Math.max(0,y|0);rw=Math.min(w-x,rw|0);rh=Math.min(h-y,rh|0);if(rw>0&&rh>0)rects.push({x0:x,y0:y,w:rw,h:rh})};
|
||||||
|
add(0,0,w,iy0);add(0,iy1,w,h-iy1);add(0,iy0,ix0,iy1-iy0);add(ix1,iy0,w-ix1,iy1-iy0);
|
||||||
|
const exposed=rects.reduce((a,r)=>a+r.w*r.h,0);if(exposed>w*h*.80)return null;
|
||||||
|
return{out,rects,reusedPixels:w*h-exposed,exposedPixels:exposed,scale};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Palette / smooth colouring ───────────────────────────────────────────
|
||||||
|
const PALETTE_CURRENT=0,PALETTE_RAINBOW=1,PALETTE_MONO=2;
|
||||||
|
const FIELD_ESCAPED=1,FIELD_INTERIOR_PROVEN=2,FIELD_INTERIOR_LIKELY=3,FIELD_UNKNOWN=4;
|
||||||
|
const stops=[[0,[4,10,27]],[.11,[12,53,79]],[.25,[31,156,184]],[.38,[91,226,234]],[.50,[66,53,151]],[.62,[139,49,170]],[.73,[232,72,145]],[.84,[255,137,64]],[.93,[255,211,99]],[1,[255,250,223]]];
|
||||||
|
const COLOR_PHASES=2048,COLOR_MIXES=64;
|
||||||
|
function hsvRgb(h,sat,val){h=((h%1)+1)%1;const x=h*6,i=Math.floor(x),f=x-i,p=val*(1-sat),q=val*(1-sat*f),s=val*(1-sat*(1-f));let r,g,b;switch(i%6){case 0:r=val;g=s;b=p;break;case 1:r=q;g=val;b=p;break;case 2:r=p;g=val;b=s;break;case 3:r=p;g=q;b=val;break;case 4:r=s;g=p;b=val;break;default:r=val;g=p;b=q}return[r*255,g*255,b*255]}
|
||||||
|
function basePalette(kind,t){
|
||||||
|
t=((t%1)+1)%1;
|
||||||
|
if(kind===PALETTE_RAINBOW)return hsvRgb(t,.92,1);
|
||||||
|
if(kind===PALETTE_MONO){const g=22+233*(.5-.5*Math.cos(Math.PI*2*t));return[g,g,g]}
|
||||||
|
let a=stops[0],b=stops[stops.length-1];for(let j=1;j<stops.length;j++){if(t<=stops[j][0]){a=stops[j-1];b=stops[j];break}}
|
||||||
|
let f=(t-a[0])/(b[0]-a[0]||1);f=f*f*(3-2*f);return[a[1][0]+(b[1][0]-a[1][0])*f,a[1][1]+(b[1][1]-a[1][1])*f,a[1][2]+(b[1][2]-a[1][2])*f]
|
||||||
|
}
|
||||||
|
function buildColorLut(kind){const lut=new Uint8Array(COLOR_PHASES*3);for(let pi=0;pi<COLOR_PHASES;pi++){const u=pi/(COLOR_PHASES-1),t=kind===PALETTE_CURRENT?(u<=.5?u*2:2-u*2):u,c=basePalette(kind,t),k=pi*3;lut[k]=c[0]|0;lut[k+1]=c[1]|0;lut[k+2]=c[2]|0}return lut}
|
||||||
|
const COLOR_LUTS=[buildColorLut(PALETTE_CURRENT),buildColorLut(PALETTE_RAINBOW),buildColorLut(PALETTE_MONO)];
|
||||||
|
const SMOOTH_U_MIN=2,SMOOTH_U_STEP=1/128,SMOOTH_U_N=4097,SMOOTH_CORR=new Float32Array(SMOOTH_U_N);
|
||||||
|
for(let i=0;i<SMOOTH_U_N;i++){const u=SMOOTH_U_MIN+i*SMOOTH_U_STEP;SMOOTH_CORR[i]=1-Math.log2(.5*u)}
|
||||||
|
const COLOR_CTX_CACHE=new Map();
|
||||||
|
function makeColorCtx(iter){let hit=COLOR_CTX_CACHE.get(iter);if(hit)return hit;const mixBin=new Uint8Array(iter+1),den=Math.log1p(Math.max(8,iter));for(let n=0;n<=iter;n++){const edge=Math.max(0,Math.min(1,Math.log1p(n)/den));mixBin[n]=Math.min(COLOR_MIXES-1,Math.round((COLOR_MIXES-1)*Math.pow(edge,.38)))}hit={mixBin};COLOR_CTX_CACHE.set(iter,hit);if(COLOR_CTX_CACHE.size>8)COLOR_CTX_CACHE.delete(COLOR_CTX_CACHE.keys().next().value);return hit}
|
||||||
|
function smoothEscape(n,m){const u=Math.log2(Math.max(4.0000001,m));let corr;const fi=(u-SMOOTH_U_MIN)/SMOOTH_U_STEP;if(fi>=0&&fi<SMOOTH_U_N-1){const i=fi|0,f=fi-i;corr=SMOOTH_CORR[i]+(SMOOTH_CORR[i+1]-SMOOTH_CORR[i])*f}else corr=1-Math.log2(.5*u);return n+corr}
|
||||||
|
function putPaletteColor(out,oi,sm,n,iter,colorCtx){let phase=state.shift+sm*state.cycle;phase-=Math.floor(phase);const pi=Math.min(COLOR_PHASES-1,(phase*COLOR_PHASES)|0),mi=colorCtx.mixBin[Math.min(iter,n)],mix=.34+.66*(mi/(COLOR_MIXES-1)),kind=state.palette,lut=COLOR_LUTS[kind]||COLOR_LUTS[0],k=pi*3,f0=kind===PALETTE_MONO?8:2,f1=kind===PALETTE_MONO?8:5,f2=kind===PALETTE_MONO?8:15;out[oi]=(f0+(lut[k]-f0)*mix)|0;out[oi+1]=(f1+(lut[k+1]-f1)*mix)|0;out[oi+2]=(f2+(lut[k+2]-f2)*mix)|0;out[oi+3]=255}
|
||||||
|
function putFastColor(out,oi,n,m,iter,colorCtx){putPaletteColor(out,oi,smoothEscape(n,m),n,iter,colorCtx)}
|
||||||
|
function makeField(size,iter){return{smooth:new Float32Array(size),iterations:new Uint32Array(size),classes:new Uint8Array(size),confidence:new Uint8Array(size),iter}}
|
||||||
|
function fieldConfidence(kind){return kind===FIELD_ESCAPED||kind===FIELD_INTERIOR_PROVEN?255:kind===FIELD_INTERIOR_LIKELY?192:0}
|
||||||
|
function putField(field,index,n,m,kind){field.classes[index]=kind;if(field.iterations)field.iterations[index]=Math.max(0,n)>>>0;if(field.confidence)field.confidence[index]=fieldConfidence(kind);if(kind===FIELD_ESCAPED)field.smooth[index]=smoothEscape(n,m);else field.smooth[index]=NaN}
|
||||||
|
function fillFieldConfidence(field){if(!field.confidence)field.confidence=new Uint8Array(field.classes.length);for(let i=0;i<field.classes.length;i++)field.confidence[i]=fieldConfidence(field.classes[i])}
|
||||||
|
function colorizeField(field){const out=new Uint8ClampedArray(field.classes.length*4),colorCtx=makeColorCtx(field.iter);for(let i=0,oi=0;i<field.classes.length;i++,oi+=4){const kind=field.classes[i];if(kind!==FIELD_ESCAPED){const neutral=kind===FIELD_UNKNOWN;out[oi]=neutral?20:0;out[oi+1]=neutral?22:0;out[oi+2]=neutral?30:0;out[oi+3]=255;continue}const sm=field.smooth[i],n=Math.max(0,Math.min(field.iter,Math.floor(sm)));putPaletteColor(out,oi,sm,n,field.iter,colorCtx)}return out}
|
||||||
|
|
||||||
|
// Embedded WebAssembly: SIMD-optimized kernel with a scalar fallback.
|
||||||
|
|
||||||
|
|
||||||
|
let wasm=null;
|
||||||
|
function instantiateWasm(b64){
|
||||||
|
const module=b64 instanceof WebAssembly.Module?b64:(()=>{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return new WebAssembly.Module(bytes)})(),inst=new WebAssembly.Instance(module,{}),ex=inst.exports;
|
||||||
|
return {module,ex,counts:()=>new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:()=>new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),refsR:()=>new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001),refsI:()=>new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001)};
|
||||||
|
}
|
||||||
|
try{wasm=instantiateWasm(WASM_SIMD_B64);wasm.simd=globalThis.MANDEL_HOSTED_SHALLOW_SIMD!==false}catch(e){try{wasm=instantiateWasm(WASM_SCALAR_B64);wasm.simd=false}catch(_e){wasm=null}}
|
||||||
|
let shallowWorker=null,shallowWorkerUrl=null,shallowWorkerBusy=false,shallowRecycle=null;
|
||||||
|
function shallowWorkerSource(){return `'use strict';let ex=null;function smooth(n,m){const u=Math.log2(Math.max(4.0000001,m));return n+1-Math.log2(.5*u)}function likely(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}self.onmessage=e=>{const d=e.data;if(d.type==='init'){ex=new WebAssembly.Instance(d.module,{}).exports;postMessage({type:'ready'});return}if(d.type!=='render'||!ex)return;try{const scale=d.sp/d.w,npx=ex.render_rows(d.cre+scale*.5,d.cim-scale*.5,d.sp,d.w,d.h,d.y,d.rows,d.iter),counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),field=d.fieldBuffer&&d.fieldBuffer.byteLength>=npx*4?new Float32Array(d.fieldBuffer,0,npx):new Float32Array(npx),iterations=d.iterationBuffer&&d.iterationBuffer.byteLength>=npx*4?new Uint32Array(d.iterationBuffer,0,npx):new Uint32Array(npx),classes=d.classBuffer&&d.classBuffer.byteLength>=npx?new Uint8Array(d.classBuffer,0,npx):new Uint8Array(npx);for(let i=0;i<npx;i++){const n=counts[i],x=i%d.w,y=d.y+((i/d.w)|0),cr=d.cre+(x+.5-d.w*.5)*scale,ci=d.cim+(d.h*.5-y-.5)*scale;iterations[i]=n;if(n<d.iter){classes[i]=1;field[i]=smooth(n,mags[i])}else{classes[i]=likely(cr,ci)?3:4;field[i]=NaN}}postMessage({type:'render',jobId:d.jobId,y:d.y,rows:d.rows,field:field.buffer,iterations:iterations.buffer,classes:classes.buffer},[field.buffer,iterations.buffer,classes.buffer])}catch(error){postMessage({type:'render',jobId:d.jobId,error:String(error&&error.message||error)})}}`}
|
||||||
|
function ensureShallowWorker(){if(shallowWorker||!wasm||typeof Worker==='undefined'||typeof Blob==='undefined')return!!shallowWorker;try{shallowWorkerUrl=URL.createObjectURL(new Blob([shallowWorkerSource()],{type:'text/javascript'}));shallowWorker=new Worker(shallowWorkerUrl);shallowWorker.postMessage({type:'init',module:wasm.module});return true}catch{shallowWorker=null;return false}}
|
||||||
|
function destroyShallowWorker(){if(shallowWorker){try{shallowWorker.terminate()}catch{}shallowWorker=null}if(shallowWorkerUrl){try{URL.revokeObjectURL(shallowWorkerUrl)}catch{}shallowWorkerUrl=null}shallowWorkerBusy=false;shallowRecycle=null}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function instantiateDeepWasm(b64){
|
||||||
|
const module=b64 instanceof WebAssembly.Module?b64:(()=>{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return new WebAssembly.Module(bytes)})();
|
||||||
|
const ex=new WebAssembly.Instance(module,{}).exports;
|
||||||
|
return {ex,counts:()=>new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:()=>new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),refsR:()=>new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001),refsI:()=>new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001)};
|
||||||
|
}
|
||||||
|
let deepWasm=null,deepWasmTried=false,deepModuleBundle=null,deepModulePromise=null;
|
||||||
|
async function compileKernelSource(source){if(source instanceof WebAssembly.Module)return source;if(/^https?:/i.test(source)){const response=await fetch(source,{cache:'force-cache'});if(!response.ok)throw new Error('WASM request failed '+response.status);if(WebAssembly.compileStreaming){try{return await WebAssembly.compileStreaming(Promise.resolve(response.clone()))}catch{}}return WebAssembly.compile(await response.arrayBuffer())}const raw=atob(source),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return WebAssembly.compile(bytes)}
|
||||||
|
async function compileKernelPair(simd,scalar){try{return{module:await compileKernelSource(simd),simd:true}}catch{return{module:await compileKernelSource(scalar),simd:false}}}
|
||||||
|
function prepareDeepModules(){if(deepModuleBundle)return Promise.resolve(deepModuleBundle);if(!deepModulePromise)deepModulePromise=Promise.all([compileKernelPair(DEEP_SIMD_B64,DEEP_SCALAR_B64),compileKernelPair(BLA_SIMD_B64,BLA_SCALAR_B64),compileKernelPair(COLOR_SIMD_B64,COLOR_SCALAR_B64)]).then(([deep,bla,color])=>deepModuleBundle={deep,bla,color}).catch(error=>{deepModulePromise=null;throw error});return deepModulePromise}
|
||||||
|
function ensureDeepWasm(){
|
||||||
|
if(deepWasm)return true;if(deepWasmTried)return false;deepWasmTried=true;if(deepModuleBundle){try{deepWasm=instantiateDeepWasm(deepModuleBundle.deep.module);deepWasm.simd=deepModuleBundle.deep.simd}catch{deepWasm=null}return!!deepWasm}if(/^https?:/i.test(DEEP_SIMD_B64)||/^https?:/i.test(DEEP_SCALAR_B64))return false;
|
||||||
|
try{deepWasm=instantiateDeepWasm(DEEP_SIMD_B64);deepWasm.simd=true}catch(e){try{deepWasm=instantiateDeepWasm(DEEP_SCALAR_B64);deepWasm.simd=false}catch(_e){deepWasm=null}}
|
||||||
|
return!!deepWasm
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Deep renderer worker pool. Each worker owns a WASM instance and performs both the
|
||||||
|
// perturbation kernel and colour mapping, so colourful frames no longer funnel all
|
||||||
|
// post-processing through the UI thread. Jobs are striped dynamically for load balance.
|
||||||
|
const deepPool={workers:[],url:null,activeToken:0,serial:0,failed:false,current:null,maxWorkers:0,benchTimer:0};
|
||||||
|
// ── Persistent deep-render workers / BLA kernel ──────────────────────────
|
||||||
|
function deepWorkerSource(){
|
||||||
|
return `'use strict';
|
||||||
|
let core=null,blaCore=null,colorCore=null,refKey='',refLoaded=0,RR=new Float64Array(150001),RI=new Float64Array(150001),blaRefKey='',blaRefLoaded=0,blaBuiltKey='',lastBlaBuildMs=0;
|
||||||
|
function ensure(){if(!core)throw new Error('deep module not initialized');return core}
|
||||||
|
function ensureBla(){if(!blaCore)throw new Error('BLA module not initialized');return blaCore}
|
||||||
|
function ensureColor(){if(!colorCore)throw new Error('color module not initialized');return colorCore}
|
||||||
|
function applyRef(d,ex){if(d.refKey!==refKey){refKey=d.refKey;refLoaded=0;blaRefKey='';blaRefLoaded=0;blaBuiltKey=''}if(d.rr){const ar=new Float64Array(d.rr),ai=new Float64Array(d.ri),st=d.rrStart|0;RR.set(ar,st);RI.set(ai,st);new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ar,st);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ai,st);refLoaded=Math.max(refLoaded,st+ar.length);if(blaCore&&blaRefKey===refKey){const bx=blaCore.ex;new Float64Array(bx.memory.buffer,bx.refs_r_ptr(),150001).set(ar,st);new Float64Array(bx.memory.buffer,bx.refs_i_ptr(),150001).set(ai,st);blaRefLoaded=Math.max(blaRefLoaded,st+ar.length);blaBuiltKey=''}}if(refLoaded<d.refLen+1)throw new Error('reference cache miss')}
|
||||||
|
function syncRefsToBla(d,key=d.blaKey,eps=d.blaEps){if(!d.useBla)return null;const b=ensureBla(),ex=b.ex;if(blaRefKey!==refKey){new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(RR.subarray(0,refLoaded));new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(RI.subarray(0,refLoaded));blaRefKey=refKey;blaRefLoaded=refLoaded;blaBuiltKey=''}else if(refLoaded>blaRefLoaded){new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(RR.subarray(blaRefLoaded,refLoaded),blaRefLoaded);blaRefLoaded=refLoaded;blaBuiltKey=''}lastBlaBuildMs=0;if(blaBuiltKey!==key){const t=performance.now(),levels=ex.build_bla(d.refLen,d.cMax,eps);lastBlaBuildMs=performance.now()-t;if(!levels)return null;blaBuiltKey=key}return b}
|
||||||
|
function smoothBatch(srcMags,npx){const c=ensureColor(),ex=c.ex,mi=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),co=new Float32Array(ex.memory.buffer,ex.corr_ptr(),65536);mi.set(srcMags.subarray(0,npx),0);ex.smooth_batch(npx);return co}
|
||||||
|
function strictOne(ex,x,y,d){const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536);ex.render_perturb_rebase_rect(d.spanMant,d.spanBucket,d.offR,d.offI,d.offBucket,d.refLen,d.w,d.h,d.cx,d.cy,x,y,1,1,d.iter,0,0,0,0,0,0,0);return[counts[0],mags[0]]}
|
||||||
|
function stats(bx){return{blaSteps:bx.stat_bla_steps?bx.stat_bla_steps():0,ptbSteps:bx.stat_ptb_steps?bx.stat_ptb_steps():0,rebases:bx.stat_rebases?bx.stat_rebases():0,interior:bx.stat_interior?bx.stat_interior():0,unresolved:bx.stat_unresolved?bx.stat_unresolved():0,fail:bx.stat_fail?bx.stat_fail():0}}
|
||||||
|
function renderBla(d,b,key,eps,bcap,pcap){const bx=b.ex;if(key!==d.blaKey||eps!==d.blaEps)b=syncRefsToBla(d,key,eps)||b;const t=performance.now(),npx=bx.render_bla_rect_v2?bx.render_bla_rect_v2(d.spanNormal,d.offRn,d.offIn,d.baseCr,d.baseCi,d.refLen,d.w,d.h,d.x0||0,d.y0,d.rectW||d.w,d.rows,d.iter,bcap||0,pcap||0,1):bx.render_bla_rect(d.spanNormal,d.offRn,d.offIn,d.refLen,d.w,d.h,d.x0||0,d.y0,d.rectW||d.w,d.rows,d.iter);return{npx,kernelMs:performance.now()-t,counts:new Uint32Array(bx.memory.buffer,bx.counts_ptr(),65536),mags:new Float64Array(bx.memory.buffer,bx.mags_ptr(),65536),stats:stats(bx)}}
|
||||||
|
function verifyAgainstSafe(d,result,rw,npx,b){const want=d.verifySamples||0;if(!want||!b)return{samples:0,mismatch:0};const counts=Uint32Array.from(result.counts.subarray(0,npx)),mags=Float64Array.from(result.mags.subarray(0,npx));result.counts=counts;result.mags=mags;const picks=[],seen=new Set(),add=i=>{if(picks.length>=want)return;i=Math.max(0,Math.min(npx-1,i|0));if(!seen.has(i)){seen.add(i);picks.push(i)}};for(let k=0;k<Math.max(2,want>>1);k++)add(((k+.37)*npx/Math.max(2,want>>1))|0);const stride=Math.max(1,Math.floor(npx/Math.max(16,want*10))),top=[];for(let i=stride;i<npx;i+=stride){const n=counts[i],p=counts[i-stride];if(n<0xfffffffe&&p<0xfffffffe&&((n>=d.iter)!==(p>=d.iter))){add(i);add(i-stride)}if(n<d.iter&&n<0xfffffffe){top.push([n,i]);top.sort((a,b)=>b[0]-a[0]);if(top.length>want)top.length=want}}for(const x of top)add(x[1]);for(let k=0;picks.length<want&&k<want*2;k++)add(((k+.73)*npx/want)|0);let mismatch=0;const bad=[];for(const i of picks){const bn=counts[i];if(bn>=0xfffffffe)continue;const x=(d.x0||0)+(i%rw),y=d.y0+((i/rw)|0),sr=renderBla({...d,x0:x,y0:y,rectW:1,rows:1},b,d.safeBlaKey||d.blaKey,d.safeBlaEps||d.blaEps,0,0),sn=sr.counts[0];if(sn>=0xfffffffe)continue;if((bn>=d.iter)!==(sn>=d.iter)||Math.abs((bn|0)-(sn|0))>(d.verifyDelta||64)){mismatch++;bad.push(i)}}return{samples:picks.length,mismatch,bad}}
|
||||||
|
self.onmessage=async e=>{const d=e.data;if(!d)return;if(d.type==='init'){try{core={ex:new WebAssembly.Instance(d.modules.deep.module,{}).exports,simd:!!d.modules.deep.simd};blaCore={ex:new WebAssembly.Instance(d.modules.bla.module,{}).exports,simd:!!d.modules.bla.simd};colorCore={ex:new WebAssembly.Instance(d.modules.color.module,{}).exports,simd:!!d.modules.color.simd};postMessage({type:'ready'})}catch(error){postMessage({type:'ready',error:String(error&&error.message||error)})}return}if(d.type!=='render'&&d.type!=='pilot'&&d.type!=='realBench')return;try{const c=ensure(),ex=c.ex;applyRef(d,ex);const b=syncRefsToBla(d);if((d.type==='pilot'||d.type==='realBench')&&!b)throw new Error('BLA unavailable');if(d.type==='pilot'||d.type==='realBench'){const reps=d.type==='realBench'?Math.max(1,d.repeats|0):1;let rr=null,total=0;for(let k=0;k<reps;k++){rr=renderBla({...d,x0:0,y0:0,rectW:d.w,rows:d.h},b,d.blaKey,d.blaEps,0,0);total+=rr.kernelMs}if(d.type==='realBench'){postMessage({type:'realBench',benchId:d.benchId,kernelMs:total,pixels:rr.npx*reps,blaBuildMs:lastBlaBuildMs,simd:b.simd});return}const co=rr.counts;let black=0,bad=0,sum=0;for(let i=0;i<rr.npx;i++){const n=co[i];if(n>=0xfffffffe){bad++;continue}sum+=Math.min(d.iter,n);if(n>=d.iter)black++}postMessage({type:'pilot',pilotId:d.pilotId,kernelMs:total,pixels:rr.npx,blaBuildMs:lastBlaBuildMs,blackRatio:black/Math.max(1,rr.npx),badRatio:bad/Math.max(1,rr.npx),meanIter:sum/Math.max(1,rr.npx-bad),stats:rr.stats,simd:b.simd});return}
|
||||||
|
const rx=d.x0||0,rw=d.rectW||d.w,field=d.fieldBuffer&&d.fieldBuffer.byteLength>=rw*d.rows*4?new Float32Array(d.fieldBuffer,0,rw*d.rows):new Float32Array(rw*d.rows),iterations=d.iterationBuffer&&d.iterationBuffer.byteLength>=rw*d.rows*4?new Uint32Array(d.iterationBuffer,0,rw*d.rows):new Uint32Array(rw*d.rows),classes=d.classBuffer&&d.classBuffer.byteLength>=rw*d.rows?new Uint8Array(d.classBuffer,0,rw*d.rows):new Uint8Array(rw*d.rows),bad=[];let result,repaired=0,verifyMs=0;if(b){result=renderBla(d,b,d.blaKey,d.blaEps,d.maxBlaSteps||0,d.maxPtbSteps||0);let unresolved=result.stats.unresolved||0;if(unresolved&&d.covered){const co=result.counts,ma=result.mags;for(let i=0;i<result.npx;i++)if(co[i]===0xfffffffe){const x=rx+(i%rw),y=d.y0+((i/rw)|0),r=strictOne(ex,x,y,d);co[i]=r[0];ma[i]=r[1]}}
|
||||||
|
const tv=performance.now();let v=verifyAgainstSafe(d,result,rw,result.npx,b);verifyMs=performance.now()-tv;if(v.mismatch){const safeKey=d.safeBlaKey||d.blaKey,safeEps=d.safeBlaEps||d.blaEps;result=renderBla(d,b,safeKey,safeEps,0,0);repaired=1}
|
||||||
|
}else{const t=performance.now(),npx=ex.render_perturb_rebase_rect(d.spanMant,d.spanBucket,d.offR,d.offI,d.offBucket,d.refLen,d.w,d.h,d.cx,d.cy,rx,d.y0,rw,d.rows,d.iter,d.skip||0,d.Ar||0,d.Ai||0,d.Ab||0,d.Br||0,d.Bi||0,d.Bb||0);result={npx,kernelMs:performance.now()-t,counts:new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),stats:{blaSteps:0,ptbSteps:0,rebases:0,interior:0,unresolved:0,fail:0}}}
|
||||||
|
const corr=smoothBatch(result.mags,result.npx);let blackCount=0,sumIter=0;for(let i=0;i<result.npx;i++){let n=result.counts[i],m=result.mags[i];const x=rx+(i%rw),y=d.y0+((i/rw)|0);if(n>=0xfffffffe){if(n===0xffffffff){const r=strictOne(ex,x,y,d);n=r[0];m=r[1]}else{n=d.iter;m=0}}iterations[i]=n;if(n===0xffffffff){classes[i]=4;field[i]=NaN;bad.push(i);continue}sumIter+=Math.min(d.iter,n);if(n>=d.iter){classes[i]=4;field[i]=NaN;blackCount++;continue}classes[i]=1;field[i]=n+corr[i]}
|
||||||
|
postMessage({type:'render',jobId:d.jobId,x0:rx,rectW:rw,y0:d.y0,rows:d.rows,field:field.buffer,iterations:iterations.buffer,classes:classes.buffer,bad,simd:c.simd,bla:!!b,blaSimd:b?b.simd:false,colorSimd:colorCore?colorCore.simd:false,kernelMs:result.kernelMs,verifyMs,blaBuildMs:lastBlaBuildMs,pixels:result.npx,blackCount,sumIter,stats:result.stats,verifySamples:d.verifySamples||0,repaired},[field.buffer,iterations.buffer,classes.buffer])}catch(err){postMessage({type:'render',jobId:d.jobId,error:String(err&&err.message||err)})}};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
function destroyDeepPool(){
|
||||||
|
if(deepPool.current)deepPool.current.cancelled=true;deepPool.current=null;
|
||||||
|
for(const w of deepPool.workers){try{w.terminate()}catch{}}
|
||||||
|
deepPool.workers.length=0;if(deepPool.url){try{URL.revokeObjectURL(deepPool.url)}catch{}deepPool.url=null}
|
||||||
|
deepPool.activeToken=0;deepPool.maxWorkers=0
|
||||||
|
}
|
||||||
|
function retireDeepAssets(){destroyDeepPool();deepPool.failed=false;deepWasm=null;deepWasmTried=false;deepModuleBundle=null;deepModulePromise=null;referenceCache.rr=null;referenceCache.ri=null;referenceCache.series=null;referenceCache.n=0;referenceCache.escape=0;referenceCache.loadedLen=0;referenceCache.conditionLog2=0;referenceCache.derivative=[0,0,NEG_BUCKET]}
|
||||||
|
function cancelDeepPoolJob(){
|
||||||
|
// Logical cancellation: keep Worker, WASM, reference and BLA caches alive.
|
||||||
|
deepPool.activeToken=0;
|
||||||
|
if(deepPool.current){deepPool.current.cancelled=true;deepPool.current=null}
|
||||||
|
}
|
||||||
|
function handleDeepWorkerMessage(worker,d){
|
||||||
|
const job=worker._job;worker._job=null;worker._busy=false;
|
||||||
|
if(d&&d.type==='ready'){if(d.error){worker._ready=false;deepPool.failed=true;destroyDeepPool();return}worker._ready=true;kickDeepWorker(worker);return}
|
||||||
|
if(!job){kickDeepWorker(worker);return}
|
||||||
|
if(job.type==='bench'){if(d&&d.type==='bench'&&d.benchId===job.benchId)job.resolve(d);else job.reject(new Error('benchmark reply mismatch'));kickDeepWorker(worker);return}
|
||||||
|
if(job.type==='pilot'||job.type==='realBench'){if(d&&((job.type==='pilot'&&d.type==='pilot'&&d.pilotId===job.id)||(job.type==='realBench'&&d.type==='realBench'&&d.benchId===job.id)))job.resolve(d);else job.reject(new Error(job.type+' reply mismatch'));kickDeepWorker(worker);return}
|
||||||
|
if(job.type==='render')job.runner.onResult(worker,d,job);
|
||||||
|
kickDeepWorker(worker)
|
||||||
|
}
|
||||||
|
function handleDeepWorkerError(worker,e){
|
||||||
|
const job=worker._job;worker._job=null;worker._busy=false;
|
||||||
|
if(job&&(job.type==='bench'||job.type==='pilot'||job.type==='realBench'))job.reject(e instanceof Error?e:new Error('worker job failed'));
|
||||||
|
else if(job&&job.type==='render')job.runner.fail(e);
|
||||||
|
else{deepPool.failed=true;destroyDeepPool()}
|
||||||
|
}
|
||||||
|
function deepWorkerLimit(){const hc=Math.max(1,navigator.hardwareConcurrency||4),memoryLimited=Number(navigator.deviceMemory||8)<=4?1:2;return Math.max(1,Math.min(memoryLimited,hc>2?hc-1:1))}
|
||||||
|
function addDeepWorker(){if(!deepModuleBundle)throw new Error('deep modules are not ready');const i=deepPool.workers.length,w=new Worker(deepPool.url);w._index=i;w._refKey='';w._refLoaded=0;w._busy=false;w._ready=false;w._job=null;w._recycle=null;w.onmessage=e=>handleDeepWorkerMessage(w,e.data);w.onerror=e=>handleDeepWorkerError(w,e);deepPool.workers.push(w);w.postMessage({type:'init',modules:deepModuleBundle});return w}
|
||||||
|
function ensureDeepPool(){
|
||||||
|
if(deepPool.failed||!deepModuleBundle||typeof Worker==='undefined'||typeof Blob==='undefined')return false;
|
||||||
|
if(deepPool.workers.length)return true;
|
||||||
|
try{
|
||||||
|
const count=deepWorkerLimit();
|
||||||
|
deepPool.url=URL.createObjectURL(new Blob([deepWorkerSource()],{type:'text/javascript'}));
|
||||||
|
deepPool.maxWorkers=count;deepWisdom.maxWorkers=count;deepWisdom.workerCount=1;addDeepWorker();
|
||||||
|
return true
|
||||||
|
}catch(e){deepPool.failed=true;destroyDeepPool();return false}
|
||||||
|
}
|
||||||
|
function prewarmDeepAssets(){prepareDeepModules().then(()=>{if(deepResolutionRatio()<=128)ensureDeepPool()}).catch(()=>{})}
|
||||||
|
function kickDeepWorker(worker){
|
||||||
|
if(worker._busy||!worker._ready)return;
|
||||||
|
const r=deepPool.current;if(!r||r.cancelled||r.token!==state.token)return;
|
||||||
|
const active=Math.max(1,Math.min(deepWisdom.ready?deepWisdom.workerCount:Math.min(2,deepPool.workers.length),deepPool.workers.length));
|
||||||
|
if(worker._index>=active)return;
|
||||||
|
r.dispatch(worker)
|
||||||
|
}
|
||||||
|
function attachReference(worker,msg,ref,refLen,refKey,transfer){
|
||||||
|
let start=worker._refKey===refKey?worker._refLoaded:0;start=Math.max(0,Math.min(start,refLen+1));
|
||||||
|
if(start<refLen+1){const rr=ref.rr.slice(start,refLen+1),ri=ref.ri.slice(start,refLen+1);msg.rr=rr.buffer;msg.ri=ri.buffer;msg.rrStart=start;transfer.push(rr.buffer,ri.buffer);worker._refKey=refKey;worker._refLoaded=refLen+1}
|
||||||
|
}
|
||||||
|
function blaProfile(profile){
|
||||||
|
const z=zoomExp(),exp=profile.covered?32:(z<16?28:23);
|
||||||
|
// Fast pass + local verification/repair is cheaper than making the entire frame
|
||||||
|
// conservative. Safe strips are rebuilt at e-48 only when a probe disagrees.
|
||||||
|
const safeExp=z<16?48:Math.min(48,Math.max(32,exp+8));
|
||||||
|
return{exp,eps:Math.pow(2,-exp),safeExp}
|
||||||
|
}
|
||||||
|
function runDeepPool(profile,snap,w,h,iter,colorIter,token,t0,out,sb,refC,off,ref,refLen,series,onFallback,rects=null){
|
||||||
|
if(!ensureDeepPool())return false;if(deepPool.current)deepPool.current.cancelled=true;
|
||||||
|
const centered=pixelCenteredOffset(snap,refC,w),workers=deepPool.workers,refKey=String(ref.id),bad=[],field=makeField(w*h,colorIter),spanNormal=fixedNum(snap.span,snap.bits),offRn=fixedNum(centered.r,snap.bits),offIn=fixedNum(centered.i,snap.bits),baseCr=fixedNum(refC.re,snap.bits),baseCi=fixedNum(refC.im,snap.bits);
|
||||||
|
const cMaxRaw=Math.hypot(offRn,offIn)+Math.abs(spanNormal)*Math.hypot(.5,h/(2*Math.max(1,w))),cBucket=cMaxRaw>0&&Number.isFinite(cMaxRaw)?Math.ceil(Math.log2(cMaxRaw)*8):0,cMaxSafe=cMaxRaw>0?Math.pow(2,cBucket/8):0,bp=blaProfile(profile),useBla=Number.isFinite(spanNormal)&&Math.abs(spanNormal)>=1e-280&&refLen>8,wp=workProfile(profile),colorCtx=makeColorCtx(colorIter);
|
||||||
|
const regions=(rects?rects:[{x0:0,y0:0,w,h}]).map(r=>({x0:Math.max(0,r.x0|0),y0:Math.max(0,r.y0|0),w:Math.max(0,Math.min(w-(r.x0|0),r.w|0)),h:Math.max(0,Math.min(h-(r.y0|0),r.h|0))})).filter(r=>r.w>0&&r.h>0),chunks=[];let preferredRows=deepWisdom.stripRows||16;if(deepWisdom.rowMsEMA>0)preferredRows=Math.round(deepWisdom.targetStripMs/deepWisdom.rowMsEMA);for(const r of regions){const rows=Math.max(2,Math.min(Math.max(1,Math.floor(65536/Math.max(1,r.w))),profile.covered?Math.max(4,preferredRows):Math.min(36,preferredRows)));for(let y=r.y0;y<r.y0+r.h;y+=rows)chunks.push({x0:r.x0,y0:y,w:r.w,rows:Math.min(rows,r.y0+r.h-y)})}chunks.sort((a,b)=>{const ad=Math.hypot((a.x0+a.w*.5)/w-state.focusX,(a.y0+a.rows*.5)/h-state.focusY),bd=Math.hypot((b.x0+b.w*.5)/w-state.focusX,(b.y0+b.rows*.5)/h-state.focusY);return ad-bd});
|
||||||
|
const totalPixels=regions.reduce((a,r)=>a+r.w*r.h,0),verifyJobs=profile.covered?8:3;
|
||||||
|
const runner={token,cancelled:false,chunkIndex:0,donePixels:0,totalPixels,failed:false,finishing:false,simd:true,blaUsed:false,kernelMs:0,verifyMs:0,blaBuildMs:0,sumIter:0,blackCount:0,badCount:0,blaSteps:0,ptbSteps:0,rebases:0,interior:0,unresolved:0,repaired:0,verifiedBuckets:new Set(),
|
||||||
|
fail(err){if(this.failed||this.cancelled)return;this.failed=true;deepPool.failed=true;destroyDeepPool();if(token===state.token)onFallback()},
|
||||||
|
nextChunk(){return this.chunkIndex<chunks.length?chunks[this.chunkIndex++]:null},
|
||||||
|
dispatch(worker){
|
||||||
|
if(this.cancelled||this.failed||token!==state.token)return false;const ch=this.nextChunk();if(!ch){this.maybeFinish();return false}
|
||||||
|
const jobId=token+':'+(++deepPool.serial),vb=Math.min(verifyJobs-1,Math.max(0,Math.floor((ch.y0+ch.rows*.5)*verifyJobs/Math.max(1,h)))),verifySamples=useBla&&!this.verifiedBuckets.has(vb)?(this.verifiedBuckets.add(vb),profile.covered?4:3):0,blaKey=refKey+':'+ref.version+':'+refLen+':'+cBucket+':e'+bp.exp,safeBlaKey=refKey+':'+ref.version+':'+refLen+':'+cBucket+':e'+bp.safeExp;
|
||||||
|
const msg={type:'render',jobId,refKey,refLen,w,h,x0:ch.x0,rectW:ch.w,y0:ch.y0,rows:ch.rows,iter,colorIter,covered:profile.covered,spanMant:sb.mant,spanBucket:sb.bucket,offR:off[0],offI:off[1],offBucket:off[2],cx:w*.5,cy:h*.5,skip:series.skip,Ar:series.Ar,Ai:series.Ai,Ab:series.Ab,Br:series.Br,Bi:series.Bi,Bb:series.Bb,shift:state.shift,cycle:state.cycle,palette:state.palette,useBla,blaEps:bp.eps,blaKey,safeBlaEps:Math.pow(2,-bp.safeExp),safeBlaKey,cMax:cMaxSafe,spanNormal,offRn,offIn,baseCr,baseCi,maxBlaSteps:wp.bla,maxPtbSteps:wp.ptb,verifySamples,verifyDelta:profile.covered?8:64},transfer=[];
|
||||||
|
attachReference(worker,msg,ref,refLen,refKey,transfer);if(worker._recycle){msg.fieldBuffer=worker._recycle.field;msg.iterationBuffer=worker._recycle.iterations;msg.classBuffer=worker._recycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);worker._recycle=null}worker._busy=true;worker._job={type:'render',runner:this,jobId,x0:ch.x0,y0:ch.y0,rectW:ch.w,rows:ch.rows,pixels:ch.w*ch.rows,started:performance.now()};try{worker.postMessage(msg,transfer)}catch(e){worker._busy=false;worker._job=null;this.fail(e);return false}return true
|
||||||
|
},
|
||||||
|
onResult(worker,d,job){
|
||||||
|
const elapsed=Math.max(.05,performance.now()-job.started),mpr=elapsed/Math.max(1,job.rows);deepWisdom.rowMsEMA=deepWisdom.rowMsEMA?deepWisdom.rowMsEMA*.86+mpr*.14:mpr;deepWisdom.stripRows=Math.round((deepWisdom.stripRows||12)*.75+Math.max(3,Math.min(128,Math.round(deepWisdom.targetStripMs/Math.max(.001,deepWisdom.rowMsEMA))))*.25);
|
||||||
|
if(this.cancelled||this.failed||token!==state.token)return;if(!d||d.jobId!==job.jobId)return;if(d.error){this.fail(new Error(d.error));return}
|
||||||
|
this.simd=this.simd&&!!d.simd;this.blaUsed=this.blaUsed||!!d.bla;this.kernelMs+=d.kernelMs||0;this.verifyMs+=d.verifyMs||0;this.blaBuildMs+=d.blaBuildMs||0;this.sumIter+=d.sumIter||0;this.blackCount+=d.blackCount||0;this.badCount+=(d.bad||[]).length;const st=d.stats||{};this.blaSteps+=st.blaSteps||0;this.ptbSteps+=st.ptbSteps||0;this.rebases+=st.rebases||0;this.interior+=st.interior||0;this.unresolved+=st.unresolved||0;this.repaired+=d.repaired||0;
|
||||||
|
const rw=d.rectW||job.rectW,rx=d.x0==null?job.x0:d.x0,sf=d.field?new Float32Array(d.field):null,it=d.iterations?new Uint32Array(d.iterations):null,cl=d.classes?new Uint8Array(d.classes):null,ro=sf&&cl?colorizeField({smooth:sf,classes:cl,iter:colorIter}):new Uint8ClampedArray(d.out);for(let yy=0;yy<d.rows;yy++){const dst=(d.y0+yy)*w+rx;out.set(ro.subarray(yy*rw*4,(yy+1)*rw*4),dst*4);if(sf)field.smooth.set(sf.subarray(yy*rw,(yy+1)*rw),dst);if(it)field.iterations.set(it.subarray(yy*rw,(yy+1)*rw),dst);if(cl)field.classes.set(cl.subarray(yy*rw,(yy+1)*rw),dst)}presentPartialStripe(ro,rx,d.y0,rw,d.rows,w,h,snap);if(d.field&&d.iterations&&d.classes)worker._recycle={field:d.field,iterations:d.iterations,classes:d.classes};if(d.bad)for(const local of d.bad){const gx=rx+(local%rw),gy=d.y0+((local/rw)|0);bad.push(gy*w+gx)}this.donePixels+=job.pixels;if(profile.covered&&workers.length<deepPool.maxWorkers&&memoryLedger().managedBytes+24*1048576<rendererMemoryBudget()&&this.totalPixels-this.donePixels>job.pixels*4&&elapsed>deepWisdom.targetStripMs){const added=addDeepWorker();deepWisdom.workerCount=workers.length;deepWisdom.ready=true;kickDeepWorker(added)}this.maybeFinish()
|
||||||
|
},
|
||||||
|
maybeFinish(){
|
||||||
|
if(this.cancelled||this.failed||token!==state.token||this.finishing||this.donePixels<this.totalPixels)return;this.finishing=true;if(deepPool.current===this)deepPool.current=null;deepPool.activeToken=0;const px=Math.max(1,this.totalPixels),kmpp=this.kernelMs/px;deepTelemetry.frames++;deepTelemetry.kernelMPP=deepTelemetry.kernelMPP?deepTelemetry.kernelMPP*.78+kmpp*.22:kmpp;deepTelemetry.meanIter=this.sumIter/px;deepTelemetry.blackRatio=this.blackCount/px;deepTelemetry.badRatio=this.badCount/px;deepTelemetry.blaBuildEMA=deepTelemetry.blaBuildEMA?deepTelemetry.blaBuildEMA*.85+this.blaBuildMs*.15:this.blaBuildMs;deepTelemetry.interiorRatio=this.interior/px;deepTelemetry.repairRatio=this.repaired/Math.max(1,regions.length);deepTelemetry.unresolvedRatio=this.unresolved/px;deepTelemetry.blaStepsPerPixel=this.blaSteps/px;deepTelemetry.ptbStepsPerPixel=this.ptbSteps/px;deepTelemetry.rebasePerPixel=this.rebases/px;deepTelemetry.lastKernelMs=this.kernelMs;deepTelemetry.lastVerifyMs=this.verifyMs;deepTelemetry.lastPixels=px;deepTelemetry.refDistance=refC.dist||0;refControl.lastMPP=kmpp;refControl.lastPixels=px;if(refControl.refId!==ref.id){refControl.refId=ref.id;refControl.baseMPP=kmpp}else if((refC.dist||0)<.2)refControl.baseMPP=refControl.baseMPP?refControl.baseMPP*.8+kmpp*.2:kmpp;persistDeepWisdom();
|
||||||
|
const label='WASM '+(this.simd?'SIMD':'scalar')+' ×'+Math.max(1,Math.min(deepWisdom.workerCount,workers.length))+' · '+(this.blaUsed?'BLA e-'+bp.exp+' + ':'')+'rebase'+(this.interior?' · 内部早期終了 '+Math.round(100*this.interior/px)+'%':'')+(this.repaired?' · 局所補修 '+this.repaired:'')+(this.totalPixels<w*h?' · 既存画像再利用 '+Math.round(100*(1-this.totalPixels/(w*h)))+'%':'');
|
||||||
|
const completeField=this.totalPixels===w*h?field:null;if(!bad.length){finishRender(token,t0,profile,snap,w,h,out,label,this.totalPixels,completeField);return}let k=0;const residual=async()=>{if(token!==state.token||this.cancelled)return;const deadline=performance.now()+5;while(k<bad.length&&performance.now()<deadline){const idx=bad[k++],x=idx%w,py=(idx/w)|0,result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,py,false,()=>token!==state.token||this.cancelled);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}if(k<bad.length)requestAnimationFrame(residual);else finishRender(token,t0,profile,snap,w,h,out,label+' · 残差 '+bad.length,this.totalPixels,completeField)};requestAnimationFrame(residual)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
deepPool.current=runner;deepPool.activeToken=token;if(totalPixels===0){queueMicrotask(()=>runner.maybeFinish());return true}const active=Math.max(1,Math.min(deepWisdom.ready?deepWisdom.workerCount:Math.min(2,workers.length),workers.length));for(let i=0;i<active;i++)kickDeepWorker(workers[i]);return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Frame reprojection / world-space detail cache ───────────────────────
|
||||||
|
const frameCanvas=document.createElement('canvas'),frameCtx=frameCanvas.getContext('2d',{alpha:false});
|
||||||
|
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
|
||||||
|
function snapshotToCurrent(s){if(s.bits===state.bits)return s;return{bits:state.bits,re:align(s.re,s.bits,state.bits),im:align(s.im,s.bits,state.bits),span:align(s.span,s.bits,state.bits)}}
|
||||||
|
function styleSignature(){return state.palette+':'+state.cycle.toFixed(6)+':'+state.shift.toFixed(5)}
|
||||||
|
function commitImage(data,w,h,snap,field=null){
|
||||||
|
frameCanvas.width=w;frameCanvas.height=h;const id=frameCtx.createImageData(w,h);id.data.set(data);frameCtx.putImageData(id,0,0);runtimeMetrics.canvasWrites++;
|
||||||
|
state.frameView=snapshotToCurrent(snap);state.fieldView=field?{field,w,h,snap:state.frameView}:null;invalidateView();
|
||||||
|
}
|
||||||
|
function presentPartialStripe(data,x,y,w,h,frameW,frameH,snap){if(frameCanvas.width!==frameW||frameCanvas.height!==frameH||!sameSnapshot(state.frameView,snap)){frameCanvas.width=frameW;frameCanvas.height=frameH;frameCtx.fillStyle='#050813';frameCtx.fillRect(0,0,frameW,frameH);state.frameView=snapshotToCurrent(snap);state.fieldView=null}const id=frameCtx.createImageData(w,h);id.data.set(data);frameCtx.putImageData(id,x,y);runtimeMetrics.canvasWrites++;invalidateView(false)}
|
||||||
|
function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span}
|
||||||
|
function recolorCurrentField(){const fv=state.fieldView;if(!fv||state.dirty||state.rendering||!sameSnapshot(fv.snap,state.frameView))return false;const out=colorizeField(fv.field);commitImage(out,fv.w,fv.h,fv.snap,fv.field);state.lastEngine='フィールド再彩色';state.lastRender=0;recolorCachedDetails();invalidateView();return true}
|
||||||
|
const validationJob={active:false,key:''};
|
||||||
|
const unknownContinuationJob={active:false,key:''};let unknownContinuationTimer=0;
|
||||||
|
function cancelUnknownContinuation(){if(unknownContinuationTimer){clearTimeout(unknownContinuationTimer);unknownContinuationTimer=0}unknownContinuationJob.active=false;state.continuationProgress=0}
|
||||||
|
function scheduleUnknownContinuation(delay=120){
|
||||||
|
const fv=state.fieldView;if(state.processMode!=='fine'||state.dirty||state.rendering||!fv||!state.unresolved)return;
|
||||||
|
const key=viewSpecKey(currentViewSpec())+':'+fv.w+'x'+fv.h+':'+fv.field.iter;
|
||||||
|
if(unknownContinuationJob.key===key||unknownContinuationTimer)return;
|
||||||
|
unknownContinuationTimer=setTimeout(()=>{unknownContinuationTimer=0;startUnknownContinuation(key)},delay)
|
||||||
|
}
|
||||||
|
function startUnknownContinuation(key){
|
||||||
|
const fv=state.fieldView;if(unknownContinuationJob.active||state.dirty||state.rendering||!fv||!sameSnapshot(fv.snap,state.frameView))return;
|
||||||
|
const field=fv.field,ranked=[];for(let i=0;i<field.classes.length;i++){if(field.classes[i]!==FIELD_UNKNOWN)continue;const x=i%fv.w,y=(i/fv.w)|0;let boundary=false;for(let yy=Math.max(0,y-1);yy<=Math.min(fv.h-1,y+1)&&!boundary;yy++)for(let xx=Math.max(0,x-1);xx<=Math.min(fv.w-1,x+1);xx++)if(field.classes[yy*fv.w+xx]===FIELD_ESCAPED){boundary=true;break}if(boundary)ranked.push({i,d:Math.hypot(x/fv.w-state.focusX,y/fv.h-state.focusY)})}if(!ranked.length)return;
|
||||||
|
const deep=deepEngineNeeded(fv.snap,fv.w),cap=Math.min(ranked.length,deep?384:4096);ranked.sort((a,b)=>a.d-b.d);const indices=ranked.slice(0,cap).map(v=>v.i);
|
||||||
|
unknownContinuationJob.active=true;unknownContinuationJob.key=key;const token=state.token,snap=fv.snap,baseIter=field.iter,extendedIter=Math.min(280000,Math.max(baseIter+256,baseIter*2)),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=fixedNum(snap.span,snap.bits)/fv.w;let p=0;
|
||||||
|
state.drawState='RESOLVING';state.continuationProgress=0;invalidateStats();
|
||||||
|
async function slice(){
|
||||||
|
if(token!==state.token||state.dirty){unknownContinuationJob.active=false;return}
|
||||||
|
const deadline=performance.now()+7;while(p<indices.length&&performance.now()<deadline){const i=indices[p++],x=i%fv.w,y=(i/fv.w)|0,result=deep?await highPrecisionDirectPixelAsync(snap,fv.w,fv.h,extendedIter,x,y,false,()=>token!==state.token||state.dirty):exportSampleShallow(cre,cim,scale,fv.w,fv.h,extendedIter,x,y);if(!result){unknownContinuationJob.active=false;return}const[n,m]=result;putField(field,i,n,m,n<extendedIter?FIELD_ESCAPED:FIELD_UNKNOWN)}
|
||||||
|
state.continuationProgress=p/indices.length;if(p<indices.length){invalidateStats();requestAnimationFrame(slice);return}
|
||||||
|
state.unresolved=field.classes.reduce((n,c)=>n+(c===FIELD_UNKNOWN),0);unknownContinuationJob.active=false;state.continuationProgress=1;state.drawState='COVERED';commitImage(colorizeField(field),fv.w,fv.h,snap,field);state.lastEngine+=' · 境界 '+indices.length+'点を追加反復';invalidateStats()
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function scheduleValidation(delay=350){if(state.processMode!=='validate'||state.dirty||state.rendering)return;scheduleBackground(()=>{if(state.processMode==='validate'&&!state.detailActive&&!unknownContinuationJob.active)startValidation();else if(state.processMode==='validate')scheduleValidation(300)},delay)}
|
||||||
|
function startValidation(){
|
||||||
|
const fv=state.fieldView;if(validationJob.active||!fv||state.dirty||state.rendering||!sameSnapshot(fv.snap,state.frameView))return;
|
||||||
|
const key=viewSpecKey(currentViewSpec())+':'+fv.w+'x'+fv.h+':'+fv.field.iter;if(validationJob.key===key)return;
|
||||||
|
validationJob.active=true;validationJob.key=key;
|
||||||
|
const token=state.token,snap=fv.snap,field=fv.field,baseIter=field.iter,extendedIter=Math.min(280000,Math.max(baseIter+256,baseIter*2));let i=0,lastStatus=0;
|
||||||
|
state.drawState='VALIDATING';state.coverage=0;invalidateStats();
|
||||||
|
async function slice(now){
|
||||||
|
if(token!==state.token||state.processMode!=='validate'){validationJob.active=false;return}
|
||||||
|
const deadline=performance.now()+9;
|
||||||
|
while(i<field.classes.length&&performance.now()<deadline){
|
||||||
|
const old=field.classes[i],x=i%fv.w,y=(i/fv.w)|0,limit=old===FIELD_ESCAPED?baseIter:extendedIter;
|
||||||
|
if(fixedAnalyticPixelProven(snap,fv.w,fv.h,x,y)){putField(field,i,limit,0,FIELD_INTERIOR_PROVEN);i++;continue}
|
||||||
|
const result=await highPrecisionDirectPixelAsync(snap,fv.w,fv.h,limit,x,y,true,()=>token!==state.token||state.processMode!=='validate');
|
||||||
|
if(!result){validationJob.active=false;return}
|
||||||
|
const[n,m]=result;if(n<limit)putField(field,i,n,m,FIELD_ESCAPED);else putField(field,i,n,0,FIELD_UNKNOWN);i++
|
||||||
|
}
|
||||||
|
if(now-lastStatus>180){lastStatus=now;state.coverage=i/field.classes.length;invalidateStats()}
|
||||||
|
if(i<field.classes.length){requestAnimationFrame(slice);return}
|
||||||
|
field.iter=extendedIter;state.unresolved=field.classes.reduce((n,c)=>n+(c!==FIELD_ESCAPED&&c!==FIELD_INTERIOR_PROVEN),0);state.drawState=state.unresolved?'VALIDATION_INCOMPLETE':'VALIDATED';state.coverage=1;validationJob.active=false;const out=colorizeField(field);commitImage(out,fv.w,fv.h,snap,field);state.lastEngine='高精度direct照合';invalidateStats()
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function drawWorldDetail(entry){
|
||||||
|
if(!entry||!entry.complete||entry.style!==styleSignature()||!entry.canvas)return;if(entry.iter){const rr=entry.iter/Math.max(1,maxIter());if(rr<.82||rr>1.22)return}
|
||||||
|
const re=align(entry.re,entry.bits,state.bits),im=align(entry.im,entry.bits,state.bits),sxv=align(entry.spanX,entry.bits,state.bits),syv=align(entry.spanY,entry.bits,state.bits);
|
||||||
|
const x=canvas.width*.5+fixedRatio(re-state.re,state.span)*canvas.width,y=canvas.height*.5-fixedRatio(im-state.im,state.span)*canvas.width;
|
||||||
|
const dw=Math.abs(fixedRatio(sxv,state.span)*canvas.width),dh=Math.abs(fixedRatio(syv,state.span)*canvas.width);
|
||||||
|
if(!Number.isFinite(x+y+dw+dh)||dw<5||dh<5||x+dw*.5<0||x-dw*.5>canvas.width||y+dh*.5<0||y-dh*.5>canvas.height)return;
|
||||||
|
// Do not magnify a cached tile beyond ~1.7 display pixels per source pixel.
|
||||||
|
if(dw/Math.max(1,entry.canvas.width)>1.7)return;
|
||||||
|
entry.lastUsed=performance.now();
|
||||||
|
ctx.imageSmoothingEnabled=true;try{ctx.imageSmoothingQuality='high'}catch{}
|
||||||
|
ctx.drawImage(entry.canvas,x-dw*.5,y-dh*.5,dw,dh);
|
||||||
|
}
|
||||||
|
function paintFrame(){
|
||||||
|
if(!state.frameView||!frameCanvas.width)return;
|
||||||
|
runtimeMetrics.canvasWrites++;
|
||||||
|
const fv=state.frameView;
|
||||||
|
const a=fixedRatio(fv.span,state.span);if(!Number.isFinite(a)||a<=0)return;
|
||||||
|
const dx=fixedRatio(fv.re-state.re,state.span)*canvas.width;
|
||||||
|
const dy=-fixedRatio(fv.im-state.im,state.span)*canvas.width;
|
||||||
|
ctx.save();ctx.setTransform(1,0,0,1,0,0);ctx.fillStyle='#050813';ctx.fillRect(0,0,canvas.width,canvas.height);
|
||||||
|
ctx.translate(canvas.width*.5+dx,canvas.height*.5+dy);ctx.scale(a,a);ctx.imageSmoothingEnabled=true;try{ctx.imageSmoothingQuality=state.lastPass===RENDER_PASS.COVERED?'high':'medium'}catch{}
|
||||||
|
const fw=canvas.width,fh=fw*(frameCanvas.height/Math.max(1,frameCanvas.width));ctx.drawImage(frameCanvas,-fw*.5,-fh*.5,fw,fh);ctx.restore();
|
||||||
|
// Only completed, world-validated tiles are composited. Partial tiles remain offscreen.
|
||||||
|
ctx.save();ctx.setTransform(1,0,0,1,0,0);for(const e of DETAIL_TILE_CACHE.values())drawWorldDetail(e);ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBudget(profile,deep){return profile.budgetMs}
|
||||||
|
function targetSize(profile,deep){
|
||||||
|
// Resolution is intentionally independent of zoom depth. 10^20, 10^100 and
|
||||||
|
// beyond use the same quality targets as low zoom; only the numerical engine changes.
|
||||||
|
const cssW=Math.max(1,canvas.clientWidth),cssH=Math.max(1,canvas.clientHeight),aspect=cssH/cssW,dpr=Math.max(.25,state.effectiveDpr||1);
|
||||||
|
if(profile.covered)return[canvas.width,canvas.height];
|
||||||
|
let nominal=Math.max(profile.minWidth,cssW*dpr*profile.nominalScale),minW=profile.minWidth,maxW=profile.maxWidth;
|
||||||
|
const measured=renderPerf[deep?'deepMPP':'shallowMPP'];
|
||||||
|
if(deep&&!profile.covered&&measured<=0){nominal=Math.min(nominal,48);minW=32}
|
||||||
|
if(measured>0){
|
||||||
|
const timedW=Math.sqrt(renderBudget(profile,deep)/Math.max(1e-7,measured*aspect));
|
||||||
|
nominal=Math.min(nominal,timedW);minW=deep?32:240
|
||||||
|
}
|
||||||
|
let w=Math.max(minW,Math.min(maxW,Math.round(nominal))),h=Math.max(deep&&!profile.covered?32:96,Math.round(w*aspect));
|
||||||
|
if(h>2200){const sc=2200/h;h=2200;w=Math.round(w*sc)}return[w,h]
|
||||||
|
}
|
||||||
|
function analyzeDetailTiles(data,w,h,baseMs,deep){
|
||||||
|
const field=state.fieldView&&state.fieldView.w===w&&state.fieldView.h===h?state.fieldView.field:null;if(!field)return[];
|
||||||
|
const leaves=[],minTile=32,rootTile=256;
|
||||||
|
function scoreRect(x0,y0,tw,th){
|
||||||
|
const step=Math.max(1,Math.floor(Math.min(tw,th)/14));let grad=0,edges=0,uncertain=0,samples=0;
|
||||||
|
for(let y=y0;y<y0+th;y+=step)for(let x=x0;x<x0+tw;x+=step){const i=y*w+x,c=field.classes[i],s=field.smooth[i];samples++;uncertain+=(255-(field.confidence?field.confidence[i]:fieldConfidence(c)))/255;if(x+step<x0+tw){const j=i+step,c2=field.classes[j];if((c===FIELD_ESCAPED)!==(c2===FIELD_ESCAPED))edges++;else if(c===FIELD_ESCAPED&&c2===FIELD_ESCAPED)grad+=Math.min(12,Math.abs(s-field.smooth[j]))}if(y+step<y0+th){const j=i+step*w,c2=field.classes[j];if((c===FIELD_ESCAPED)!==(c2===FIELD_ESCAPED))edges++;else if(c===FIELD_ESCAPED&&c2===FIELD_ESCAPED)grad+=Math.min(12,Math.abs(s-field.smooth[j]))}}
|
||||||
|
return edges/Math.max(1,samples)*4.4+grad/Math.max(1,samples*12)*1.5+uncertain/Math.max(1,samples)*.9
|
||||||
|
}
|
||||||
|
function visit(x,y,tw,th){const score=scoreRect(x,y,tw,th);if(score<.055)return;if((tw>minTile||th>minTile)&&score>.11){const aw=Math.max(1,tw>>1),bw=tw-aw,ah=Math.max(1,th>>1),bh=th-ah;visit(x,y,aw,ah);if(bw)visit(x+aw,y,bw,ah);if(bh)visit(x,y+ah,aw,bh);if(bw&&bh)visit(x+aw,y+ah,bw,bh);return}leaves.push({x,y,w:tw,h:th,score,area:tw*th})}
|
||||||
|
for(let y=0;y<h;y+=rootTile)for(let x=0;x<w;x+=rootTile)visit(x,y,Math.min(rootTile,w-x),Math.min(rootTile,h-y));
|
||||||
|
leaves.sort((a,b)=>{const ad=Math.hypot((a.x+a.w*.5)/w-state.focusX,(a.y+a.h*.5)/h-state.focusY),bd=Math.hypot((b.x+b.w*.5)/w-state.focusX,(b.y+b.h*.5)/h-state.focusY);return(b.score+.08/(.08+bd))-(a.score+.08/(.08+ad))});const byteCap=Math.max(0,detailCacheBudget()-detailCacheBytes),baseBudget=renderBudget(RENDER_PROFILE[RENDER_PASS.COVERED],deep),spare=Math.max(0,baseBudget*1.7-baseMs),sampleCap=Math.floor(w*h*Math.min(2.6,spare/Math.max(1,baseMs)));let bytes=0,sampleArea=0,n=0;while(n<leaves.length){const tile=leaves[n],sampleScale=tile.score>=1.15?4:2,costBytes=tile.area*(sampleScale*sampleScale*10+4),costSamples=tile.area*sampleScale*sampleScale;if(bytes+costBytes>byteCap||sampleArea+costSamples>sampleCap)break;bytes+=costBytes;sampleArea+=costSamples;n++}return leaves.slice(0,n)
|
||||||
|
}
|
||||||
|
function detailGeometry(tile,plan){
|
||||||
|
const s=plan.snap,den=BigInt(2*plan.w),re=s.re+s.span*BigInt(2*tile.x+tile.w-plan.w)/den,im=s.im+s.span*BigInt(plan.h-2*tile.y-tile.h)/den;
|
||||||
|
const spanX=s.span*BigInt(tile.w)/BigInt(plan.w),spanY=s.span*BigInt(tile.h)/BigInt(plan.w);
|
||||||
|
return{bits:s.bits,re,im,spanX,spanY}
|
||||||
|
}
|
||||||
|
function detailKey(tile,plan,sampleScale){const g=detailGeometry(tile,plan);return [g.bits,g.re,g.im,g.spanX,g.spanY,plan.iter,'aa'+sampleScale,'centered-v23'].join(':')}
|
||||||
|
function rendererMemoryBudget(){return(Number(navigator.deviceMemory||8)<=4||matchMedia('(max-width:700px)').matches?96:192)*1048576}
|
||||||
|
function wasmBytes(core){try{return core&&core.ex&&core.ex.memory?core.ex.memory.buffer.byteLength:0}catch{return 0}}
|
||||||
|
function memoryLedger(includeDetail=true){
|
||||||
|
const screenCanvasBytes=canvas.width*canvas.height*4,frameCanvasBytes=frameCanvas.width*frameCanvas.height*4;
|
||||||
|
const fieldBytes=state.fieldView?state.fieldView.w*state.fieldView.h*10:0;
|
||||||
|
const referenceBytes=(referenceCache.rr?.byteLength||0)+(referenceCache.ri?.byteLength||0);
|
||||||
|
const mainWasmBytes=wasmBytes(wasm)+wasmBytes(deepWasm);
|
||||||
|
// Worker memories are isolated, so browsers do not expose their byteLength.
|
||||||
|
// Account them conservatively: reference copies + kernel scratch/linear memory.
|
||||||
|
const workerEstimateBytes=(shallowWorker?2*1048576:0)+deepPool.workers.length*24*1048576;
|
||||||
|
const renderBytes=state.rendering?(()=>{const size=targetSize(renderProfile(state.lastPass),deepMode),pixels=size[0]*size[1];return pixels*18})():0;
|
||||||
|
const exportBytes=exportJob&&exportJob.active?exportJob.bytes||0:0,activeDetailBytes=activeDetailTiles.reduce((sum,e)=>sum+(e.transientBytes||0),0);
|
||||||
|
const cacheBytes=includeDetail?detailCacheBytes:0;
|
||||||
|
const managedBytes=fieldBytes+referenceBytes+mainWasmBytes+workerEstimateBytes+renderBytes+exportBytes+activeDetailBytes+cacheBytes;
|
||||||
|
const canvasBytes=screenCanvasBytes+frameCanvasBytes;
|
||||||
|
return{managedBytes,logicalBytes:managedBytes+canvasBytes,canvasBytes,screenCanvasBytes,frameCanvasBytes,fieldBytes,referenceBytes,mainWasmBytes,workerEstimateBytes,renderBytes,exportBytes,activeDetailBytes,detailCacheBytes:cacheBytes,budget:rendererMemoryBudget()}
|
||||||
|
}
|
||||||
|
function detailCacheBudget(){const base=memoryLedger(false).managedBytes,reserve=16*1048576;return Math.max(0,Math.floor(Math.min(rendererMemoryBudget()*.18,rendererMemoryBudget()-base-reserve)))}
|
||||||
|
function detailEntryBytes(entry){return entry&&entry.canvas?entry.canvas.width*entry.canvas.height*4+(entry.field?entry.field.classes.length*10:0):0}
|
||||||
|
function detailDistance(entry){try{const re=align(entry.re,entry.bits,state.bits),im=align(entry.im,entry.bits,state.bits);return Math.hypot(fixedRatio(re-state.re,state.span),fixedRatio(im-state.im,state.span))}catch{return Infinity}}
|
||||||
|
function trimDetailCache(){const budget=detailCacheBudget();while(detailCacheBytes>budget&&DETAIL_TILE_CACHE.size){let victim=null,rank=-Infinity;const now=performance.now();for(const [key,entry]of DETAIL_TILE_CACHE){const age=Math.max(0,now-(entry.lastUsed||0))/60000,distance=detailDistance(entry),score=(Number.isFinite(distance)?distance:1000)*4+age;if(score>rank){rank=score;victim=[key,entry]}}if(!victim)break;DETAIL_TILE_CACHE.delete(victim[0]);detailCacheBytes=Math.max(0,detailCacheBytes-detailEntryBytes(victim[1]))}}
|
||||||
|
function linearChannel(v){v/=255;return v<=.04045?v/12.92:Math.pow((v+.055)/1.055,2.4)}
|
||||||
|
function srgbChannel(v){v=Math.max(0,Math.min(1,v));return Math.round(255*(v<=.0031308?12.92*v:1.055*Math.pow(v,1/2.4)-.055))}
|
||||||
|
function resolveSubsampleField(field,w,h,scale=2){const hi=colorizeField(field),c=document.createElement('canvas');c.width=w;c.height=h;const cc=c.getContext('2d',{alpha:false}),id=cc.createImageData(w,h),out=id.data,samples=scale*scale;for(let y=0;y<h;y++)for(let x=0;x<w;x++){let r=0,g=0,b=0;for(let sy=0;sy<scale;sy++)for(let sx=0;sx<scale;sx++){const i=(((y*scale+sy)*w*scale)+(x*scale+sx))*4;r+=linearChannel(hi[i]);g+=linearChannel(hi[i+1]);b+=linearChannel(hi[i+2])}const oi=(y*w+x)*4;out[oi]=srgbChannel(r/samples);out[oi+1]=srgbChannel(g/samples);out[oi+2]=srgbChannel(b/samples);out[oi+3]=255}cc.putImageData(id,0,0);return c}
|
||||||
|
function recolorDetailEntry(entry){if(!entry||!entry.field)return;entry.canvas=resolveSubsampleField(entry.field,entry.baseW,entry.baseH,entry.sampleScale||2);entry.style=styleSignature()}
|
||||||
|
function recolorCachedDetails(){cancelDetailRefinement(true);for(const entry of DETAIL_TILE_CACHE.values())recolorDetailEntry(entry)}
|
||||||
|
function makeDetailTask(tile,plan){
|
||||||
|
const sampleScale=tile.score>=1.15?4:2,key=detailKey(tile,plan,sampleScale),cached=DETAIL_TILE_CACHE.get(key);if(cached){DETAIL_TILE_CACHE.delete(key);DETAIL_TILE_CACHE.set(key,cached);cached.lastUsed=performance.now();if(cached.style!==styleSignature())recolorDetailEntry(cached);return{cached:true,entry:cached}}
|
||||||
|
const c=document.createElement('canvas');c.width=Math.max(2,tile.w*sampleScale);c.height=Math.max(2,tile.h*sampleScale);const dc=c.getContext('2d',{alpha:false});
|
||||||
|
dc.imageSmoothingEnabled=true;try{dc.imageSmoothingQuality='high'}catch{};dc.drawImage(frameCanvas,tile.x,tile.y,tile.w,tile.h,0,0,c.width,c.height);
|
||||||
|
const g=detailGeometry(tile,plan),entry={...g,canvas:c,field:null,baseW:tile.w,baseH:tile.h,sampleScale,style:styleSignature(),key,score:tile.score,iter:plan.iter,complete:false,lastUsed:performance.now(),transientBytes:c.width*c.height*18};
|
||||||
|
const task={tile,canvas:c,ctx:dc,field:makeField(c.width*c.height,plan.iter),entry,phases:0,plan};activeDetailTiles.push(entry);return task
|
||||||
|
}
|
||||||
|
function validateDetailTask(task){
|
||||||
|
const plan=task&&task.plan,t=task&&task.tile,base=plan&&plan.baseData;if(!plan||!t||!base)return false;
|
||||||
|
let samples=0,blackMismatch=0,rgbDiff=0,colorSamples=0;const sampleScale=task.entry.sampleScale||2,step=Math.max(3,Math.floor(Math.min(t.w,t.h)/7)),pix=task.ctx.getImageData(0,0,task.canvas.width,task.canvas.height).data;
|
||||||
|
for(let by=t.y+1;by<t.y+t.h-1;by+=step)for(let bx=t.x+1;bx<t.x+t.w-1;bx+=step){
|
||||||
|
const bi=(by*plan.w+bx)*4,lx=Math.min(task.canvas.width-1,Math.max(0,sampleScale*(bx-t.x)+(sampleScale>>1))),ly=Math.min(task.canvas.height-1,Math.max(0,sampleScale*(by-t.y)+(sampleScale>>1))),hi=(ly*task.canvas.width+lx)*4;
|
||||||
|
const bb=(base[bi]|base[bi+1]|base[bi+2])===0,hb=(pix[hi]|pix[hi+1]|pix[hi+2])===0;samples++;if(bb!==hb)blackMismatch++;else if(!bb){rgbDiff+=Math.abs(base[bi]-pix[hi])+Math.abs(base[bi+1]-pix[hi+1])+Math.abs(base[bi+2]-pix[hi+2]);colorSamples++}
|
||||||
|
}
|
||||||
|
if(samples<4)return false;const classRate=blackMismatch/samples,meanDiff=colorSamples?rgbDiff/(3*colorSamples):0;return classRate<=.035&&meanDiff<=48
|
||||||
|
}
|
||||||
|
function cacheDetailTask(task){
|
||||||
|
if(!task||task.cached)return;const i=activeDetailTiles.indexOf(task.entry);if(i>=0)activeDetailTiles.splice(i,1);
|
||||||
|
if(!validateDetailTask(task))return;fillFieldConfidence(task.field);task.entry.field=task.field;task.entry.canvas=resolveSubsampleField(task.field,task.tile.w,task.tile.h,task.entry.sampleScale);task.entry.style=styleSignature();task.entry.complete=true;task.entry.lastUsed=performance.now();task.entry.transientBytes=0;const old=DETAIL_TILE_CACHE.get(task.entry.key);if(old)detailCacheBytes-=detailEntryBytes(old);DETAIL_TILE_CACHE.delete(task.entry.key);DETAIL_TILE_CACHE.set(task.entry.key,task.entry);detailCacheBytes+=detailEntryBytes(task.entry);
|
||||||
|
trimDetailCache()
|
||||||
|
}
|
||||||
|
function phaseRect(task,phase){
|
||||||
|
const t=task.tile,scale=task.entry.sampleScale,left=(phase%scale)*t.w,top=((phase/scale)|0)*t.h;
|
||||||
|
return{x0:t.x*scale+left,y0:t.y*scale+top,rw:t.w,rows:t.h,dx:left,dy:top}
|
||||||
|
}
|
||||||
|
function prepareDeepTileContext(plan,iter,sampleScale,done){
|
||||||
|
const cacheKey='i'+iter+':s'+sampleScale;if(plan.deepCtx&&plan.deepCtx[cacheKey]){done(plan.deepCtx[cacheKey]);return}if(!plan.deepCtx)plan.deepCtx={};
|
||||||
|
const snap=plan.snap,refC=chooseReference(snap),W=plan.w*sampleScale,H=plan.h*sampleScale,refX=W*.5-.5+fixedRatio(refC.re-snap.re,snap.span)*W,refY=H*.5-.5-fixedRatio(refC.im-snap.im,snap.span)*W;
|
||||||
|
const cornerR=Math.max(Math.hypot(refX,refY),Math.hypot(W-refX,refY),Math.hypot(refX,H-refY),Math.hypot(W-refX,H-refY))/Math.max(1,W),logMaxDc=log2FixedAt(snap.span,snap.bits)+Math.log2(Math.max(1e-300,cornerR));
|
||||||
|
buildReferenceAt(snap.bits,refC.re,refC.im,iter,state.token,(ref,refLen)=>{
|
||||||
|
if(plan.gen!==state.detailGeneration)return;const series=computeSeries(ref,refLen,logMaxDc),centered=pixelCenteredOffset(snap,refC,W),sb=spanMantBucket(snap.span,snap.bits),off=fixedComplexScaled(centered.r,centered.i,snap.bits),spanNormal=fixedNum(snap.span,snap.bits),offRn=fixedNum(centered.r,snap.bits),offIn=fixedNum(centered.i,snap.bits);
|
||||||
|
const cMaxRaw=Math.hypot(offRn,offIn)+Math.abs(spanNormal)*Math.hypot(.5,H/(2*Math.max(1,W))),cBucket=cMaxRaw>0&&Number.isFinite(cMaxRaw)?Math.ceil(Math.log2(cMaxRaw)*8):0,cMaxSafe=cMaxRaw>0?Math.pow(2,cBucket/8):0;
|
||||||
|
const ctx={snap,refC,ref,refLen,series,sb,off,spanNormal,offRn,offIn,cBucket,cMaxSafe,W,H,iter};plan.deepCtx[cacheKey]=ctx;done(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function renderGuardedDeepDetailRect(task,phase,iter,done){
|
||||||
|
const plan=task.plan,r=phaseRect(task,phase),W=plan.w*task.entry.sampleScale,H=plan.h*task.entry.sampleScale,snap=plan.snap;let yy=0;
|
||||||
|
async function slice(){if(plan.gen!==state.detailGeneration){done(false);return}const deadline=performance.now()+6;while(yy<r.rows&&performance.now()<deadline){const gy=r.y0+yy;for(let xx=0;xx<r.rw;xx++){const gx=r.x0+xx,result=await highPrecisionDirectPixelAsync(snap,W,H,iter,gx,gy,true,()=>plan.gen!==state.detailGeneration);if(!result){done(false);return}const[n,m]=result,fi=(r.dy+yy)*task.canvas.width+r.dx+xx;putField(task.field,fi,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN)}yy++}if(yy<r.rows){requestAnimationFrame(slice);return}const stripe=makeField(r.rw*r.rows,iter);for(let y=0;y<r.rows;y++){const src=(r.dy+y)*task.canvas.width+r.dx,dst=y*r.rw;stripe.smooth.set(task.field.smooth.subarray(src,src+r.rw),dst);stripe.iterations.set(task.field.iterations.subarray(src,src+r.rw),dst);stripe.classes.set(task.field.classes.subarray(src,src+r.rw),dst)}const id=task.ctx.createImageData(r.rw,r.rows);id.data.set(colorizeField(stripe));task.ctx.putImageData(id,r.dx,r.dy);task.phases|=(1<<phase);invalidateView(false);done(true)}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function sendDeepDetailRect(task,phase,iter,colorIter,done){
|
||||||
|
const plan=task.plan,gen=plan.gen;if(gen!==state.detailGeneration){done(false);return}if(state.processMode==='validate'||task.tile.score>1.35){renderGuardedDeepDetailRect(task,phase,iter,done);return}if(!ensureDeepPool()){done(false);return}
|
||||||
|
prepareDeepTileContext(plan,iter,task.entry.sampleScale,dc=>{
|
||||||
|
if(gen!==state.detailGeneration){done(false);return}const worker=deepPool.workers.find(w=>!w._busy);if(!worker){scheduleBackground(()=>sendDeepDetailRect(task,phase,iter,colorIter,done),18);return}
|
||||||
|
const r=phaseRect(task,phase),ref=dc.ref,refKey=String(ref.id),bpExp=32,useBla=Number.isFinite(dc.spanNormal)&&Math.abs(dc.spanNormal)>=1e-280&&dc.refLen>8,blaEps=Math.pow(2,-bpExp),blaKey=refKey+':'+ref.version+':'+dc.refLen+':'+dc.cBucket+':e'+bpExp;
|
||||||
|
const jobId='detail:'+gen+':'+Date.now()+':'+Math.random(),msg={type:'render',jobId,refKey,refLen:dc.refLen,w:dc.W,h:dc.H,x0:r.x0,y0:r.y0,rectW:r.rw,rows:r.rows,iter,colorIter,spanMant:dc.sb.mant,spanBucket:dc.sb.bucket,offR:dc.off[0],offI:dc.off[1],offBucket:dc.off[2],cx:dc.W*.5,cy:dc.H*.5,skip:dc.series.skip,Ar:dc.series.Ar,Ai:dc.series.Ai,Ab:dc.series.Ab,Br:dc.series.Br,Bi:dc.series.Bi,Bb:dc.series.Bb,shift:state.shift,cycle:state.cycle,palette:state.palette,useBla,blaEps,blaKey,cMax:dc.cMaxSafe,spanNormal:dc.spanNormal,offRn:dc.offRn,offIn:dc.offIn,baseCr:fixedNum(dc.refC.re,dc.snap.bits),baseCi:fixedNum(dc.refC.im,dc.snap.bits),maxBlaSteps:0,maxPtbSteps:0,verifySamples:3,verifyDelta:6,safeBlaEps:Math.pow(2,-48),safeBlaKey:blaKey+':safe48'},transfer=[];
|
||||||
|
let start=worker._refKey===refKey?worker._refLoaded:0;start=Math.max(0,Math.min(start,dc.refLen+1));if(start<dc.refLen+1){const rr=ref.rr.slice(start,dc.refLen+1),ri=ref.ri.slice(start,dc.refLen+1);msg.rr=rr.buffer;msg.ri=ri.buffer;msg.rrStart=start;transfer.push(rr.buffer,ri.buffer);worker._refKey=refKey;worker._refLoaded=dc.refLen+1}
|
||||||
|
const runner={token:state.token,cancelled:false,fail(){done(false)},onResult(w,d){if(gen!==state.detailGeneration||!d||d.error){done(false);return}const sf=d.field?new Float32Array(d.field):null,it=d.iterations?new Uint32Array(d.iterations):null,cl=d.classes?new Uint8Array(d.classes):null,arr=sf&&cl?colorizeField({smooth:sf,classes:cl,iter:colorIter}):new Uint8ClampedArray(d.out),id=task.ctx.createImageData(r.rw,r.rows);id.data.set(arr);task.ctx.putImageData(id,r.dx,r.dy);if(sf&&it&&cl)for(let yy=0;yy<r.rows;yy++){const dst=(r.dy+yy)*task.canvas.width+r.dx;task.field.smooth.set(sf.subarray(yy*r.rw,(yy+1)*r.rw),dst);task.field.iterations.set(it.subarray(yy*r.rw,(yy+1)*r.rw),dst);task.field.classes.set(cl.subarray(yy*r.rw,(yy+1)*r.rw),dst)}if(d.field&&d.iterations&&d.classes)w._recycle={field:d.field,iterations:d.iterations,classes:d.classes};task.phases|=(1<<phase);task.entry.canvas=task.canvas;invalidateView(false);done(true)}};
|
||||||
|
if(worker._recycle){msg.fieldBuffer=worker._recycle.field;msg.iterationBuffer=worker._recycle.iterations;msg.classBuffer=worker._recycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);worker._recycle=null}worker._busy=true;worker._job={type:'render',runner,jobId,y0:r.y0,rows:r.rows,started:performance.now()};try{worker.postMessage(msg,transfer)}catch(e){worker._busy=false;worker._job=null;done(false)}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function renderShallowDetailRect(task,phase,done){
|
||||||
|
const plan=task.plan,r=phaseRect(task,phase),sampleScale=task.entry.sampleScale,W=plan.w*sampleScale,H=plan.h*sampleScale,iter=plan.iter,snap=plan.snap,sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/W,colorCtx=makeColorCtx(iter),out=new Uint8ClampedArray(r.rw*r.rows*4);let yy=0;
|
||||||
|
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
function slice(){if(plan.gen!==state.detailGeneration){done(false);return}const deadline=performance.now()+5;while(yy<r.rows&&performance.now()<deadline){const gy=r.y0+yy,ci=cim+(H*.5-gy-.5)*scale;let oi=yy*r.rw*4;for(let xx=0;xx<r.rw;xx++){const gx=r.x0+xx,cr=cre+(gx+.5-W*.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0,inside=inBulbs(cr,ci);if(inside)n=iter;else while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}const m=n<iter?Math.max(4.0000001,zr2+zi2):0,fi=(r.dy+yy)*task.canvas.width+r.dx+xx;putField(task.field,fi,n,m,n<iter?FIELD_ESCAPED:(inside?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN));if(n>=iter){out[oi]=out[oi+1]=out[oi+2]=0;out[oi+3]=255}else putFastColor(out,oi,n,m,iter,colorCtx);oi+=4}yy++}if(yy<r.rows)requestAnimationFrame(slice);else{const id=task.ctx.createImageData(r.rw,r.rows),data=id.data;data.set(out);task.ctx.putImageData(id,r.dx,r.dy);task.phases|=(1<<phase);invalidateView(false);done(true)}}requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function pumpDetailRefinement(){
|
||||||
|
const plan=detailPlan;if(!plan||plan.gen!==state.detailGeneration||!state.hq){state.detailActive=false;return}if(state.rendering||unknownContinuationJob.active||performance.now()-state.lastInteraction<720){scheduleBackground(pumpDetailRefinement,90);return}
|
||||||
|
while(plan.index<plan.tasks.length&&plan.tasks[plan.index].cached)plan.index++;
|
||||||
|
if(plan.index>=plan.tasks.length){state.detailActive=false;state.detailDone=plan.tasks.length;state.drawState='REFINED';updateStats();scheduleValidation();return}
|
||||||
|
const task=plan.tasks[plan.index],phase=task.nextPhase||0,cb=ok=>{if(!ok){const i=activeDetailTiles.indexOf(task.entry);if(i>=0)activeDetailTiles.splice(i,1);plan.index++;scheduleBackground(pumpDetailRefinement,10);return}task.nextPhase=phase+1;if(task.nextPhase>=task.entry.sampleScale*task.entry.sampleScale){cacheDetailTask(task);plan.index++}state.detailDone=plan.tasks.reduce((n,t)=>n+(t.cached||t.entry.complete?1:0),0);updateStats();scheduleBackground(pumpDetailRefinement,0)};
|
||||||
|
if(plan.deep)sendDeepDetailRect(task,phase,plan.iter,plan.iter,cb);else renderShallowDetailRect(task,phase,cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Idle-only adaptive detail refinement ────────────────────────────────
|
||||||
|
function scheduleDetailRefinement(data,w,h,snap,iter,baseMs,deep){
|
||||||
|
if(!state.hq)return;cancelDetailRefinement(true);const gen=state.detailGeneration,tiles=analyzeDetailTiles(data,w,h,baseMs,deep);if(!tiles.length){state.drawState='REFINED';invalidateStats();scheduleValidation();return}
|
||||||
|
const plan={gen,snap,w,h,iter,deep,tiles,tasks:[],index:0,deepCtx:null,baseData:data};for(const t of tiles)plan.tasks.push(makeDetailTask(t,plan));detailPlan=plan;state.detailActive=true;state.drawState='REFINING';state.detailQueued=plan.tasks.length;state.detailDone=plan.tasks.filter(t=>t.cached).length;scheduleBackground(pumpDetailRefinement,160)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishRender(token,t0,profile,snap,w,h,out,engine,computedPixels=w*h,field=null){
|
||||||
|
if(token!==state.token)return;const elapsed=Math.max(.1,performance.now()-t0),deep=deepEngineNeeded(snap,Math.max(1,canvas.width));
|
||||||
|
// Reproject-only frames must not poison performance estimates with near-zero work.
|
||||||
|
if(computedPixels>Math.max(512,w*h*.01)){const mpp=elapsed/computedPixels,key=deep?'deepMPP':'shallowMPP';renderPerf[key]=renderPerf[key]?renderPerf[key]*.72+mpp*.28:mpp}
|
||||||
|
if(field){fillFieldConfidence(field);out=colorizeField(field)}commitImage(out,w,h,snap,field);state.rendering=false;state.lastRender=elapsed;state.lastPass=profile.id;state.dirty=false;state.lastEngine=engine;state.lastFrameDone=performance.now();state.coverage=Math.min(1,w*h/Math.max(1,canvas.width*canvas.height));state.drawState=profile.covered&&state.coverage>=.999?'COVERED':'PREVIEW';state.unresolved=field?field.classes.reduce((n,c)=>n+(c===FIELD_UNKNOWN),0):0;flushPendingPrecision();
|
||||||
|
if(!profile.covered&&adaptStandardDeepBudget(deep)){updateStats();return}
|
||||||
|
if(profile.covered&&state.unresolved)scheduleUnknownContinuation();
|
||||||
|
if(profile.covered&&state.hq){const gen=state.detailGeneration;const schedule=()=>{if(gen!==state.detailGeneration||state.rendering)return;if(unknownContinuationJob.active||unknownContinuationTimer){scheduleBackground(schedule,90);return}const fv=state.fieldView,base=fv&&sameSnapshot(fv.snap,snap)?colorizeField(fv.field):out;scheduleDetailRefinement(base,w,h,snap,fv?fv.field.iter:iterationPlan(profile,deep).colorIter,elapsed,deep)};scheduleIdle(schedule,350);clearPanReuse()}
|
||||||
|
if(profile.covered&&state.processMode==='validate')scheduleValidation();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Shallow renderers ───────────────────────────────────────────────────
|
||||||
|
function renderShallowReuse(profile,snap,w,h,iter,token,t0,reuse){
|
||||||
|
const out=reuse.out,colorCtx=makeColorCtx(iter),sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/w,rects=reuse.rects;let ri=0,yy=0;
|
||||||
|
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,qq=x*x+y2;if(qq*(qq+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
function slice(){if(token!==state.token)return;const deadline=performance.now()+7;while(ri<rects.length&&performance.now()<deadline){const r=rects[ri];while(yy<r.h&&performance.now()<deadline){const gy=r.y0+yy,ci=cim+(h*.5-gy-.5)*scale;for(let xx=0;xx<r.w;xx++){const gx=r.x0+xx,cr=cre+(gx+.5-w*.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0;if(inBulbs(cr,ci))n=iter;else while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}const oi=(gy*w+gx)*4;if(n>=iter){out[oi]=out[oi+1]=out[oi+2]=0;out[oi+3]=255}else putFastColor(out,oi,n,Math.max(4.0000001,zr2+zi2),iter,colorCtx)}yy++}if(yy>=r.h){ri++;yy=0}}
|
||||||
|
if(ri<rects.length)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'JavaScript f64 · 既存画像再利用 '+Math.round(100*reuse.reusedPixels/(w*h))+'%',reuse.exposedPixels)
|
||||||
|
}requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function renderWasmMain(profile,snap,w,h,iter,token,t0){
|
||||||
|
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),colorCtx=makeColorCtx(iter);
|
||||||
|
const cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;
|
||||||
|
function proven(cr,ci){const y2=ci*ci,x=cr-.25,q0=x*x+y2;if(q0*(q0+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
let y=0;const counts=wasm.counts(),mags=wasm.mags();
|
||||||
|
function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+(profile.covered?11:8);
|
||||||
|
while(y<h&&performance.now()<deadline){
|
||||||
|
const rows=Math.min(24,h-y),y0=y,npx=wasm.ex.render_rows(cre+scale*.5,cim-scale*.5,sp,w,h,y,rows,iter);let oi=y*w*4;
|
||||||
|
for(let i=0;i<npx;i++){
|
||||||
|
const n=counts[i],m=mags[i],px=i%w,py=y0+((i/w)|0);
|
||||||
|
const cr=cre+(px+.5-w*.5)*scale,ci=cim+(h*.5-py-.5)*scale,kind=n<iter?FIELD_ESCAPED:(proven(cr,ci)?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN);putField(field,py*w+px,n,m,kind);
|
||||||
|
if(n>=iter){out[oi++]=0;out[oi++]=0;out[oi++]=0;out[oi++]=255}
|
||||||
|
else{
|
||||||
|
putFastColor(out,oi,n,Math.max(4.0000001,m),iter,colorCtx);oi+=4;
|
||||||
|
}
|
||||||
|
}y+=rows;
|
||||||
|
}
|
||||||
|
if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'WebAssembly f64',w*h,field);
|
||||||
|
}requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
function renderWasm(profile,snap,w,h,iter,token,t0){
|
||||||
|
if(!ensureShallowWorker()||shallowWorkerBusy){renderWasmMain(profile,snap,w,h,iter,token,t0);return}
|
||||||
|
const worker=shallowWorker,out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),jobId='shallow:'+token+':'+Date.now(),rowsPerChunk=Math.min(h,Math.max(4,Math.floor(32768/Math.max(1,w)))),chunks=[];for(let y=0;y<h;y+=rowsPerChunk)chunks.push({y,rows:Math.min(rowsPerChunk,h-y)});chunks.sort((a,b)=>Math.abs((a.y+a.rows*.5)/h-state.focusY)-Math.abs((b.y+b.rows*.5)/h-state.focusY));let next=0;shallowWorkerBusy=true;
|
||||||
|
const dispatch=()=>{if(token!==state.token){shallowWorkerBusy=false;return}if(next>=chunks.length){shallowWorkerBusy=false;finishRender(token,t0,profile,snap,w,h,out,'WebAssembly '+(wasm.simd?'SIMD':'scalar')+' Worker',w*h,field);return}const chunk=chunks[next++],msg={type:'render',jobId,cre,cim,sp,w,h,y:chunk.y,rows:chunk.rows,iter},transfer=[];if(shallowRecycle){msg.fieldBuffer=shallowRecycle.field;msg.iterationBuffer=shallowRecycle.iterations;msg.classBuffer=shallowRecycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);shallowRecycle=null}worker.postMessage(msg,transfer)};
|
||||||
|
worker.onmessage=e=>{const d=e.data;if(!d||d.type==='ready')return;if(d.jobId!==jobId)return;if(d.error){shallowWorkerBusy=false;renderWasmMain(profile,snap,w,h,iter,token,t0);return}const sf=new Float32Array(d.field),it=new Uint32Array(d.iterations),cl=new Uint8Array(d.classes);if(token!==state.token){shallowRecycle={field:d.field,iterations:d.iterations,classes:d.classes};shallowWorkerBusy=false;return}const stripe=colorizeField({smooth:sf,classes:cl,iter}),dst=d.y*w;field.smooth.set(sf,dst);field.iterations.set(it,dst);field.classes.set(cl,dst);out.set(stripe,dst*4);presentPartialStripe(stripe,0,d.y,w,d.rows,w,h,snap);shallowRecycle={field:d.field,iterations:d.iterations,classes:d.classes};dispatch()};worker.onerror=()=>{shallowWorkerBusy=false;destroyShallowWorker();if(token===state.token)renderWasmMain(profile,snap,w,h,iter,token,t0)};dispatch()
|
||||||
|
}
|
||||||
|
function renderJsDouble(profile,snap,w,h,iter,token,t0){
|
||||||
|
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),colorCtx=makeColorCtx(iter),sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/w;let y=0;
|
||||||
|
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,qq=x*x+y2;if(qq*(qq+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
function slice(){if(token!==state.token)return;const deadline=performance.now()+(profile.covered?10:7);while(y<h&&performance.now()<deadline){let oi=y*w*4,ci=cim+(h*.5-y-.5)*scale,cr=cre+(.5-w*.5)*scale;for(let x=0;x<w;x++,cr+=scale){let zr=0,zi=0,zr2=0,zi2=0,n=0,inside=inBulbs(cr,ci);if(inside)n=iter;else{let oldr=0,oldi=0;while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++;if((n&63)===0){if(Math.abs(zr-oldr)+Math.abs(zi-oldi)<1e-15){n=iter;break}oldr=zr;oldi=zi}}}const mm=n>=iter?0:Math.max(4.0000001,zr2+zi2),kind=n<iter?FIELD_ESCAPED:(inside?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN);putField(field,y*w+x,n,mm,kind);if(n>=iter){out[oi++]=0;out[oi++]=0;out[oi++]=0;out[oi++]=255}else{putFastColor(out,oi,n,mm,iter,colorCtx);oi+=4}}y++}if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'JavaScript f64',w*h,field);}requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
function spanMantBucket(span,bits){
|
||||||
|
const l=log2FixedAt(span,bits);let bucket=Math.round(l/256),mant=Math.pow(2,l-bucket*256);return{mant,bucket};
|
||||||
|
}
|
||||||
|
function normalizeBucket(r,i,b){
|
||||||
|
let m=Math.max(Math.abs(r),Math.abs(i));if(!m)return[0,0,NEG_BUCKET];
|
||||||
|
while(m>HI128){r*=INV256;i*=INV256;b++;m*=INV256}
|
||||||
|
while(m<LO128){r*=POW256;i*=POW256;b--;m*=POW256}
|
||||||
|
return[r,i,b];
|
||||||
|
}
|
||||||
|
function scAdd(a,b){
|
||||||
|
if(a[2]===NEG_BUCKET)return b;if(b[2]===NEG_BUCKET)return a;
|
||||||
|
const eb=Math.max(a[2],b[2]);let r=0,i=0;
|
||||||
|
if(a[2]===eb){r+=a[0];i+=a[1]}else if(a[2]===eb-1){r+=a[0]*INV256;i+=a[1]*INV256}
|
||||||
|
if(b[2]===eb){r+=b[0];i+=b[1]}else if(b[2]===eb-1){r+=b[0]*INV256;i+=b[1]*INV256}
|
||||||
|
return normalizeBucket(r,i,eb);
|
||||||
|
}
|
||||||
|
function scLog2(a){return a[2]===NEG_BUCKET?-Infinity:Math.log2(Math.hypot(a[0],a[1]))+256*a[2]}
|
||||||
|
function fixedMantAtBucket(v,bits,bucket){
|
||||||
|
if(v===0n)return 0;const neg=v<0n;let a=neg?-v:v,bl=bitLen(a),take=Math.min(53,bl),sh=bl-take,top=Number(a>>BigInt(sh));
|
||||||
|
const exp=sh-bits-256*bucket;const n=top*Math.pow(2,exp);return neg?-n:n;
|
||||||
|
}
|
||||||
|
function fixedComplexScaled(r,i,bits){
|
||||||
|
if(r===0n&&i===0n)return[0,0,NEG_BUCKET];
|
||||||
|
const lr=r===0n?-Infinity:log2FixedAt(r,bits),li=i===0n?-Infinity:log2FixedAt(i,bits),bucket=Math.round(Math.max(lr,li)/256);
|
||||||
|
return normalizeBucket(fixedMantAtBucket(r,bits,bucket),fixedMantAtBucket(i,bits,bucket),bucket);
|
||||||
|
}
|
||||||
|
function pixelCenteredOffset(snap,refC,w){const half=roundDiv(snap.span,BigInt(2*Math.max(1,w)));return{r:snap.re-refC.re+half,i:snap.im-refC.im-half}}
|
||||||
|
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||||
|
function fixedPixelPoint(snap,w,h,x,y,bits=snap.bits){const den=BigInt(2*w),re=align(snap.re,snap.bits,bits),im=align(snap.im,snap.bits,bits),span=align(snap.span,snap.bits,bits);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||||
|
function fixedAnalyticInterior(cr,ci,bits){const S=1n<<BigInt(bits),X=cr-(S>>2n),Y=ci,Q=X*X+Y*Y;if(4n*Q*(Q+X*S)<=Y*Y*S*S)return true;const D=cr+S;return 16n*(D*D+Y*Y)<=S*S}
|
||||||
|
function fixedAnalyticPixelProven(snap,w,h,x,y){const p=fixedPixelPoint(snap,w,h,x,y),g=fixedPixelPoint(snap,w,h,x,y,snap.bits+64);return fixedAnalyticInterior(p[0],p[1],snap.bits)&&fixedAnalyticInterior(g[0],g[1],snap.bits+64)}
|
||||||
|
function roundShift(v,bits){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(bits)-1n),q=(a+half)>>BigInt(bits);return neg?-q:q}
|
||||||
|
// ── Arbitrary-precision reference orbit / perturbation setup ────────────
|
||||||
|
const referenceCache={id:0,bits:0,re:0n,im:0n,n:0,escape:0,zr:0n,zi:0n,rr:null,ri:null,series:null,version:0,loadedLen:0,derivative:[0,0,NEG_BUCKET],conditionLog2:0,checkpointVersion:0,checkpointBits:0,checkpointCount:0,checkpointMismatch:false};
|
||||||
|
function promoteReferenceCache(shift,oldBits){
|
||||||
|
const c=referenceCache;if(!c.rr||c.bits!==oldBits)return;
|
||||||
|
const sh=BigInt(shift);c.re<<=sh;c.im<<=sh;c.zr<<=sh;c.zi<<=sh;c.bits+=shift;
|
||||||
|
// rr/ri are normalized Float64 values, so neither they nor Worker-side copies
|
||||||
|
// need to change when only the fixed-point radix moves.
|
||||||
|
}
|
||||||
|
function sameReference(bits,re,im){
|
||||||
|
return referenceCache.rr&&referenceCache.bits===bits&&referenceCache.re===re&&referenceCache.im===im;
|
||||||
|
}
|
||||||
|
function resetReference(bits,re,im){
|
||||||
|
const c=referenceCache;c.id++;c.bits=bits;c.re=re;c.im=im;c.n=0;c.escape=0;c.zr=0n;c.zi=0n;c.derivative=[0,0,NEG_BUCKET];c.conditionLog2=0;
|
||||||
|
if(!c.rr){c.rr=new Float64Array(150001);c.ri=new Float64Array(150001)}
|
||||||
|
c.series=null;c.version=1;c.loadedLen=0;c.checkpointVersion=0;c.checkpointBits=0;c.checkpointCount=0;c.checkpointMismatch=false;
|
||||||
|
}
|
||||||
|
function invalidateReferenceOrbit(){const c=referenceCache;c.id++;c.bits=0;c.re=0n;c.im=0n;c.n=0;c.escape=0;c.zr=0n;c.zi=0n;c.rr=null;c.ri=null;c.series=null;c.version=0;c.loadedLen=0;c.derivative=[0,0,NEG_BUCKET];c.conditionLog2=0;c.checkpointVersion=0;c.checkpointBits=0;c.checkpointCount=0;c.checkpointMismatch=false;for(const worker of deepPool.workers){worker._refKey='';worker._refLoaded=0}}
|
||||||
|
function buildReferenceAt(bits,cRe,cIm,iter,token,done){
|
||||||
|
if(!sameReference(bits,cRe,cIm))resetReference(bits,cRe,cIm);
|
||||||
|
const c=referenceCache,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,startN=c.n,refT0=performance.now();
|
||||||
|
if((c.escape&&c.escape<=iter)||c.n>=iter){
|
||||||
|
const refLen=c.escape&&c.escape<=iter?c.escape:iter;done(c,refLen);return
|
||||||
|
}
|
||||||
|
function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+6.5;
|
||||||
|
while(c.n<iter&&!c.escape&&performance.now()<deadline){
|
||||||
|
c.rr[c.n]=fixedOrbitNum(c.zr,bits);c.ri[c.n]=fixedOrbitNum(c.zi,bits);const Rr=c.rr[c.n],Ri=c.ri[c.n],d=c.derivative,term=d[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(Rr*d[0]-Ri*d[1]),2*(Rr*d[1]+Ri*d[0]),d[2]);c.derivative=scAdd(term,[1,0,0]);c.conditionLog2=Math.max(c.conditionLog2,scLog2(c.derivative));
|
||||||
|
const zr2=roundShift(c.zr*c.zr,bits),zi2=roundShift(c.zi*c.zi,bits);c.zi=roundShift(2n*c.zr*c.zi,bits)+cIm;c.zr=zr2-zi2+cRe;c.n++;
|
||||||
|
const mag=roundShift(c.zr*c.zr,bits)+roundShift(c.zi*c.zi,bits);if(mag>BAIL)c.escape=c.n;
|
||||||
|
}
|
||||||
|
if(c.escape||c.n>=iter){
|
||||||
|
c.rr[c.n]=fixedOrbitNum(c.zr,bits);c.ri[c.n]=fixedOrbitNum(c.zi,bits);c.version++;if(c.n>startN){const ms=performance.now()-refT0;refControl.lastBuildMs=ms;refControl.buildEMA=refControl.buildEMA?refControl.buildEMA*.82+ms*.18:ms}
|
||||||
|
const refLen=c.escape&&c.escape<=iter?c.escape:iter;done(c,refLen)
|
||||||
|
}else requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
function verifyReferenceCheckpoints(ref,refLen,token,done){
|
||||||
|
const guardBits=ref.bits+64;
|
||||||
|
if(ref.checkpointVersion===ref.version&&ref.checkpointBits===guardBits&&!ref.checkpointMismatch){done(true);return}
|
||||||
|
const cRe=align(ref.re,ref.bits,guardBits),cIm=align(ref.im,ref.bits,guardBits),ONE=1n<<BigInt(guardBits),BAIL=16n*ONE;
|
||||||
|
const 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,n=0,escape=0,checked=0,mismatch=false;
|
||||||
|
function compare(){if(!targets.has(n)||n>refLen)return;checked++;const rr=fixedOrbitNum(zr,guardBits),ri=fixedOrbitNum(zi,guardBits);if(!Object.is(rr,ref.rr[n])||!Object.is(ri,ref.ri[n]))mismatch=true}
|
||||||
|
function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+6;compare();
|
||||||
|
while(n<refLen&&!escape&&!mismatch&&performance.now()<deadline){const zr2=roundShift(zr*zr,guardBits),zi2=roundShift(zi*zi,guardBits);zi=roundShift(2n*zr*zi,guardBits)+cIm;zr=zr2-zi2+cRe;n++;const mag=roundShift(zr*zr,guardBits)+roundShift(zi*zi,guardBits);if(mag>BAIL)escape=n;compare()}
|
||||||
|
if(mismatch||escape||n>=refLen){if((ref.escape||0)!==escape&&((ref.escape||0)<=refLen||escape<=refLen))mismatch=true;ref.checkpointVersion=ref.version;ref.checkpointBits=guardBits;ref.checkpointCount=checked;ref.checkpointMismatch=mismatch;done(!mismatch)}else requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function computeSeries(ref,refLen,logMaxDc){
|
||||||
|
// A series valid for a wider image remains valid after zooming further in.
|
||||||
|
if(ref.series&&logMaxDc<=ref.series.logMaxDc+.02&&ref.series.skip<refLen)return ref.series;
|
||||||
|
let A=[0,0,NEG_BUCKET],Bc=[0,0,NEG_BUCKET],bestSkip=0,bestA=[0,0,NEG_BUCKET],bestB=[0,0,NEG_BUCKET];
|
||||||
|
const LOG_LIMIT=Math.log2(2.2e-4),LOG_RATIO=Math.log2(.10);
|
||||||
|
for(let n=0;n<refLen;n++){
|
||||||
|
const R=ref.rr[n],I=ref.ri[n],oldA=A;
|
||||||
|
const at=oldA[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(R*oldA[0]-I*oldA[1]),2*(R*oldA[1]+I*oldA[0]),oldA[2]);
|
||||||
|
A=scAdd(at,[1,0,0]);
|
||||||
|
const bt=Bc[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(R*Bc[0]-I*Bc[1]),2*(R*Bc[1]+I*Bc[0]),Bc[2]);
|
||||||
|
const a2=oldA[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(oldA[0]*oldA[0]-oldA[1]*oldA[1],2*oldA[0]*oldA[1],2*oldA[2]);
|
||||||
|
Bc=scAdd(bt,a2);
|
||||||
|
const l1=scLog2(A)+logMaxDc,l2=scLog2(Bc)+2*logMaxDc;
|
||||||
|
if(Number.isFinite(l1)&&l1<LOG_LIMIT&&(!Number.isFinite(l2)||(l2<LOG_LIMIT&&l2<l1+LOG_RATIO))){bestSkip=n+1;bestA=A.slice();bestB=Bc.slice()}
|
||||||
|
}
|
||||||
|
if(bestSkip>=refLen)bestSkip=Math.max(0,refLen-1);
|
||||||
|
ref.series={logMaxDc,skip:bestSkip,Ar:bestA[0],Ai:bestA[1],Ab:bestA[2],Br:bestB[0],Bi:bestB[1],Bb:bestB[2]};
|
||||||
|
return ref.series;
|
||||||
|
}
|
||||||
|
function chooseReference(snap){
|
||||||
|
const c=referenceCache;if(c.rr&&c.n>8){const rr=align(c.re,c.bits,snap.bits),ri=align(c.im,c.bits,snap.bits),dx=fixedRatio(rr-snap.re,snap.span),dy=fixedRatio(ri-snap.im,snap.span),dist=Math.hypot(dx,dy);if(Number.isFinite(dist)){const hard=2.25;if(dist<=hard){let keep=true;if(dist>.45&&refControl.refId===c.id&&refControl.baseMPP>0&&refControl.lastMPP>refControl.baseMPP*1.34&&performance.now()-refControl.lastRecenterAt>refControl.cooldownMs){const extra=(refControl.lastMPP-refControl.baseMPP)*Math.max(1,refControl.lastPixels),build=Math.max(5,refControl.buildEMA||18);if(extra>build*1.65)keep=false}if(keep)return{re:rr,im:ri,reused:true,dist};refControl.lastRecenterAt=performance.now()}}}return{re:snap.re,im:snap.im,reused:false,dist:0}
|
||||||
|
}
|
||||||
|
function putDeepPixel(out,w,computeIter,colorIter,x,y,n,m,colorCtx){
|
||||||
|
const oi=(y*w+x)*4,mm=n>=computeIter?0:Math.max(4.0000001,m);
|
||||||
|
|
||||||
|
if(n>=computeIter){out[oi]=0;out[oi+1]=0;out[oi+2]=0;out[oi+3]=255;return}
|
||||||
|
putFastColor(out,oi,n,mm,colorIter,colorCtx);
|
||||||
|
}
|
||||||
|
function directOrbitState(cr,ci,bits,iter){return{cr,ci,bits,iter,zr:0n,zi:0n,mag:0n,n:0,done:false,four:4n*(1n<<BigInt(bits))}}
|
||||||
|
function stepDirectOrbit(orbit,deadline){let batch=0;while(!orbit.done){const zr2=roundShift(orbit.zr*orbit.zr,orbit.bits),zi2=roundShift(orbit.zi*orbit.zi,orbit.bits);orbit.mag=zr2+zi2;if(orbit.mag>orbit.four||orbit.n>=orbit.iter){orbit.done=true;break}orbit.zi=roundShift(2n*orbit.zr*orbit.zi,orbit.bits)+orbit.ci;orbit.zr=zr2-zi2+orbit.cr;orbit.n++;if((++batch&31)===0&&performance.now()>=deadline)break}if(orbit.n>=orbit.iter)orbit.done=true}
|
||||||
|
async function highPrecisionDirectPixelAsync(snap,w,h,iter,x,y,guarded=true,cancelled=()=>false){const p=fixedPixelPoint(snap,w,h,x,y),base=directOrbitState(p[0],p[1],snap.bits,iter);let guard=null;if(guarded){const bits=snap.bits+64,g=fixedPixelPoint(snap,w,h,x,y,bits);guard=directOrbitState(g[0],g[1],bits,iter)}while(!base.done||(guard&&!guard.done)){if(cancelled())return null;const deadline=performance.now()+6;stepDirectOrbit(base,deadline);if(guard)stepDirectOrbit(guard,deadline);if(!base.done||(guard&&!guard.done))await new Promise(requestAnimationFrame)}if(guard&&(guard.n!==base.n||((guard.n<iter)!==(base.n<iter))))return[iter,0];const chosen=guard||base;return[chosen.n,chosen.n<iter?Math.max(4.0000001,fixedOrbitNum(chosen.mag,chosen.bits)):0]}
|
||||||
|
function renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0){
|
||||||
|
const cap=profile.covered?w:390,scale=Math.min(1,cap/w);if(scale<1){h=Math.max(100,Math.round(h*scale));w=Math.max(160,Math.round(w*scale))}
|
||||||
|
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,colorIter),colorCtx=makeColorCtx(colorIter);let y=0;
|
||||||
|
state.lastEngine='BigInt direct fallback';
|
||||||
|
async function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+7;
|
||||||
|
while(y<h&&performance.now()<deadline){const y0=y;for(let x=0;x<w;x++){const result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,y,false,()=>token!==state.token);if(!result)return;const[n,m]=result;putField(field,y*w+x,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,y,n,m,colorCtx)}presentPartialStripe(out.subarray(y0*w*4,(y0+1)*w*4),0,y0,w,1,w,h,snap);y++}
|
||||||
|
if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'BigInt direct fallback',w*h,field);
|
||||||
|
}requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
function prepareDeepContext(snap,refC,iter,token,done){
|
||||||
|
const probeW=64,probeH=Math.max(28,Math.round(probeW*canvas.clientHeight/Math.max(1,canvas.clientWidth))),refX=probeW*.5-.5+fixedRatio(refC.re-snap.re,snap.span)*probeW,refY=probeH*.5-.5-fixedRatio(refC.im-snap.im,snap.span)*probeW,cornerR=Math.max(Math.hypot(refX,refY),Math.hypot(probeW-refX,refY),Math.hypot(refX,probeH-refY),Math.hypot(probeW-refX,probeH-refY))/Math.max(1,probeW),logMaxDc=log2FixedAt(snap.span,snap.bits)+Math.log2(Math.max(1e-300,cornerR));
|
||||||
|
buildReferenceAt(snap.bits,refC.re,refC.im,iter,token,(ref,refLen)=>{if(token!==state.token)return;if(ensureOrbitPrecision(iter,Math.max(1,canvas.width))){state.dirty=true;invalidateView();return}const finish=()=>done({ref,refLen,series:computeSeries(ref,refLen,logMaxDc)});if(state.processMode!=='validate'){finish();return}verifyReferenceCheckpoints(ref,refLen,token,ok=>{if(token!==state.token)return;if(ok){finish();return}promoteState(32);invalidateReferenceOrbit();state.dirty=true;invalidateView()})})
|
||||||
|
}
|
||||||
|
function renderPerturbPrepared(profile,snap,w,h,iter,colorIter,token,t0,refC,ref,refLen,series,reuse=null){
|
||||||
|
const workerReady=ensureDeepPool();if(!workerReady&&(!ensureDeepWasm()||!deepWasm.ex.render_perturb_rebase_rect)){renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0);return}
|
||||||
|
const centered=pixelCenteredOffset(snap,refC,w),out=reuse?reuse.out:new Uint8ClampedArray(w*h*4),field=makeField(w*h,colorIter),colorCtx=makeColorCtx(colorIter),sb=spanMantBucket(snap.span,snap.bits),off=fixedComplexScaled(centered.r,centered.i,snap.bits);
|
||||||
|
const fallback=(strict=false)=>{if(token!==state.token)return;if(!ensureDeepWasm()||!deepWasm.ex.render_perturb_rebase_rect){renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0);return}if(ref.loadedLen<refLen+1){deepWasm.refsR().set(ref.rr.subarray(ref.loadedLen,refLen+1),ref.loadedLen);deepWasm.refsI().set(ref.ri.subarray(ref.loadedLen,refLen+1),ref.loadedLen);ref.loadedLen=refLen+1}const counts=deepWasm.counts(),mags=deepWasm.mags(),bad=[];let y=0;function rows(){if(token!==state.token)return;const deadline=performance.now()+(profile.covered?11:8);while(y<h&&performance.now()<deadline){const rows=Math.min(Math.max(1,Math.floor(65536/Math.max(1,w))),h-y),y0=y,npx=deepWasm.ex.render_perturb_rebase_rect(sb.mant,sb.bucket,off[0],off[1],off[2],refLen,w,h,w*.5,h*.5,0,y,w,rows,iter,strict?0:series.skip,strict?0:series.Ar,strict?0:series.Ai,strict?0:series.Ab,strict?0:series.Br,strict?0:series.Bi,strict?0:series.Bb);for(let i=0;i<npx;i++){const x=i%w,py=y0+((i/w)|0),n=counts[i],m=mags[i];if(n===0xffffffff){bad.push(py*w+x);continue}putField(field,py*w+x,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}y+=rows}if(y<h)requestAnimationFrame(rows);else if(bad.length)runResidual();else finish()}function runResidual(){let k=0;async function slice(){if(token!==state.token)return;const deadline=performance.now()+6;while(k<bad.length&&performance.now()<deadline){const idx=bad[k++],x=idx%w,py=(idx/w)|0,result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,py,false,()=>token!==state.token);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}if(k<bad.length)requestAnimationFrame(slice);else finish()}requestAnimationFrame(slice)}function finish(){state.lastEngine='WASM '+(deepWasm.simd?'SIMD':'scalar')+' · '+(strict?'strict ':'')+'main-thread rebase · skip '+(strict?0:series.skip);finishRender(token,t0,profile,snap,w,h,out,state.lastEngine,w*h,field)}requestAnimationFrame(rows)};
|
||||||
|
// Never abort and restart an in-flight frame because a budget estimate was wrong.
|
||||||
|
// Finish it once and feed the measured cost into the next frame.
|
||||||
|
if(!workerReady||!runDeepPool(profile,snap,w,h,iter,colorIter,token,t0,out,sb,refC,off,ref,refLen,series,fallback,reuse?reuse.rects:null))fallback();
|
||||||
|
}
|
||||||
|
async function renderDeepAdaptive(profile,snap,plan,token,t0){
|
||||||
|
try{await prepareDeepModules()}catch{if(token===state.token){const [fallbackW,fallbackH]=targetSize(profile,true);renderDeepDirect(profile,snap,fallbackW,fallbackH,plan.colorIter,plan.colorIter,token,t0)}return}if(token!==state.token)return;
|
||||||
|
const iter=plan.colorIter,colorIter=plan.colorIter,[w,h]=targetSize(profile,true),reuse=buildPanReuse(snap,w,h,iter,profile);
|
||||||
|
// A zoom-in can be represented entirely by the previous frame. Commit that preview
|
||||||
|
// immediately; the normal idle HQ pass performs the exact full-resolution render.
|
||||||
|
if(reuse&&reuse.rects.length===0&&!profile.covered){finishRender(token,t0,profile,snap,w,h,reuse.out,'既存画像再利用 100%',0);return}
|
||||||
|
const refC=chooseReference(snap);state.lastEngine='参照軌道を準備中…';prepareDeepContext(snap,refC,iter,token,ctx=>{if(token!==state.token)return;renderPerturbPrepared(profile,snap,w,h,iter,colorIter,token,t0,refC,ctx.ref,ctx.refLen,ctx.series,reuse)})
|
||||||
|
}
|
||||||
|
function render(pass=RENDER_PASS.PREVIEW){
|
||||||
|
const profile=renderProfile(pass);
|
||||||
|
if(pass===RENDER_PASS.COVERED){state.fieldView=null;trimDetailCache()}
|
||||||
|
runtimeMetrics.renderStarts++;if(state.pointerActive||state.wheelActive)runtimeMetrics.renderStartsDuringGesture++;
|
||||||
|
let snap=snapshot(),deep=deepEngineNeeded(snap);if(!deep&&state.adaptivePixelBudget){state.adaptivePixelBudget=0;resize()}if(deep&&ensureOrbitPrecision(maxIter()))snap=snapshot();const plan=iterationPlan(profile,deep),token=++state.token,t0=performance.now();state.rendering=true;state.drawState=profile.covered?'COVERING':'PREVIEW';state.lastPass=profile.id;state.lastEngine=deep?'参照軌道を準備中…':(wasm?('WebAssembly '+(wasm.simd?'SIMD':'scalar')):'JavaScript f64');
|
||||||
|
if(!deep){const ratio=deepResolutionRatio(snap);if(ratio<=128)prewarmDeepAssets();else if(deepPool.workers.length||deepWasm||deepModuleBundle||referenceCache.rr)retireDeepAssets()}
|
||||||
|
invalidateStats();
|
||||||
|
if(deep)renderDeepAdaptive(profile,snap,plan,token,t0);else{const [w,h]=targetSize(profile,false),reuse=buildPanReuse(snap,w,h,plan.computeIter,profile);if(reuse)renderShallowReuse(profile,snap,w,h,plan.computeIter,token,t0,reuse);else if(wasm)renderWasm(profile,snap,w,h,plan.computeIter,token,t0);else renderJsDouble(profile,snap,w,h,plan.computeIter,token,t0)}
|
||||||
|
}
|
||||||
|
function screenPixelBudget(){
|
||||||
|
const lowMemory=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(state.processMode==='power')return 1*1048576;if(state.processMode==='fine'||state.processMode==='validate')return(lowMemory||small?4:8)*1048576;
|
||||||
|
const base=(lowMemory||small?2:4)*1048576;return state.adaptivePixelBudget?Math.min(base,state.adaptivePixelBudget):base
|
||||||
|
}
|
||||||
|
function adaptStandardDeepBudget(deep){if(state.processMode!=='standard'||!deep)return false;const measuredMPP=renderPerf.deepMPP||.03,target=Math.max(32768,Math.min(4*1048576,Math.round(1400/measuredMPP))),oldBudget=state.adaptivePixelBudget||screenPixelBudget();if(target/oldBudget>=.8&&target/oldBudget<=1.25)return false;const oldPixels=canvas.width*canvas.height;state.adaptivePixelBudget=target;resize();return canvas.width*canvas.height!==oldPixels}
|
||||||
|
function resize(){
|
||||||
|
const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=screenPixelBudget(),nativeDpr=Math.max(1,window.devicePixelRatio||1),budgetDpr=Math.sqrt(budget/Math.max(1,cssW*cssH)),minDpr=Math.min(1,64/Math.max(cssW,cssH)),dpr=Math.max(minDpr,Math.min(nativeDpr,budgetDpr)),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){clearPanReuse();canvas.width=w;canvas.height=h;trimDetailCache();state.dirty=true;invalidateView()}
|
||||||
|
}
|
||||||
|
function jaEngine(s){return String(s||'').replace('preparing cached rebase reference…','参照軌道を準備中…').replace('persistent workers','常駐Worker').replace('main-thread','メインスレッド').replace('strict','厳密').replace('fallback','フォールバック').replace('rebase','リベース').replace('skip','スキップ').replace('residual','残差').replace('starting','起動中')}
|
||||||
|
function updateStats(){
|
||||||
|
runtimeMetrics.domWrites++;
|
||||||
|
const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';
|
||||||
|
$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();const deep=deepEngineNeeded(),diagnosticProfile=RENDER_PROFILE[RENDER_PASS.COVERED],ip=iterationPlan(diagnosticProfile,deep);$('#engine').textContent=deep?'高精度深部':'標準精度';$('#render').textContent=state.rendering?'描画中…':(state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了');let status;if(state.drawState==='REPROJECTED')status='再投影';else if(state.drawState==='PREVIEW')status=state.rendering?'プレビュー描画中':'プレビュー';else if(state.drawState==='COVERING')status='全域描画中';else if(state.drawState==='RESOLVING')status='未確定を追加計算 '+Math.round(state.continuationProgress*100)+'%';else if(state.drawState==='REFINING')status='境界AA '+state.detailDone+'/'+state.detailQueued;else if(state.drawState==='REFINED')status='境界AA 完了';else if(state.drawState==='VALIDATING')status='精度照合 '+Math.round(state.coverage*100)+'%';else if(state.drawState==='VALIDATION_INCOMPLETE')status='検証未完了';else if(state.drawState==='VALIDATED')status='検証完了';else status='全域描画 完了';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.effectiveDpr<(devicePixelRatio||1)*.99)status+=' · '+canvas.width+'×'+canvas.height;$('#badge').textContent=status;$('#compactStatus').textContent=status;const ledger=memoryLedger(),bp=blaProfile(diagnosticProfile);$('#diagEngine').textContent='engine: '+jaEngine(state.lastEngine)+' | workers '+deepPool.workers.length+' | '+(wasm&&wasm.simd?'SIMD':'scalar/JS');$('#diagNumeric').textContent='numeric: '+state.bits+' bit | iter '+ip.colorIter+' | BLA ε 2^-'+bp.exp+' | condition '+Math.round(referenceCache.conditionLog2||0)+' bit';$('#diagMemory').textContent='memory: managed '+(ledger.managedBytes/1048576).toFixed(1)+' / '+(ledger.budget/1048576).toFixed(0)+' MiB | canvas est. '+(ledger.canvasBytes/1048576).toFixed(1)+' MiB';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Scheduler / interaction loop ────────────────────────────────────────
|
||||||
|
function nextQualityDue(deep){
|
||||||
|
if(state.processMode==='power'||state.dirty||state.rendering||state.pointerActive||state.wheelActive)return Infinity;
|
||||||
|
if(deep&&state.lastPass!==RENDER_PASS.COVERED)return Math.max(state.lastInteraction+680,state.lastFrameDone+260);
|
||||||
|
if(!deep&&state.lastPass!==RENDER_PASS.COVERED)return Math.max(state.lastInteraction+520,state.lastFrameDone+280);
|
||||||
|
return Infinity
|
||||||
|
}
|
||||||
|
function loop(now){
|
||||||
|
schedulerRAF=0;if(document.hidden)return;
|
||||||
|
const deep=deepEngineNeeded();
|
||||||
|
if(!state.pointerActive&&!state.wheelActive&&!state.rendering){
|
||||||
|
if(state.dirty)render(RENDER_PASS.PREVIEW);
|
||||||
|
else{
|
||||||
|
const due=nextQualityDue(deep);
|
||||||
|
if(due<=now)render(RENDER_PASS.COVERED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(needsPaint){needsPaint=false;paintFrame()}
|
||||||
|
if(needsStats){needsStats=false;updateStats()}
|
||||||
|
if(!state.rendering&&!state.pointerActive&&!state.wheelActive&&!state.dirty){
|
||||||
|
const due=nextQualityDue(deep);if(Number.isFinite(due))requestScheduler(Math.max(1,due-performance.now()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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){state.wheelActive=true;cancelDetailRefinement(true);cancelRender();capturePanSource()}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(wheelSettleTimer);wheelSettleTimer=setTimeout(()=>{wheelSettleTimer=0;state.wheelActive=false;state.lastInteraction=performance.now();state.dirty=true;recordView();saveHash(false);invalidateView()},110)},{passive:false});
|
||||||
|
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(pts.size===0){clearTimeout(pointerSettleTimer);pointerSettleTimer=0;clearTimeout(wheelSettleTimer);wheelSettleTimer=0;state.wheelActive=false;state.pointerActive=true;cancelDetailRefinement(true);cancelRender();capturePanSource()}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else if(pts.size===2){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 end(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(pointerSettleTimer);pointerSettleTimer=setTimeout(()=>{pointerSettleTimer=0;state.pointerActive=false;state.lastInteraction=performance.now();state.dirty=true;recordView();saveHash(false);invalidateView()},90)}canvas.addEventListener('pointerup',end);canvas.addEventListener('pointercancel',end);
|
||||||
|
function applyUiVisibility(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',String(!state.uiHidden))}
|
||||||
|
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;applyUiVisibility();try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}};
|
||||||
|
$('#zin').onclick=()=>{capturePanSource();zoomAt(innerWidth/2,innerHeight/2,.5);recordView();saveHash(false)};$('#zout').onclick=()=>{capturePanSource();zoomAt(innerWidth/2,innerHeight/2,2);recordView();saveHash(false)};$('#reset').onclick=()=>{reset();recordView()};
|
||||||
|
const exportJob={active:false,cancelled:false,bytes:0};let exportForcePrecision=false;
|
||||||
|
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1500)}
|
||||||
|
function exportLedgerBytes(){return memoryLedger().logicalBytes}
|
||||||
|
function exportSampleShallow(cre,cim,scale,W,H,iter,x,y){const cr=cre+(x+.5-W*.5)*scale,ci=cim+(H*.5-y-.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0;while(n<iter&&zr2+zi2<=4){zi=2*zr*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}return[n,n<iter?Math.max(4.0000001,zr2+zi2):0]}
|
||||||
|
async function runExport(){if(exportJob.active)return;const scaleChoice=Number($('#exportScale').value),w=Math.max(64,Math.min(16384,Math.round(scaleChoice?canvas.width*scaleChoice:Number($('#exportWidth').value)||canvas.width))),h=Math.max(1,Math.round(w*canvas.height/Math.max(1,canvas.width))),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),needed=w*h*8,budget=rendererMemoryBudget(),available=Math.max(0,budget-exportLedgerBytes());if(w>16384||h>16384){$('#exportStatus').textContent='辺の長さは 16384px 以下にしてください。';return}if(needed>available){$('#exportStatus').textContent='メモリ予算を超えます。幅または倍率を下げてください(必要 '+Math.ceil(needed/1048576)+' MiB / 空き '+Math.floor(available/1048576)+' MiB)。';return}const outCanvas=document.createElement('canvas');outCanvas.width=w;outCanvas.height=h;const oc=outCanvas.getContext('2d',{alpha:false});if(!oc){$('#exportStatus').textContent='出力 Canvas を作成できません。';return}const snap=snapshot(),iter=maxIter(),deep=deepEngineNeeded(snap,w),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),W=w*ss,H=h*ss,pixelScale=sp/W,colorCtx=makeColorCtx(iter),tmp=new Uint8ClampedArray(4),tiles=[],tileW=exportForcePrecision?4:(deep?16:128),tileH=exportForcePrecision?1:(deep?4:24);for(let y=0;y<h;y+=tileH)for(let x=0;x<w;x+=tileW)tiles.push({x,y,w:Math.min(tileW,w-x),h:Math.min(tileH,h-y)});exportJob.active=true;exportJob.cancelled=false;exportJob.bytes=needed;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;$('#exportStatus').textContent='タイル描画中…';let done=0,unresolvedSamples=0;try{for(const tile of tiles){if(exportJob.cancelled)throw new Error('cancelled');const id=oc.createImageData(tile.w,tile.h),data=id.data;for(let yy=0;yy<tile.h;yy++)for(let xx=0;xx<tile.w;xx++){let ar=0,ag=0,ab=0;for(let sy=0;sy<ss;sy++)for(let sx=0;sx<ss;sx++){const gx=(tile.x+xx)*ss+sx,gy=(tile.y+yy)*ss+sy,sample=deep?await highPrecisionDirectPixelAsync(snap,W,H,iter,gx,gy,exportForcePrecision,()=>exportJob.cancelled):exportSampleShallow(cre,cim,pixelScale,W,H,iter,gx,gy);if(!sample)throw new Error('cancelled');const[n,m]=sample;if(n<iter){putFastColor(tmp,0,n,m,iter,colorCtx);ar+=linearChannel(tmp[0]);ag+=linearChannel(tmp[1]);ab+=linearChannel(tmp[2])}else{ar+=linearChannel(20);ag+=linearChannel(22);ab+=linearChannel(30);unresolvedSamples++}}const samples=ss*ss,oi=(yy*tile.w+xx)*4;data[oi]=srgbChannel(ar/samples);data[oi+1]=srgbChannel(ag/samples);data[oi+2]=srgbChannel(ab/samples);data[oi+3]=255}oc.putImageData(id,tile.x,tile.y);done++;$('#exportProgress').value=done/tiles.length;$('#exportStatus').textContent='生成中 '+Math.round(done/tiles.length*100)+'%';await new Promise(requestAnimationFrame)}if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGを圧縮中…';const blob=await new Promise(resolve=>outCanvas.toBlob(resolve,'image/png'));if(!blob)throw new Error('PNG encode failed');const stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v23',rendererVersion:23,pixelContract:'centered',width:w,height:h,supersampling:ss,sampleCount:w*h*ss*ss,unresolvedSamples,membershipCertified:false,precisionPolicy:{mode:exportForcePrecision?'validated-direct':'balanced',baseBits:snap.bits,agreementGuardBits:exportForcePrecision?64:0},iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},numericEngine:deep?'bigint-fixed-direct':'javascript-f64-direct',kernelSha256:globalThis.MANDEL_KERNEL_META||null,colorSpace:'sRGB with linear-light sample resolve',encoder:{mime:'image/png',api:'HTMLCanvasElement.toBlob'},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},determinism:{randomSeed:null,tiled:true,allTilesCompleted:true}};downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');$('#exportStatus').textContent='PNG と座標メタデータを保存しました。'}catch(e){$('#exportStatus').textContent=String(e&&e.message)==='cancelled'?'出力を中止しました。':'出力に失敗しました: '+String(e&&e.message||e)}finally{exportJob.active=false;exportJob.bytes=0;$('#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','')};
|
||||||
|
$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(!blob)return;downloadBlob(blob,'mandelbrot-'+state.drawState.toLowerCase()+'-'+Date.now()+'.png');$('#exportStatus').textContent='現在の表示('+state.drawState+')を保存しました。'},'image/png');
|
||||||
|
$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(Math.min(16384,canvas.width*s))};$('#exportStart').onclick=async()=>{exportForcePrecision=$('#exportPrecision').value==='validated';try{await runExport()}finally{exportForcePrecision=false}};$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};
|
||||||
|
function toast(s){const t=$('#toast');t.textContent=s;t.classList.add('show');clearTimeout(toast._t);toast._t=setTimeout(()=>t.classList.remove('show'),1500)}
|
||||||
|
let lastWrittenHash='',navigationHash='';
|
||||||
|
const viewHistory=[];let viewHistoryIndex=-1;
|
||||||
|
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(),key=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===key)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;state.bits=v.bits;state.re=v.re;state.im=v.im;state.span=v.span;state.palette=v.palette;state.cycle=v.cycle;state.shift=v.shift;state.baseIter=v.baseIter;state.adaptive=v.adaptive;clearDetailCache();clearPanReuse();ensurePrecision();syncControls();saveHash(false);setDirty()}
|
||||||
|
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
|
||||||
|
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
|
||||||
|
function saveHash(push){const p=new URLSearchParams();p.set('v','23');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{if(location.protocol==='file:'||location.origin==='null'){if(location.hash!==h)location.hash=h}else{push?history.pushState(null,'',h):history.replaceState(null,'',h)}}catch{}}
|
||||||
|
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 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(Math.round(state.baseIter));$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
|
||||||
|
function applyHashNavigation(){const h=location.hash;if(h===lastWrittenHash){lastWrittenHash='';return}if(h===navigationHash)return;navigationHash=h;setTimeout(()=>{navigationHash=''},0);if(loadHash()){clearDetailCache();clearPanReuse();recordView();syncControls();setDirty()}}
|
||||||
|
$('#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);setDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
|
||||||
|
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:23,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()}};
|
||||||
|
function bind(id,key,out,fmt,flush=false){const el=$(id),o=$(out),apply=()=>{state[key]=Number(el.value);o.textContent=fmt(Number(el.value));if(flush)clearDetailCache();setDirty()};el.addEventListener('input',apply);apply()}
|
||||||
|
function bindColor(id,key,out,fmt){const el=$(id),o=$(out),apply=()=>{state[key]=Number(el.value);o.textContent=fmt(Number(el.value));if(!recolorCurrentField())setDirty()};el.addEventListener('input',apply);apply()}
|
||||||
|
bind('#iters','baseIter','#itersO',x=>String(Math.round(x)),true);bindColor('#cycle','cycle','#cycleO',x=>x.toFixed(4));bindColor('#shift','shift','#shiftO',x=>x.toFixed(2));
|
||||||
|
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));if(!recolorCurrentField())setDirty()};
|
||||||
|
$('#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';state.adaptivePixelBudget=0;$('#hq').checked=state.hq;try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}resize();setDirty()};
|
||||||
|
$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;clearDetailCache();setDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;if(!state.hq)cancelDetailRefinement(true);setDirty()};
|
||||||
|
addEventListener('resize',()=>{resize();setDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let handled=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'){capturePanSource();pan(innerWidth*.08,0)}else if(e.key==='ArrowRight'){capturePanSource();pan(-innerWidth*.08,0)}else if(e.key==='ArrowUp'){capturePanSource();pan(0,innerHeight*.08)}else if(e.key==='ArrowDown'){capturePanSource();pan(0,-innerHeight*.08)}else handled=false;if(handled){e.preventDefault();recordView();saveHash(false)}});addEventListener('popstate',applyHashNavigation);addEventListener('hashchange',applyHashNavigation);
|
||||||
|
function stopSchedulers(){if(schedulerRAF){cancelAnimationFrame(schedulerRAF);schedulerRAF=0}if(schedulerTimer){clearTimeout(schedulerTimer);schedulerTimer=0;schedulerDue=0}if(wheelSettleTimer){clearTimeout(wheelSettleTimer);wheelSettleTimer=0}if(pointerSettleTimer){clearTimeout(pointerSettleTimer);pointerSettleTimer=0}cancelUnknownContinuation()}
|
||||||
|
addEventListener('visibilitychange',()=>{if(document.hidden){exportJob.cancelled=true;stopSchedulers();state.wheelActive=false;cancelRender();cancelDetailRefinement(true)}else{state.lastInteraction=performance.now();state.dirty=true;invalidateView()}});
|
||||||
|
globalThis.__MANDEL_DIAG__={snapshot:()=>{const deep=deepEngineNeeded(),ledger=memoryLedger(),coveredProfile=RENDER_PROFILE[RENDER_PASS.COVERED];return{rendererVersion:23,pixelContract:'centered',processMode:state.processMode,automaticTarget:modeTarget(),drawState:state.drawState,coverage:state.coverage,unresolved:state.unresolved,zoom:zoomExp(),deep,legacyDeepThreshold:LEGACY_DEEP_ZOOM_THRESHOLD,bits:state.bits,palette:state.palette,rendering:state.rendering,validating:validationJob.active,exporting:exportJob.active,lastRender:state.lastRender,lastPass:state.lastPass,transformReuse:!!state.panReuse,detailCache:DETAIL_TILE_CACHE.size,detailCacheBytes,detailCacheBudget:detailCacheBudget(),detailActive:state.detailActive,frame:frameCanvas.width+'x'+frameCanvas.height,frameCoverage:frameCanvas.width/Math.max(1,canvas.width),hqTarget:targetSize(coveredProfile,deep).join('x'),scheduler:{raf:!!schedulerRAF,timer:!!schedulerTimer,needsPaint,needsStats,wheelActive:state.wheelActive,pointerSettle:!!pointerSettleTimer,unknownTimer:!!unknownContinuationTimer,backgroundJobs:runtimeMetrics.pendingBackgroundJobs},screen:{effectiveDpr:state.effectiveDpr,deviceDpr:window.devicePixelRatio||1,pixelBudget:state.screenPixelBudget,pixels:canvas.width*canvas.height},memory:ledger,shallowAssets:{wasmReady:!!wasm,worker:!!shallowWorker,workerBusy:shallowWorkerBusy},deepAssets:{wasmReady:!!deepWasm,workers:deepPool.workers.length},runtimeMetrics:{...runtimeMetrics},telemetry:{...deepTelemetry},renderPerf:{...renderPerf},wisdom:{workerCount:deepWisdom.workerCount,rowMsEMA:deepWisdom.rowMsEMA,stripRows:deepWisdom.stripRows,realBench:deepWisdom.realBench},reference:{id:referenceCache.id,bits:referenceCache.bits,n:referenceCache.n,escape:referenceCache.escape,buildMs:refControl.lastBuildMs,buildEMA:refControl.buildEMA,baseMPP:refControl.baseMPP,lastMPP:refControl.lastMPP,conditionLog2:referenceCache.conditionLog2,checkpointBits:referenceCache.checkpointBits,checkpointCount:referenceCache.checkpointCount,checkpointMismatch:referenceCache.checkpointMismatch}}},memoryLedger:()=>memoryLedger()};
|
||||||
|
addEventListener('pagehide',()=>{stopSchedulers();destroyShallowWorker();destroyDeepPool()},{once:true});
|
||||||
|
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const savedMode=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(savedMode)){state.processMode=savedMode;state.hq=savedMode==='fine'||savedMode==='validate'}}catch{}restoreDeepWisdom();applyUiVisibility();resize();if(!loadHash())reset();else setDirty();recordView();syncControls();ctx.fillStyle='#050813';ctx.fillRect(0,0,canvas.width,canvas.height);render(RENDER_PASS.PREVIEW);requestScheduler();
|
||||||
|
})();
|
||||||
BIN
dist/wasm/bla-scalar.4f06913487044823.wasm
vendored
Normal file
BIN
dist/wasm/bla-scalar.4f06913487044823.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/bla-scalar.wasm
vendored
Normal file
BIN
dist/wasm/bla-scalar.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/bla-simd.f80f136e9fb676ce.wasm
vendored
Normal file
BIN
dist/wasm/bla-simd.f80f136e9fb676ce.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/bla-simd.wasm
vendored
Normal file
BIN
dist/wasm/bla-simd.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/color-scalar.aa2914ec1acacaa2.wasm
vendored
Normal file
BIN
dist/wasm/color-scalar.aa2914ec1acacaa2.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/color-scalar.wasm
vendored
Normal file
BIN
dist/wasm/color-scalar.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/color-simd.8cf83374ed0c680b.wasm
vendored
Normal file
BIN
dist/wasm/color-simd.8cf83374ed0c680b.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/color-simd.wasm
vendored
Normal file
BIN
dist/wasm/color-simd.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/deep-scalar.d133bbecc8f6dbdf.wasm
vendored
Normal file
BIN
dist/wasm/deep-scalar.d133bbecc8f6dbdf.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/deep-scalar.wasm
vendored
Normal file
BIN
dist/wasm/deep-scalar.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/deep-simd.d49385f06e4a8f55.wasm
vendored
Normal file
BIN
dist/wasm/deep-simd.d49385f06e4a8f55.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/deep-simd.wasm
vendored
Normal file
BIN
dist/wasm/deep-simd.wasm
vendored
Normal file
Binary file not shown.
55
dist/wasm/manifest.json
vendored
Normal file
55
dist/wasm/manifest.json
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"format": "mandelbrot-wasm-manifest-v1",
|
||||||
|
"generatedUtc": "2026-08-22T10:48:30.5652322Z",
|
||||||
|
"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
Normal file
BIN
dist/wasm/wasm-scalar.d19b26c04e1b2f59.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/wasm-scalar.wasm
vendored
Normal file
BIN
dist/wasm/wasm-scalar.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm
vendored
Normal file
BIN
dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm
vendored
Normal file
Binary file not shown.
BIN
dist/wasm/wasm-simd.wasm
vendored
Normal file
BIN
dist/wasm/wasm-simd.wasm
vendored
Normal file
Binary file not shown.
11
hosted-headers.txt
Normal file
11
hosted-headers.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
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
|
||||||
36
hosted-loader.js
Normal file
36
hosted-loader.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
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);
|
||||||
69
index.html
Normal file
69
index.html
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||||
|
<meta name="theme-color" content="#050813">
|
||||||
|
<title>Mandelbrot Deep Zoom v23</title>
|
||||||
|
<style>
|
||||||
|
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
|
||||||
|
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
|
||||||
|
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
|
||||||
|
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
|
||||||
|
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
|
||||||
|
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
|
||||||
|
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
|
||||||
|
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}#palette{color:#111;background:#f4f5f8}#palette option{color:#111;background:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
|
||||||
|
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
|
||||||
|
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.exact-grid{display:grid;grid-template-columns:54px 1fr;gap:6px;margin-top:8px}.exact-grid input{min-width:0;width:100%;min-height:44px;border:1px solid var(--line);border-radius:8px;background:#070c19;color:var(--text);padding:6px;font:11px ui-monospace,monospace}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
|
||||||
|
.diagnostics{margin-top:8px;color:var(--muted);font:10.5px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.exact-grid input{user-select:text;-webkit-user-select:text}
|
||||||
|
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
|
||||||
|
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
|
||||||
|
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}
|
||||||
|
#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
|
||||||
|
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
|
||||||
|
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
|
||||||
|
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
|
||||||
|
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord">…</span></div><div class="row"><span class="muted">倍率</span> <span id="zoom">…</span> <span class="muted">表示幅</span> <span id="span">…</span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render">…</span></div></div></div>
|
||||||
|
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
|
||||||
|
<div id="controls" class="panel">
|
||||||
|
<div class="toolbar"><button id="zin" aria-label="中心を拡大">+</button><button id="zout" aria-label="中心を縮小">−</button><button id="reset">リセット</button><button id="png">出力</button></div>
|
||||||
|
<div class="group">
|
||||||
|
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">精度優先</option></select><output></output></div>
|
||||||
|
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
|
||||||
|
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0.001" max="0.05" step="0.0005" value="0.008"><output id="cycleO" for="cycle">0.0080</output></div>
|
||||||
|
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
|
||||||
|
<details><summary>詳細設定・正確な座標</summary>
|
||||||
|
<div class="line"><label for="iters">基準反復</label><input id="iters" type="range" min="100" max="2500" step="25" value="350"><output id="itersO" for="iters">350</output></div>
|
||||||
|
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> 境界AAを追加</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></details>
|
||||||
|
</div>
|
||||||
|
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
|
||||||
|
<div class="hint">ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。</div>
|
||||||
|
</div>
|
||||||
|
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI−</button>
|
||||||
|
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||||
|
<dialog id="exportDialog" aria-labelledby="exportTitle">
|
||||||
|
<h2 id="exportTitle">高解像度 PNG 出力</h2>
|
||||||
|
<div class="export-grid">
|
||||||
|
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
|
||||||
|
<label for="exportWidth">幅(px)</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
|
||||||
|
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×(高速)</option><option value="2">2×2 AA</option></select>
|
||||||
|
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="validated">Validated direct</option></select>
|
||||||
|
</div>
|
||||||
|
<progress id="exportProgress" max="1" value="0" hidden></progress>
|
||||||
|
<div id="exportStatus" role="status" aria-live="polite"></div>
|
||||||
|
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
|
||||||
|
</dialog>
|
||||||
|
<script src="kernels.js"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
kernels.js
Normal file
22
kernels.js
Normal file
File diff suppressed because one or more lines are too long
912
script.js
Normal file
912
script.js
Normal file
|
|
@ -0,0 +1,912 @@
|
||||||
|
(()=>{'use strict';
|
||||||
|
const K=globalThis.MANDEL_KERNELS;
|
||||||
|
if(!K)throw new Error('kernels.js が読み込まれていません');
|
||||||
|
const {WASM_SIMD_B64,WASM_SCALAR_B64,DEEP_SIMD_B64,DEEP_SCALAR_B64,BLA_SIMD_B64,BLA_SCALAR_B64,COLOR_SIMD_B64,COLOR_SCALAR_B64}=K;
|
||||||
|
const $=s=>document.querySelector(s);
|
||||||
|
const canvas=$('#view');
|
||||||
|
const ctx=canvas.getContext('2d',{alpha:false,desynchronized:true})||canvas.getContext('2d',{alpha:false});
|
||||||
|
if(!ctx){document.body.innerHTML='<div style="padding:30px;color:white">Canvas 2Dを利用できません。</div>';return;}
|
||||||
|
|
||||||
|
const INITIAL_BITS=256;
|
||||||
|
const MIN_SPAN_BITS=224;
|
||||||
|
const TARGET_SPAN_BITS=240;
|
||||||
|
const RATIO_DEN=4503599627370496n; // 2^52
|
||||||
|
const POW256=1.157920892373162e77;
|
||||||
|
const INV256=8.636168555094445e-78;
|
||||||
|
const HI128=3.402823669209385e38;
|
||||||
|
const LO128=2.938735877055719e-39;
|
||||||
|
const NEG_BUCKET=-1000000000;
|
||||||
|
const LEGACY_DEEP_ZOOM_THRESHOLD=11.5;
|
||||||
|
const RENDER_PASS=Object.freeze({PREVIEW:'preview',COVERED:'covered'});
|
||||||
|
const MODE_TARGET=Object.freeze({power:'PREVIEW',standard:'COVERED',fine:'REFINED',validate:'VALIDATED'});
|
||||||
|
function modeTarget(mode=state.processMode){return MODE_TARGET[mode]||MODE_TARGET.standard}
|
||||||
|
const RENDER_PROFILE=Object.freeze({
|
||||||
|
[RENDER_PASS.PREVIEW]:Object.freeze({id:RENDER_PASS.PREVIEW,covered:false,budgetMs:110,nominalScale:.42,minWidth:360,maxWidth:900,densityLimit:1.55,blaSteps:1700,ptbSteps:2500}),
|
||||||
|
[RENDER_PASS.COVERED]:Object.freeze({id:RENDER_PASS.COVERED,covered:true,budgetMs:1050,nominalScale:1,minWidth:0,maxWidth:Infinity,densityLimit:1,blaSteps:0,ptbSteps:0})
|
||||||
|
});
|
||||||
|
function renderProfile(pass){return RENDER_PROFILE[pass]||RENDER_PROFILE[RENDER_PASS.PREVIEW]}
|
||||||
|
let deepMode=false;
|
||||||
|
|
||||||
|
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,lastRender:0,lastEngine:'起動中',lastPass:null,lastInteraction:performance.now(),dirty:true,
|
||||||
|
uiHidden:false,frameView:null,fieldView:null,lastFrameDone:0,
|
||||||
|
detailGeneration:0,detailActive:false,detailQueued:0,detailDone:0,
|
||||||
|
drawState:'REPROJECTED',coverage:0,unresolved:0,
|
||||||
|
precisionPending:false,
|
||||||
|
pointerActive:false,wheelActive:false,panReuse:null,effectiveDpr:1,screenPixelBudget:0,adaptivePixelBudget:0,continuationProgress:0,focusX:.5,focusY:.5
|
||||||
|
};
|
||||||
|
|
||||||
|
// Deep telemetry is collected from the actual BLA/perturbation
|
||||||
|
// kernel rather than from a synthetic JavaScript loop.
|
||||||
|
const deepTelemetry={
|
||||||
|
frames:0,kernelMPP:0,meanIter:0,blackRatio:0,badRatio:0,blaBuildEMA:0,
|
||||||
|
refDistance:0,interiorRatio:0,repairRatio:0,unresolvedRatio:0,
|
||||||
|
blaStepsPerPixel:0,ptbStepsPerPixel:0,rebasePerPixel:0,lastPilotMs:0,lastPilotMPP:0,lastKernelMs:0,lastVerifyMs:0,lastPixels:0
|
||||||
|
};
|
||||||
|
const refControl={buildEMA:18,lastBuildMs:0,lastRecenterAt:0,refId:0,baseMPP:0,lastMPP:0,lastPixels:0,cooldownMs:900};
|
||||||
|
|
||||||
|
// Adaptive quality controller: instead of tying a quality level to a fixed
|
||||||
|
// pixel width, learn the recent cost per pixel and spend a bounded amount of time.
|
||||||
|
const renderPerf={deepMPP:0,shallowMPP:0};
|
||||||
|
const runtimeMetrics={canvasWrites:0,domWrites:0,renderStarts:0,renderStartsDuringGesture:0,longTasks:0,maxLongTaskMs:0,pendingBackgroundJobs:0};
|
||||||
|
try{if('PerformanceObserver'in globalThis){const observer=new PerformanceObserver(list=>{for(const entry of list.getEntries()){runtimeMetrics.longTasks++;runtimeMetrics.maxLongTaskMs=Math.max(runtimeMetrics.maxLongTaskMs,entry.duration||0)}});observer.observe({type:'longtask',buffered:true})}}catch{}
|
||||||
|
function scheduleBackground(fn,delay=0){runtimeMetrics.pendingBackgroundJobs++;return setTimeout(()=>{runtimeMetrics.pendingBackgroundJobs=Math.max(0,runtimeMetrics.pendingBackgroundJobs-1);fn()},delay)}
|
||||||
|
function scheduleIdle(fn,timeout=350){runtimeMetrics.pendingBackgroundJobs++;const run=()=>{runtimeMetrics.pendingBackgroundJobs=Math.max(0,runtimeMetrics.pendingBackgroundJobs-1);fn()};return'requestIdleCallback'in window?requestIdleCallback(run,{timeout}):setTimeout(run,Math.min(80,timeout))}
|
||||||
|
const DETAIL_TILE_CACHE=new Map();
|
||||||
|
let detailCacheBytes=0;
|
||||||
|
const activeDetailTiles=[];
|
||||||
|
let detailPlan=null;
|
||||||
|
let schedulerRAF=0,schedulerTimer=0,schedulerDue=0,needsPaint=true,needsStats=true,wheelSettleTimer=0,pointerSettleTimer=0;
|
||||||
|
|
||||||
|
function requestScheduler(delay=0){
|
||||||
|
if(document.hidden)return;
|
||||||
|
if(delay>0){
|
||||||
|
const due=performance.now()+delay;
|
||||||
|
if(schedulerTimer&&schedulerDue<=due)return;
|
||||||
|
if(schedulerTimer)clearTimeout(schedulerTimer);
|
||||||
|
schedulerDue=due;
|
||||||
|
schedulerTimer=setTimeout(()=>{schedulerTimer=0;schedulerDue=0;requestScheduler()},Math.max(0,Math.ceil(delay)));
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if(schedulerTimer){clearTimeout(schedulerTimer);schedulerTimer=0;schedulerDue=0}
|
||||||
|
if(!schedulerRAF)schedulerRAF=requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
function invalidateView(withStats=true){needsPaint=true;if(withStats)needsStats=true;requestScheduler()}
|
||||||
|
function invalidateStats(){needsStats=true;requestScheduler()}
|
||||||
|
|
||||||
|
// Device-specific runtime wisdom. It is populated by a small background CPU
|
||||||
|
// concurrency benchmark and then refined by real deep-render strip timings.
|
||||||
|
const deepWisdom={
|
||||||
|
ready:false,running:false,maxWorkers:1,workerCount:1,
|
||||||
|
targetStripMs:12,stripRows:24,rowMsEMA:0,
|
||||||
|
realBench:false
|
||||||
|
};
|
||||||
|
function deepWisdomStorageKey(){
|
||||||
|
const meta=globalThis.MANDEL_KERNEL_META||{},kernel=String(meta.BLA_SIMD_B64||meta.BLA_SCALAR_B64||'embedded').slice(0,16),hc=Math.max(1,navigator.hardwareConcurrency||1),dm=Number(navigator.deviceMemory||0);return'mandelbrot.wisdom.v23.'+[kernel,hc,dm].join('.')
|
||||||
|
}
|
||||||
|
function restoreDeepWisdom(){
|
||||||
|
try{const saved=JSON.parse(localStorage.getItem(deepWisdomStorageKey())||'null');if(!saved||saved.version!==23)return;const row=Number(saved.rowMsEMA),rows=Number(saved.stripRows),workers=Number(saved.workerCount);if(Number.isFinite(row)&&row>0&&row<1000)deepWisdom.rowMsEMA=row;if(Number.isInteger(rows)&&rows>=2&&rows<=128)deepWisdom.stripRows=rows;if(Number.isInteger(workers)&&workers>=1&&workers<=deepWorkerLimit())deepWisdom.workerCount=workers;deepWisdom.ready=deepWisdom.rowMsEMA>0}catch{}
|
||||||
|
}
|
||||||
|
function persistDeepWisdom(){
|
||||||
|
if(!deepWisdom.rowMsEMA)return;try{localStorage.setItem(deepWisdomStorageKey(),JSON.stringify({version:23,rowMsEMA:deepWisdom.rowMsEMA,stripRows:deepWisdom.stripRows,workerCount:deepWisdom.workerCount}))}catch{}
|
||||||
|
}
|
||||||
|
|
||||||
|
function one(bits=state.bits){return 1n<<BigInt(bits)}
|
||||||
|
function fromFrac(n,d=1n){return n*one()/d}
|
||||||
|
function fromDec(s){
|
||||||
|
s=String(s).trim();let neg=s[0]==='-';if(neg)s=s.slice(1);
|
||||||
|
const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0;
|
||||||
|
const a=mant.split('.'),i=a[0]||'0',f=a[1]||'';
|
||||||
|
let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',decPlaces=f.length-exp;
|
||||||
|
if(decPlaces<0){digits+='0'.repeat(-decPlaces);decPlaces=0}
|
||||||
|
const den=10n**BigInt(decPlaces),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,places=Math.max(0,f-e);return Math.max(64,Math.ceil(places*Math.log2(10))+32)}
|
||||||
|
function bitLen(n){n=n<0n?-n:n;return n===0n?0:n.toString(2).length}
|
||||||
|
function fixedNum(v,bits=state.bits){
|
||||||
|
if(v===0n)return 0;let 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;
|
||||||
|
}
|
||||||
|
// Hot-path conversion for orbit values known to stay O(1).
|
||||||
|
function fixedOrbitNum(v,bits){
|
||||||
|
if(v===0n)return 0;
|
||||||
|
const sh=bits-54;
|
||||||
|
if(sh>0)return Number(v>>BigInt(sh))*Math.pow(2,-54);
|
||||||
|
return Number(v)*Math.pow(2,-bits);
|
||||||
|
}
|
||||||
|
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 zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
|
||||||
|
function fixedRatio(a,b){
|
||||||
|
if(b===0n)return 0;if(a===0n)return 0;const neg=a<0n;if(neg)a=-a;
|
||||||
|
const q=(a<<52n)/b;const v=Number(q)/4503599627370496;return neg?-v:v;
|
||||||
|
}
|
||||||
|
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
|
||||||
|
function promoteState(shift){
|
||||||
|
// Precision promotion used to invalidate the reference orbit. Keep the same
|
||||||
|
// mathematical values by shifting their fixed-point representation instead.
|
||||||
|
// If a render is in flight, logically cancel it before mutating shared cache state.
|
||||||
|
if(state.rendering)cancelRender();
|
||||||
|
const oldBits=state.bits,s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;
|
||||||
|
if(state.frameView){state.frameView.re<<=s;state.frameView.im<<=s;state.frameView.span<<=s;state.frameView.bits+=shift}
|
||||||
|
state.bits+=shift;
|
||||||
|
promoteReferenceCache(shift,oldBits);
|
||||||
|
}
|
||||||
|
function ensurePrecision(){
|
||||||
|
const bl=bitLen(state.span);
|
||||||
|
if(bl>=MIN_SPAN_BITS)return false;
|
||||||
|
// Defer representation-only precision promotion until the current frame finishes.
|
||||||
|
if(state.rendering){state.precisionPending=true;return false}
|
||||||
|
state.precisionPending=false;promoteState(TARGET_SPAN_BITS-bl);return true
|
||||||
|
}
|
||||||
|
function flushPendingPrecision(){
|
||||||
|
if(state.rendering||!state.precisionPending)return false;state.precisionPending=false;
|
||||||
|
const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS){promoteState(TARGET_SPAN_BITS-bl);return true}return false
|
||||||
|
}
|
||||||
|
function mulRatio(v,factor){
|
||||||
|
const n=BigInt(Math.max(1,Math.round(factor*Number(RATIO_DEN))));return v*n/RATIO_DEN;
|
||||||
|
}
|
||||||
|
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 d=state.bits,q=v*(10n**BigInt(d))>>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 fmtSpan(){const l=log2Fixed(state.span);if(!Number.isFinite(l))return'0';const e=Math.floor(l/Math.log2(10)),m=Math.pow(2,l-e*Math.log2(10));return m.toFixed(4)+'e'+e}
|
||||||
|
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp();if(z<12)return Math.min(12000,Math.round(state.baseIter+22*Math.sqrt(z)*Math.log2(2+z)));return Math.min(140000,Math.round(state.baseIter+150*z+260*Math.log2(2+z)))}
|
||||||
|
function ensureOrbitPrecision(iter,width=Math.max(1,canvas.width)){const conditionBits=referenceCache.rr?Math.max(0,Math.min(256,Math.ceil(referenceCache.conditionLog2||0))):0,required=160+conditionBits,available=bitLen(state.span)-Math.ceil(Math.log2(width))-Math.ceil(Math.log2(Math.max(2,iter)));if(available>=required)return false;promoteState(required+32-available);invalidateReferenceOrbit();return true}
|
||||||
|
function f64Ulp(x){x=Math.abs(x);if(!Number.isFinite(x))return Infinity;if(x===0)return Number.MIN_VALUE;return Math.pow(2,Math.floor(Math.log2(x))-52)}
|
||||||
|
function deepResolutionRatio(snap=snapshot(),width=Math.max(1,canvas.width)){const step=Math.abs(fixedNum(snap.span,snap.bits))/Math.max(1,width),ulp=Math.max(f64Ulp(fixedNum(snap.re,snap.bits)),f64Ulp(fixedNum(snap.im,snap.bits)));return step===0||!Number.isFinite(step)?0:step/Math.max(Number.MIN_VALUE,ulp)}
|
||||||
|
function deepEngineNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){if(exportForcePrecision)return true;const ratio=deepResolutionRatio(snap,width),orbitRisk=ratio<=128&&(deepTelemetry.badRatio>1e-4||deepTelemetry.unresolvedRatio>2e-3||deepTelemetry.repairRatio>.08);deepMode=orbitRisk||(deepMode?ratio<64:ratio<=32);return deepMode}
|
||||||
|
function iterationPlan(profile,deep){
|
||||||
|
const colorIter=maxIter();return{colorIter,computeIter:colorIter};
|
||||||
|
}
|
||||||
|
function workProfile(profile){return{bla:profile.blaSteps,ptb:profile.ptbSteps}}
|
||||||
|
function cancelRender(){state.token++;state.rendering=false;cancelDeepPoolJob();flushPendingPrecision()}
|
||||||
|
function cancelDetailRefinement(clearCurrent=true){
|
||||||
|
state.detailGeneration++;state.detailActive=false;state.detailQueued=0;state.detailDone=0;detailPlan=null;
|
||||||
|
if(clearCurrent)activeDetailTiles.length=0;
|
||||||
|
}
|
||||||
|
function clearDetailCache(){DETAIL_TILE_CACHE.clear();detailCacheBytes=0;activeDetailTiles.length=0;cancelDetailRefinement(false)}
|
||||||
|
function setDirty(cancel=true){cancelUnknownContinuation();state.lastInteraction=performance.now();state.dirty=true;state.lastPass=null;state.drawState=state.frameView?'REPROJECTED':'PREVIEW';state.coverage=0;cancelDetailRefinement(true);if(cancel)cancelRender();invalidateView()}
|
||||||
|
function reset(){
|
||||||
|
clearDetailCache();clearPanReuse();state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);setDirty();saveHash(false)
|
||||||
|
}
|
||||||
|
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){
|
||||||
|
if(!state.panReuse)capturePanSource();
|
||||||
|
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);
|
||||||
|
const 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();setDirty(!(state.pointerActive||state.wheelActive));
|
||||||
|
}
|
||||||
|
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*100000))/BigInt(Math.round(w*100000));
|
||||||
|
const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));
|
||||||
|
state.im+=ys*BigInt(Math.round(dy*100000))/BigInt(Math.round(h*100000));setDirty(!(state.pointerActive||state.wheelActive));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function clearPanReuse(){state.panReuse=null}
|
||||||
|
function capturePanSource(){
|
||||||
|
if(!state.frameView||!frameCanvas.width||!frameCanvas.height)return false;
|
||||||
|
const fv=state.frameView;state.panReuse={canvas:frameCanvas,snap:{bits:fv.bits,re:fv.re,im:fv.im,span:fv.span},style:styleSignature(),pass:state.lastPass,iter:maxIter(),created:performance.now()};return true
|
||||||
|
}
|
||||||
|
function buildPanReuse(snap,w,h,iter,profile){
|
||||||
|
const src=state.panReuse;if(!src||!src.canvas||src.style!==styleSignature())return null;
|
||||||
|
// Final HQ must recompute after a real scale change. Interactive/base passes may
|
||||||
|
// reproject a much larger range so wheel/pinch zoom can reuse existing pixels.
|
||||||
|
const re=align(src.snap.re,src.snap.bits,snap.bits),im=align(src.snap.im,src.snap.bits,snap.bits),sp=align(src.snap.span,src.snap.bits,snap.bits),scale=fixedRatio(sp,snap.span);
|
||||||
|
if(!Number.isFinite(scale)||scale<.22||scale>4.5)return null;
|
||||||
|
const scaleChange=Math.abs(Math.log2(Math.max(1e-12,scale)));
|
||||||
|
// Covered is a sampling contract, not a presentation shortcut. Reprojection
|
||||||
|
// is allowed only for Preview; the target grid is always recomputed in full.
|
||||||
|
if(profile.covered)return null;
|
||||||
|
const dx=fixedRatio(re-snap.re,snap.span)*w,dy=-fixedRatio(im-snap.im,snap.span)*w,move=Math.hypot(dx/Math.max(1,w),dy/Math.max(1,w));if(!Number.isFinite(move)||move>1.35)return null;
|
||||||
|
const dw=w*scale,dh=w*scale*(src.canvas.height/Math.max(1,src.canvas.width)),density=dw/Math.max(1,src.canvas.width);
|
||||||
|
// A zoomed preview may stretch old pixels temporarily, but the idle HQ pass will
|
||||||
|
// recompute it. Keep the stretch bounded so interaction never becomes misleading.
|
||||||
|
if(density>profile.densityLimit)return null;
|
||||||
|
const x0=w*.5+dx-dw*.5,y0=h*.5+dy-dh*.5,x1=x0+dw,y1=y0+dh;
|
||||||
|
let ix0=Math.max(0,Math.ceil(x0)+2),iy0=Math.max(0,Math.ceil(y0)+2),ix1=Math.min(w,Math.floor(x1)-2),iy1=Math.min(h,Math.floor(y1)-2);
|
||||||
|
if(ix1<=ix0||iy1<=iy0)return null;
|
||||||
|
const overlap=(ix1-ix0)*(iy1-iy0),frac=overlap/Math.max(1,w*h);if(frac<.20)return null;
|
||||||
|
const c=document.createElement('canvas');c.width=w;c.height=h;const cc=c.getContext('2d',{alpha:false});if(!cc)return null;cc.fillStyle='#050813';cc.fillRect(0,0,w,h);cc.imageSmoothingEnabled=true;try{cc.imageSmoothingQuality='high'}catch{};cc.drawImage(src.canvas,x0,y0,dw,dh);
|
||||||
|
const out=new Uint8ClampedArray(cc.getImageData(0,0,w,h).data),rects=[];
|
||||||
|
const add=(x,y,rw,rh)=>{x=Math.max(0,x|0);y=Math.max(0,y|0);rw=Math.min(w-x,rw|0);rh=Math.min(h-y,rh|0);if(rw>0&&rh>0)rects.push({x0:x,y0:y,w:rw,h:rh})};
|
||||||
|
add(0,0,w,iy0);add(0,iy1,w,h-iy1);add(0,iy0,ix0,iy1-iy0);add(ix1,iy0,w-ix1,iy1-iy0);
|
||||||
|
const exposed=rects.reduce((a,r)=>a+r.w*r.h,0);if(exposed>w*h*.80)return null;
|
||||||
|
return{out,rects,reusedPixels:w*h-exposed,exposedPixels:exposed,scale};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Palette / smooth colouring ───────────────────────────────────────────
|
||||||
|
const PALETTE_CURRENT=0,PALETTE_RAINBOW=1,PALETTE_MONO=2;
|
||||||
|
const FIELD_ESCAPED=1,FIELD_INTERIOR_PROVEN=2,FIELD_INTERIOR_LIKELY=3,FIELD_UNKNOWN=4;
|
||||||
|
const stops=[[0,[4,10,27]],[.11,[12,53,79]],[.25,[31,156,184]],[.38,[91,226,234]],[.50,[66,53,151]],[.62,[139,49,170]],[.73,[232,72,145]],[.84,[255,137,64]],[.93,[255,211,99]],[1,[255,250,223]]];
|
||||||
|
const COLOR_PHASES=2048,COLOR_MIXES=64;
|
||||||
|
function hsvRgb(h,sat,val){h=((h%1)+1)%1;const x=h*6,i=Math.floor(x),f=x-i,p=val*(1-sat),q=val*(1-sat*f),s=val*(1-sat*(1-f));let r,g,b;switch(i%6){case 0:r=val;g=s;b=p;break;case 1:r=q;g=val;b=p;break;case 2:r=p;g=val;b=s;break;case 3:r=p;g=q;b=val;break;case 4:r=s;g=p;b=val;break;default:r=val;g=p;b=q}return[r*255,g*255,b*255]}
|
||||||
|
function basePalette(kind,t){
|
||||||
|
t=((t%1)+1)%1;
|
||||||
|
if(kind===PALETTE_RAINBOW)return hsvRgb(t,.92,1);
|
||||||
|
if(kind===PALETTE_MONO){const g=22+233*(.5-.5*Math.cos(Math.PI*2*t));return[g,g,g]}
|
||||||
|
let a=stops[0],b=stops[stops.length-1];for(let j=1;j<stops.length;j++){if(t<=stops[j][0]){a=stops[j-1];b=stops[j];break}}
|
||||||
|
let f=(t-a[0])/(b[0]-a[0]||1);f=f*f*(3-2*f);return[a[1][0]+(b[1][0]-a[1][0])*f,a[1][1]+(b[1][1]-a[1][1])*f,a[1][2]+(b[1][2]-a[1][2])*f]
|
||||||
|
}
|
||||||
|
function buildColorLut(kind){const lut=new Uint8Array(COLOR_PHASES*3);for(let pi=0;pi<COLOR_PHASES;pi++){const u=pi/(COLOR_PHASES-1),t=kind===PALETTE_CURRENT?(u<=.5?u*2:2-u*2):u,c=basePalette(kind,t),k=pi*3;lut[k]=c[0]|0;lut[k+1]=c[1]|0;lut[k+2]=c[2]|0}return lut}
|
||||||
|
const COLOR_LUTS=[buildColorLut(PALETTE_CURRENT),buildColorLut(PALETTE_RAINBOW),buildColorLut(PALETTE_MONO)];
|
||||||
|
const SMOOTH_U_MIN=2,SMOOTH_U_STEP=1/128,SMOOTH_U_N=4097,SMOOTH_CORR=new Float32Array(SMOOTH_U_N);
|
||||||
|
for(let i=0;i<SMOOTH_U_N;i++){const u=SMOOTH_U_MIN+i*SMOOTH_U_STEP;SMOOTH_CORR[i]=1-Math.log2(.5*u)}
|
||||||
|
const COLOR_CTX_CACHE=new Map();
|
||||||
|
function makeColorCtx(iter){let hit=COLOR_CTX_CACHE.get(iter);if(hit)return hit;const mixBin=new Uint8Array(iter+1),den=Math.log1p(Math.max(8,iter));for(let n=0;n<=iter;n++){const edge=Math.max(0,Math.min(1,Math.log1p(n)/den));mixBin[n]=Math.min(COLOR_MIXES-1,Math.round((COLOR_MIXES-1)*Math.pow(edge,.38)))}hit={mixBin};COLOR_CTX_CACHE.set(iter,hit);if(COLOR_CTX_CACHE.size>8)COLOR_CTX_CACHE.delete(COLOR_CTX_CACHE.keys().next().value);return hit}
|
||||||
|
function smoothEscape(n,m){const u=Math.log2(Math.max(4.0000001,m));let corr;const fi=(u-SMOOTH_U_MIN)/SMOOTH_U_STEP;if(fi>=0&&fi<SMOOTH_U_N-1){const i=fi|0,f=fi-i;corr=SMOOTH_CORR[i]+(SMOOTH_CORR[i+1]-SMOOTH_CORR[i])*f}else corr=1-Math.log2(.5*u);return n+corr}
|
||||||
|
function putPaletteColor(out,oi,sm,n,iter,colorCtx){let phase=state.shift+sm*state.cycle;phase-=Math.floor(phase);const pi=Math.min(COLOR_PHASES-1,(phase*COLOR_PHASES)|0),mi=colorCtx.mixBin[Math.min(iter,n)],mix=.34+.66*(mi/(COLOR_MIXES-1)),kind=state.palette,lut=COLOR_LUTS[kind]||COLOR_LUTS[0],k=pi*3,f0=kind===PALETTE_MONO?8:2,f1=kind===PALETTE_MONO?8:5,f2=kind===PALETTE_MONO?8:15;out[oi]=(f0+(lut[k]-f0)*mix)|0;out[oi+1]=(f1+(lut[k+1]-f1)*mix)|0;out[oi+2]=(f2+(lut[k+2]-f2)*mix)|0;out[oi+3]=255}
|
||||||
|
function putFastColor(out,oi,n,m,iter,colorCtx){putPaletteColor(out,oi,smoothEscape(n,m),n,iter,colorCtx)}
|
||||||
|
function makeField(size,iter){return{smooth:new Float32Array(size),iterations:new Uint32Array(size),classes:new Uint8Array(size),confidence:new Uint8Array(size),iter}}
|
||||||
|
function fieldConfidence(kind){return kind===FIELD_ESCAPED||kind===FIELD_INTERIOR_PROVEN?255:kind===FIELD_INTERIOR_LIKELY?192:0}
|
||||||
|
function putField(field,index,n,m,kind){field.classes[index]=kind;if(field.iterations)field.iterations[index]=Math.max(0,n)>>>0;if(field.confidence)field.confidence[index]=fieldConfidence(kind);if(kind===FIELD_ESCAPED)field.smooth[index]=smoothEscape(n,m);else field.smooth[index]=NaN}
|
||||||
|
function fillFieldConfidence(field){if(!field.confidence)field.confidence=new Uint8Array(field.classes.length);for(let i=0;i<field.classes.length;i++)field.confidence[i]=fieldConfidence(field.classes[i])}
|
||||||
|
function colorizeField(field){const out=new Uint8ClampedArray(field.classes.length*4),colorCtx=makeColorCtx(field.iter);for(let i=0,oi=0;i<field.classes.length;i++,oi+=4){const kind=field.classes[i];if(kind!==FIELD_ESCAPED){const neutral=kind===FIELD_UNKNOWN;out[oi]=neutral?20:0;out[oi+1]=neutral?22:0;out[oi+2]=neutral?30:0;out[oi+3]=255;continue}const sm=field.smooth[i],n=Math.max(0,Math.min(field.iter,Math.floor(sm)));putPaletteColor(out,oi,sm,n,field.iter,colorCtx)}return out}
|
||||||
|
|
||||||
|
// Embedded WebAssembly: SIMD-optimized kernel with a scalar fallback.
|
||||||
|
|
||||||
|
|
||||||
|
let wasm=null;
|
||||||
|
function instantiateWasm(b64){
|
||||||
|
const module=b64 instanceof WebAssembly.Module?b64:(()=>{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return new WebAssembly.Module(bytes)})(),inst=new WebAssembly.Instance(module,{}),ex=inst.exports;
|
||||||
|
return {module,ex,counts:()=>new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:()=>new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),refsR:()=>new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001),refsI:()=>new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001)};
|
||||||
|
}
|
||||||
|
try{wasm=instantiateWasm(WASM_SIMD_B64);wasm.simd=globalThis.MANDEL_HOSTED_SHALLOW_SIMD!==false}catch(e){try{wasm=instantiateWasm(WASM_SCALAR_B64);wasm.simd=false}catch(_e){wasm=null}}
|
||||||
|
let shallowWorker=null,shallowWorkerUrl=null,shallowWorkerBusy=false,shallowRecycle=null;
|
||||||
|
function shallowWorkerSource(){return `'use strict';let ex=null;function smooth(n,m){const u=Math.log2(Math.max(4.0000001,m));return n+1-Math.log2(.5*u)}function likely(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}self.onmessage=e=>{const d=e.data;if(d.type==='init'){ex=new WebAssembly.Instance(d.module,{}).exports;postMessage({type:'ready'});return}if(d.type!=='render'||!ex)return;try{const scale=d.sp/d.w,npx=ex.render_rows(d.cre+scale*.5,d.cim-scale*.5,d.sp,d.w,d.h,d.y,d.rows,d.iter),counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),field=d.fieldBuffer&&d.fieldBuffer.byteLength>=npx*4?new Float32Array(d.fieldBuffer,0,npx):new Float32Array(npx),iterations=d.iterationBuffer&&d.iterationBuffer.byteLength>=npx*4?new Uint32Array(d.iterationBuffer,0,npx):new Uint32Array(npx),classes=d.classBuffer&&d.classBuffer.byteLength>=npx?new Uint8Array(d.classBuffer,0,npx):new Uint8Array(npx);for(let i=0;i<npx;i++){const n=counts[i],x=i%d.w,y=d.y+((i/d.w)|0),cr=d.cre+(x+.5-d.w*.5)*scale,ci=d.cim+(d.h*.5-y-.5)*scale;iterations[i]=n;if(n<d.iter){classes[i]=1;field[i]=smooth(n,mags[i])}else{classes[i]=likely(cr,ci)?3:4;field[i]=NaN}}postMessage({type:'render',jobId:d.jobId,y:d.y,rows:d.rows,field:field.buffer,iterations:iterations.buffer,classes:classes.buffer},[field.buffer,iterations.buffer,classes.buffer])}catch(error){postMessage({type:'render',jobId:d.jobId,error:String(error&&error.message||error)})}}`}
|
||||||
|
function ensureShallowWorker(){if(shallowWorker||!wasm||typeof Worker==='undefined'||typeof Blob==='undefined')return!!shallowWorker;try{shallowWorkerUrl=URL.createObjectURL(new Blob([shallowWorkerSource()],{type:'text/javascript'}));shallowWorker=new Worker(shallowWorkerUrl);shallowWorker.postMessage({type:'init',module:wasm.module});return true}catch{shallowWorker=null;return false}}
|
||||||
|
function destroyShallowWorker(){if(shallowWorker){try{shallowWorker.terminate()}catch{}shallowWorker=null}if(shallowWorkerUrl){try{URL.revokeObjectURL(shallowWorkerUrl)}catch{}shallowWorkerUrl=null}shallowWorkerBusy=false;shallowRecycle=null}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function instantiateDeepWasm(b64){
|
||||||
|
const module=b64 instanceof WebAssembly.Module?b64:(()=>{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return new WebAssembly.Module(bytes)})();
|
||||||
|
const ex=new WebAssembly.Instance(module,{}).exports;
|
||||||
|
return {ex,counts:()=>new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:()=>new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),refsR:()=>new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001),refsI:()=>new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001)};
|
||||||
|
}
|
||||||
|
let deepWasm=null,deepWasmTried=false,deepModuleBundle=null,deepModulePromise=null;
|
||||||
|
async function compileKernelSource(source){if(source instanceof WebAssembly.Module)return source;if(/^https?:/i.test(source)){const response=await fetch(source,{cache:'force-cache'});if(!response.ok)throw new Error('WASM request failed '+response.status);if(WebAssembly.compileStreaming){try{return await WebAssembly.compileStreaming(Promise.resolve(response.clone()))}catch{}}return WebAssembly.compile(await response.arrayBuffer())}const raw=atob(source),bytes=new Uint8Array(raw.length);for(let i=0;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return WebAssembly.compile(bytes)}
|
||||||
|
async function compileKernelPair(simd,scalar){try{return{module:await compileKernelSource(simd),simd:true}}catch{return{module:await compileKernelSource(scalar),simd:false}}}
|
||||||
|
function prepareDeepModules(){if(deepModuleBundle)return Promise.resolve(deepModuleBundle);if(!deepModulePromise)deepModulePromise=Promise.all([compileKernelPair(DEEP_SIMD_B64,DEEP_SCALAR_B64),compileKernelPair(BLA_SIMD_B64,BLA_SCALAR_B64),compileKernelPair(COLOR_SIMD_B64,COLOR_SCALAR_B64)]).then(([deep,bla,color])=>deepModuleBundle={deep,bla,color}).catch(error=>{deepModulePromise=null;throw error});return deepModulePromise}
|
||||||
|
function ensureDeepWasm(){
|
||||||
|
if(deepWasm)return true;if(deepWasmTried)return false;deepWasmTried=true;if(deepModuleBundle){try{deepWasm=instantiateDeepWasm(deepModuleBundle.deep.module);deepWasm.simd=deepModuleBundle.deep.simd}catch{deepWasm=null}return!!deepWasm}if(/^https?:/i.test(DEEP_SIMD_B64)||/^https?:/i.test(DEEP_SCALAR_B64))return false;
|
||||||
|
try{deepWasm=instantiateDeepWasm(DEEP_SIMD_B64);deepWasm.simd=true}catch(e){try{deepWasm=instantiateDeepWasm(DEEP_SCALAR_B64);deepWasm.simd=false}catch(_e){deepWasm=null}}
|
||||||
|
return!!deepWasm
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Deep renderer worker pool. Each worker owns a WASM instance and performs both the
|
||||||
|
// perturbation kernel and colour mapping, so colourful frames no longer funnel all
|
||||||
|
// post-processing through the UI thread. Jobs are striped dynamically for load balance.
|
||||||
|
const deepPool={workers:[],url:null,activeToken:0,serial:0,failed:false,current:null,maxWorkers:0,benchTimer:0};
|
||||||
|
// ── Persistent deep-render workers / BLA kernel ──────────────────────────
|
||||||
|
function deepWorkerSource(){
|
||||||
|
return `'use strict';
|
||||||
|
let core=null,blaCore=null,colorCore=null,refKey='',refLoaded=0,RR=new Float64Array(150001),RI=new Float64Array(150001),blaRefKey='',blaRefLoaded=0,blaBuiltKey='',lastBlaBuildMs=0;
|
||||||
|
function ensure(){if(!core)throw new Error('deep module not initialized');return core}
|
||||||
|
function ensureBla(){if(!blaCore)throw new Error('BLA module not initialized');return blaCore}
|
||||||
|
function ensureColor(){if(!colorCore)throw new Error('color module not initialized');return colorCore}
|
||||||
|
function applyRef(d,ex){if(d.refKey!==refKey){refKey=d.refKey;refLoaded=0;blaRefKey='';blaRefLoaded=0;blaBuiltKey=''}if(d.rr){const ar=new Float64Array(d.rr),ai=new Float64Array(d.ri),st=d.rrStart|0;RR.set(ar,st);RI.set(ai,st);new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ar,st);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ai,st);refLoaded=Math.max(refLoaded,st+ar.length);if(blaCore&&blaRefKey===refKey){const bx=blaCore.ex;new Float64Array(bx.memory.buffer,bx.refs_r_ptr(),150001).set(ar,st);new Float64Array(bx.memory.buffer,bx.refs_i_ptr(),150001).set(ai,st);blaRefLoaded=Math.max(blaRefLoaded,st+ar.length);blaBuiltKey=''}}if(refLoaded<d.refLen+1)throw new Error('reference cache miss')}
|
||||||
|
function syncRefsToBla(d,key=d.blaKey,eps=d.blaEps){if(!d.useBla)return null;const b=ensureBla(),ex=b.ex;if(blaRefKey!==refKey){new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(RR.subarray(0,refLoaded));new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(RI.subarray(0,refLoaded));blaRefKey=refKey;blaRefLoaded=refLoaded;blaBuiltKey=''}else if(refLoaded>blaRefLoaded){new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(RR.subarray(blaRefLoaded,refLoaded),blaRefLoaded);blaRefLoaded=refLoaded;blaBuiltKey=''}lastBlaBuildMs=0;if(blaBuiltKey!==key){const t=performance.now(),levels=ex.build_bla(d.refLen,d.cMax,eps);lastBlaBuildMs=performance.now()-t;if(!levels)return null;blaBuiltKey=key}return b}
|
||||||
|
function smoothBatch(srcMags,npx){const c=ensureColor(),ex=c.ex,mi=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),co=new Float32Array(ex.memory.buffer,ex.corr_ptr(),65536);mi.set(srcMags.subarray(0,npx),0);ex.smooth_batch(npx);return co}
|
||||||
|
function strictOne(ex,x,y,d){const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536);ex.render_perturb_rebase_rect(d.spanMant,d.spanBucket,d.offR,d.offI,d.offBucket,d.refLen,d.w,d.h,d.cx,d.cy,x,y,1,1,d.iter,0,0,0,0,0,0,0);return[counts[0],mags[0]]}
|
||||||
|
function stats(bx){return{blaSteps:bx.stat_bla_steps?bx.stat_bla_steps():0,ptbSteps:bx.stat_ptb_steps?bx.stat_ptb_steps():0,rebases:bx.stat_rebases?bx.stat_rebases():0,interior:bx.stat_interior?bx.stat_interior():0,unresolved:bx.stat_unresolved?bx.stat_unresolved():0,fail:bx.stat_fail?bx.stat_fail():0}}
|
||||||
|
function renderBla(d,b,key,eps,bcap,pcap){const bx=b.ex;if(key!==d.blaKey||eps!==d.blaEps)b=syncRefsToBla(d,key,eps)||b;const t=performance.now(),npx=bx.render_bla_rect_v2?bx.render_bla_rect_v2(d.spanNormal,d.offRn,d.offIn,d.baseCr,d.baseCi,d.refLen,d.w,d.h,d.x0||0,d.y0,d.rectW||d.w,d.rows,d.iter,bcap||0,pcap||0,1):bx.render_bla_rect(d.spanNormal,d.offRn,d.offIn,d.refLen,d.w,d.h,d.x0||0,d.y0,d.rectW||d.w,d.rows,d.iter);return{npx,kernelMs:performance.now()-t,counts:new Uint32Array(bx.memory.buffer,bx.counts_ptr(),65536),mags:new Float64Array(bx.memory.buffer,bx.mags_ptr(),65536),stats:stats(bx)}}
|
||||||
|
function verifyAgainstSafe(d,result,rw,npx,b){const want=d.verifySamples||0;if(!want||!b)return{samples:0,mismatch:0};const counts=Uint32Array.from(result.counts.subarray(0,npx)),mags=Float64Array.from(result.mags.subarray(0,npx));result.counts=counts;result.mags=mags;const picks=[],seen=new Set(),add=i=>{if(picks.length>=want)return;i=Math.max(0,Math.min(npx-1,i|0));if(!seen.has(i)){seen.add(i);picks.push(i)}};for(let k=0;k<Math.max(2,want>>1);k++)add(((k+.37)*npx/Math.max(2,want>>1))|0);const stride=Math.max(1,Math.floor(npx/Math.max(16,want*10))),top=[];for(let i=stride;i<npx;i+=stride){const n=counts[i],p=counts[i-stride];if(n<0xfffffffe&&p<0xfffffffe&&((n>=d.iter)!==(p>=d.iter))){add(i);add(i-stride)}if(n<d.iter&&n<0xfffffffe){top.push([n,i]);top.sort((a,b)=>b[0]-a[0]);if(top.length>want)top.length=want}}for(const x of top)add(x[1]);for(let k=0;picks.length<want&&k<want*2;k++)add(((k+.73)*npx/want)|0);let mismatch=0;const bad=[];for(const i of picks){const bn=counts[i];if(bn>=0xfffffffe)continue;const x=(d.x0||0)+(i%rw),y=d.y0+((i/rw)|0),sr=renderBla({...d,x0:x,y0:y,rectW:1,rows:1},b,d.safeBlaKey||d.blaKey,d.safeBlaEps||d.blaEps,0,0),sn=sr.counts[0];if(sn>=0xfffffffe)continue;if((bn>=d.iter)!==(sn>=d.iter)||Math.abs((bn|0)-(sn|0))>(d.verifyDelta||64)){mismatch++;bad.push(i)}}return{samples:picks.length,mismatch,bad}}
|
||||||
|
self.onmessage=async e=>{const d=e.data;if(!d)return;if(d.type==='init'){try{core={ex:new WebAssembly.Instance(d.modules.deep.module,{}).exports,simd:!!d.modules.deep.simd};blaCore={ex:new WebAssembly.Instance(d.modules.bla.module,{}).exports,simd:!!d.modules.bla.simd};colorCore={ex:new WebAssembly.Instance(d.modules.color.module,{}).exports,simd:!!d.modules.color.simd};postMessage({type:'ready'})}catch(error){postMessage({type:'ready',error:String(error&&error.message||error)})}return}if(d.type!=='render'&&d.type!=='pilot'&&d.type!=='realBench')return;try{const c=ensure(),ex=c.ex;applyRef(d,ex);const b=syncRefsToBla(d);if((d.type==='pilot'||d.type==='realBench')&&!b)throw new Error('BLA unavailable');if(d.type==='pilot'||d.type==='realBench'){const reps=d.type==='realBench'?Math.max(1,d.repeats|0):1;let rr=null,total=0;for(let k=0;k<reps;k++){rr=renderBla({...d,x0:0,y0:0,rectW:d.w,rows:d.h},b,d.blaKey,d.blaEps,0,0);total+=rr.kernelMs}if(d.type==='realBench'){postMessage({type:'realBench',benchId:d.benchId,kernelMs:total,pixels:rr.npx*reps,blaBuildMs:lastBlaBuildMs,simd:b.simd});return}const co=rr.counts;let black=0,bad=0,sum=0;for(let i=0;i<rr.npx;i++){const n=co[i];if(n>=0xfffffffe){bad++;continue}sum+=Math.min(d.iter,n);if(n>=d.iter)black++}postMessage({type:'pilot',pilotId:d.pilotId,kernelMs:total,pixels:rr.npx,blaBuildMs:lastBlaBuildMs,blackRatio:black/Math.max(1,rr.npx),badRatio:bad/Math.max(1,rr.npx),meanIter:sum/Math.max(1,rr.npx-bad),stats:rr.stats,simd:b.simd});return}
|
||||||
|
const rx=d.x0||0,rw=d.rectW||d.w,field=d.fieldBuffer&&d.fieldBuffer.byteLength>=rw*d.rows*4?new Float32Array(d.fieldBuffer,0,rw*d.rows):new Float32Array(rw*d.rows),iterations=d.iterationBuffer&&d.iterationBuffer.byteLength>=rw*d.rows*4?new Uint32Array(d.iterationBuffer,0,rw*d.rows):new Uint32Array(rw*d.rows),classes=d.classBuffer&&d.classBuffer.byteLength>=rw*d.rows?new Uint8Array(d.classBuffer,0,rw*d.rows):new Uint8Array(rw*d.rows),bad=[];let result,repaired=0,verifyMs=0;if(b){result=renderBla(d,b,d.blaKey,d.blaEps,d.maxBlaSteps||0,d.maxPtbSteps||0);let unresolved=result.stats.unresolved||0;if(unresolved&&d.covered){const co=result.counts,ma=result.mags;for(let i=0;i<result.npx;i++)if(co[i]===0xfffffffe){const x=rx+(i%rw),y=d.y0+((i/rw)|0),r=strictOne(ex,x,y,d);co[i]=r[0];ma[i]=r[1]}}
|
||||||
|
const tv=performance.now();let v=verifyAgainstSafe(d,result,rw,result.npx,b);verifyMs=performance.now()-tv;if(v.mismatch){const safeKey=d.safeBlaKey||d.blaKey,safeEps=d.safeBlaEps||d.blaEps;result=renderBla(d,b,safeKey,safeEps,0,0);repaired=1}
|
||||||
|
}else{const t=performance.now(),npx=ex.render_perturb_rebase_rect(d.spanMant,d.spanBucket,d.offR,d.offI,d.offBucket,d.refLen,d.w,d.h,d.cx,d.cy,rx,d.y0,rw,d.rows,d.iter,d.skip||0,d.Ar||0,d.Ai||0,d.Ab||0,d.Br||0,d.Bi||0,d.Bb||0);result={npx,kernelMs:performance.now()-t,counts:new Uint32Array(ex.memory.buffer,ex.counts_ptr(),65536),mags:new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),stats:{blaSteps:0,ptbSteps:0,rebases:0,interior:0,unresolved:0,fail:0}}}
|
||||||
|
const corr=smoothBatch(result.mags,result.npx);let blackCount=0,sumIter=0;for(let i=0;i<result.npx;i++){let n=result.counts[i],m=result.mags[i];const x=rx+(i%rw),y=d.y0+((i/rw)|0);if(n>=0xfffffffe){if(n===0xffffffff){const r=strictOne(ex,x,y,d);n=r[0];m=r[1]}else{n=d.iter;m=0}}iterations[i]=n;if(n===0xffffffff){classes[i]=4;field[i]=NaN;bad.push(i);continue}sumIter+=Math.min(d.iter,n);if(n>=d.iter){classes[i]=4;field[i]=NaN;blackCount++;continue}classes[i]=1;field[i]=n+corr[i]}
|
||||||
|
postMessage({type:'render',jobId:d.jobId,x0:rx,rectW:rw,y0:d.y0,rows:d.rows,field:field.buffer,iterations:iterations.buffer,classes:classes.buffer,bad,simd:c.simd,bla:!!b,blaSimd:b?b.simd:false,colorSimd:colorCore?colorCore.simd:false,kernelMs:result.kernelMs,verifyMs,blaBuildMs:lastBlaBuildMs,pixels:result.npx,blackCount,sumIter,stats:result.stats,verifySamples:d.verifySamples||0,repaired},[field.buffer,iterations.buffer,classes.buffer])}catch(err){postMessage({type:'render',jobId:d.jobId,error:String(err&&err.message||err)})}};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
function destroyDeepPool(){
|
||||||
|
if(deepPool.current)deepPool.current.cancelled=true;deepPool.current=null;
|
||||||
|
for(const w of deepPool.workers){try{w.terminate()}catch{}}
|
||||||
|
deepPool.workers.length=0;if(deepPool.url){try{URL.revokeObjectURL(deepPool.url)}catch{}deepPool.url=null}
|
||||||
|
deepPool.activeToken=0;deepPool.maxWorkers=0
|
||||||
|
}
|
||||||
|
function retireDeepAssets(){destroyDeepPool();deepPool.failed=false;deepWasm=null;deepWasmTried=false;deepModuleBundle=null;deepModulePromise=null;referenceCache.rr=null;referenceCache.ri=null;referenceCache.series=null;referenceCache.n=0;referenceCache.escape=0;referenceCache.loadedLen=0;referenceCache.conditionLog2=0;referenceCache.derivative=[0,0,NEG_BUCKET]}
|
||||||
|
function cancelDeepPoolJob(){
|
||||||
|
// Logical cancellation: keep Worker, WASM, reference and BLA caches alive.
|
||||||
|
deepPool.activeToken=0;
|
||||||
|
if(deepPool.current){deepPool.current.cancelled=true;deepPool.current=null}
|
||||||
|
}
|
||||||
|
function handleDeepWorkerMessage(worker,d){
|
||||||
|
const job=worker._job;worker._job=null;worker._busy=false;
|
||||||
|
if(d&&d.type==='ready'){if(d.error){worker._ready=false;deepPool.failed=true;destroyDeepPool();return}worker._ready=true;kickDeepWorker(worker);return}
|
||||||
|
if(!job){kickDeepWorker(worker);return}
|
||||||
|
if(job.type==='bench'){if(d&&d.type==='bench'&&d.benchId===job.benchId)job.resolve(d);else job.reject(new Error('benchmark reply mismatch'));kickDeepWorker(worker);return}
|
||||||
|
if(job.type==='pilot'||job.type==='realBench'){if(d&&((job.type==='pilot'&&d.type==='pilot'&&d.pilotId===job.id)||(job.type==='realBench'&&d.type==='realBench'&&d.benchId===job.id)))job.resolve(d);else job.reject(new Error(job.type+' reply mismatch'));kickDeepWorker(worker);return}
|
||||||
|
if(job.type==='render')job.runner.onResult(worker,d,job);
|
||||||
|
kickDeepWorker(worker)
|
||||||
|
}
|
||||||
|
function handleDeepWorkerError(worker,e){
|
||||||
|
const job=worker._job;worker._job=null;worker._busy=false;
|
||||||
|
if(job&&(job.type==='bench'||job.type==='pilot'||job.type==='realBench'))job.reject(e instanceof Error?e:new Error('worker job failed'));
|
||||||
|
else if(job&&job.type==='render')job.runner.fail(e);
|
||||||
|
else{deepPool.failed=true;destroyDeepPool()}
|
||||||
|
}
|
||||||
|
function deepWorkerLimit(){const hc=Math.max(1,navigator.hardwareConcurrency||4),memoryLimited=Number(navigator.deviceMemory||8)<=4?1:2;return Math.max(1,Math.min(memoryLimited,hc>2?hc-1:1))}
|
||||||
|
function addDeepWorker(){if(!deepModuleBundle)throw new Error('deep modules are not ready');const i=deepPool.workers.length,w=new Worker(deepPool.url);w._index=i;w._refKey='';w._refLoaded=0;w._busy=false;w._ready=false;w._job=null;w._recycle=null;w.onmessage=e=>handleDeepWorkerMessage(w,e.data);w.onerror=e=>handleDeepWorkerError(w,e);deepPool.workers.push(w);w.postMessage({type:'init',modules:deepModuleBundle});return w}
|
||||||
|
function ensureDeepPool(){
|
||||||
|
if(deepPool.failed||!deepModuleBundle||typeof Worker==='undefined'||typeof Blob==='undefined')return false;
|
||||||
|
if(deepPool.workers.length)return true;
|
||||||
|
try{
|
||||||
|
const count=deepWorkerLimit();
|
||||||
|
deepPool.url=URL.createObjectURL(new Blob([deepWorkerSource()],{type:'text/javascript'}));
|
||||||
|
deepPool.maxWorkers=count;deepWisdom.maxWorkers=count;deepWisdom.workerCount=1;addDeepWorker();
|
||||||
|
return true
|
||||||
|
}catch(e){deepPool.failed=true;destroyDeepPool();return false}
|
||||||
|
}
|
||||||
|
function prewarmDeepAssets(){prepareDeepModules().then(()=>{if(deepResolutionRatio()<=128)ensureDeepPool()}).catch(()=>{})}
|
||||||
|
function kickDeepWorker(worker){
|
||||||
|
if(worker._busy||!worker._ready)return;
|
||||||
|
const r=deepPool.current;if(!r||r.cancelled||r.token!==state.token)return;
|
||||||
|
const active=Math.max(1,Math.min(deepWisdom.ready?deepWisdom.workerCount:Math.min(2,deepPool.workers.length),deepPool.workers.length));
|
||||||
|
if(worker._index>=active)return;
|
||||||
|
r.dispatch(worker)
|
||||||
|
}
|
||||||
|
function attachReference(worker,msg,ref,refLen,refKey,transfer){
|
||||||
|
let start=worker._refKey===refKey?worker._refLoaded:0;start=Math.max(0,Math.min(start,refLen+1));
|
||||||
|
if(start<refLen+1){const rr=ref.rr.slice(start,refLen+1),ri=ref.ri.slice(start,refLen+1);msg.rr=rr.buffer;msg.ri=ri.buffer;msg.rrStart=start;transfer.push(rr.buffer,ri.buffer);worker._refKey=refKey;worker._refLoaded=refLen+1}
|
||||||
|
}
|
||||||
|
function blaProfile(profile){
|
||||||
|
const z=zoomExp(),exp=profile.covered?32:(z<16?28:23);
|
||||||
|
// Fast pass + local verification/repair is cheaper than making the entire frame
|
||||||
|
// conservative. Safe strips are rebuilt at e-48 only when a probe disagrees.
|
||||||
|
const safeExp=z<16?48:Math.min(48,Math.max(32,exp+8));
|
||||||
|
return{exp,eps:Math.pow(2,-exp),safeExp}
|
||||||
|
}
|
||||||
|
function runDeepPool(profile,snap,w,h,iter,colorIter,token,t0,out,sb,refC,off,ref,refLen,series,onFallback,rects=null){
|
||||||
|
if(!ensureDeepPool())return false;if(deepPool.current)deepPool.current.cancelled=true;
|
||||||
|
const centered=pixelCenteredOffset(snap,refC,w),workers=deepPool.workers,refKey=String(ref.id),bad=[],field=makeField(w*h,colorIter),spanNormal=fixedNum(snap.span,snap.bits),offRn=fixedNum(centered.r,snap.bits),offIn=fixedNum(centered.i,snap.bits),baseCr=fixedNum(refC.re,snap.bits),baseCi=fixedNum(refC.im,snap.bits);
|
||||||
|
const cMaxRaw=Math.hypot(offRn,offIn)+Math.abs(spanNormal)*Math.hypot(.5,h/(2*Math.max(1,w))),cBucket=cMaxRaw>0&&Number.isFinite(cMaxRaw)?Math.ceil(Math.log2(cMaxRaw)*8):0,cMaxSafe=cMaxRaw>0?Math.pow(2,cBucket/8):0,bp=blaProfile(profile),useBla=Number.isFinite(spanNormal)&&Math.abs(spanNormal)>=1e-280&&refLen>8,wp=workProfile(profile),colorCtx=makeColorCtx(colorIter);
|
||||||
|
const regions=(rects?rects:[{x0:0,y0:0,w,h}]).map(r=>({x0:Math.max(0,r.x0|0),y0:Math.max(0,r.y0|0),w:Math.max(0,Math.min(w-(r.x0|0),r.w|0)),h:Math.max(0,Math.min(h-(r.y0|0),r.h|0))})).filter(r=>r.w>0&&r.h>0),chunks=[];let preferredRows=deepWisdom.stripRows||16;if(deepWisdom.rowMsEMA>0)preferredRows=Math.round(deepWisdom.targetStripMs/deepWisdom.rowMsEMA);for(const r of regions){const rows=Math.max(2,Math.min(Math.max(1,Math.floor(65536/Math.max(1,r.w))),profile.covered?Math.max(4,preferredRows):Math.min(36,preferredRows)));for(let y=r.y0;y<r.y0+r.h;y+=rows)chunks.push({x0:r.x0,y0:y,w:r.w,rows:Math.min(rows,r.y0+r.h-y)})}chunks.sort((a,b)=>{const ad=Math.hypot((a.x0+a.w*.5)/w-state.focusX,(a.y0+a.rows*.5)/h-state.focusY),bd=Math.hypot((b.x0+b.w*.5)/w-state.focusX,(b.y0+b.rows*.5)/h-state.focusY);return ad-bd});
|
||||||
|
const totalPixels=regions.reduce((a,r)=>a+r.w*r.h,0),verifyJobs=profile.covered?8:3;
|
||||||
|
const runner={token,cancelled:false,chunkIndex:0,donePixels:0,totalPixels,failed:false,finishing:false,simd:true,blaUsed:false,kernelMs:0,verifyMs:0,blaBuildMs:0,sumIter:0,blackCount:0,badCount:0,blaSteps:0,ptbSteps:0,rebases:0,interior:0,unresolved:0,repaired:0,verifiedBuckets:new Set(),
|
||||||
|
fail(err){if(this.failed||this.cancelled)return;this.failed=true;deepPool.failed=true;destroyDeepPool();if(token===state.token)onFallback()},
|
||||||
|
nextChunk(){return this.chunkIndex<chunks.length?chunks[this.chunkIndex++]:null},
|
||||||
|
dispatch(worker){
|
||||||
|
if(this.cancelled||this.failed||token!==state.token)return false;const ch=this.nextChunk();if(!ch){this.maybeFinish();return false}
|
||||||
|
const jobId=token+':'+(++deepPool.serial),vb=Math.min(verifyJobs-1,Math.max(0,Math.floor((ch.y0+ch.rows*.5)*verifyJobs/Math.max(1,h)))),verifySamples=useBla&&!this.verifiedBuckets.has(vb)?(this.verifiedBuckets.add(vb),profile.covered?4:3):0,blaKey=refKey+':'+ref.version+':'+refLen+':'+cBucket+':e'+bp.exp,safeBlaKey=refKey+':'+ref.version+':'+refLen+':'+cBucket+':e'+bp.safeExp;
|
||||||
|
const msg={type:'render',jobId,refKey,refLen,w,h,x0:ch.x0,rectW:ch.w,y0:ch.y0,rows:ch.rows,iter,colorIter,covered:profile.covered,spanMant:sb.mant,spanBucket:sb.bucket,offR:off[0],offI:off[1],offBucket:off[2],cx:w*.5,cy:h*.5,skip:series.skip,Ar:series.Ar,Ai:series.Ai,Ab:series.Ab,Br:series.Br,Bi:series.Bi,Bb:series.Bb,shift:state.shift,cycle:state.cycle,palette:state.palette,useBla,blaEps:bp.eps,blaKey,safeBlaEps:Math.pow(2,-bp.safeExp),safeBlaKey,cMax:cMaxSafe,spanNormal,offRn,offIn,baseCr,baseCi,maxBlaSteps:wp.bla,maxPtbSteps:wp.ptb,verifySamples,verifyDelta:profile.covered?8:64},transfer=[];
|
||||||
|
attachReference(worker,msg,ref,refLen,refKey,transfer);if(worker._recycle){msg.fieldBuffer=worker._recycle.field;msg.iterationBuffer=worker._recycle.iterations;msg.classBuffer=worker._recycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);worker._recycle=null}worker._busy=true;worker._job={type:'render',runner:this,jobId,x0:ch.x0,y0:ch.y0,rectW:ch.w,rows:ch.rows,pixels:ch.w*ch.rows,started:performance.now()};try{worker.postMessage(msg,transfer)}catch(e){worker._busy=false;worker._job=null;this.fail(e);return false}return true
|
||||||
|
},
|
||||||
|
onResult(worker,d,job){
|
||||||
|
const elapsed=Math.max(.05,performance.now()-job.started),mpr=elapsed/Math.max(1,job.rows);deepWisdom.rowMsEMA=deepWisdom.rowMsEMA?deepWisdom.rowMsEMA*.86+mpr*.14:mpr;deepWisdom.stripRows=Math.round((deepWisdom.stripRows||12)*.75+Math.max(3,Math.min(128,Math.round(deepWisdom.targetStripMs/Math.max(.001,deepWisdom.rowMsEMA))))*.25);
|
||||||
|
if(this.cancelled||this.failed||token!==state.token)return;if(!d||d.jobId!==job.jobId)return;if(d.error){this.fail(new Error(d.error));return}
|
||||||
|
this.simd=this.simd&&!!d.simd;this.blaUsed=this.blaUsed||!!d.bla;this.kernelMs+=d.kernelMs||0;this.verifyMs+=d.verifyMs||0;this.blaBuildMs+=d.blaBuildMs||0;this.sumIter+=d.sumIter||0;this.blackCount+=d.blackCount||0;this.badCount+=(d.bad||[]).length;const st=d.stats||{};this.blaSteps+=st.blaSteps||0;this.ptbSteps+=st.ptbSteps||0;this.rebases+=st.rebases||0;this.interior+=st.interior||0;this.unresolved+=st.unresolved||0;this.repaired+=d.repaired||0;
|
||||||
|
const rw=d.rectW||job.rectW,rx=d.x0==null?job.x0:d.x0,sf=d.field?new Float32Array(d.field):null,it=d.iterations?new Uint32Array(d.iterations):null,cl=d.classes?new Uint8Array(d.classes):null,ro=sf&&cl?colorizeField({smooth:sf,classes:cl,iter:colorIter}):new Uint8ClampedArray(d.out);for(let yy=0;yy<d.rows;yy++){const dst=(d.y0+yy)*w+rx;out.set(ro.subarray(yy*rw*4,(yy+1)*rw*4),dst*4);if(sf)field.smooth.set(sf.subarray(yy*rw,(yy+1)*rw),dst);if(it)field.iterations.set(it.subarray(yy*rw,(yy+1)*rw),dst);if(cl)field.classes.set(cl.subarray(yy*rw,(yy+1)*rw),dst)}presentPartialStripe(ro,rx,d.y0,rw,d.rows,w,h,snap);if(d.field&&d.iterations&&d.classes)worker._recycle={field:d.field,iterations:d.iterations,classes:d.classes};if(d.bad)for(const local of d.bad){const gx=rx+(local%rw),gy=d.y0+((local/rw)|0);bad.push(gy*w+gx)}this.donePixels+=job.pixels;if(profile.covered&&workers.length<deepPool.maxWorkers&&memoryLedger().managedBytes+24*1048576<rendererMemoryBudget()&&this.totalPixels-this.donePixels>job.pixels*4&&elapsed>deepWisdom.targetStripMs){const added=addDeepWorker();deepWisdom.workerCount=workers.length;deepWisdom.ready=true;kickDeepWorker(added)}this.maybeFinish()
|
||||||
|
},
|
||||||
|
maybeFinish(){
|
||||||
|
if(this.cancelled||this.failed||token!==state.token||this.finishing||this.donePixels<this.totalPixels)return;this.finishing=true;if(deepPool.current===this)deepPool.current=null;deepPool.activeToken=0;const px=Math.max(1,this.totalPixels),kmpp=this.kernelMs/px;deepTelemetry.frames++;deepTelemetry.kernelMPP=deepTelemetry.kernelMPP?deepTelemetry.kernelMPP*.78+kmpp*.22:kmpp;deepTelemetry.meanIter=this.sumIter/px;deepTelemetry.blackRatio=this.blackCount/px;deepTelemetry.badRatio=this.badCount/px;deepTelemetry.blaBuildEMA=deepTelemetry.blaBuildEMA?deepTelemetry.blaBuildEMA*.85+this.blaBuildMs*.15:this.blaBuildMs;deepTelemetry.interiorRatio=this.interior/px;deepTelemetry.repairRatio=this.repaired/Math.max(1,regions.length);deepTelemetry.unresolvedRatio=this.unresolved/px;deepTelemetry.blaStepsPerPixel=this.blaSteps/px;deepTelemetry.ptbStepsPerPixel=this.ptbSteps/px;deepTelemetry.rebasePerPixel=this.rebases/px;deepTelemetry.lastKernelMs=this.kernelMs;deepTelemetry.lastVerifyMs=this.verifyMs;deepTelemetry.lastPixels=px;deepTelemetry.refDistance=refC.dist||0;refControl.lastMPP=kmpp;refControl.lastPixels=px;if(refControl.refId!==ref.id){refControl.refId=ref.id;refControl.baseMPP=kmpp}else if((refC.dist||0)<.2)refControl.baseMPP=refControl.baseMPP?refControl.baseMPP*.8+kmpp*.2:kmpp;persistDeepWisdom();
|
||||||
|
const label='WASM '+(this.simd?'SIMD':'scalar')+' ×'+Math.max(1,Math.min(deepWisdom.workerCount,workers.length))+' · '+(this.blaUsed?'BLA e-'+bp.exp+' + ':'')+'rebase'+(this.interior?' · 内部早期終了 '+Math.round(100*this.interior/px)+'%':'')+(this.repaired?' · 局所補修 '+this.repaired:'')+(this.totalPixels<w*h?' · 既存画像再利用 '+Math.round(100*(1-this.totalPixels/(w*h)))+'%':'');
|
||||||
|
const completeField=this.totalPixels===w*h?field:null;if(!bad.length){finishRender(token,t0,profile,snap,w,h,out,label,this.totalPixels,completeField);return}let k=0;const residual=async()=>{if(token!==state.token||this.cancelled)return;const deadline=performance.now()+5;while(k<bad.length&&performance.now()<deadline){const idx=bad[k++],x=idx%w,py=(idx/w)|0,result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,py,false,()=>token!==state.token||this.cancelled);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}if(k<bad.length)requestAnimationFrame(residual);else finishRender(token,t0,profile,snap,w,h,out,label+' · 残差 '+bad.length,this.totalPixels,completeField)};requestAnimationFrame(residual)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
deepPool.current=runner;deepPool.activeToken=token;if(totalPixels===0){queueMicrotask(()=>runner.maybeFinish());return true}const active=Math.max(1,Math.min(deepWisdom.ready?deepWisdom.workerCount:Math.min(2,workers.length),workers.length));for(let i=0;i<active;i++)kickDeepWorker(workers[i]);return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Frame reprojection / world-space detail cache ───────────────────────
|
||||||
|
const frameCanvas=document.createElement('canvas'),frameCtx=frameCanvas.getContext('2d',{alpha:false});
|
||||||
|
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
|
||||||
|
function snapshotToCurrent(s){if(s.bits===state.bits)return s;return{bits:state.bits,re:align(s.re,s.bits,state.bits),im:align(s.im,s.bits,state.bits),span:align(s.span,s.bits,state.bits)}}
|
||||||
|
function styleSignature(){return state.palette+':'+state.cycle.toFixed(6)+':'+state.shift.toFixed(5)}
|
||||||
|
function commitImage(data,w,h,snap,field=null){
|
||||||
|
frameCanvas.width=w;frameCanvas.height=h;const id=frameCtx.createImageData(w,h);id.data.set(data);frameCtx.putImageData(id,0,0);runtimeMetrics.canvasWrites++;
|
||||||
|
state.frameView=snapshotToCurrent(snap);state.fieldView=field?{field,w,h,snap:state.frameView}:null;invalidateView();
|
||||||
|
}
|
||||||
|
function presentPartialStripe(data,x,y,w,h,frameW,frameH,snap){if(frameCanvas.width!==frameW||frameCanvas.height!==frameH||!sameSnapshot(state.frameView,snap)){frameCanvas.width=frameW;frameCanvas.height=frameH;frameCtx.fillStyle='#050813';frameCtx.fillRect(0,0,frameW,frameH);state.frameView=snapshotToCurrent(snap);state.fieldView=null}const id=frameCtx.createImageData(w,h);id.data.set(data);frameCtx.putImageData(id,x,y);runtimeMetrics.canvasWrites++;invalidateView(false)}
|
||||||
|
function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span}
|
||||||
|
function recolorCurrentField(){const fv=state.fieldView;if(!fv||state.dirty||state.rendering||!sameSnapshot(fv.snap,state.frameView))return false;const out=colorizeField(fv.field);commitImage(out,fv.w,fv.h,fv.snap,fv.field);state.lastEngine='フィールド再彩色';state.lastRender=0;recolorCachedDetails();invalidateView();return true}
|
||||||
|
const validationJob={active:false,key:''};
|
||||||
|
const unknownContinuationJob={active:false,key:''};let unknownContinuationTimer=0;
|
||||||
|
function cancelUnknownContinuation(){if(unknownContinuationTimer){clearTimeout(unknownContinuationTimer);unknownContinuationTimer=0}unknownContinuationJob.active=false;state.continuationProgress=0}
|
||||||
|
function scheduleUnknownContinuation(delay=120){
|
||||||
|
const fv=state.fieldView;if(state.processMode!=='fine'||state.dirty||state.rendering||!fv||!state.unresolved)return;
|
||||||
|
const key=viewSpecKey(currentViewSpec())+':'+fv.w+'x'+fv.h+':'+fv.field.iter;
|
||||||
|
if(unknownContinuationJob.key===key||unknownContinuationTimer)return;
|
||||||
|
unknownContinuationTimer=setTimeout(()=>{unknownContinuationTimer=0;startUnknownContinuation(key)},delay)
|
||||||
|
}
|
||||||
|
function startUnknownContinuation(key){
|
||||||
|
const fv=state.fieldView;if(unknownContinuationJob.active||state.dirty||state.rendering||!fv||!sameSnapshot(fv.snap,state.frameView))return;
|
||||||
|
const field=fv.field,ranked=[];for(let i=0;i<field.classes.length;i++){if(field.classes[i]!==FIELD_UNKNOWN)continue;const x=i%fv.w,y=(i/fv.w)|0;let boundary=false;for(let yy=Math.max(0,y-1);yy<=Math.min(fv.h-1,y+1)&&!boundary;yy++)for(let xx=Math.max(0,x-1);xx<=Math.min(fv.w-1,x+1);xx++)if(field.classes[yy*fv.w+xx]===FIELD_ESCAPED){boundary=true;break}if(boundary)ranked.push({i,d:Math.hypot(x/fv.w-state.focusX,y/fv.h-state.focusY)})}if(!ranked.length)return;
|
||||||
|
const deep=deepEngineNeeded(fv.snap,fv.w),cap=Math.min(ranked.length,deep?384:4096);ranked.sort((a,b)=>a.d-b.d);const indices=ranked.slice(0,cap).map(v=>v.i);
|
||||||
|
unknownContinuationJob.active=true;unknownContinuationJob.key=key;const token=state.token,snap=fv.snap,baseIter=field.iter,extendedIter=Math.min(280000,Math.max(baseIter+256,baseIter*2)),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=fixedNum(snap.span,snap.bits)/fv.w;let p=0;
|
||||||
|
state.drawState='RESOLVING';state.continuationProgress=0;invalidateStats();
|
||||||
|
async function slice(){
|
||||||
|
if(token!==state.token||state.dirty){unknownContinuationJob.active=false;return}
|
||||||
|
const deadline=performance.now()+7;while(p<indices.length&&performance.now()<deadline){const i=indices[p++],x=i%fv.w,y=(i/fv.w)|0,result=deep?await highPrecisionDirectPixelAsync(snap,fv.w,fv.h,extendedIter,x,y,false,()=>token!==state.token||state.dirty):exportSampleShallow(cre,cim,scale,fv.w,fv.h,extendedIter,x,y);if(!result){unknownContinuationJob.active=false;return}const[n,m]=result;putField(field,i,n,m,n<extendedIter?FIELD_ESCAPED:FIELD_UNKNOWN)}
|
||||||
|
state.continuationProgress=p/indices.length;if(p<indices.length){invalidateStats();requestAnimationFrame(slice);return}
|
||||||
|
state.unresolved=field.classes.reduce((n,c)=>n+(c===FIELD_UNKNOWN),0);unknownContinuationJob.active=false;state.continuationProgress=1;state.drawState='COVERED';commitImage(colorizeField(field),fv.w,fv.h,snap,field);state.lastEngine+=' · 境界 '+indices.length+'点を追加反復';invalidateStats()
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function scheduleValidation(delay=350){if(state.processMode!=='validate'||state.dirty||state.rendering)return;scheduleBackground(()=>{if(state.processMode==='validate'&&!state.detailActive&&!unknownContinuationJob.active)startValidation();else if(state.processMode==='validate')scheduleValidation(300)},delay)}
|
||||||
|
function startValidation(){
|
||||||
|
const fv=state.fieldView;if(validationJob.active||!fv||state.dirty||state.rendering||!sameSnapshot(fv.snap,state.frameView))return;
|
||||||
|
const key=viewSpecKey(currentViewSpec())+':'+fv.w+'x'+fv.h+':'+fv.field.iter;if(validationJob.key===key)return;
|
||||||
|
validationJob.active=true;validationJob.key=key;
|
||||||
|
const token=state.token,snap=fv.snap,field=fv.field,baseIter=field.iter,extendedIter=Math.min(280000,Math.max(baseIter+256,baseIter*2));let i=0,lastStatus=0;
|
||||||
|
state.drawState='VALIDATING';state.coverage=0;invalidateStats();
|
||||||
|
async function slice(now){
|
||||||
|
if(token!==state.token||state.processMode!=='validate'){validationJob.active=false;return}
|
||||||
|
const deadline=performance.now()+9;
|
||||||
|
while(i<field.classes.length&&performance.now()<deadline){
|
||||||
|
const old=field.classes[i],x=i%fv.w,y=(i/fv.w)|0,limit=old===FIELD_ESCAPED?baseIter:extendedIter;
|
||||||
|
if(fixedAnalyticPixelProven(snap,fv.w,fv.h,x,y)){putField(field,i,limit,0,FIELD_INTERIOR_PROVEN);i++;continue}
|
||||||
|
const result=await highPrecisionDirectPixelAsync(snap,fv.w,fv.h,limit,x,y,true,()=>token!==state.token||state.processMode!=='validate');
|
||||||
|
if(!result){validationJob.active=false;return}
|
||||||
|
const[n,m]=result;if(n<limit)putField(field,i,n,m,FIELD_ESCAPED);else putField(field,i,n,0,FIELD_UNKNOWN);i++
|
||||||
|
}
|
||||||
|
if(now-lastStatus>180){lastStatus=now;state.coverage=i/field.classes.length;invalidateStats()}
|
||||||
|
if(i<field.classes.length){requestAnimationFrame(slice);return}
|
||||||
|
field.iter=extendedIter;state.unresolved=field.classes.reduce((n,c)=>n+(c!==FIELD_ESCAPED&&c!==FIELD_INTERIOR_PROVEN),0);state.drawState=state.unresolved?'VALIDATION_INCOMPLETE':'VALIDATED';state.coverage=1;validationJob.active=false;const out=colorizeField(field);commitImage(out,fv.w,fv.h,snap,field);state.lastEngine='高精度direct照合';invalidateStats()
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function drawWorldDetail(entry){
|
||||||
|
if(!entry||!entry.complete||entry.style!==styleSignature()||!entry.canvas)return;if(entry.iter){const rr=entry.iter/Math.max(1,maxIter());if(rr<.82||rr>1.22)return}
|
||||||
|
const re=align(entry.re,entry.bits,state.bits),im=align(entry.im,entry.bits,state.bits),sxv=align(entry.spanX,entry.bits,state.bits),syv=align(entry.spanY,entry.bits,state.bits);
|
||||||
|
const x=canvas.width*.5+fixedRatio(re-state.re,state.span)*canvas.width,y=canvas.height*.5-fixedRatio(im-state.im,state.span)*canvas.width;
|
||||||
|
const dw=Math.abs(fixedRatio(sxv,state.span)*canvas.width),dh=Math.abs(fixedRatio(syv,state.span)*canvas.width);
|
||||||
|
if(!Number.isFinite(x+y+dw+dh)||dw<5||dh<5||x+dw*.5<0||x-dw*.5>canvas.width||y+dh*.5<0||y-dh*.5>canvas.height)return;
|
||||||
|
// Do not magnify a cached tile beyond ~1.7 display pixels per source pixel.
|
||||||
|
if(dw/Math.max(1,entry.canvas.width)>1.7)return;
|
||||||
|
entry.lastUsed=performance.now();
|
||||||
|
ctx.imageSmoothingEnabled=true;try{ctx.imageSmoothingQuality='high'}catch{}
|
||||||
|
ctx.drawImage(entry.canvas,x-dw*.5,y-dh*.5,dw,dh);
|
||||||
|
}
|
||||||
|
function paintFrame(){
|
||||||
|
if(!state.frameView||!frameCanvas.width)return;
|
||||||
|
runtimeMetrics.canvasWrites++;
|
||||||
|
const fv=state.frameView;
|
||||||
|
const a=fixedRatio(fv.span,state.span);if(!Number.isFinite(a)||a<=0)return;
|
||||||
|
const dx=fixedRatio(fv.re-state.re,state.span)*canvas.width;
|
||||||
|
const dy=-fixedRatio(fv.im-state.im,state.span)*canvas.width;
|
||||||
|
ctx.save();ctx.setTransform(1,0,0,1,0,0);ctx.fillStyle='#050813';ctx.fillRect(0,0,canvas.width,canvas.height);
|
||||||
|
ctx.translate(canvas.width*.5+dx,canvas.height*.5+dy);ctx.scale(a,a);ctx.imageSmoothingEnabled=true;try{ctx.imageSmoothingQuality=state.lastPass===RENDER_PASS.COVERED?'high':'medium'}catch{}
|
||||||
|
const fw=canvas.width,fh=fw*(frameCanvas.height/Math.max(1,frameCanvas.width));ctx.drawImage(frameCanvas,-fw*.5,-fh*.5,fw,fh);ctx.restore();
|
||||||
|
// Only completed, world-validated tiles are composited. Partial tiles remain offscreen.
|
||||||
|
ctx.save();ctx.setTransform(1,0,0,1,0,0);for(const e of DETAIL_TILE_CACHE.values())drawWorldDetail(e);ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBudget(profile,deep){return profile.budgetMs}
|
||||||
|
function targetSize(profile,deep){
|
||||||
|
// Resolution is intentionally independent of zoom depth. 10^20, 10^100 and
|
||||||
|
// beyond use the same quality targets as low zoom; only the numerical engine changes.
|
||||||
|
const cssW=Math.max(1,canvas.clientWidth),cssH=Math.max(1,canvas.clientHeight),aspect=cssH/cssW,dpr=Math.max(.25,state.effectiveDpr||1);
|
||||||
|
if(profile.covered)return[canvas.width,canvas.height];
|
||||||
|
let nominal=Math.max(profile.minWidth,cssW*dpr*profile.nominalScale),minW=profile.minWidth,maxW=profile.maxWidth;
|
||||||
|
const measured=renderPerf[deep?'deepMPP':'shallowMPP'];
|
||||||
|
if(deep&&!profile.covered&&measured<=0){nominal=Math.min(nominal,48);minW=32}
|
||||||
|
if(measured>0){
|
||||||
|
const timedW=Math.sqrt(renderBudget(profile,deep)/Math.max(1e-7,measured*aspect));
|
||||||
|
nominal=Math.min(nominal,timedW);minW=deep?32:240
|
||||||
|
}
|
||||||
|
let w=Math.max(minW,Math.min(maxW,Math.round(nominal))),h=Math.max(deep&&!profile.covered?32:96,Math.round(w*aspect));
|
||||||
|
if(h>2200){const sc=2200/h;h=2200;w=Math.round(w*sc)}return[w,h]
|
||||||
|
}
|
||||||
|
function analyzeDetailTiles(data,w,h,baseMs,deep){
|
||||||
|
const field=state.fieldView&&state.fieldView.w===w&&state.fieldView.h===h?state.fieldView.field:null;if(!field)return[];
|
||||||
|
const leaves=[],minTile=32,rootTile=256;
|
||||||
|
function scoreRect(x0,y0,tw,th){
|
||||||
|
const step=Math.max(1,Math.floor(Math.min(tw,th)/14));let grad=0,edges=0,uncertain=0,samples=0;
|
||||||
|
for(let y=y0;y<y0+th;y+=step)for(let x=x0;x<x0+tw;x+=step){const i=y*w+x,c=field.classes[i],s=field.smooth[i];samples++;uncertain+=(255-(field.confidence?field.confidence[i]:fieldConfidence(c)))/255;if(x+step<x0+tw){const j=i+step,c2=field.classes[j];if((c===FIELD_ESCAPED)!==(c2===FIELD_ESCAPED))edges++;else if(c===FIELD_ESCAPED&&c2===FIELD_ESCAPED)grad+=Math.min(12,Math.abs(s-field.smooth[j]))}if(y+step<y0+th){const j=i+step*w,c2=field.classes[j];if((c===FIELD_ESCAPED)!==(c2===FIELD_ESCAPED))edges++;else if(c===FIELD_ESCAPED&&c2===FIELD_ESCAPED)grad+=Math.min(12,Math.abs(s-field.smooth[j]))}}
|
||||||
|
return edges/Math.max(1,samples)*4.4+grad/Math.max(1,samples*12)*1.5+uncertain/Math.max(1,samples)*.9
|
||||||
|
}
|
||||||
|
function visit(x,y,tw,th){const score=scoreRect(x,y,tw,th);if(score<.055)return;if((tw>minTile||th>minTile)&&score>.11){const aw=Math.max(1,tw>>1),bw=tw-aw,ah=Math.max(1,th>>1),bh=th-ah;visit(x,y,aw,ah);if(bw)visit(x+aw,y,bw,ah);if(bh)visit(x,y+ah,aw,bh);if(bw&&bh)visit(x+aw,y+ah,bw,bh);return}leaves.push({x,y,w:tw,h:th,score,area:tw*th})}
|
||||||
|
for(let y=0;y<h;y+=rootTile)for(let x=0;x<w;x+=rootTile)visit(x,y,Math.min(rootTile,w-x),Math.min(rootTile,h-y));
|
||||||
|
leaves.sort((a,b)=>{const ad=Math.hypot((a.x+a.w*.5)/w-state.focusX,(a.y+a.h*.5)/h-state.focusY),bd=Math.hypot((b.x+b.w*.5)/w-state.focusX,(b.y+b.h*.5)/h-state.focusY);return(b.score+.08/(.08+bd))-(a.score+.08/(.08+ad))});const byteCap=Math.max(0,detailCacheBudget()-detailCacheBytes),baseBudget=renderBudget(RENDER_PROFILE[RENDER_PASS.COVERED],deep),spare=Math.max(0,baseBudget*1.7-baseMs),sampleCap=Math.floor(w*h*Math.min(2.6,spare/Math.max(1,baseMs)));let bytes=0,sampleArea=0,n=0;while(n<leaves.length){const tile=leaves[n],sampleScale=tile.score>=1.15?4:2,costBytes=tile.area*(sampleScale*sampleScale*10+4),costSamples=tile.area*sampleScale*sampleScale;if(bytes+costBytes>byteCap||sampleArea+costSamples>sampleCap)break;bytes+=costBytes;sampleArea+=costSamples;n++}return leaves.slice(0,n)
|
||||||
|
}
|
||||||
|
function detailGeometry(tile,plan){
|
||||||
|
const s=plan.snap,den=BigInt(2*plan.w),re=s.re+s.span*BigInt(2*tile.x+tile.w-plan.w)/den,im=s.im+s.span*BigInt(plan.h-2*tile.y-tile.h)/den;
|
||||||
|
const spanX=s.span*BigInt(tile.w)/BigInt(plan.w),spanY=s.span*BigInt(tile.h)/BigInt(plan.w);
|
||||||
|
return{bits:s.bits,re,im,spanX,spanY}
|
||||||
|
}
|
||||||
|
function detailKey(tile,plan,sampleScale){const g=detailGeometry(tile,plan);return [g.bits,g.re,g.im,g.spanX,g.spanY,plan.iter,'aa'+sampleScale,'centered-v23'].join(':')}
|
||||||
|
function rendererMemoryBudget(){return(Number(navigator.deviceMemory||8)<=4||matchMedia('(max-width:700px)').matches?96:192)*1048576}
|
||||||
|
function wasmBytes(core){try{return core&&core.ex&&core.ex.memory?core.ex.memory.buffer.byteLength:0}catch{return 0}}
|
||||||
|
function memoryLedger(includeDetail=true){
|
||||||
|
const screenCanvasBytes=canvas.width*canvas.height*4,frameCanvasBytes=frameCanvas.width*frameCanvas.height*4;
|
||||||
|
const fieldBytes=state.fieldView?state.fieldView.w*state.fieldView.h*10:0;
|
||||||
|
const referenceBytes=(referenceCache.rr?.byteLength||0)+(referenceCache.ri?.byteLength||0);
|
||||||
|
const mainWasmBytes=wasmBytes(wasm)+wasmBytes(deepWasm);
|
||||||
|
// Worker memories are isolated, so browsers do not expose their byteLength.
|
||||||
|
// Account them conservatively: reference copies + kernel scratch/linear memory.
|
||||||
|
const workerEstimateBytes=(shallowWorker?2*1048576:0)+deepPool.workers.length*24*1048576;
|
||||||
|
const renderBytes=state.rendering?(()=>{const size=targetSize(renderProfile(state.lastPass),deepMode),pixels=size[0]*size[1];return pixels*18})():0;
|
||||||
|
const exportBytes=exportJob&&exportJob.active?exportJob.bytes||0:0,activeDetailBytes=activeDetailTiles.reduce((sum,e)=>sum+(e.transientBytes||0),0);
|
||||||
|
const cacheBytes=includeDetail?detailCacheBytes:0;
|
||||||
|
const managedBytes=fieldBytes+referenceBytes+mainWasmBytes+workerEstimateBytes+renderBytes+exportBytes+activeDetailBytes+cacheBytes;
|
||||||
|
const canvasBytes=screenCanvasBytes+frameCanvasBytes;
|
||||||
|
return{managedBytes,logicalBytes:managedBytes+canvasBytes,canvasBytes,screenCanvasBytes,frameCanvasBytes,fieldBytes,referenceBytes,mainWasmBytes,workerEstimateBytes,renderBytes,exportBytes,activeDetailBytes,detailCacheBytes:cacheBytes,budget:rendererMemoryBudget()}
|
||||||
|
}
|
||||||
|
function detailCacheBudget(){const base=memoryLedger(false).managedBytes,reserve=16*1048576;return Math.max(0,Math.floor(Math.min(rendererMemoryBudget()*.18,rendererMemoryBudget()-base-reserve)))}
|
||||||
|
function detailEntryBytes(entry){return entry&&entry.canvas?entry.canvas.width*entry.canvas.height*4+(entry.field?entry.field.classes.length*10:0):0}
|
||||||
|
function detailDistance(entry){try{const re=align(entry.re,entry.bits,state.bits),im=align(entry.im,entry.bits,state.bits);return Math.hypot(fixedRatio(re-state.re,state.span),fixedRatio(im-state.im,state.span))}catch{return Infinity}}
|
||||||
|
function trimDetailCache(){const budget=detailCacheBudget();while(detailCacheBytes>budget&&DETAIL_TILE_CACHE.size){let victim=null,rank=-Infinity;const now=performance.now();for(const [key,entry]of DETAIL_TILE_CACHE){const age=Math.max(0,now-(entry.lastUsed||0))/60000,distance=detailDistance(entry),score=(Number.isFinite(distance)?distance:1000)*4+age;if(score>rank){rank=score;victim=[key,entry]}}if(!victim)break;DETAIL_TILE_CACHE.delete(victim[0]);detailCacheBytes=Math.max(0,detailCacheBytes-detailEntryBytes(victim[1]))}}
|
||||||
|
function linearChannel(v){v/=255;return v<=.04045?v/12.92:Math.pow((v+.055)/1.055,2.4)}
|
||||||
|
function srgbChannel(v){v=Math.max(0,Math.min(1,v));return Math.round(255*(v<=.0031308?12.92*v:1.055*Math.pow(v,1/2.4)-.055))}
|
||||||
|
function resolveSubsampleField(field,w,h,scale=2){const hi=colorizeField(field),c=document.createElement('canvas');c.width=w;c.height=h;const cc=c.getContext('2d',{alpha:false}),id=cc.createImageData(w,h),out=id.data,samples=scale*scale;for(let y=0;y<h;y++)for(let x=0;x<w;x++){let r=0,g=0,b=0;for(let sy=0;sy<scale;sy++)for(let sx=0;sx<scale;sx++){const i=(((y*scale+sy)*w*scale)+(x*scale+sx))*4;r+=linearChannel(hi[i]);g+=linearChannel(hi[i+1]);b+=linearChannel(hi[i+2])}const oi=(y*w+x)*4;out[oi]=srgbChannel(r/samples);out[oi+1]=srgbChannel(g/samples);out[oi+2]=srgbChannel(b/samples);out[oi+3]=255}cc.putImageData(id,0,0);return c}
|
||||||
|
function recolorDetailEntry(entry){if(!entry||!entry.field)return;entry.canvas=resolveSubsampleField(entry.field,entry.baseW,entry.baseH,entry.sampleScale||2);entry.style=styleSignature()}
|
||||||
|
function recolorCachedDetails(){cancelDetailRefinement(true);for(const entry of DETAIL_TILE_CACHE.values())recolorDetailEntry(entry)}
|
||||||
|
function makeDetailTask(tile,plan){
|
||||||
|
const sampleScale=tile.score>=1.15?4:2,key=detailKey(tile,plan,sampleScale),cached=DETAIL_TILE_CACHE.get(key);if(cached){DETAIL_TILE_CACHE.delete(key);DETAIL_TILE_CACHE.set(key,cached);cached.lastUsed=performance.now();if(cached.style!==styleSignature())recolorDetailEntry(cached);return{cached:true,entry:cached}}
|
||||||
|
const c=document.createElement('canvas');c.width=Math.max(2,tile.w*sampleScale);c.height=Math.max(2,tile.h*sampleScale);const dc=c.getContext('2d',{alpha:false});
|
||||||
|
dc.imageSmoothingEnabled=true;try{dc.imageSmoothingQuality='high'}catch{};dc.drawImage(frameCanvas,tile.x,tile.y,tile.w,tile.h,0,0,c.width,c.height);
|
||||||
|
const g=detailGeometry(tile,plan),entry={...g,canvas:c,field:null,baseW:tile.w,baseH:tile.h,sampleScale,style:styleSignature(),key,score:tile.score,iter:plan.iter,complete:false,lastUsed:performance.now(),transientBytes:c.width*c.height*18};
|
||||||
|
const task={tile,canvas:c,ctx:dc,field:makeField(c.width*c.height,plan.iter),entry,phases:0,plan};activeDetailTiles.push(entry);return task
|
||||||
|
}
|
||||||
|
function validateDetailTask(task){
|
||||||
|
const plan=task&&task.plan,t=task&&task.tile,base=plan&&plan.baseData;if(!plan||!t||!base)return false;
|
||||||
|
let samples=0,blackMismatch=0,rgbDiff=0,colorSamples=0;const sampleScale=task.entry.sampleScale||2,step=Math.max(3,Math.floor(Math.min(t.w,t.h)/7)),pix=task.ctx.getImageData(0,0,task.canvas.width,task.canvas.height).data;
|
||||||
|
for(let by=t.y+1;by<t.y+t.h-1;by+=step)for(let bx=t.x+1;bx<t.x+t.w-1;bx+=step){
|
||||||
|
const bi=(by*plan.w+bx)*4,lx=Math.min(task.canvas.width-1,Math.max(0,sampleScale*(bx-t.x)+(sampleScale>>1))),ly=Math.min(task.canvas.height-1,Math.max(0,sampleScale*(by-t.y)+(sampleScale>>1))),hi=(ly*task.canvas.width+lx)*4;
|
||||||
|
const bb=(base[bi]|base[bi+1]|base[bi+2])===0,hb=(pix[hi]|pix[hi+1]|pix[hi+2])===0;samples++;if(bb!==hb)blackMismatch++;else if(!bb){rgbDiff+=Math.abs(base[bi]-pix[hi])+Math.abs(base[bi+1]-pix[hi+1])+Math.abs(base[bi+2]-pix[hi+2]);colorSamples++}
|
||||||
|
}
|
||||||
|
if(samples<4)return false;const classRate=blackMismatch/samples,meanDiff=colorSamples?rgbDiff/(3*colorSamples):0;return classRate<=.035&&meanDiff<=48
|
||||||
|
}
|
||||||
|
function cacheDetailTask(task){
|
||||||
|
if(!task||task.cached)return;const i=activeDetailTiles.indexOf(task.entry);if(i>=0)activeDetailTiles.splice(i,1);
|
||||||
|
if(!validateDetailTask(task))return;fillFieldConfidence(task.field);task.entry.field=task.field;task.entry.canvas=resolveSubsampleField(task.field,task.tile.w,task.tile.h,task.entry.sampleScale);task.entry.style=styleSignature();task.entry.complete=true;task.entry.lastUsed=performance.now();task.entry.transientBytes=0;const old=DETAIL_TILE_CACHE.get(task.entry.key);if(old)detailCacheBytes-=detailEntryBytes(old);DETAIL_TILE_CACHE.delete(task.entry.key);DETAIL_TILE_CACHE.set(task.entry.key,task.entry);detailCacheBytes+=detailEntryBytes(task.entry);
|
||||||
|
trimDetailCache()
|
||||||
|
}
|
||||||
|
function phaseRect(task,phase){
|
||||||
|
const t=task.tile,scale=task.entry.sampleScale,left=(phase%scale)*t.w,top=((phase/scale)|0)*t.h;
|
||||||
|
return{x0:t.x*scale+left,y0:t.y*scale+top,rw:t.w,rows:t.h,dx:left,dy:top}
|
||||||
|
}
|
||||||
|
function prepareDeepTileContext(plan,iter,sampleScale,done){
|
||||||
|
const cacheKey='i'+iter+':s'+sampleScale;if(plan.deepCtx&&plan.deepCtx[cacheKey]){done(plan.deepCtx[cacheKey]);return}if(!plan.deepCtx)plan.deepCtx={};
|
||||||
|
const snap=plan.snap,refC=chooseReference(snap),W=plan.w*sampleScale,H=plan.h*sampleScale,refX=W*.5-.5+fixedRatio(refC.re-snap.re,snap.span)*W,refY=H*.5-.5-fixedRatio(refC.im-snap.im,snap.span)*W;
|
||||||
|
const cornerR=Math.max(Math.hypot(refX,refY),Math.hypot(W-refX,refY),Math.hypot(refX,H-refY),Math.hypot(W-refX,H-refY))/Math.max(1,W),logMaxDc=log2FixedAt(snap.span,snap.bits)+Math.log2(Math.max(1e-300,cornerR));
|
||||||
|
buildReferenceAt(snap.bits,refC.re,refC.im,iter,state.token,(ref,refLen)=>{
|
||||||
|
if(plan.gen!==state.detailGeneration)return;const series=computeSeries(ref,refLen,logMaxDc),centered=pixelCenteredOffset(snap,refC,W),sb=spanMantBucket(snap.span,snap.bits),off=fixedComplexScaled(centered.r,centered.i,snap.bits),spanNormal=fixedNum(snap.span,snap.bits),offRn=fixedNum(centered.r,snap.bits),offIn=fixedNum(centered.i,snap.bits);
|
||||||
|
const cMaxRaw=Math.hypot(offRn,offIn)+Math.abs(spanNormal)*Math.hypot(.5,H/(2*Math.max(1,W))),cBucket=cMaxRaw>0&&Number.isFinite(cMaxRaw)?Math.ceil(Math.log2(cMaxRaw)*8):0,cMaxSafe=cMaxRaw>0?Math.pow(2,cBucket/8):0;
|
||||||
|
const ctx={snap,refC,ref,refLen,series,sb,off,spanNormal,offRn,offIn,cBucket,cMaxSafe,W,H,iter};plan.deepCtx[cacheKey]=ctx;done(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function renderGuardedDeepDetailRect(task,phase,iter,done){
|
||||||
|
const plan=task.plan,r=phaseRect(task,phase),W=plan.w*task.entry.sampleScale,H=plan.h*task.entry.sampleScale,snap=plan.snap;let yy=0;
|
||||||
|
async function slice(){if(plan.gen!==state.detailGeneration){done(false);return}const deadline=performance.now()+6;while(yy<r.rows&&performance.now()<deadline){const gy=r.y0+yy;for(let xx=0;xx<r.rw;xx++){const gx=r.x0+xx,result=await highPrecisionDirectPixelAsync(snap,W,H,iter,gx,gy,true,()=>plan.gen!==state.detailGeneration);if(!result){done(false);return}const[n,m]=result,fi=(r.dy+yy)*task.canvas.width+r.dx+xx;putField(task.field,fi,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN)}yy++}if(yy<r.rows){requestAnimationFrame(slice);return}const stripe=makeField(r.rw*r.rows,iter);for(let y=0;y<r.rows;y++){const src=(r.dy+y)*task.canvas.width+r.dx,dst=y*r.rw;stripe.smooth.set(task.field.smooth.subarray(src,src+r.rw),dst);stripe.iterations.set(task.field.iterations.subarray(src,src+r.rw),dst);stripe.classes.set(task.field.classes.subarray(src,src+r.rw),dst)}const id=task.ctx.createImageData(r.rw,r.rows);id.data.set(colorizeField(stripe));task.ctx.putImageData(id,r.dx,r.dy);task.phases|=(1<<phase);invalidateView(false);done(true)}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function sendDeepDetailRect(task,phase,iter,colorIter,done){
|
||||||
|
const plan=task.plan,gen=plan.gen;if(gen!==state.detailGeneration){done(false);return}if(state.processMode==='validate'||task.tile.score>1.35){renderGuardedDeepDetailRect(task,phase,iter,done);return}if(!ensureDeepPool()){done(false);return}
|
||||||
|
prepareDeepTileContext(plan,iter,task.entry.sampleScale,dc=>{
|
||||||
|
if(gen!==state.detailGeneration){done(false);return}const worker=deepPool.workers.find(w=>!w._busy);if(!worker){scheduleBackground(()=>sendDeepDetailRect(task,phase,iter,colorIter,done),18);return}
|
||||||
|
const r=phaseRect(task,phase),ref=dc.ref,refKey=String(ref.id),bpExp=32,useBla=Number.isFinite(dc.spanNormal)&&Math.abs(dc.spanNormal)>=1e-280&&dc.refLen>8,blaEps=Math.pow(2,-bpExp),blaKey=refKey+':'+ref.version+':'+dc.refLen+':'+dc.cBucket+':e'+bpExp;
|
||||||
|
const jobId='detail:'+gen+':'+Date.now()+':'+Math.random(),msg={type:'render',jobId,refKey,refLen:dc.refLen,w:dc.W,h:dc.H,x0:r.x0,y0:r.y0,rectW:r.rw,rows:r.rows,iter,colorIter,spanMant:dc.sb.mant,spanBucket:dc.sb.bucket,offR:dc.off[0],offI:dc.off[1],offBucket:dc.off[2],cx:dc.W*.5,cy:dc.H*.5,skip:dc.series.skip,Ar:dc.series.Ar,Ai:dc.series.Ai,Ab:dc.series.Ab,Br:dc.series.Br,Bi:dc.series.Bi,Bb:dc.series.Bb,shift:state.shift,cycle:state.cycle,palette:state.palette,useBla,blaEps,blaKey,cMax:dc.cMaxSafe,spanNormal:dc.spanNormal,offRn:dc.offRn,offIn:dc.offIn,baseCr:fixedNum(dc.refC.re,dc.snap.bits),baseCi:fixedNum(dc.refC.im,dc.snap.bits),maxBlaSteps:0,maxPtbSteps:0,verifySamples:3,verifyDelta:6,safeBlaEps:Math.pow(2,-48),safeBlaKey:blaKey+':safe48'},transfer=[];
|
||||||
|
let start=worker._refKey===refKey?worker._refLoaded:0;start=Math.max(0,Math.min(start,dc.refLen+1));if(start<dc.refLen+1){const rr=ref.rr.slice(start,dc.refLen+1),ri=ref.ri.slice(start,dc.refLen+1);msg.rr=rr.buffer;msg.ri=ri.buffer;msg.rrStart=start;transfer.push(rr.buffer,ri.buffer);worker._refKey=refKey;worker._refLoaded=dc.refLen+1}
|
||||||
|
const runner={token:state.token,cancelled:false,fail(){done(false)},onResult(w,d){if(gen!==state.detailGeneration||!d||d.error){done(false);return}const sf=d.field?new Float32Array(d.field):null,it=d.iterations?new Uint32Array(d.iterations):null,cl=d.classes?new Uint8Array(d.classes):null,arr=sf&&cl?colorizeField({smooth:sf,classes:cl,iter:colorIter}):new Uint8ClampedArray(d.out),id=task.ctx.createImageData(r.rw,r.rows);id.data.set(arr);task.ctx.putImageData(id,r.dx,r.dy);if(sf&&it&&cl)for(let yy=0;yy<r.rows;yy++){const dst=(r.dy+yy)*task.canvas.width+r.dx;task.field.smooth.set(sf.subarray(yy*r.rw,(yy+1)*r.rw),dst);task.field.iterations.set(it.subarray(yy*r.rw,(yy+1)*r.rw),dst);task.field.classes.set(cl.subarray(yy*r.rw,(yy+1)*r.rw),dst)}if(d.field&&d.iterations&&d.classes)w._recycle={field:d.field,iterations:d.iterations,classes:d.classes};task.phases|=(1<<phase);task.entry.canvas=task.canvas;invalidateView(false);done(true)}};
|
||||||
|
if(worker._recycle){msg.fieldBuffer=worker._recycle.field;msg.iterationBuffer=worker._recycle.iterations;msg.classBuffer=worker._recycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);worker._recycle=null}worker._busy=true;worker._job={type:'render',runner,jobId,y0:r.y0,rows:r.rows,started:performance.now()};try{worker.postMessage(msg,transfer)}catch(e){worker._busy=false;worker._job=null;done(false)}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function renderShallowDetailRect(task,phase,done){
|
||||||
|
const plan=task.plan,r=phaseRect(task,phase),sampleScale=task.entry.sampleScale,W=plan.w*sampleScale,H=plan.h*sampleScale,iter=plan.iter,snap=plan.snap,sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/W,colorCtx=makeColorCtx(iter),out=new Uint8ClampedArray(r.rw*r.rows*4);let yy=0;
|
||||||
|
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
function slice(){if(plan.gen!==state.detailGeneration){done(false);return}const deadline=performance.now()+5;while(yy<r.rows&&performance.now()<deadline){const gy=r.y0+yy,ci=cim+(H*.5-gy-.5)*scale;let oi=yy*r.rw*4;for(let xx=0;xx<r.rw;xx++){const gx=r.x0+xx,cr=cre+(gx+.5-W*.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0,inside=inBulbs(cr,ci);if(inside)n=iter;else while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}const m=n<iter?Math.max(4.0000001,zr2+zi2):0,fi=(r.dy+yy)*task.canvas.width+r.dx+xx;putField(task.field,fi,n,m,n<iter?FIELD_ESCAPED:(inside?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN));if(n>=iter){out[oi]=out[oi+1]=out[oi+2]=0;out[oi+3]=255}else putFastColor(out,oi,n,m,iter,colorCtx);oi+=4}yy++}if(yy<r.rows)requestAnimationFrame(slice);else{const id=task.ctx.createImageData(r.rw,r.rows),data=id.data;data.set(out);task.ctx.putImageData(id,r.dx,r.dy);task.phases|=(1<<phase);invalidateView(false);done(true)}}requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function pumpDetailRefinement(){
|
||||||
|
const plan=detailPlan;if(!plan||plan.gen!==state.detailGeneration||!state.hq){state.detailActive=false;return}if(state.rendering||unknownContinuationJob.active||performance.now()-state.lastInteraction<720){scheduleBackground(pumpDetailRefinement,90);return}
|
||||||
|
while(plan.index<plan.tasks.length&&plan.tasks[plan.index].cached)plan.index++;
|
||||||
|
if(plan.index>=plan.tasks.length){state.detailActive=false;state.detailDone=plan.tasks.length;state.drawState='REFINED';updateStats();scheduleValidation();return}
|
||||||
|
const task=plan.tasks[plan.index],phase=task.nextPhase||0,cb=ok=>{if(!ok){const i=activeDetailTiles.indexOf(task.entry);if(i>=0)activeDetailTiles.splice(i,1);plan.index++;scheduleBackground(pumpDetailRefinement,10);return}task.nextPhase=phase+1;if(task.nextPhase>=task.entry.sampleScale*task.entry.sampleScale){cacheDetailTask(task);plan.index++}state.detailDone=plan.tasks.reduce((n,t)=>n+(t.cached||t.entry.complete?1:0),0);updateStats();scheduleBackground(pumpDetailRefinement,0)};
|
||||||
|
if(plan.deep)sendDeepDetailRect(task,phase,plan.iter,plan.iter,cb);else renderShallowDetailRect(task,phase,cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Idle-only adaptive detail refinement ────────────────────────────────
|
||||||
|
function scheduleDetailRefinement(data,w,h,snap,iter,baseMs,deep){
|
||||||
|
if(!state.hq)return;cancelDetailRefinement(true);const gen=state.detailGeneration,tiles=analyzeDetailTiles(data,w,h,baseMs,deep);if(!tiles.length){state.drawState='REFINED';invalidateStats();scheduleValidation();return}
|
||||||
|
const plan={gen,snap,w,h,iter,deep,tiles,tasks:[],index:0,deepCtx:null,baseData:data};for(const t of tiles)plan.tasks.push(makeDetailTask(t,plan));detailPlan=plan;state.detailActive=true;state.drawState='REFINING';state.detailQueued=plan.tasks.length;state.detailDone=plan.tasks.filter(t=>t.cached).length;scheduleBackground(pumpDetailRefinement,160)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishRender(token,t0,profile,snap,w,h,out,engine,computedPixels=w*h,field=null){
|
||||||
|
if(token!==state.token)return;const elapsed=Math.max(.1,performance.now()-t0),deep=deepEngineNeeded(snap,Math.max(1,canvas.width));
|
||||||
|
// Reproject-only frames must not poison performance estimates with near-zero work.
|
||||||
|
if(computedPixels>Math.max(512,w*h*.01)){const mpp=elapsed/computedPixels,key=deep?'deepMPP':'shallowMPP';renderPerf[key]=renderPerf[key]?renderPerf[key]*.72+mpp*.28:mpp}
|
||||||
|
if(field){fillFieldConfidence(field);out=colorizeField(field)}commitImage(out,w,h,snap,field);state.rendering=false;state.lastRender=elapsed;state.lastPass=profile.id;state.dirty=false;state.lastEngine=engine;state.lastFrameDone=performance.now();state.coverage=Math.min(1,w*h/Math.max(1,canvas.width*canvas.height));state.drawState=profile.covered&&state.coverage>=.999?'COVERED':'PREVIEW';state.unresolved=field?field.classes.reduce((n,c)=>n+(c===FIELD_UNKNOWN),0):0;flushPendingPrecision();
|
||||||
|
if(!profile.covered&&adaptStandardDeepBudget(deep)){updateStats();return}
|
||||||
|
if(profile.covered&&state.unresolved)scheduleUnknownContinuation();
|
||||||
|
if(profile.covered&&state.hq){const gen=state.detailGeneration;const schedule=()=>{if(gen!==state.detailGeneration||state.rendering)return;if(unknownContinuationJob.active||unknownContinuationTimer){scheduleBackground(schedule,90);return}const fv=state.fieldView,base=fv&&sameSnapshot(fv.snap,snap)?colorizeField(fv.field):out;scheduleDetailRefinement(base,w,h,snap,fv?fv.field.iter:iterationPlan(profile,deep).colorIter,elapsed,deep)};scheduleIdle(schedule,350);clearPanReuse()}
|
||||||
|
if(profile.covered&&state.processMode==='validate')scheduleValidation();
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Shallow renderers ───────────────────────────────────────────────────
|
||||||
|
function renderShallowReuse(profile,snap,w,h,iter,token,t0,reuse){
|
||||||
|
const out=reuse.out,colorCtx=makeColorCtx(iter),sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/w,rects=reuse.rects;let ri=0,yy=0;
|
||||||
|
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,qq=x*x+y2;if(qq*(qq+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
function slice(){if(token!==state.token)return;const deadline=performance.now()+7;while(ri<rects.length&&performance.now()<deadline){const r=rects[ri];while(yy<r.h&&performance.now()<deadline){const gy=r.y0+yy,ci=cim+(h*.5-gy-.5)*scale;for(let xx=0;xx<r.w;xx++){const gx=r.x0+xx,cr=cre+(gx+.5-w*.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0;if(inBulbs(cr,ci))n=iter;else while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}const oi=(gy*w+gx)*4;if(n>=iter){out[oi]=out[oi+1]=out[oi+2]=0;out[oi+3]=255}else putFastColor(out,oi,n,Math.max(4.0000001,zr2+zi2),iter,colorCtx)}yy++}if(yy>=r.h){ri++;yy=0}}
|
||||||
|
if(ri<rects.length)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'JavaScript f64 · 既存画像再利用 '+Math.round(100*reuse.reusedPixels/(w*h))+'%',reuse.exposedPixels)
|
||||||
|
}requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function renderWasmMain(profile,snap,w,h,iter,token,t0){
|
||||||
|
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),colorCtx=makeColorCtx(iter);
|
||||||
|
const cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;
|
||||||
|
function proven(cr,ci){const y2=ci*ci,x=cr-.25,q0=x*x+y2;if(q0*(q0+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
let y=0;const counts=wasm.counts(),mags=wasm.mags();
|
||||||
|
function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+(profile.covered?11:8);
|
||||||
|
while(y<h&&performance.now()<deadline){
|
||||||
|
const rows=Math.min(24,h-y),y0=y,npx=wasm.ex.render_rows(cre+scale*.5,cim-scale*.5,sp,w,h,y,rows,iter);let oi=y*w*4;
|
||||||
|
for(let i=0;i<npx;i++){
|
||||||
|
const n=counts[i],m=mags[i],px=i%w,py=y0+((i/w)|0);
|
||||||
|
const cr=cre+(px+.5-w*.5)*scale,ci=cim+(h*.5-py-.5)*scale,kind=n<iter?FIELD_ESCAPED:(proven(cr,ci)?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN);putField(field,py*w+px,n,m,kind);
|
||||||
|
if(n>=iter){out[oi++]=0;out[oi++]=0;out[oi++]=0;out[oi++]=255}
|
||||||
|
else{
|
||||||
|
putFastColor(out,oi,n,Math.max(4.0000001,m),iter,colorCtx);oi+=4;
|
||||||
|
}
|
||||||
|
}y+=rows;
|
||||||
|
}
|
||||||
|
if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'WebAssembly f64',w*h,field);
|
||||||
|
}requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
function renderWasm(profile,snap,w,h,iter,token,t0){
|
||||||
|
if(!ensureShallowWorker()||shallowWorkerBusy){renderWasmMain(profile,snap,w,h,iter,token,t0);return}
|
||||||
|
const worker=shallowWorker,out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),jobId='shallow:'+token+':'+Date.now(),rowsPerChunk=Math.min(h,Math.max(4,Math.floor(32768/Math.max(1,w)))),chunks=[];for(let y=0;y<h;y+=rowsPerChunk)chunks.push({y,rows:Math.min(rowsPerChunk,h-y)});chunks.sort((a,b)=>Math.abs((a.y+a.rows*.5)/h-state.focusY)-Math.abs((b.y+b.rows*.5)/h-state.focusY));let next=0;shallowWorkerBusy=true;
|
||||||
|
const dispatch=()=>{if(token!==state.token){shallowWorkerBusy=false;return}if(next>=chunks.length){shallowWorkerBusy=false;finishRender(token,t0,profile,snap,w,h,out,'WebAssembly '+(wasm.simd?'SIMD':'scalar')+' Worker',w*h,field);return}const chunk=chunks[next++],msg={type:'render',jobId,cre,cim,sp,w,h,y:chunk.y,rows:chunk.rows,iter},transfer=[];if(shallowRecycle){msg.fieldBuffer=shallowRecycle.field;msg.iterationBuffer=shallowRecycle.iterations;msg.classBuffer=shallowRecycle.classes;transfer.push(msg.fieldBuffer,msg.iterationBuffer,msg.classBuffer);shallowRecycle=null}worker.postMessage(msg,transfer)};
|
||||||
|
worker.onmessage=e=>{const d=e.data;if(!d||d.type==='ready')return;if(d.jobId!==jobId)return;if(d.error){shallowWorkerBusy=false;renderWasmMain(profile,snap,w,h,iter,token,t0);return}const sf=new Float32Array(d.field),it=new Uint32Array(d.iterations),cl=new Uint8Array(d.classes);if(token!==state.token){shallowRecycle={field:d.field,iterations:d.iterations,classes:d.classes};shallowWorkerBusy=false;return}const stripe=colorizeField({smooth:sf,classes:cl,iter}),dst=d.y*w;field.smooth.set(sf,dst);field.iterations.set(it,dst);field.classes.set(cl,dst);out.set(stripe,dst*4);presentPartialStripe(stripe,0,d.y,w,d.rows,w,h,snap);shallowRecycle={field:d.field,iterations:d.iterations,classes:d.classes};dispatch()};worker.onerror=()=>{shallowWorkerBusy=false;destroyShallowWorker();if(token===state.token)renderWasmMain(profile,snap,w,h,iter,token,t0)};dispatch()
|
||||||
|
}
|
||||||
|
function renderJsDouble(profile,snap,w,h,iter,token,t0){
|
||||||
|
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,iter),colorCtx=makeColorCtx(iter),sp=fixedNum(snap.span,snap.bits),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),scale=sp/w;let y=0;
|
||||||
|
function inBulbs(cr,ci){const y2=ci*ci,x=cr-.25,qq=x*x+y2;if(qq*(qq+x)<=.25*y2)return true;const x2=cr+1;return x2*x2+y2<=.0625}
|
||||||
|
function slice(){if(token!==state.token)return;const deadline=performance.now()+(profile.covered?10:7);while(y<h&&performance.now()<deadline){let oi=y*w*4,ci=cim+(h*.5-y-.5)*scale,cr=cre+(.5-w*.5)*scale;for(let x=0;x<w;x++,cr+=scale){let zr=0,zi=0,zr2=0,zi2=0,n=0,inside=inBulbs(cr,ci);if(inside)n=iter;else{let oldr=0,oldi=0;while(n<iter&&zr2+zi2<=4){zi=(zr+zr)*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++;if((n&63)===0){if(Math.abs(zr-oldr)+Math.abs(zi-oldi)<1e-15){n=iter;break}oldr=zr;oldi=zi}}}const mm=n>=iter?0:Math.max(4.0000001,zr2+zi2),kind=n<iter?FIELD_ESCAPED:(inside?FIELD_INTERIOR_LIKELY:FIELD_UNKNOWN);putField(field,y*w+x,n,mm,kind);if(n>=iter){out[oi++]=0;out[oi++]=0;out[oi++]=0;out[oi++]=255}else{putFastColor(out,oi,n,mm,iter,colorCtx);oi+=4}}y++}if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'JavaScript f64',w*h,field);}requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
function spanMantBucket(span,bits){
|
||||||
|
const l=log2FixedAt(span,bits);let bucket=Math.round(l/256),mant=Math.pow(2,l-bucket*256);return{mant,bucket};
|
||||||
|
}
|
||||||
|
function normalizeBucket(r,i,b){
|
||||||
|
let m=Math.max(Math.abs(r),Math.abs(i));if(!m)return[0,0,NEG_BUCKET];
|
||||||
|
while(m>HI128){r*=INV256;i*=INV256;b++;m*=INV256}
|
||||||
|
while(m<LO128){r*=POW256;i*=POW256;b--;m*=POW256}
|
||||||
|
return[r,i,b];
|
||||||
|
}
|
||||||
|
function scAdd(a,b){
|
||||||
|
if(a[2]===NEG_BUCKET)return b;if(b[2]===NEG_BUCKET)return a;
|
||||||
|
const eb=Math.max(a[2],b[2]);let r=0,i=0;
|
||||||
|
if(a[2]===eb){r+=a[0];i+=a[1]}else if(a[2]===eb-1){r+=a[0]*INV256;i+=a[1]*INV256}
|
||||||
|
if(b[2]===eb){r+=b[0];i+=b[1]}else if(b[2]===eb-1){r+=b[0]*INV256;i+=b[1]*INV256}
|
||||||
|
return normalizeBucket(r,i,eb);
|
||||||
|
}
|
||||||
|
function scLog2(a){return a[2]===NEG_BUCKET?-Infinity:Math.log2(Math.hypot(a[0],a[1]))+256*a[2]}
|
||||||
|
function fixedMantAtBucket(v,bits,bucket){
|
||||||
|
if(v===0n)return 0;const neg=v<0n;let a=neg?-v:v,bl=bitLen(a),take=Math.min(53,bl),sh=bl-take,top=Number(a>>BigInt(sh));
|
||||||
|
const exp=sh-bits-256*bucket;const n=top*Math.pow(2,exp);return neg?-n:n;
|
||||||
|
}
|
||||||
|
function fixedComplexScaled(r,i,bits){
|
||||||
|
if(r===0n&&i===0n)return[0,0,NEG_BUCKET];
|
||||||
|
const lr=r===0n?-Infinity:log2FixedAt(r,bits),li=i===0n?-Infinity:log2FixedAt(i,bits),bucket=Math.round(Math.max(lr,li)/256);
|
||||||
|
return normalizeBucket(fixedMantAtBucket(r,bits,bucket),fixedMantAtBucket(i,bits,bucket),bucket);
|
||||||
|
}
|
||||||
|
function pixelCenteredOffset(snap,refC,w){const half=roundDiv(snap.span,BigInt(2*Math.max(1,w)));return{r:snap.re-refC.re+half,i:snap.im-refC.im-half}}
|
||||||
|
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
|
||||||
|
function fixedPixelPoint(snap,w,h,x,y,bits=snap.bits){const den=BigInt(2*w),re=align(snap.re,snap.bits,bits),im=align(snap.im,snap.bits,bits),span=align(snap.span,snap.bits,bits);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
|
||||||
|
function fixedAnalyticInterior(cr,ci,bits){const S=1n<<BigInt(bits),X=cr-(S>>2n),Y=ci,Q=X*X+Y*Y;if(4n*Q*(Q+X*S)<=Y*Y*S*S)return true;const D=cr+S;return 16n*(D*D+Y*Y)<=S*S}
|
||||||
|
function fixedAnalyticPixelProven(snap,w,h,x,y){const p=fixedPixelPoint(snap,w,h,x,y),g=fixedPixelPoint(snap,w,h,x,y,snap.bits+64);return fixedAnalyticInterior(p[0],p[1],snap.bits)&&fixedAnalyticInterior(g[0],g[1],snap.bits+64)}
|
||||||
|
function roundShift(v,bits){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(bits)-1n),q=(a+half)>>BigInt(bits);return neg?-q:q}
|
||||||
|
// ── Arbitrary-precision reference orbit / perturbation setup ────────────
|
||||||
|
const referenceCache={id:0,bits:0,re:0n,im:0n,n:0,escape:0,zr:0n,zi:0n,rr:null,ri:null,series:null,version:0,loadedLen:0,derivative:[0,0,NEG_BUCKET],conditionLog2:0,checkpointVersion:0,checkpointBits:0,checkpointCount:0,checkpointMismatch:false};
|
||||||
|
function promoteReferenceCache(shift,oldBits){
|
||||||
|
const c=referenceCache;if(!c.rr||c.bits!==oldBits)return;
|
||||||
|
const sh=BigInt(shift);c.re<<=sh;c.im<<=sh;c.zr<<=sh;c.zi<<=sh;c.bits+=shift;
|
||||||
|
// rr/ri are normalized Float64 values, so neither they nor Worker-side copies
|
||||||
|
// need to change when only the fixed-point radix moves.
|
||||||
|
}
|
||||||
|
function sameReference(bits,re,im){
|
||||||
|
return referenceCache.rr&&referenceCache.bits===bits&&referenceCache.re===re&&referenceCache.im===im;
|
||||||
|
}
|
||||||
|
function resetReference(bits,re,im){
|
||||||
|
const c=referenceCache;c.id++;c.bits=bits;c.re=re;c.im=im;c.n=0;c.escape=0;c.zr=0n;c.zi=0n;c.derivative=[0,0,NEG_BUCKET];c.conditionLog2=0;
|
||||||
|
if(!c.rr){c.rr=new Float64Array(150001);c.ri=new Float64Array(150001)}
|
||||||
|
c.series=null;c.version=1;c.loadedLen=0;c.checkpointVersion=0;c.checkpointBits=0;c.checkpointCount=0;c.checkpointMismatch=false;
|
||||||
|
}
|
||||||
|
function invalidateReferenceOrbit(){const c=referenceCache;c.id++;c.bits=0;c.re=0n;c.im=0n;c.n=0;c.escape=0;c.zr=0n;c.zi=0n;c.rr=null;c.ri=null;c.series=null;c.version=0;c.loadedLen=0;c.derivative=[0,0,NEG_BUCKET];c.conditionLog2=0;c.checkpointVersion=0;c.checkpointBits=0;c.checkpointCount=0;c.checkpointMismatch=false;for(const worker of deepPool.workers){worker._refKey='';worker._refLoaded=0}}
|
||||||
|
function buildReferenceAt(bits,cRe,cIm,iter,token,done){
|
||||||
|
if(!sameReference(bits,cRe,cIm))resetReference(bits,cRe,cIm);
|
||||||
|
const c=referenceCache,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,startN=c.n,refT0=performance.now();
|
||||||
|
if((c.escape&&c.escape<=iter)||c.n>=iter){
|
||||||
|
const refLen=c.escape&&c.escape<=iter?c.escape:iter;done(c,refLen);return
|
||||||
|
}
|
||||||
|
function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+6.5;
|
||||||
|
while(c.n<iter&&!c.escape&&performance.now()<deadline){
|
||||||
|
c.rr[c.n]=fixedOrbitNum(c.zr,bits);c.ri[c.n]=fixedOrbitNum(c.zi,bits);const Rr=c.rr[c.n],Ri=c.ri[c.n],d=c.derivative,term=d[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(Rr*d[0]-Ri*d[1]),2*(Rr*d[1]+Ri*d[0]),d[2]);c.derivative=scAdd(term,[1,0,0]);c.conditionLog2=Math.max(c.conditionLog2,scLog2(c.derivative));
|
||||||
|
const zr2=roundShift(c.zr*c.zr,bits),zi2=roundShift(c.zi*c.zi,bits);c.zi=roundShift(2n*c.zr*c.zi,bits)+cIm;c.zr=zr2-zi2+cRe;c.n++;
|
||||||
|
const mag=roundShift(c.zr*c.zr,bits)+roundShift(c.zi*c.zi,bits);if(mag>BAIL)c.escape=c.n;
|
||||||
|
}
|
||||||
|
if(c.escape||c.n>=iter){
|
||||||
|
c.rr[c.n]=fixedOrbitNum(c.zr,bits);c.ri[c.n]=fixedOrbitNum(c.zi,bits);c.version++;if(c.n>startN){const ms=performance.now()-refT0;refControl.lastBuildMs=ms;refControl.buildEMA=refControl.buildEMA?refControl.buildEMA*.82+ms*.18:ms}
|
||||||
|
const refLen=c.escape&&c.escape<=iter?c.escape:iter;done(c,refLen)
|
||||||
|
}else requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
function verifyReferenceCheckpoints(ref,refLen,token,done){
|
||||||
|
const guardBits=ref.bits+64;
|
||||||
|
if(ref.checkpointVersion===ref.version&&ref.checkpointBits===guardBits&&!ref.checkpointMismatch){done(true);return}
|
||||||
|
const cRe=align(ref.re,ref.bits,guardBits),cIm=align(ref.im,ref.bits,guardBits),ONE=1n<<BigInt(guardBits),BAIL=16n*ONE;
|
||||||
|
const 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,n=0,escape=0,checked=0,mismatch=false;
|
||||||
|
function compare(){if(!targets.has(n)||n>refLen)return;checked++;const rr=fixedOrbitNum(zr,guardBits),ri=fixedOrbitNum(zi,guardBits);if(!Object.is(rr,ref.rr[n])||!Object.is(ri,ref.ri[n]))mismatch=true}
|
||||||
|
function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+6;compare();
|
||||||
|
while(n<refLen&&!escape&&!mismatch&&performance.now()<deadline){const zr2=roundShift(zr*zr,guardBits),zi2=roundShift(zi*zi,guardBits);zi=roundShift(2n*zr*zi,guardBits)+cIm;zr=zr2-zi2+cRe;n++;const mag=roundShift(zr*zr,guardBits)+roundShift(zi*zi,guardBits);if(mag>BAIL)escape=n;compare()}
|
||||||
|
if(mismatch||escape||n>=refLen){if((ref.escape||0)!==escape&&((ref.escape||0)<=refLen||escape<=refLen))mismatch=true;ref.checkpointVersion=ref.version;ref.checkpointBits=guardBits;ref.checkpointCount=checked;ref.checkpointMismatch=mismatch;done(!mismatch)}else requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(slice)
|
||||||
|
}
|
||||||
|
function computeSeries(ref,refLen,logMaxDc){
|
||||||
|
// A series valid for a wider image remains valid after zooming further in.
|
||||||
|
if(ref.series&&logMaxDc<=ref.series.logMaxDc+.02&&ref.series.skip<refLen)return ref.series;
|
||||||
|
let A=[0,0,NEG_BUCKET],Bc=[0,0,NEG_BUCKET],bestSkip=0,bestA=[0,0,NEG_BUCKET],bestB=[0,0,NEG_BUCKET];
|
||||||
|
const LOG_LIMIT=Math.log2(2.2e-4),LOG_RATIO=Math.log2(.10);
|
||||||
|
for(let n=0;n<refLen;n++){
|
||||||
|
const R=ref.rr[n],I=ref.ri[n],oldA=A;
|
||||||
|
const at=oldA[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(R*oldA[0]-I*oldA[1]),2*(R*oldA[1]+I*oldA[0]),oldA[2]);
|
||||||
|
A=scAdd(at,[1,0,0]);
|
||||||
|
const bt=Bc[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(2*(R*Bc[0]-I*Bc[1]),2*(R*Bc[1]+I*Bc[0]),Bc[2]);
|
||||||
|
const a2=oldA[2]===NEG_BUCKET?[0,0,NEG_BUCKET]:normalizeBucket(oldA[0]*oldA[0]-oldA[1]*oldA[1],2*oldA[0]*oldA[1],2*oldA[2]);
|
||||||
|
Bc=scAdd(bt,a2);
|
||||||
|
const l1=scLog2(A)+logMaxDc,l2=scLog2(Bc)+2*logMaxDc;
|
||||||
|
if(Number.isFinite(l1)&&l1<LOG_LIMIT&&(!Number.isFinite(l2)||(l2<LOG_LIMIT&&l2<l1+LOG_RATIO))){bestSkip=n+1;bestA=A.slice();bestB=Bc.slice()}
|
||||||
|
}
|
||||||
|
if(bestSkip>=refLen)bestSkip=Math.max(0,refLen-1);
|
||||||
|
ref.series={logMaxDc,skip:bestSkip,Ar:bestA[0],Ai:bestA[1],Ab:bestA[2],Br:bestB[0],Bi:bestB[1],Bb:bestB[2]};
|
||||||
|
return ref.series;
|
||||||
|
}
|
||||||
|
function chooseReference(snap){
|
||||||
|
const c=referenceCache;if(c.rr&&c.n>8){const rr=align(c.re,c.bits,snap.bits),ri=align(c.im,c.bits,snap.bits),dx=fixedRatio(rr-snap.re,snap.span),dy=fixedRatio(ri-snap.im,snap.span),dist=Math.hypot(dx,dy);if(Number.isFinite(dist)){const hard=2.25;if(dist<=hard){let keep=true;if(dist>.45&&refControl.refId===c.id&&refControl.baseMPP>0&&refControl.lastMPP>refControl.baseMPP*1.34&&performance.now()-refControl.lastRecenterAt>refControl.cooldownMs){const extra=(refControl.lastMPP-refControl.baseMPP)*Math.max(1,refControl.lastPixels),build=Math.max(5,refControl.buildEMA||18);if(extra>build*1.65)keep=false}if(keep)return{re:rr,im:ri,reused:true,dist};refControl.lastRecenterAt=performance.now()}}}return{re:snap.re,im:snap.im,reused:false,dist:0}
|
||||||
|
}
|
||||||
|
function putDeepPixel(out,w,computeIter,colorIter,x,y,n,m,colorCtx){
|
||||||
|
const oi=(y*w+x)*4,mm=n>=computeIter?0:Math.max(4.0000001,m);
|
||||||
|
|
||||||
|
if(n>=computeIter){out[oi]=0;out[oi+1]=0;out[oi+2]=0;out[oi+3]=255;return}
|
||||||
|
putFastColor(out,oi,n,mm,colorIter,colorCtx);
|
||||||
|
}
|
||||||
|
function directOrbitState(cr,ci,bits,iter){return{cr,ci,bits,iter,zr:0n,zi:0n,mag:0n,n:0,done:false,four:4n*(1n<<BigInt(bits))}}
|
||||||
|
function stepDirectOrbit(orbit,deadline){let batch=0;while(!orbit.done){const zr2=roundShift(orbit.zr*orbit.zr,orbit.bits),zi2=roundShift(orbit.zi*orbit.zi,orbit.bits);orbit.mag=zr2+zi2;if(orbit.mag>orbit.four||orbit.n>=orbit.iter){orbit.done=true;break}orbit.zi=roundShift(2n*orbit.zr*orbit.zi,orbit.bits)+orbit.ci;orbit.zr=zr2-zi2+orbit.cr;orbit.n++;if((++batch&31)===0&&performance.now()>=deadline)break}if(orbit.n>=orbit.iter)orbit.done=true}
|
||||||
|
async function highPrecisionDirectPixelAsync(snap,w,h,iter,x,y,guarded=true,cancelled=()=>false){const p=fixedPixelPoint(snap,w,h,x,y),base=directOrbitState(p[0],p[1],snap.bits,iter);let guard=null;if(guarded){const bits=snap.bits+64,g=fixedPixelPoint(snap,w,h,x,y,bits);guard=directOrbitState(g[0],g[1],bits,iter)}while(!base.done||(guard&&!guard.done)){if(cancelled())return null;const deadline=performance.now()+6;stepDirectOrbit(base,deadline);if(guard)stepDirectOrbit(guard,deadline);if(!base.done||(guard&&!guard.done))await new Promise(requestAnimationFrame)}if(guard&&(guard.n!==base.n||((guard.n<iter)!==(base.n<iter))))return[iter,0];const chosen=guard||base;return[chosen.n,chosen.n<iter?Math.max(4.0000001,fixedOrbitNum(chosen.mag,chosen.bits)):0]}
|
||||||
|
function renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0){
|
||||||
|
const cap=profile.covered?w:390,scale=Math.min(1,cap/w);if(scale<1){h=Math.max(100,Math.round(h*scale));w=Math.max(160,Math.round(w*scale))}
|
||||||
|
const out=new Uint8ClampedArray(w*h*4),field=makeField(w*h,colorIter),colorCtx=makeColorCtx(colorIter);let y=0;
|
||||||
|
state.lastEngine='BigInt direct fallback';
|
||||||
|
async function slice(){
|
||||||
|
if(token!==state.token)return;const deadline=performance.now()+7;
|
||||||
|
while(y<h&&performance.now()<deadline){const y0=y;for(let x=0;x<w;x++){const result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,y,false,()=>token!==state.token);if(!result)return;const[n,m]=result;putField(field,y*w+x,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,y,n,m,colorCtx)}presentPartialStripe(out.subarray(y0*w*4,(y0+1)*w*4),0,y0,w,1,w,h,snap);y++}
|
||||||
|
if(y<h)requestAnimationFrame(slice);else finishRender(token,t0,profile,snap,w,h,out,'BigInt direct fallback',w*h,field);
|
||||||
|
}requestAnimationFrame(slice);
|
||||||
|
}
|
||||||
|
function prepareDeepContext(snap,refC,iter,token,done){
|
||||||
|
const probeW=64,probeH=Math.max(28,Math.round(probeW*canvas.clientHeight/Math.max(1,canvas.clientWidth))),refX=probeW*.5-.5+fixedRatio(refC.re-snap.re,snap.span)*probeW,refY=probeH*.5-.5-fixedRatio(refC.im-snap.im,snap.span)*probeW,cornerR=Math.max(Math.hypot(refX,refY),Math.hypot(probeW-refX,refY),Math.hypot(refX,probeH-refY),Math.hypot(probeW-refX,probeH-refY))/Math.max(1,probeW),logMaxDc=log2FixedAt(snap.span,snap.bits)+Math.log2(Math.max(1e-300,cornerR));
|
||||||
|
buildReferenceAt(snap.bits,refC.re,refC.im,iter,token,(ref,refLen)=>{if(token!==state.token)return;if(ensureOrbitPrecision(iter,Math.max(1,canvas.width))){state.dirty=true;invalidateView();return}const finish=()=>done({ref,refLen,series:computeSeries(ref,refLen,logMaxDc)});if(state.processMode!=='validate'){finish();return}verifyReferenceCheckpoints(ref,refLen,token,ok=>{if(token!==state.token)return;if(ok){finish();return}promoteState(32);invalidateReferenceOrbit();state.dirty=true;invalidateView()})})
|
||||||
|
}
|
||||||
|
function renderPerturbPrepared(profile,snap,w,h,iter,colorIter,token,t0,refC,ref,refLen,series,reuse=null){
|
||||||
|
const workerReady=ensureDeepPool();if(!workerReady&&(!ensureDeepWasm()||!deepWasm.ex.render_perturb_rebase_rect)){renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0);return}
|
||||||
|
const centered=pixelCenteredOffset(snap,refC,w),out=reuse?reuse.out:new Uint8ClampedArray(w*h*4),field=makeField(w*h,colorIter),colorCtx=makeColorCtx(colorIter),sb=spanMantBucket(snap.span,snap.bits),off=fixedComplexScaled(centered.r,centered.i,snap.bits);
|
||||||
|
const fallback=(strict=false)=>{if(token!==state.token)return;if(!ensureDeepWasm()||!deepWasm.ex.render_perturb_rebase_rect){renderDeepDirect(profile,snap,w,h,iter,colorIter,token,t0);return}if(ref.loadedLen<refLen+1){deepWasm.refsR().set(ref.rr.subarray(ref.loadedLen,refLen+1),ref.loadedLen);deepWasm.refsI().set(ref.ri.subarray(ref.loadedLen,refLen+1),ref.loadedLen);ref.loadedLen=refLen+1}const counts=deepWasm.counts(),mags=deepWasm.mags(),bad=[];let y=0;function rows(){if(token!==state.token)return;const deadline=performance.now()+(profile.covered?11:8);while(y<h&&performance.now()<deadline){const rows=Math.min(Math.max(1,Math.floor(65536/Math.max(1,w))),h-y),y0=y,npx=deepWasm.ex.render_perturb_rebase_rect(sb.mant,sb.bucket,off[0],off[1],off[2],refLen,w,h,w*.5,h*.5,0,y,w,rows,iter,strict?0:series.skip,strict?0:series.Ar,strict?0:series.Ai,strict?0:series.Ab,strict?0:series.Br,strict?0:series.Bi,strict?0:series.Bb);for(let i=0;i<npx;i++){const x=i%w,py=y0+((i/w)|0),n=counts[i],m=mags[i];if(n===0xffffffff){bad.push(py*w+x);continue}putField(field,py*w+x,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}y+=rows}if(y<h)requestAnimationFrame(rows);else if(bad.length)runResidual();else finish()}function runResidual(){let k=0;async function slice(){if(token!==state.token)return;const deadline=performance.now()+6;while(k<bad.length&&performance.now()<deadline){const idx=bad[k++],x=idx%w,py=(idx/w)|0,result=await highPrecisionDirectPixelAsync(snap,w,h,iter,x,py,false,()=>token!==state.token);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n<iter?FIELD_ESCAPED:FIELD_UNKNOWN);putDeepPixel(out,w,iter,colorIter,x,py,n,m,colorCtx)}if(k<bad.length)requestAnimationFrame(slice);else finish()}requestAnimationFrame(slice)}function finish(){state.lastEngine='WASM '+(deepWasm.simd?'SIMD':'scalar')+' · '+(strict?'strict ':'')+'main-thread rebase · skip '+(strict?0:series.skip);finishRender(token,t0,profile,snap,w,h,out,state.lastEngine,w*h,field)}requestAnimationFrame(rows)};
|
||||||
|
// Never abort and restart an in-flight frame because a budget estimate was wrong.
|
||||||
|
// Finish it once and feed the measured cost into the next frame.
|
||||||
|
if(!workerReady||!runDeepPool(profile,snap,w,h,iter,colorIter,token,t0,out,sb,refC,off,ref,refLen,series,fallback,reuse?reuse.rects:null))fallback();
|
||||||
|
}
|
||||||
|
async function renderDeepAdaptive(profile,snap,plan,token,t0){
|
||||||
|
try{await prepareDeepModules()}catch{if(token===state.token){const [fallbackW,fallbackH]=targetSize(profile,true);renderDeepDirect(profile,snap,fallbackW,fallbackH,plan.colorIter,plan.colorIter,token,t0)}return}if(token!==state.token)return;
|
||||||
|
const iter=plan.colorIter,colorIter=plan.colorIter,[w,h]=targetSize(profile,true),reuse=buildPanReuse(snap,w,h,iter,profile);
|
||||||
|
// A zoom-in can be represented entirely by the previous frame. Commit that preview
|
||||||
|
// immediately; the normal idle HQ pass performs the exact full-resolution render.
|
||||||
|
if(reuse&&reuse.rects.length===0&&!profile.covered){finishRender(token,t0,profile,snap,w,h,reuse.out,'既存画像再利用 100%',0);return}
|
||||||
|
const refC=chooseReference(snap);state.lastEngine='参照軌道を準備中…';prepareDeepContext(snap,refC,iter,token,ctx=>{if(token!==state.token)return;renderPerturbPrepared(profile,snap,w,h,iter,colorIter,token,t0,refC,ctx.ref,ctx.refLen,ctx.series,reuse)})
|
||||||
|
}
|
||||||
|
function render(pass=RENDER_PASS.PREVIEW){
|
||||||
|
const profile=renderProfile(pass);
|
||||||
|
if(pass===RENDER_PASS.COVERED){state.fieldView=null;trimDetailCache()}
|
||||||
|
runtimeMetrics.renderStarts++;if(state.pointerActive||state.wheelActive)runtimeMetrics.renderStartsDuringGesture++;
|
||||||
|
let snap=snapshot(),deep=deepEngineNeeded(snap);if(!deep&&state.adaptivePixelBudget){state.adaptivePixelBudget=0;resize()}if(deep&&ensureOrbitPrecision(maxIter()))snap=snapshot();const plan=iterationPlan(profile,deep),token=++state.token,t0=performance.now();state.rendering=true;state.drawState=profile.covered?'COVERING':'PREVIEW';state.lastPass=profile.id;state.lastEngine=deep?'参照軌道を準備中…':(wasm?('WebAssembly '+(wasm.simd?'SIMD':'scalar')):'JavaScript f64');
|
||||||
|
if(!deep){const ratio=deepResolutionRatio(snap);if(ratio<=128)prewarmDeepAssets();else if(deepPool.workers.length||deepWasm||deepModuleBundle||referenceCache.rr)retireDeepAssets()}
|
||||||
|
invalidateStats();
|
||||||
|
if(deep)renderDeepAdaptive(profile,snap,plan,token,t0);else{const [w,h]=targetSize(profile,false),reuse=buildPanReuse(snap,w,h,plan.computeIter,profile);if(reuse)renderShallowReuse(profile,snap,w,h,plan.computeIter,token,t0,reuse);else if(wasm)renderWasm(profile,snap,w,h,plan.computeIter,token,t0);else renderJsDouble(profile,snap,w,h,plan.computeIter,token,t0)}
|
||||||
|
}
|
||||||
|
function screenPixelBudget(){
|
||||||
|
const lowMemory=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(state.processMode==='power')return 1*1048576;if(state.processMode==='fine'||state.processMode==='validate')return(lowMemory||small?4:8)*1048576;
|
||||||
|
const base=(lowMemory||small?2:4)*1048576;return state.adaptivePixelBudget?Math.min(base,state.adaptivePixelBudget):base
|
||||||
|
}
|
||||||
|
function adaptStandardDeepBudget(deep){if(state.processMode!=='standard'||!deep)return false;const measuredMPP=renderPerf.deepMPP||.03,target=Math.max(32768,Math.min(4*1048576,Math.round(1400/measuredMPP))),oldBudget=state.adaptivePixelBudget||screenPixelBudget();if(target/oldBudget>=.8&&target/oldBudget<=1.25)return false;const oldPixels=canvas.width*canvas.height;state.adaptivePixelBudget=target;resize();return canvas.width*canvas.height!==oldPixels}
|
||||||
|
function resize(){
|
||||||
|
const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=screenPixelBudget(),nativeDpr=Math.max(1,window.devicePixelRatio||1),budgetDpr=Math.sqrt(budget/Math.max(1,cssW*cssH)),minDpr=Math.min(1,64/Math.max(cssW,cssH)),dpr=Math.max(minDpr,Math.min(nativeDpr,budgetDpr)),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){clearPanReuse();canvas.width=w;canvas.height=h;trimDetailCache();state.dirty=true;invalidateView()}
|
||||||
|
}
|
||||||
|
function jaEngine(s){return String(s||'').replace('preparing cached rebase reference…','参照軌道を準備中…').replace('persistent workers','常駐Worker').replace('main-thread','メインスレッド').replace('strict','厳密').replace('fallback','フォールバック').replace('rebase','リベース').replace('skip','スキップ').replace('residual','残差').replace('starting','起動中')}
|
||||||
|
function updateStats(){
|
||||||
|
runtimeMetrics.domWrites++;
|
||||||
|
const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';
|
||||||
|
$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();const deep=deepEngineNeeded(),diagnosticProfile=RENDER_PROFILE[RENDER_PASS.COVERED],ip=iterationPlan(diagnosticProfile,deep);$('#engine').textContent=deep?'高精度深部':'標準精度';$('#render').textContent=state.rendering?'描画中…':(state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了');let status;if(state.drawState==='REPROJECTED')status='再投影';else if(state.drawState==='PREVIEW')status=state.rendering?'プレビュー描画中':'プレビュー';else if(state.drawState==='COVERING')status='全域描画中';else if(state.drawState==='RESOLVING')status='未確定を追加計算 '+Math.round(state.continuationProgress*100)+'%';else if(state.drawState==='REFINING')status='境界AA '+state.detailDone+'/'+state.detailQueued;else if(state.drawState==='REFINED')status='境界AA 完了';else if(state.drawState==='VALIDATING')status='精度照合 '+Math.round(state.coverage*100)+'%';else if(state.drawState==='VALIDATION_INCOMPLETE')status='検証未完了';else if(state.drawState==='VALIDATED')status='検証完了';else status='全域描画 完了';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.effectiveDpr<(devicePixelRatio||1)*.99)status+=' · '+canvas.width+'×'+canvas.height;$('#badge').textContent=status;$('#compactStatus').textContent=status;const ledger=memoryLedger(),bp=blaProfile(diagnosticProfile);$('#diagEngine').textContent='engine: '+jaEngine(state.lastEngine)+' | workers '+deepPool.workers.length+' | '+(wasm&&wasm.simd?'SIMD':'scalar/JS');$('#diagNumeric').textContent='numeric: '+state.bits+' bit | iter '+ip.colorIter+' | BLA ε 2^-'+bp.exp+' | condition '+Math.round(referenceCache.conditionLog2||0)+' bit';$('#diagMemory').textContent='memory: managed '+(ledger.managedBytes/1048576).toFixed(1)+' / '+(ledger.budget/1048576).toFixed(0)+' MiB | canvas est. '+(ledger.canvasBytes/1048576).toFixed(1)+' MiB';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── Scheduler / interaction loop ────────────────────────────────────────
|
||||||
|
function nextQualityDue(deep){
|
||||||
|
if(state.processMode==='power'||state.dirty||state.rendering||state.pointerActive||state.wheelActive)return Infinity;
|
||||||
|
if(deep&&state.lastPass!==RENDER_PASS.COVERED)return Math.max(state.lastInteraction+680,state.lastFrameDone+260);
|
||||||
|
if(!deep&&state.lastPass!==RENDER_PASS.COVERED)return Math.max(state.lastInteraction+520,state.lastFrameDone+280);
|
||||||
|
return Infinity
|
||||||
|
}
|
||||||
|
function loop(now){
|
||||||
|
schedulerRAF=0;if(document.hidden)return;
|
||||||
|
const deep=deepEngineNeeded();
|
||||||
|
if(!state.pointerActive&&!state.wheelActive&&!state.rendering){
|
||||||
|
if(state.dirty)render(RENDER_PASS.PREVIEW);
|
||||||
|
else{
|
||||||
|
const due=nextQualityDue(deep);
|
||||||
|
if(due<=now)render(RENDER_PASS.COVERED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(needsPaint){needsPaint=false;paintFrame()}
|
||||||
|
if(needsStats){needsStats=false;updateStats()}
|
||||||
|
if(!state.rendering&&!state.pointerActive&&!state.wheelActive&&!state.dirty){
|
||||||
|
const due=nextQualityDue(deep);if(Number.isFinite(due))requestScheduler(Math.max(1,due-performance.now()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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){state.wheelActive=true;cancelDetailRefinement(true);cancelRender();capturePanSource()}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(wheelSettleTimer);wheelSettleTimer=setTimeout(()=>{wheelSettleTimer=0;state.wheelActive=false;state.lastInteraction=performance.now();state.dirty=true;recordView();saveHash(false);invalidateView()},110)},{passive:false});
|
||||||
|
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(pts.size===0){clearTimeout(pointerSettleTimer);pointerSettleTimer=0;clearTimeout(wheelSettleTimer);wheelSettleTimer=0;state.wheelActive=false;state.pointerActive=true;cancelDetailRefinement(true);cancelRender();capturePanSource()}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else if(pts.size===2){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 end(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(pointerSettleTimer);pointerSettleTimer=setTimeout(()=>{pointerSettleTimer=0;state.pointerActive=false;state.lastInteraction=performance.now();state.dirty=true;recordView();saveHash(false);invalidateView()},90)}canvas.addEventListener('pointerup',end);canvas.addEventListener('pointercancel',end);
|
||||||
|
function applyUiVisibility(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',String(!state.uiHidden))}
|
||||||
|
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;applyUiVisibility();try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}};
|
||||||
|
$('#zin').onclick=()=>{capturePanSource();zoomAt(innerWidth/2,innerHeight/2,.5);recordView();saveHash(false)};$('#zout').onclick=()=>{capturePanSource();zoomAt(innerWidth/2,innerHeight/2,2);recordView();saveHash(false)};$('#reset').onclick=()=>{reset();recordView()};
|
||||||
|
const exportJob={active:false,cancelled:false,bytes:0};let exportForcePrecision=false;
|
||||||
|
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),1500)}
|
||||||
|
function exportLedgerBytes(){return memoryLedger().logicalBytes}
|
||||||
|
function exportSampleShallow(cre,cim,scale,W,H,iter,x,y){const cr=cre+(x+.5-W*.5)*scale,ci=cim+(H*.5-y-.5)*scale;let zr=0,zi=0,zr2=0,zi2=0,n=0;while(n<iter&&zr2+zi2<=4){zi=2*zr*zi+ci;zr=zr2-zi2+cr;zr2=zr*zr;zi2=zi*zi;n++}return[n,n<iter?Math.max(4.0000001,zr2+zi2):0]}
|
||||||
|
async function runExport(){if(exportJob.active)return;const scaleChoice=Number($('#exportScale').value),w=Math.max(64,Math.min(16384,Math.round(scaleChoice?canvas.width*scaleChoice:Number($('#exportWidth').value)||canvas.width))),h=Math.max(1,Math.round(w*canvas.height/Math.max(1,canvas.width))),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),needed=w*h*8,budget=rendererMemoryBudget(),available=Math.max(0,budget-exportLedgerBytes());if(w>16384||h>16384){$('#exportStatus').textContent='辺の長さは 16384px 以下にしてください。';return}if(needed>available){$('#exportStatus').textContent='メモリ予算を超えます。幅または倍率を下げてください(必要 '+Math.ceil(needed/1048576)+' MiB / 空き '+Math.floor(available/1048576)+' MiB)。';return}const outCanvas=document.createElement('canvas');outCanvas.width=w;outCanvas.height=h;const oc=outCanvas.getContext('2d',{alpha:false});if(!oc){$('#exportStatus').textContent='出力 Canvas を作成できません。';return}const snap=snapshot(),iter=maxIter(),deep=deepEngineNeeded(snap,w),cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),W=w*ss,H=h*ss,pixelScale=sp/W,colorCtx=makeColorCtx(iter),tmp=new Uint8ClampedArray(4),tiles=[],tileW=exportForcePrecision?4:(deep?16:128),tileH=exportForcePrecision?1:(deep?4:24);for(let y=0;y<h;y+=tileH)for(let x=0;x<w;x+=tileW)tiles.push({x,y,w:Math.min(tileW,w-x),h:Math.min(tileH,h-y)});exportJob.active=true;exportJob.cancelled=false;exportJob.bytes=needed;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;$('#exportStatus').textContent='タイル描画中…';let done=0,unresolvedSamples=0;try{for(const tile of tiles){if(exportJob.cancelled)throw new Error('cancelled');const id=oc.createImageData(tile.w,tile.h),data=id.data;for(let yy=0;yy<tile.h;yy++)for(let xx=0;xx<tile.w;xx++){let ar=0,ag=0,ab=0;for(let sy=0;sy<ss;sy++)for(let sx=0;sx<ss;sx++){const gx=(tile.x+xx)*ss+sx,gy=(tile.y+yy)*ss+sy,sample=deep?await highPrecisionDirectPixelAsync(snap,W,H,iter,gx,gy,exportForcePrecision,()=>exportJob.cancelled):exportSampleShallow(cre,cim,pixelScale,W,H,iter,gx,gy);if(!sample)throw new Error('cancelled');const[n,m]=sample;if(n<iter){putFastColor(tmp,0,n,m,iter,colorCtx);ar+=linearChannel(tmp[0]);ag+=linearChannel(tmp[1]);ab+=linearChannel(tmp[2])}else{ar+=linearChannel(20);ag+=linearChannel(22);ab+=linearChannel(30);unresolvedSamples++}}const samples=ss*ss,oi=(yy*tile.w+xx)*4;data[oi]=srgbChannel(ar/samples);data[oi+1]=srgbChannel(ag/samples);data[oi+2]=srgbChannel(ab/samples);data[oi+3]=255}oc.putImageData(id,tile.x,tile.y);done++;$('#exportProgress').value=done/tiles.length;$('#exportStatus').textContent='生成中 '+Math.round(done/tiles.length*100)+'%';await new Promise(requestAnimationFrame)}if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGを圧縮中…';const blob=await new Promise(resolve=>outCanvas.toBlob(resolve,'image/png'));if(!blob)throw new Error('PNG encode failed');const stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v23',rendererVersion:23,pixelContract:'centered',width:w,height:h,supersampling:ss,sampleCount:w*h*ss*ss,unresolvedSamples,membershipCertified:false,precisionPolicy:{mode:exportForcePrecision?'validated-direct':'balanced',baseBits:snap.bits,agreementGuardBits:exportForcePrecision?64:0},iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},numericEngine:deep?'bigint-fixed-direct':'javascript-f64-direct',kernelSha256:globalThis.MANDEL_KERNEL_META||null,colorSpace:'sRGB with linear-light sample resolve',encoder:{mime:'image/png',api:'HTMLCanvasElement.toBlob'},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},determinism:{randomSeed:null,tiled:true,allTilesCompleted:true}};downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');$('#exportStatus').textContent='PNG と座標メタデータを保存しました。'}catch(e){$('#exportStatus').textContent=String(e&&e.message)==='cancelled'?'出力を中止しました。':'出力に失敗しました: '+String(e&&e.message||e)}finally{exportJob.active=false;exportJob.bytes=0;$('#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','')};
|
||||||
|
$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(!blob)return;downloadBlob(blob,'mandelbrot-'+state.drawState.toLowerCase()+'-'+Date.now()+'.png');$('#exportStatus').textContent='現在の表示('+state.drawState+')を保存しました。'},'image/png');
|
||||||
|
$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(Math.min(16384,canvas.width*s))};$('#exportStart').onclick=async()=>{exportForcePrecision=$('#exportPrecision').value==='validated';try{await runExport()}finally{exportForcePrecision=false}};$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};
|
||||||
|
function toast(s){const t=$('#toast');t.textContent=s;t.classList.add('show');clearTimeout(toast._t);toast._t=setTimeout(()=>t.classList.remove('show'),1500)}
|
||||||
|
let lastWrittenHash='',navigationHash='';
|
||||||
|
const viewHistory=[];let viewHistoryIndex=-1;
|
||||||
|
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(),key=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===key)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;state.bits=v.bits;state.re=v.re;state.im=v.im;state.span=v.span;state.palette=v.palette;state.cycle=v.cycle;state.shift=v.shift;state.baseIter=v.baseIter;state.adaptive=v.adaptive;clearDetailCache();clearPanReuse();ensurePrecision();syncControls();saveHash(false);setDirty()}
|
||||||
|
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
|
||||||
|
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
|
||||||
|
function saveHash(push){const p=new URLSearchParams();p.set('v','23');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{if(location.protocol==='file:'||location.origin==='null'){if(location.hash!==h)location.hash=h}else{push?history.pushState(null,'',h):history.replaceState(null,'',h)}}catch{}}
|
||||||
|
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 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(Math.round(state.baseIter));$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
|
||||||
|
function applyHashNavigation(){const h=location.hash;if(h===lastWrittenHash){lastWrittenHash='';return}if(h===navigationHash)return;navigationHash=h;setTimeout(()=>{navigationHash=''},0);if(loadHash()){clearDetailCache();clearPanReuse();recordView();syncControls();setDirty()}}
|
||||||
|
$('#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);setDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
|
||||||
|
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:23,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()}};
|
||||||
|
function bind(id,key,out,fmt,flush=false){const el=$(id),o=$(out),apply=()=>{state[key]=Number(el.value);o.textContent=fmt(Number(el.value));if(flush)clearDetailCache();setDirty()};el.addEventListener('input',apply);apply()}
|
||||||
|
function bindColor(id,key,out,fmt){const el=$(id),o=$(out),apply=()=>{state[key]=Number(el.value);o.textContent=fmt(Number(el.value));if(!recolorCurrentField())setDirty()};el.addEventListener('input',apply);apply()}
|
||||||
|
bind('#iters','baseIter','#itersO',x=>String(Math.round(x)),true);bindColor('#cycle','cycle','#cycleO',x=>x.toFixed(4));bindColor('#shift','shift','#shiftO',x=>x.toFixed(2));
|
||||||
|
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));if(!recolorCurrentField())setDirty()};
|
||||||
|
$('#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';state.adaptivePixelBudget=0;$('#hq').checked=state.hq;try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}resize();setDirty()};
|
||||||
|
$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;clearDetailCache();setDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;if(!state.hq)cancelDetailRefinement(true);setDirty()};
|
||||||
|
addEventListener('resize',()=>{resize();setDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let handled=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'){capturePanSource();pan(innerWidth*.08,0)}else if(e.key==='ArrowRight'){capturePanSource();pan(-innerWidth*.08,0)}else if(e.key==='ArrowUp'){capturePanSource();pan(0,innerHeight*.08)}else if(e.key==='ArrowDown'){capturePanSource();pan(0,-innerHeight*.08)}else handled=false;if(handled){e.preventDefault();recordView();saveHash(false)}});addEventListener('popstate',applyHashNavigation);addEventListener('hashchange',applyHashNavigation);
|
||||||
|
function stopSchedulers(){if(schedulerRAF){cancelAnimationFrame(schedulerRAF);schedulerRAF=0}if(schedulerTimer){clearTimeout(schedulerTimer);schedulerTimer=0;schedulerDue=0}if(wheelSettleTimer){clearTimeout(wheelSettleTimer);wheelSettleTimer=0}if(pointerSettleTimer){clearTimeout(pointerSettleTimer);pointerSettleTimer=0}cancelUnknownContinuation()}
|
||||||
|
addEventListener('visibilitychange',()=>{if(document.hidden){exportJob.cancelled=true;stopSchedulers();state.wheelActive=false;cancelRender();cancelDetailRefinement(true)}else{state.lastInteraction=performance.now();state.dirty=true;invalidateView()}});
|
||||||
|
globalThis.__MANDEL_DIAG__={snapshot:()=>{const deep=deepEngineNeeded(),ledger=memoryLedger(),coveredProfile=RENDER_PROFILE[RENDER_PASS.COVERED];return{rendererVersion:23,pixelContract:'centered',processMode:state.processMode,automaticTarget:modeTarget(),drawState:state.drawState,coverage:state.coverage,unresolved:state.unresolved,zoom:zoomExp(),deep,legacyDeepThreshold:LEGACY_DEEP_ZOOM_THRESHOLD,bits:state.bits,palette:state.palette,rendering:state.rendering,validating:validationJob.active,exporting:exportJob.active,lastRender:state.lastRender,lastPass:state.lastPass,transformReuse:!!state.panReuse,detailCache:DETAIL_TILE_CACHE.size,detailCacheBytes,detailCacheBudget:detailCacheBudget(),detailActive:state.detailActive,frame:frameCanvas.width+'x'+frameCanvas.height,frameCoverage:frameCanvas.width/Math.max(1,canvas.width),hqTarget:targetSize(coveredProfile,deep).join('x'),scheduler:{raf:!!schedulerRAF,timer:!!schedulerTimer,needsPaint,needsStats,wheelActive:state.wheelActive,pointerSettle:!!pointerSettleTimer,unknownTimer:!!unknownContinuationTimer,backgroundJobs:runtimeMetrics.pendingBackgroundJobs},screen:{effectiveDpr:state.effectiveDpr,deviceDpr:window.devicePixelRatio||1,pixelBudget:state.screenPixelBudget,pixels:canvas.width*canvas.height},memory:ledger,shallowAssets:{wasmReady:!!wasm,worker:!!shallowWorker,workerBusy:shallowWorkerBusy},deepAssets:{wasmReady:!!deepWasm,workers:deepPool.workers.length},runtimeMetrics:{...runtimeMetrics},telemetry:{...deepTelemetry},renderPerf:{...renderPerf},wisdom:{workerCount:deepWisdom.workerCount,rowMsEMA:deepWisdom.rowMsEMA,stripRows:deepWisdom.stripRows,realBench:deepWisdom.realBench},reference:{id:referenceCache.id,bits:referenceCache.bits,n:referenceCache.n,escape:referenceCache.escape,buildMs:refControl.lastBuildMs,buildEMA:refControl.buildEMA,baseMPP:refControl.baseMPP,lastMPP:refControl.lastMPP,conditionLog2:referenceCache.conditionLog2,checkpointBits:referenceCache.checkpointBits,checkpointCount:referenceCache.checkpointCount,checkpointMismatch:referenceCache.checkpointMismatch}}},memoryLedger:()=>memoryLedger()};
|
||||||
|
addEventListener('pagehide',()=>{stopSchedulers();destroyShallowWorker();destroyDeepPool()},{once:true});
|
||||||
|
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const savedMode=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(savedMode)){state.processMode=savedMode;state.hq=savedMode==='fine'||savedMode==='validate'}}catch{}restoreDeepWisdom();applyUiVisibility();resize();if(!loadHash())reset();else setDirty();recordView();syncControls();ctx.fillStyle='#050813';ctx.fillRect(0,0,canvas.width,canvas.height);render(RENDER_PASS.PREVIEW);requestScheduler();
|
||||||
|
})();
|
||||||
43
scripts/build-hosted.ps1
Normal file
43
scripts/build-hosted.ps1
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
param([string]$OutputDirectory = (Join-Path $PSScriptRoot '..\dist\hosted'))
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
$output = [System.IO.Path]::GetFullPath($OutputDirectory)
|
||||||
|
if (-not $output.StartsWith($workspace, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
throw "OutputDirectory must stay inside the workspace: $output"
|
||||||
|
}
|
||||||
|
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'extract-wasm.ps1') | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "WASM extraction failed with exit code $LASTEXITCODE." }
|
||||||
|
New-Item -ItemType Directory -Path $output -Force | Out-Null
|
||||||
|
$manifestPath = Join-Path $workspace 'dist\wasm\manifest.json'
|
||||||
|
$manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
$html = Get-Content -LiteralPath (Join-Path $workspace 'index.html') -Raw -Encoding UTF8
|
||||||
|
$html = [regex]::Replace($html, '<script src="kernels\.js"></script>\s*<script src="script\.js"></script>', '<script type="module" src="./hosted-loader.js"></script>')
|
||||||
|
Set-Content -LiteralPath (Join-Path $output 'index.html') -Value $html -Encoding UTF8
|
||||||
|
Copy-Item -LiteralPath (Join-Path $workspace 'script.js') -Destination (Join-Path $output 'script.js') -Force
|
||||||
|
$loader = Get-Content -LiteralPath (Join-Path $workspace 'hosted-loader.js') -Raw -Encoding UTF8
|
||||||
|
$kernelMeta = [ordered]@{}
|
||||||
|
foreach ($payload in $manifest.payloads) { $kernelMeta[$payload.symbol] = $payload.sha256 }
|
||||||
|
$loader = $loader.Replace('/*__KERNEL_META__*/{}', ($kernelMeta | ConvertTo-Json -Compress))
|
||||||
|
$expectedHashed = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
||||||
|
foreach ($payload in $manifest.payloads) {
|
||||||
|
$extension = [System.IO.Path]::GetExtension($payload.file)
|
||||||
|
$stem = [System.IO.Path]::GetFileNameWithoutExtension($payload.file)
|
||||||
|
$hashed = "$stem.$($payload.sha256.Substring(0,16))$extension"
|
||||||
|
[void]$expectedHashed.Add($hashed)
|
||||||
|
Copy-Item -LiteralPath (Join-Path (Split-Path $manifestPath) $payload.file) -Destination (Join-Path (Split-Path $manifestPath) $hashed) -Force
|
||||||
|
$loader = $loader.Replace($payload.file, $hashed)
|
||||||
|
}
|
||||||
|
$wasmDirectory = (Resolve-Path -LiteralPath (Split-Path $manifestPath)).Path
|
||||||
|
if (-not $wasmDirectory.StartsWith($workspace + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
throw "Refusing to prune hashed assets outside the workspace: $wasmDirectory"
|
||||||
|
}
|
||||||
|
foreach ($asset in Get-ChildItem -LiteralPath $wasmDirectory -File) {
|
||||||
|
if ($asset.Name -match '^[a-z0-9-]+\.[0-9a-f]{16}\.wasm$' -and -not $expectedHashed.Contains($asset.Name)) {
|
||||||
|
Remove-Item -LiteralPath $asset.FullName -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[System.IO.File]::WriteAllText((Join-Path $output 'hosted-loader.js'), $loader, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
Copy-Item -LiteralPath (Join-Path $workspace 'hosted-headers.txt') -Destination (Join-Path (Split-Path $output) '_headers') -Force
|
||||||
|
Write-Output $output
|
||||||
50
scripts/build-kernels.ps1
Normal file
50
scripts/build-kernels.ps1
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
param(
|
||||||
|
[string]$ManifestPath = (Join-Path $PSScriptRoot '..\dist\wasm\manifest.json'),
|
||||||
|
[string]$OutputPath = (Join-Path $PSScriptRoot '..\kernels.js')
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
$output = [System.IO.Path]::GetFullPath($OutputPath)
|
||||||
|
if (-not $output.StartsWith($workspace, [System.StringComparison]::OrdinalIgnoreCase)) { throw 'OutputPath must stay inside the workspace.' }
|
||||||
|
$manifest = Get-Content -LiteralPath $ManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
$values = @{}
|
||||||
|
foreach ($payload in $manifest.payloads) {
|
||||||
|
$path = Join-Path (Split-Path $ManifestPath) $payload.file
|
||||||
|
$values[$payload.symbol] = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path))
|
||||||
|
}
|
||||||
|
foreach ($required in @('WASM_SIMD_B64','WASM_SCALAR_B64','DEEP_SIMD_B64','DEEP_SCALAR_B64','BLA_SIMD_B64','BLA_SCALAR_B64','COLOR_SIMD_B64','COLOR_SCALAR_B64')) {
|
||||||
|
if (-not $values.ContainsKey($required)) { throw "Missing payload: $required" }
|
||||||
|
}
|
||||||
|
$colorDeduplicated = $values.COLOR_SIMD_B64 -eq $values.COLOR_SCALAR_B64
|
||||||
|
$lines = @(
|
||||||
|
"'use strict';"
|
||||||
|
'// Generated from dist/wasm/manifest.json. Run scripts/extract-wasm.ps1 before replacing compatibility inputs.'
|
||||||
|
)
|
||||||
|
if ($colorDeduplicated) { $lines += "const COLOR_B64='$($values.COLOR_SIMD_B64)';" }
|
||||||
|
$lines += @(
|
||||||
|
'globalThis.MANDEL_KERNELS=Object.freeze({'
|
||||||
|
" WASM_SIMD_B64:'$($values.WASM_SIMD_B64)',"
|
||||||
|
" WASM_SCALAR_B64:'$($values.WASM_SCALAR_B64)',"
|
||||||
|
" DEEP_SIMD_B64:'$($values.DEEP_SIMD_B64)',"
|
||||||
|
" DEEP_SCALAR_B64:'$($values.DEEP_SCALAR_B64)',"
|
||||||
|
" BLA_SIMD_B64:'$($values.BLA_SIMD_B64)',"
|
||||||
|
" BLA_SCALAR_B64:'$($values.BLA_SCALAR_B64)',"
|
||||||
|
)
|
||||||
|
if ($colorDeduplicated) {
|
||||||
|
$lines += @(' COLOR_SIMD_B64:COLOR_B64,', ' COLOR_SCALAR_B64:COLOR_B64')
|
||||||
|
} else {
|
||||||
|
$lines += @(" COLOR_SIMD_B64:'$($values.COLOR_SIMD_B64)',", " COLOR_SCALAR_B64:'$($values.COLOR_SCALAR_B64)'")
|
||||||
|
}
|
||||||
|
$lines += @(
|
||||||
|
'});'
|
||||||
|
'globalThis.MANDEL_KERNEL_META=Object.freeze({'
|
||||||
|
)
|
||||||
|
for ($index = 0; $index -lt $manifest.payloads.Count; $index++) {
|
||||||
|
$payload = $manifest.payloads[$index]
|
||||||
|
$comma = if ($index -lt $manifest.payloads.Count - 1) { ',' } else { '' }
|
||||||
|
$lines += " '$($payload.symbol)':'$($payload.sha256)'$comma"
|
||||||
|
}
|
||||||
|
$lines += '});'
|
||||||
|
[System.IO.File]::WriteAllText($output, ($lines -join [Environment]::NewLine) + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
Write-Output $output
|
||||||
13
scripts/build-standalone.ps1
Normal file
13
scripts/build-standalone.ps1
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
param([string]$OutputDirectory = (Join-Path $PSScriptRoot '..\dist\standalone'))
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
$output = [System.IO.Path]::GetFullPath($OutputDirectory)
|
||||||
|
if (-not $output.StartsWith($workspace, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
throw "OutputDirectory must stay inside the workspace: $output"
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Path $output -Force | Out-Null
|
||||||
|
foreach ($name in @('index.html','script.js','kernels.js')) {
|
||||||
|
Copy-Item -LiteralPath (Join-Path $workspace $name) -Destination (Join-Path $output $name) -Force
|
||||||
|
}
|
||||||
|
Write-Output $output
|
||||||
64
scripts/build-wasm.ps1
Normal file
64
scripts/build-wasm.ps1
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
param(
|
||||||
|
[string]$Compiler = 'clang',
|
||||||
|
[string]$OutputDirectory = (Join-Path $PSScriptRoot '..\build\wasm-v23'),
|
||||||
|
[switch]$Promote
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
$output = [System.IO.Path]::GetFullPath($OutputDirectory)
|
||||||
|
if (-not $output.StartsWith($workspace, [System.StringComparison]::OrdinalIgnoreCase)) { throw 'OutputDirectory must stay inside the workspace.' }
|
||||||
|
$lock = Get-Content -LiteralPath (Join-Path $workspace 'toolchain.lock.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
$versionLine = (& $Compiler --version | Select-Object -First 1)
|
||||||
|
if ($versionLine -notmatch [regex]::Escape($lock.version)) { throw "Expected clang $($lock.version), got: $versionLine" }
|
||||||
|
New-Item -ItemType Directory -Path $output -Force | Out-Null
|
||||||
|
|
||||||
|
$common = @('--target=wasm32-unknown-unknown') + @($lock.commonFlags)
|
||||||
|
$variants = @(
|
||||||
|
[pscustomobject]@{ name='simd'; flags=@($lock.simdFlags) },
|
||||||
|
[pscustomobject]@{ name='scalar'; flags=@($lock.scalarFlags) }
|
||||||
|
)
|
||||||
|
$modules = @(
|
||||||
|
[pscustomobject]@{ symbol='WASM'; name='wasm'; source='shallow_kernel.c'; memory=2097152 },
|
||||||
|
[pscustomobject]@{ symbol='DEEP'; name='deep'; source='deep_kernel.c'; memory=8388608 },
|
||||||
|
[pscustomobject]@{ symbol='BLA'; name='bla'; source='bla_kernel_v18.c'; memory=33554432 },
|
||||||
|
[pscustomobject]@{ symbol='COLOR'; name='color'; source='color_kernel.c'; memory=2097152 }
|
||||||
|
)
|
||||||
|
$payloads = @()
|
||||||
|
foreach ($module in $modules) {
|
||||||
|
foreach ($variant in $variants) {
|
||||||
|
$file = "$($module.name)-$($variant.name).wasm"
|
||||||
|
$path = Join-Path $output $file
|
||||||
|
$args = $common + @($variant.flags) + @("-Wl,--initial-memory=$($module.memory)", "-Wl,--max-memory=$($module.memory)", '-o', $path, (Join-Path $workspace "src\$($module.source)") )
|
||||||
|
& $Compiler @args
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "clang failed for $file" }
|
||||||
|
$payloads += [ordered]@{
|
||||||
|
symbol = "$($module.symbol)_$($variant.name.ToUpperInvariant())_B64"
|
||||||
|
file = $file
|
||||||
|
bytes = (Get-Item -LiteralPath $path).Length
|
||||||
|
sha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
source = "src/$($module.source)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$manifest = [ordered]@{
|
||||||
|
format = 'mandelbrot-source-wasm-manifest-v23'
|
||||||
|
toolchain = $lock
|
||||||
|
abi = 'src/abi.json'
|
||||||
|
payloads = $payloads
|
||||||
|
}
|
||||||
|
$manifestPath = Join-Path $output 'manifest.json'
|
||||||
|
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
|
||||||
|
|
||||||
|
if ($Promote) {
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'tests\kernel-golden.ps1') -GeneratedDirectory $output
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Generated-WASM golden gate failed; payloads were not promoted.' }
|
||||||
|
$generatedKernels = Join-Path $output 'kernels.js'
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'scripts\build-kernels.ps1') -ManifestPath $manifestPath -OutputPath $generatedKernels
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'kernels.js generation failed; payloads were not promoted.' }
|
||||||
|
$dist = Join-Path $workspace 'dist\wasm'
|
||||||
|
foreach ($payload in $payloads) { Copy-Item -LiteralPath (Join-Path $output $payload.file) -Destination (Join-Path $dist $payload.file) -Force }
|
||||||
|
Copy-Item -LiteralPath $manifestPath -Destination (Join-Path $dist 'manifest.json') -Force
|
||||||
|
Copy-Item -LiteralPath $generatedKernels -Destination (Join-Path $workspace 'kernels.js') -Force
|
||||||
|
}
|
||||||
|
Write-Output $manifestPath
|
||||||
56
scripts/extract-wasm.ps1
Normal file
56
scripts/extract-wasm.ps1
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
param(
|
||||||
|
[string]$KernelFile = (Join-Path $PSScriptRoot '..\kernels.js'),
|
||||||
|
[string]$OutputDirectory = (Join-Path $PSScriptRoot '..\dist\wasm')
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$kernelPath = (Resolve-Path -LiteralPath $KernelFile).Path
|
||||||
|
$workspacePath = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
$resolvedOutput = [System.IO.Path]::GetFullPath($OutputDirectory)
|
||||||
|
if (-not $resolvedOutput.StartsWith($workspacePath, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
throw "OutputDirectory must stay inside the workspace: $resolvedOutput"
|
||||||
|
}
|
||||||
|
|
||||||
|
$source = Get-Content -LiteralPath $kernelPath -Raw -Encoding UTF8
|
||||||
|
$kernelObject = [regex]::Match($source, 'MANDEL_KERNELS\s*=\s*Object\.freeze\(\{(?<body>[\s\S]*?)\}\);')
|
||||||
|
if (-not $kernelObject.Success) { throw 'MANDEL_KERNELS payload object was not found.' }
|
||||||
|
$payloadSource = $kernelObject.Groups['body'].Value
|
||||||
|
$entries = [ordered]@{}
|
||||||
|
foreach ($match in [regex]::Matches($payloadSource, '([A-Z0-9_]+_B64)\s*:\s*''([^'']+)''')) {
|
||||||
|
$entries[$match.Groups[1].Value] = $match.Groups[2].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generated kernels.js may store byte-identical color variants once.
|
||||||
|
# Keep both public symbols in the manifest so hosted assets and compatibility
|
||||||
|
# checks remain stable when the standalone source uses the shared blob form.
|
||||||
|
$sharedColor = [regex]::Match($source, 'const\s+COLOR_B64\s*=\s*''([^'']+)''')
|
||||||
|
if ($sharedColor.Success) {
|
||||||
|
foreach ($symbol in @('COLOR_SIMD_B64', 'COLOR_SCALAR_B64')) {
|
||||||
|
if ($payloadSource -match ([regex]::Escape($symbol) + '\s*:\s*COLOR_B64')) {
|
||||||
|
$entries[$symbol] = $sharedColor.Groups[1].Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($entries.Count -lt 8) { throw "Expected at least 8 embedded WASM payload symbols; found $($entries.Count)." }
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $resolvedOutput -Force | Out-Null
|
||||||
|
$manifest = [ordered]@{
|
||||||
|
format = 'mandelbrot-wasm-manifest-v1'
|
||||||
|
generatedUtc = [DateTime]::UtcNow.ToString('o')
|
||||||
|
input = $kernelPath.Substring($workspacePath.Length).TrimStart('\').Replace('\','/')
|
||||||
|
payloads = @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($entry in $entries.GetEnumerator()) {
|
||||||
|
$symbol = $entry.Key
|
||||||
|
$bytes = [Convert]::FromBase64String($entry.Value)
|
||||||
|
$name = ($symbol -replace '_B64$','').ToLowerInvariant().Replace('_','-') + '.wasm'
|
||||||
|
$path = Join-Path $resolvedOutput $name
|
||||||
|
[System.IO.File]::WriteAllBytes($path, $bytes)
|
||||||
|
$hash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
$manifest.payloads += [ordered]@{ symbol=$symbol; file=$name; bytes=$bytes.Length; sha256=$hash }
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifestPath = Join-Path $resolvedOutput 'manifest.json'
|
||||||
|
$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
|
||||||
|
Write-Output $manifestPath
|
||||||
98
scripts/source-baseline.ps1
Normal file
98
scripts/source-baseline.ps1
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'build-hosted.ps1') | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Hosted build failed with exit code $LASTEXITCODE." }
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'build-standalone.ps1') | Out-Null
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Standalone build failed with exit code $LASTEXITCODE." }
|
||||||
|
$contract = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'tests\source-contract.ps1') | ConvertFrom-Json
|
||||||
|
if ($LASTEXITCODE -ne 0 -or $contract.status -ne 'pass') { throw 'Source contract failed; baseline was not written.' }
|
||||||
|
$wasmManifest = Get-Content -LiteralPath (Join-Path $workspace 'dist\wasm\manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
$shallowPayload = $wasmManifest.payloads | Where-Object symbol -eq 'WASM_SIMD_B64' | Select-Object -First 1
|
||||||
|
$shallowHashed = ([System.IO.Path]::GetFileNameWithoutExtension($shallowPayload.file) + '.' + $shallowPayload.sha256.Substring(0,16) + [System.IO.Path]::GetExtension($shallowPayload.file))
|
||||||
|
|
||||||
|
function File-Info([string]$Path) {
|
||||||
|
$item = Get-Item -LiteralPath $Path
|
||||||
|
[pscustomobject][ordered]@{ file=$item.FullName.Substring($workspace.Length).TrimStart('\').Replace('\','/'); bytes=$item.Length; sha256=(Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() }
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = Get-Command node -ErrorAction SilentlyContinue
|
||||||
|
$clang = Get-Command clang -ErrorAction SilentlyContinue
|
||||||
|
$nodeVersion = if ($node) { (& $node.Source --version | Select-Object -First 1) } else { $null }
|
||||||
|
$sourceBuildVerified = $false
|
||||||
|
$sourceBuildReason = 'Pinned clang 17.0.6 and Node CLI were not available.'
|
||||||
|
if ($node -and $clang -and ((& $clang.Source --version | Select-Object -First 1) -match '17\.0\.6')) {
|
||||||
|
$generatedManifest = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'scripts\build-wasm.ps1') -Compiler $clang.Source
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Source-WASM build failed while recording the baseline.' }
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'tests\kernel-golden.ps1') -GeneratedDirectory (Split-Path $generatedManifest)
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Generated-WASM golden failed while recording the baseline.' }
|
||||||
|
$sourceBuildVerified = $true
|
||||||
|
$sourceBuildReason = $null
|
||||||
|
} else {
|
||||||
|
$previousPath = Join-Path $workspace 'audit\v23-source-baseline.json'
|
||||||
|
$candidateManifest = Join-Path $workspace 'build\wasm-v23\manifest.json'
|
||||||
|
if ((Test-Path -LiteralPath $previousPath) -and (Test-Path -LiteralPath $candidateManifest)) {
|
||||||
|
$previous = Get-Content -LiteralPath $previousPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
$candidateHash = (Get-FileHash -LiteralPath $candidateManifest -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
$sourceInputsMatch = $true
|
||||||
|
foreach ($record in $previous.sourceBuild.files) {
|
||||||
|
$recordPath = Join-Path $workspace ($record.file.Replace('/', [System.IO.Path]::DirectorySeparatorChar))
|
||||||
|
if (-not (Test-Path -LiteralPath $recordPath) -or (Get-FileHash -LiteralPath $recordPath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $record.sha256) { $sourceInputsMatch = $false; break }
|
||||||
|
}
|
||||||
|
if ($sourceInputsMatch -and $previous.sourceBuild.compiledInThisEnvironment -and $previous.sourceBuild.goldenStatus -eq 'pass' -and $previous.sourceBuild.generatedManifest.sha256 -eq $candidateHash) {
|
||||||
|
$generatedManifest = $candidateManifest
|
||||||
|
$sourceBuildVerified = $true
|
||||||
|
$nodeVersion = $previous.sourceBuild.node
|
||||||
|
$sourceBuildReason = 'Verified generated manifest reused; fixed toolchain was removed after the successful gate.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hostedFiles = @(
|
||||||
|
File-Info (Join-Path $workspace 'dist\hosted\index.html')
|
||||||
|
File-Info (Join-Path $workspace 'dist\hosted\hosted-loader.js')
|
||||||
|
File-Info (Join-Path $workspace 'dist\hosted\script.js')
|
||||||
|
File-Info (Join-Path $workspace "dist\wasm\$shallowHashed")
|
||||||
|
)
|
||||||
|
$standaloneFiles = @(
|
||||||
|
File-Info (Join-Path $workspace 'dist\standalone\index.html')
|
||||||
|
File-Info (Join-Path $workspace 'dist\standalone\script.js')
|
||||||
|
File-Info (Join-Path $workspace 'dist\standalone\kernels.js')
|
||||||
|
)
|
||||||
|
$kernelSources = @(
|
||||||
|
File-Info (Join-Path $workspace 'src\shallow_kernel.c')
|
||||||
|
File-Info (Join-Path $workspace 'src\deep_kernel.c')
|
||||||
|
File-Info (Join-Path $workspace 'src\bla_kernel_v18.c')
|
||||||
|
File-Info (Join-Path $workspace 'src\color_kernel.c')
|
||||||
|
File-Info (Join-Path $workspace 'src\abi.json')
|
||||||
|
File-Info (Join-Path $workspace 'toolchain.lock.json')
|
||||||
|
)
|
||||||
|
$baseline = [ordered]@{
|
||||||
|
format = 'mandelbrot-source-baseline-v23'
|
||||||
|
generatedUtc = [DateTime]::UtcNow.ToString('o')
|
||||||
|
rendererVersion = 23
|
||||||
|
contract = $contract
|
||||||
|
hosted = [ordered]@{
|
||||||
|
firstViewFiles = $hostedFiles
|
||||||
|
firstViewUncompressedBytes = ($hostedFiles | Measure-Object bytes -Sum).Sum
|
||||||
|
deepRequestsBeforeDeepView = 0
|
||||||
|
note = 'Transfer compression, parse, compile, and runtime timings require the fixed browser benchmark.'
|
||||||
|
}
|
||||||
|
standalone = [ordered]@{
|
||||||
|
files = $standaloneFiles
|
||||||
|
uncompressedBytes = ($standaloneFiles | Measure-Object bytes -Sum).Sum
|
||||||
|
}
|
||||||
|
sourceBuild = [ordered]@{
|
||||||
|
files=$kernelSources
|
||||||
|
compiler='clang 17.0.6'
|
||||||
|
node=$nodeVersion
|
||||||
|
compiledInThisEnvironment=$sourceBuildVerified
|
||||||
|
goldenStatus=if ($sourceBuildVerified) { 'pass' } else { 'not-run' }
|
||||||
|
generatedManifest=if ($sourceBuildVerified) { File-Info $generatedManifest } else { $null }
|
||||||
|
reason=$sourceBuildReason
|
||||||
|
}
|
||||||
|
browserBenchmark = [ordered]@{ status='not-run'; report='audit/v23-browser-baseline.json'; reason='No in-app browser binding was available in this environment.' }
|
||||||
|
}
|
||||||
|
$path = Join-Path $workspace 'audit\v23-source-baseline.json'
|
||||||
|
$baseline | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8
|
||||||
|
Write-Output $path
|
||||||
32
scripts/test-all.ps1
Normal file
32
scripts/test-all.ps1
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
param([switch]$RequireToolchain)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
|
||||||
|
function Run-CheckedPowerShell([string]$Path) {
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $Path
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "PowerShell test failed: $Path" }
|
||||||
|
}
|
||||||
|
Run-CheckedPowerShell (Join-Path $workspace 'tests\kernel-source-contract.ps1')
|
||||||
|
Run-CheckedPowerShell (Join-Path $workspace 'tests\source-contract.ps1')
|
||||||
|
|
||||||
|
$node = Get-Command node -ErrorAction SilentlyContinue
|
||||||
|
if ($node) {
|
||||||
|
foreach ($test in @('js-syntax.mjs','module-clone.mjs','precision-reference.mjs','analytic-interior.mjs','pixel-mapping.mjs','pixel-contract.mjs','runtime-budget.mjs')) {
|
||||||
|
& $node.Source (Join-Path $workspace "tests\$test")
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Node regression test failed: $test" }
|
||||||
|
}
|
||||||
|
} elseif ($RequireToolchain) { throw 'Node.js is required by -RequireToolchain.' }
|
||||||
|
else { Write-Warning 'Node.js is unavailable; executable JavaScript/WASM tests were skipped.' }
|
||||||
|
|
||||||
|
$clang = Get-Command clang -ErrorAction SilentlyContinue
|
||||||
|
$sourceGolden = $false
|
||||||
|
if ($clang -and $node -and ((& $clang.Source --version | Select-Object -First 1) -match '17\.0\.6')) {
|
||||||
|
$generated = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'scripts\build-wasm.ps1') -Compiler $clang.Source
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Source-WASM build failed.' }
|
||||||
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'tests\kernel-golden.ps1') -GeneratedDirectory (Split-Path $generated)
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Generated-WASM golden failed.' }
|
||||||
|
$sourceGolden = $true
|
||||||
|
} elseif ($RequireToolchain) { throw 'clang 17.0.6 is required by -RequireToolchain.' }
|
||||||
|
else { Write-Warning 'clang 17.0.6 is unavailable; source-WASM build/golden was skipped.' }
|
||||||
|
|
||||||
|
[ordered]@{ status='pass'; nodeTests=[bool]$node; sourceWasmGolden=$sourceGolden } | ConvertTo-Json
|
||||||
23
src/abi.json
Normal file
23
src/abi.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"format": "mandelbrot-kernel-abi-v23",
|
||||||
|
"pixelContract": "sample centers at x + 0.5, y + 0.5",
|
||||||
|
"limits": { "outputPixels": 65536, "referenceSamples": 150001 },
|
||||||
|
"modules": {
|
||||||
|
"shallow": {
|
||||||
|
"source": "shallow_kernel.c",
|
||||||
|
"exports": ["memory", "counts_ptr", "mags_ptr", "render_rows"]
|
||||||
|
},
|
||||||
|
"deep": {
|
||||||
|
"source": "deep_kernel.c",
|
||||||
|
"exports": ["memory", "counts_ptr", "mags_ptr", "refs_r_ptr", "refs_i_ptr", "render_perturb_rebase_rect"]
|
||||||
|
},
|
||||||
|
"bla": {
|
||||||
|
"source": "bla_kernel_v18.c",
|
||||||
|
"exports": ["memory", "counts_ptr", "mags_ptr", "refs_r_ptr", "refs_i_ptr", "build_bla", "render_bla_rect", "render_bla_rect_v2", "stat_bla_steps", "stat_ptb_steps", "stat_rebases", "stat_interior", "stat_unresolved", "stat_fail"]
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"source": "color_kernel.c",
|
||||||
|
"exports": ["memory", "mags_ptr", "corr_ptr", "smooth_batch"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
204
src/bla_kernel_v18.c
Normal file
204
src/bla_kernel_v18.c
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define MAX_REF 150001
|
||||||
|
#define MAX_BLA 300100
|
||||||
|
#define OUT_MAX 65536
|
||||||
|
#define MAX_LEVELS 20
|
||||||
|
#define STATUS_FAIL 0xffffffffu
|
||||||
|
#define STATUS_UNRESOLVED 0xfffffffeu
|
||||||
|
|
||||||
|
void *memset(void *dst,int c,unsigned long n){unsigned char *p=(unsigned char*)dst;for(unsigned long i=0;i<n;i++)p[i]=(unsigned char)c;return dst;}
|
||||||
|
|
||||||
|
static uint32_t counts[OUT_MAX];
|
||||||
|
static double mags[OUT_MAX];
|
||||||
|
static double refs_r[MAX_REF];
|
||||||
|
static double refs_i[MAX_REF];
|
||||||
|
static double bla_ar[MAX_BLA];
|
||||||
|
static double bla_ai[MAX_BLA];
|
||||||
|
static double bla_br[MAX_BLA];
|
||||||
|
static double bla_bi[MAX_BLA];
|
||||||
|
static double bla_rad[MAX_BLA];
|
||||||
|
static int32_t bla_len[MAX_BLA];
|
||||||
|
static int32_t level_off[MAX_LEVELS];
|
||||||
|
static int32_t level_cnt[MAX_LEVELS];
|
||||||
|
static int32_t level_num = 0;
|
||||||
|
static int32_t built_ref_len = 0;
|
||||||
|
|
||||||
|
static uint32_t st_bla=0,st_ptb=0,st_rebase=0,st_interior=0,st_unresolved=0,st_fail=0;
|
||||||
|
static uint64_t st_iter=0;
|
||||||
|
|
||||||
|
__attribute__((export_name("counts_ptr"))) uintptr_t counts_ptr(void){ return (uintptr_t)counts; }
|
||||||
|
__attribute__((export_name("mags_ptr"))) uintptr_t mags_ptr(void){ return (uintptr_t)mags; }
|
||||||
|
__attribute__((export_name("refs_r_ptr"))) uintptr_t refs_r_ptr(void){ return (uintptr_t)refs_r; }
|
||||||
|
__attribute__((export_name("refs_i_ptr"))) uintptr_t refs_i_ptr(void){ return (uintptr_t)refs_i; }
|
||||||
|
__attribute__((export_name("stat_bla_steps"))) uint32_t stat_bla_steps(void){ return st_bla; }
|
||||||
|
__attribute__((export_name("stat_ptb_steps"))) uint32_t stat_ptb_steps(void){ return st_ptb; }
|
||||||
|
__attribute__((export_name("stat_rebases"))) uint32_t stat_rebases(void){ return st_rebase; }
|
||||||
|
__attribute__((export_name("stat_interior"))) uint32_t stat_interior(void){ return st_interior; }
|
||||||
|
__attribute__((export_name("stat_unresolved"))) uint32_t stat_unresolved(void){ return st_unresolved; }
|
||||||
|
__attribute__((export_name("stat_fail"))) uint32_t stat_fail(void){ return st_fail; }
|
||||||
|
__attribute__((export_name("stat_iter_lo"))) uint32_t stat_iter_lo(void){ return (uint32_t)st_iter; }
|
||||||
|
__attribute__((export_name("stat_iter_hi"))) uint32_t stat_iter_hi(void){ return (uint32_t)(st_iter>>32); }
|
||||||
|
|
||||||
|
static inline double hypot2(double x,double y){ return __builtin_sqrt(x*x+y*y); }
|
||||||
|
static inline int finite2(double x){ return __builtin_isfinite(x); }
|
||||||
|
|
||||||
|
__attribute__((export_name("build_bla"))) int build_bla(int ref_len, double cmax, double eps){
|
||||||
|
if(ref_len < 3 || ref_len > MAX_REF) { level_num=0; return 0; }
|
||||||
|
int n = ref_len - 1;
|
||||||
|
int off = 0;
|
||||||
|
if(n > MAX_BLA) { level_num=0; return 0; }
|
||||||
|
level_off[0]=0; level_cnt[0]=n; level_num=1;
|
||||||
|
for(int j=0;j<n;j++){
|
||||||
|
int m=j+1;
|
||||||
|
double ar=2.0*refs_r[m], ai=2.0*refs_i[m];
|
||||||
|
double am=hypot2(ar,ai);
|
||||||
|
bla_ar[j]=ar; bla_ai[j]=ai; bla_br[j]=1.0; bla_bi[j]=0.0; bla_len[j]=1;
|
||||||
|
double r=0.0;
|
||||||
|
if(am>0.0 && finite2(am)){
|
||||||
|
r=eps*am-cmax/am;
|
||||||
|
if(r<0.0 || !finite2(r)) r=0.0;
|
||||||
|
}
|
||||||
|
bla_rad[j]=r;
|
||||||
|
}
|
||||||
|
int prev_off=0, prev_n=n;
|
||||||
|
while(prev_n>1 && level_num<MAX_LEVELS){
|
||||||
|
int cur_n=(prev_n+1)>>1;
|
||||||
|
off=prev_off+prev_n;
|
||||||
|
if(off+cur_n>MAX_BLA){ level_num=0; return 0; }
|
||||||
|
level_off[level_num]=off; level_cnt[level_num]=cur_n;
|
||||||
|
for(int j=0;j<cur_n;j++){
|
||||||
|
int x=prev_off+(j<<1), z=off+j;
|
||||||
|
if((j<<1)+1>=prev_n){
|
||||||
|
bla_ar[z]=bla_ar[x]; bla_ai[z]=bla_ai[x]; bla_br[z]=bla_br[x]; bla_bi[z]=bla_bi[x]; bla_rad[z]=bla_rad[x]; bla_len[z]=bla_len[x];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int y=x+1;
|
||||||
|
double axr=bla_ar[x], axi=bla_ai[x], bxr=bla_br[x], bxi=bla_bi[x], rx=bla_rad[x];
|
||||||
|
double ayr=bla_ar[y], ayi=bla_ai[y], byr=bla_br[y], byi=bla_bi[y], ry=bla_rad[y];
|
||||||
|
double azr=ayr*axr-ayi*axi, azi=ayr*axi+ayi*axr;
|
||||||
|
double bzr=ayr*bxr-ayi*bxi+byr, bzi=ayr*bxi+ayi*bxr+byi;
|
||||||
|
double am=hypot2(axr,axi), bm=hypot2(bxr,bxi);
|
||||||
|
double v = am>0.0 ? (ry-bm*cmax)/am : -1.0;
|
||||||
|
double rz = rx < v ? rx : v;
|
||||||
|
if(rz<0.0) rz=0.0;
|
||||||
|
if(!finite2(azr)||!finite2(azi)||!finite2(bzr)||!finite2(bzi)||!finite2(rz)) rz=0.0;
|
||||||
|
bla_ar[z]=azr; bla_ai[z]=azi; bla_br[z]=bzr; bla_bi[z]=bzi; bla_rad[z]=rz; bla_len[z]=bla_len[x]+bla_len[y];
|
||||||
|
}
|
||||||
|
prev_off=off; prev_n=cur_n; level_num++;
|
||||||
|
}
|
||||||
|
built_ref_len=ref_len;
|
||||||
|
return level_num;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int analytic_interior(double cr,double ci){
|
||||||
|
double y2=ci*ci;
|
||||||
|
double x=cr-0.25, q=x*x+y2;
|
||||||
|
if(q*(q+x) < 0.25*y2-1e-15) return 1;
|
||||||
|
x=cr+1.0;
|
||||||
|
if(x*x+y2 < 0.0625-1e-15) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The work limits count low-precision *operations*, not Mandelbrot iterations.
|
||||||
|
A BLA may skip thousands of mathematical iterations in one operation.
|
||||||
|
max_* <= 0 means unlimited. STATUS_UNRESOLVED means "retry locally", never
|
||||||
|
"interior". Derivative contraction is a cheap attracting-cycle detector.
|
||||||
|
*/
|
||||||
|
static inline int render_one_v2(
|
||||||
|
double dc_r,double dc_i,double base_cr,double base_ci,
|
||||||
|
int ref_len,int iter,int max_bla_steps,int max_ptb_steps,int interior_on,
|
||||||
|
uint32_t *out_n,double *out_m
|
||||||
|
){
|
||||||
|
double zr=0.0,zi=0.0;
|
||||||
|
double dr=1.0,di=0.0;
|
||||||
|
int dvalid=0;
|
||||||
|
int n=0,m=0,bla_steps=0,ptb_steps=0;
|
||||||
|
if(interior_on && analytic_interior(base_cr+dc_r,base_ci+dc_i)){
|
||||||
|
*out_n=(uint32_t)iter;*out_m=0.0;st_interior++;return 1;
|
||||||
|
}
|
||||||
|
while(n<iter){
|
||||||
|
if(m>ref_len){st_fail++;return 0;}
|
||||||
|
double vr=refs_r[m]+zr, vi=refs_i[m]+zi;
|
||||||
|
double mag=vr*vr+vi*vi;
|
||||||
|
if(mag>4.0){ *out_n=(uint32_t)n; *out_m=mag; st_iter+=(uint64_t)n; return 1; }
|
||||||
|
if(interior_on && dvalid && n>=12){
|
||||||
|
double dm=dr*dr+di*di;
|
||||||
|
if(finite2(dm) && dm<1e-6){*out_n=(uint32_t)iter;*out_m=0.0;st_interior++;st_iter+=(uint64_t)n;return 1;}
|
||||||
|
}
|
||||||
|
if(m>0 && mag < zr*zr+zi*zi){ zr=vr; zi=vi; m=0; st_rebase++; continue; }
|
||||||
|
int used=0;
|
||||||
|
if(m>=1 && m<ref_len && level_num>0){
|
||||||
|
int rel=m-1;
|
||||||
|
double z2=zr*zr+zi*zi;
|
||||||
|
int kmax = rel==0 ? level_num-1 : __builtin_ctz((unsigned)rel);
|
||||||
|
if(kmax>=level_num) kmax=level_num-1;
|
||||||
|
for(int k=kmax;k>=0;k--){
|
||||||
|
int idx=rel>>k;
|
||||||
|
if(idx>=level_cnt[k]) continue;
|
||||||
|
int bi=level_off[k]+idx;
|
||||||
|
int skip=bla_len[bi];
|
||||||
|
double rad=bla_rad[bi];
|
||||||
|
if(skip<2 || m+skip>ref_len || n+skip>iter || !(z2<rad*rad)) continue;
|
||||||
|
if(max_bla_steps>0 && bla_steps>=max_bla_steps){st_unresolved++;*out_n=STATUS_UNRESOLVED;*out_m=0.0;st_iter+=(uint64_t)n;return 2;}
|
||||||
|
double nr=bla_ar[bi]*zr-bla_ai[bi]*zi+bla_br[bi]*dc_r-bla_bi[bi]*dc_i;
|
||||||
|
double ni=bla_ar[bi]*zi+bla_ai[bi]*zr+bla_br[bi]*dc_i+bla_bi[bi]*dc_r;
|
||||||
|
if(!finite2(nr)||!finite2(ni)) continue;
|
||||||
|
if(interior_on && dvalid){
|
||||||
|
double nd=bla_ar[bi]*dr-bla_ai[bi]*di;
|
||||||
|
double ndi=bla_ar[bi]*di+bla_ai[bi]*dr;
|
||||||
|
if(finite2(nd)&&finite2(ndi)){dr=nd;di=ndi;} else dvalid=0;
|
||||||
|
}
|
||||||
|
zr=nr;zi=ni;m+=skip;n+=skip;bla_steps++;st_bla++;used=1;break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(used) continue;
|
||||||
|
if(m>=ref_len){st_fail++;return 0;}
|
||||||
|
if(max_ptb_steps>0 && ptb_steps>=max_ptb_steps){st_unresolved++;*out_n=STATUS_UNRESOLVED;*out_m=0.0;st_iter+=(uint64_t)n;return 2;}
|
||||||
|
if(interior_on){
|
||||||
|
if(n==0){dr=1.0;di=0.0;dvalid=1;}
|
||||||
|
else if(dvalid){
|
||||||
|
double nd=2.0*(vr*dr-vi*di), ndi=2.0*(vr*di+vi*dr);
|
||||||
|
if(finite2(nd)&&finite2(ndi)){dr=nd;di=ndi;} else dvalid=0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double Rr=refs_r[m],Ri=refs_i[m];
|
||||||
|
double nr=2.0*(Rr*zr-Ri*zi)+(zr*zr-zi*zi)+dc_r;
|
||||||
|
double ni=2.0*(Rr*zi+Ri*zr)+2.0*zr*zi+dc_i;
|
||||||
|
if(!finite2(nr)||!finite2(ni)){st_fail++;return 0;}
|
||||||
|
zr=nr;zi=ni;m++;n++;ptb_steps++;st_ptb++;
|
||||||
|
}
|
||||||
|
*out_n=(uint32_t)iter; *out_m=0.0; st_iter+=(uint64_t)n; return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("render_bla_rect_v2"))) int render_bla_rect_v2(
|
||||||
|
double span,double off_r,double off_i,double base_cr,double base_ci,
|
||||||
|
int ref_len,int w,int h,int x0,int y0,int rw,int rh,int iter,
|
||||||
|
int max_bla_steps,int max_ptb_steps,int interior_on
|
||||||
|
){
|
||||||
|
if(ref_len!=built_ref_len || level_num<=0 || w<=0 || h<=0 || rw<=0 || rh<=0) return 0;
|
||||||
|
int total=rw*rh; if(total>OUT_MAX) return 0;
|
||||||
|
st_bla=st_ptb=st_rebase=st_interior=st_unresolved=st_fail=0;st_iter=0;
|
||||||
|
int k=0;
|
||||||
|
for(int yy=0;yy<rh;yy++){
|
||||||
|
int y=y0+yy;
|
||||||
|
double dc_i=off_i+span*((0.5*(double)h-(double)y)/(double)w);
|
||||||
|
for(int xx=0;xx<rw;xx++,k++){
|
||||||
|
int x=x0+xx;
|
||||||
|
double dc_r=off_r+span*((double)x/(double)w-0.5);
|
||||||
|
uint32_t n; double mm;
|
||||||
|
int ok=render_one_v2(dc_r,dc_i,base_cr,base_ci,ref_len,iter,max_bla_steps,max_ptb_steps,interior_on,&n,&mm);
|
||||||
|
if(ok==0){ counts[k]=STATUS_FAIL; mags[k]=0.0; }
|
||||||
|
else { counts[k]=n; mags[k]=mm; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compatibility entry point used by old probes/fallback paths. */
|
||||||
|
__attribute__((export_name("render_bla_rect"))) int render_bla_rect(
|
||||||
|
double span,double off_r,double off_i,int ref_len,int w,int h,int x0,int y0,int rw,int rh,int iter
|
||||||
|
){
|
||||||
|
return render_bla_rect_v2(span,off_r,off_i,0.0,0.0,ref_len,w,h,x0,y0,rw,rh,iter,0,0,0);
|
||||||
|
}
|
||||||
37
src/color_kernel.c
Normal file
37
src/color_kernel.c
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define OUT_MAX 65536
|
||||||
|
|
||||||
|
static double mags[OUT_MAX];
|
||||||
|
static float corrections[OUT_MAX];
|
||||||
|
|
||||||
|
__attribute__((export_name("mags_ptr"))) uintptr_t mags_ptr(void) { return (uintptr_t)mags; }
|
||||||
|
__attribute__((export_name("corr_ptr"))) uintptr_t corr_ptr(void) { return (uintptr_t)corrections; }
|
||||||
|
|
||||||
|
/* Freestanding log2: range reduction plus an odd atanh series for ln(m). */
|
||||||
|
static double log2_local(double x) {
|
||||||
|
union { double d; uint64_t u; } v = { x };
|
||||||
|
int exponent = (int)((v.u >> 52) & 0x7ffu) - 1023;
|
||||||
|
v.u = (v.u & UINT64_C(0x000fffffffffffff)) | UINT64_C(0x3ff0000000000000);
|
||||||
|
const double y = (v.d - 1.0) / (v.d + 1.0);
|
||||||
|
const double y2 = y * y;
|
||||||
|
double term = y, sum = term;
|
||||||
|
term *= y2; sum += term / 3.0;
|
||||||
|
term *= y2; sum += term / 5.0;
|
||||||
|
term *= y2; sum += term / 7.0;
|
||||||
|
term *= y2; sum += term / 9.0;
|
||||||
|
term *= y2; sum += term / 11.0;
|
||||||
|
return (double)exponent + (2.0 * sum) * 1.4426950408889634074;
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("smooth_batch"))) int smooth_batch(int count) {
|
||||||
|
if (count < 0) return 0;
|
||||||
|
if (count > OUT_MAX) count = OUT_MAX;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
double m = mags[i];
|
||||||
|
if (!(m > 4.0)) m = 4.0000001;
|
||||||
|
const double u = log2_local(m);
|
||||||
|
corrections[i] = (float)(1.0 - log2_local(0.5 * u));
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
112
src/deep_kernel.c
Normal file
112
src/deep_kernel.c
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define MAX_REF 150001
|
||||||
|
#define OUT_MAX 65536
|
||||||
|
#define NEG_BUCKET (-1000000000)
|
||||||
|
#define STATUS_FAIL UINT32_C(0xffffffff)
|
||||||
|
|
||||||
|
static uint32_t counts[OUT_MAX];
|
||||||
|
static double mags[OUT_MAX];
|
||||||
|
static double refs_r[MAX_REF];
|
||||||
|
static double refs_i[MAX_REF];
|
||||||
|
|
||||||
|
__attribute__((export_name("counts_ptr"))) uintptr_t counts_ptr(void) { return (uintptr_t)counts; }
|
||||||
|
__attribute__((export_name("mags_ptr"))) uintptr_t mags_ptr(void) { return (uintptr_t)mags; }
|
||||||
|
__attribute__((export_name("refs_r_ptr"))) uintptr_t refs_r_ptr(void) { return (uintptr_t)refs_r; }
|
||||||
|
__attribute__((export_name("refs_i_ptr"))) uintptr_t refs_i_ptr(void) { return (uintptr_t)refs_i; }
|
||||||
|
|
||||||
|
typedef struct { double r, i; int bucket; } scaled_complex;
|
||||||
|
|
||||||
|
static scaled_complex normalize(scaled_complex z) {
|
||||||
|
double ar = z.r < 0.0 ? -z.r : z.r;
|
||||||
|
double ai = z.i < 0.0 ? -z.i : z.i;
|
||||||
|
double m = ar > ai ? ar : ai;
|
||||||
|
if (m == 0.0) return (scaled_complex){0.0, 0.0, NEG_BUCKET};
|
||||||
|
while (m > 3.402823669209385e38) { z.r *= 8.636168555094445e-78; z.i *= 8.636168555094445e-78; z.bucket++; m *= 8.636168555094445e-78; }
|
||||||
|
while (m < 2.938735877055719e-39) { z.r *= 1.157920892373162e77; z.i *= 1.157920892373162e77; z.bucket--; m *= 1.157920892373162e77; }
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
static scaled_complex add_scaled(scaled_complex a, scaled_complex b) {
|
||||||
|
if (a.bucket == NEG_BUCKET) return b;
|
||||||
|
if (b.bucket == NEG_BUCKET) return a;
|
||||||
|
int bucket = a.bucket > b.bucket ? a.bucket : b.bucket;
|
||||||
|
double ar = 0.0, ai = 0.0;
|
||||||
|
if (a.bucket == bucket) { ar += a.r; ai += a.i; }
|
||||||
|
else if (a.bucket == bucket - 1) { ar += a.r * 8.636168555094445e-78; ai += a.i * 8.636168555094445e-78; }
|
||||||
|
if (b.bucket == bucket) { ar += b.r; ai += b.i; }
|
||||||
|
else if (b.bucket == bucket - 1) { ar += b.r * 8.636168555094445e-78; ai += b.i * 8.636168555094445e-78; }
|
||||||
|
return normalize((scaled_complex){ar, ai, bucket});
|
||||||
|
}
|
||||||
|
|
||||||
|
static scaled_complex multiply_scaled(scaled_complex a, scaled_complex b) {
|
||||||
|
if (a.bucket == NEG_BUCKET || b.bucket == NEG_BUCKET) return (scaled_complex){0.0, 0.0, NEG_BUCKET};
|
||||||
|
return normalize((scaled_complex){a.r*b.r-a.i*b.i, a.r*b.i+a.i*b.r, a.bucket+b.bucket});
|
||||||
|
}
|
||||||
|
|
||||||
|
static scaled_complex multiply_reference(scaled_complex z, double rr, double ri) {
|
||||||
|
if (z.bucket == NEG_BUCKET) return z;
|
||||||
|
return normalize((scaled_complex){2.0*(rr*z.r-ri*z.i), 2.0*(rr*z.i+ri*z.r), z.bucket});
|
||||||
|
}
|
||||||
|
|
||||||
|
static double scale_bucket(double x, int bucket) {
|
||||||
|
if (bucket > 3) return x < 0.0 ? -1.0e308 : 1.0e308;
|
||||||
|
if (bucket < -3) return 0.0;
|
||||||
|
while (bucket > 0) { x *= 1.157920892373162e77; bucket--; }
|
||||||
|
while (bucket < 0) { x *= 8.636168555094445e-78; bucket++; }
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int render_one(scaled_complex dc, int ref_len, int max_iter, uint32_t *out_n, double *out_mag) {
|
||||||
|
scaled_complex delta = {0.0, 0.0, NEG_BUCKET};
|
||||||
|
int ref_index = 0;
|
||||||
|
for (int n = 0; n < max_iter; n++) {
|
||||||
|
if (ref_index >= ref_len) { *out_n = STATUS_FAIL; *out_mag = 0.0; return 0; }
|
||||||
|
const double dr = scale_bucket(delta.r, delta.bucket);
|
||||||
|
const double di = scale_bucket(delta.i, delta.bucket);
|
||||||
|
const double rr = refs_r[ref_index], ri = refs_i[ref_index];
|
||||||
|
scaled_complex linear = multiply_reference(delta, rr, ri);
|
||||||
|
scaled_complex square = multiply_scaled(delta, delta);
|
||||||
|
delta = add_scaled(add_scaled(linear, square), dc);
|
||||||
|
ref_index++;
|
||||||
|
const double zr = refs_r[ref_index] + scale_bucket(delta.r, delta.bucket);
|
||||||
|
const double zi = refs_i[ref_index] + scale_bucket(delta.i, delta.bucket);
|
||||||
|
const double mag = zr*zr + zi*zi;
|
||||||
|
if (mag > 4.0) { *out_n = (uint32_t)(n + 1); *out_mag = mag; return 1; }
|
||||||
|
const double dm = dr*dr + di*di;
|
||||||
|
if (ref_index > 0 && dm > mag) {
|
||||||
|
delta = normalize((scaled_complex){zr, zi, 0});
|
||||||
|
ref_index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*out_n = (uint32_t)max_iter; *out_mag = 0.0; return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((export_name("render_perturb_rebase_rect"))) int render_perturb_rebase_rect(
|
||||||
|
double span_mant, int span_bucket, double off_r, double off_i, int off_bucket,
|
||||||
|
int ref_len, int width, int height, double cx, double cy,
|
||||||
|
int x0, int y0, int rect_width, int rows, int max_iter,
|
||||||
|
int skip, double ar, double ai, int a_bucket,
|
||||||
|
double br, double bi, int b_bucket
|
||||||
|
) {
|
||||||
|
(void)skip; (void)ar; (void)ai; (void)a_bucket; (void)br; (void)bi; (void)b_bucket;
|
||||||
|
if (ref_len < 1 || ref_len >= MAX_REF || width <= 0 || rect_width <= 0 || rows <= 0) return 0;
|
||||||
|
const int total = rect_width * rows;
|
||||||
|
if (total > OUT_MAX) return 0;
|
||||||
|
const scaled_complex offset = normalize((scaled_complex){off_r, off_i, off_bucket});
|
||||||
|
int out = 0;
|
||||||
|
for (int yy = 0; yy < rows; yy++) {
|
||||||
|
const int y = y0 + yy;
|
||||||
|
for (int xx = 0; xx < rect_width; xx++, out++) {
|
||||||
|
const int x = x0 + xx;
|
||||||
|
scaled_complex pixel = {span_mant * ((double)x - cx) / (double)width,
|
||||||
|
span_mant * (cy - (double)y) / (double)width,
|
||||||
|
span_bucket};
|
||||||
|
scaled_complex dc = add_scaled(offset, normalize(pixel));
|
||||||
|
uint32_t n; double mag;
|
||||||
|
render_one(dc, ref_len, max_iter, &n, &mag);
|
||||||
|
counts[out] = n; mags[out] = mag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
44
src/shallow_kernel.c
Normal file
44
src/shallow_kernel.c
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define OUT_MAX 65536
|
||||||
|
|
||||||
|
static uint32_t counts[OUT_MAX];
|
||||||
|
static double mags[OUT_MAX];
|
||||||
|
|
||||||
|
__attribute__((export_name("counts_ptr"))) uintptr_t counts_ptr(void) { return (uintptr_t)counts; }
|
||||||
|
__attribute__((export_name("mags_ptr"))) uintptr_t mags_ptr(void) { return (uintptr_t)mags; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* center_re/center_im are already shifted by half a pixel by the JavaScript
|
||||||
|
* adapter. This preserves the historical ABI while making every sample land
|
||||||
|
* at (x + 0.5, y + 0.5) in the renderer-v23 contract.
|
||||||
|
*/
|
||||||
|
__attribute__((export_name("render_rows"))) int render_rows(
|
||||||
|
double center_re, double center_im, double span,
|
||||||
|
int width, int height, int y0, int rows, int max_iter
|
||||||
|
) {
|
||||||
|
if (width <= 0 || height <= 0 || rows <= 0 || max_iter <= 0) return 0;
|
||||||
|
int total = width * rows;
|
||||||
|
if (total > OUT_MAX) return 0;
|
||||||
|
const double scale = span / (double)width;
|
||||||
|
int out = 0;
|
||||||
|
for (int yy = 0; yy < rows; yy++) {
|
||||||
|
const int y = y0 + yy;
|
||||||
|
const double ci = center_im + scale * (0.5 * (double)height - (double)y);
|
||||||
|
for (int x = 0; x < width; x++, out++) {
|
||||||
|
const double cr = center_re + scale * ((double)x - 0.5 * (double)width);
|
||||||
|
double zr = 0.0, zi = 0.0, zr2 = 0.0, zi2 = 0.0;
|
||||||
|
int n = 0;
|
||||||
|
while (n < max_iter && zr2 + zi2 <= 4.0) {
|
||||||
|
zi = 2.0 * zr * zi + ci;
|
||||||
|
zr = zr2 - zi2 + cr;
|
||||||
|
zr2 = zr * zr;
|
||||||
|
zi2 = zi * zi;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
counts[out] = (uint32_t)n;
|
||||||
|
mags[out] = n < max_iter ? zr2 + zi2 : 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
29
tests/analytic-interior.mjs
Normal file
29
tests/analytic-interior.mjs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
const fromFraction = (numerator, denominator, bits) => numerator * (1n << BigInt(bits)) / denominator;
|
||||||
|
|
||||||
|
function fixedAnalyticInterior(cr, ci, bits) {
|
||||||
|
const scale = 1n << BigInt(bits);
|
||||||
|
const x = cr - (scale >> 2n), y = ci, q = x * x + y * y;
|
||||||
|
if (4n * q * (q + x * scale) <= y * y * scale * scale) return true;
|
||||||
|
const d = cr + scale;
|
||||||
|
return 16n * (d * d + y * y) <= scale * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cases = [
|
||||||
|
{ id: 'origin-cardioid', re: [0n, 1n], im: [0n, 1n], expected: true },
|
||||||
|
{ id: 'cardioid-cusp', re: [1n, 4n], im: [0n, 1n], expected: true },
|
||||||
|
{ id: 'period2-center', re: [-1n, 1n], im: [0n, 1n], expected: true },
|
||||||
|
{ id: 'period2-boundary', re: [-5n, 4n], im: [0n, 1n], expected: true },
|
||||||
|
{ id: 'right-exterior', re: [13n, 50n], im: [0n, 1n], expected: false },
|
||||||
|
{ id: 'period3-not-analytic', re: [-123n, 1000n], im: [745n, 1000n], expected: false },
|
||||||
|
{ id: 'far-exterior', re: [1n, 1n], im: [1n, 1n], expected: false }
|
||||||
|
];
|
||||||
|
|
||||||
|
let checked = 0;
|
||||||
|
for (const bits of [256, 320]) for (const sample of cases) {
|
||||||
|
const cr = fromFraction(sample.re[0], sample.re[1], bits);
|
||||||
|
const ci = fromFraction(sample.im[0], sample.im[1], bits);
|
||||||
|
const actual = fixedAnalyticInterior(cr, ci, bits);
|
||||||
|
if (actual !== sample.expected) throw new Error(`${sample.id} at ${bits} bit: expected ${sample.expected}, got ${actual}`);
|
||||||
|
checked++;
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify({ status: 'pass', checked, precisions: [256, 320], proof: 'integer cardioid/period-2 inequalities' }));
|
||||||
27
tests/browser-benchmark.html
Normal file
27
tests/browser-benchmark.html
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!doctype html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Mandelbrot v23 browser benchmark</title>
|
||||||
|
<style>body{font:14px system-ui;margin:20px;background:#111827;color:#eef2ff}button{padding:8px 14px}iframe{position:fixed;left:-20000px;top:0;border:0}pre{white-space:pre-wrap}</style>
|
||||||
|
<h1>v23 browser benchmark</h1>
|
||||||
|
<p>HTTP(S)でこのworkspaceを配信して実行します。iframeの実寸と実DPRを結果へ記録します。</p>
|
||||||
|
<button id="run">Run corpus</button>
|
||||||
|
<pre id="output">Ready.</pre>
|
||||||
|
<iframe id="app" width="1440" height="900"></iframe>
|
||||||
|
<script type="module">
|
||||||
|
const out=document.querySelector('#output'),frame=document.querySelector('#app');
|
||||||
|
const wait=ms=>new Promise(r=>setTimeout(r,ms));
|
||||||
|
function fixed(decimal,bits){let s=String(decimal).toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);let[m,e='0']=s.split('e'),[i,f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-Number(e);if(places<0){digits+='0'.repeat(-places);places=0}let v=BigInt(digits)*(1n<<BigInt(bits))/(10n**BigInt(places));return neg?-v:v}
|
||||||
|
function bitsFor(span){const n=Math.abs(Number(span));return Number.isFinite(n)&&n>0?Math.max(256,Math.ceil(-Math.log2(n))+256):1280}
|
||||||
|
function hash(scene){const bits=bitsFor(scene.span),p=new URLSearchParams({v:'23',b:String(bits),re:String(fixed(scene.re,bits)),im:String(fixed(scene.im,bits)),sp:String(fixed(scene.span,bits)),pal:'0',cy:'.008',sh:'.18',it:'350',ad:'1'});return'#'+p}
|
||||||
|
async function poll(fn,timeout,label){const started=performance.now();while(performance.now()-started<timeout){let value;try{value=fn()}catch{}if(value)return{value,ms:performance.now()-started};await wait(25)}throw new Error('timeout: '+label)}
|
||||||
|
const p95=values=>values.slice().sort((a,b)=>a-b)[Math.min(values.length-1,Math.floor(values.length*.95))]||0;
|
||||||
|
async function canvasHash(win){const blob=await new Promise(resolve=>win.document.querySelector('#view').toBlob(resolve,'image/png'));const digest=await crypto.subtle.digest('SHA-256',await blob.arrayBuffer());return[...new Uint8Array(digest)].map(x=>x.toString(16).padStart(2,'0')).join('')}
|
||||||
|
function accessibilityAudit(win){const doc=win.document,failures=[],interactive=[...doc.querySelectorAll('button,select,input,summary')];for(const el of interactive){const target=el.type==='checkbox'?el.closest('label')||el:el,r=target.getBoundingClientRect();if(r.width<44||r.height<44)failures.push('target:'+el.id);if(el.matches('input:not([type=checkbox]),select')&&el.id&&!doc.querySelector(`label[for="${el.id}"]`))failures.push('label:'+el.id)}if(!doc.querySelector('#view[tabindex][aria-label]'))failures.push('canvas-keyboard-name');if(!doc.querySelector('[aria-live]'))failures.push('live-status');if(/user-scalable\s*=\s*no/i.test(doc.querySelector('meta[name=viewport]')?.content||''))failures.push('page-zoom-disabled');const css=[...doc.querySelectorAll('style')].map(x=>x.textContent).join('\n');if(!css.includes('prefers-reduced-motion'))failures.push('reduced-motion');if(!css.includes('prefers-reduced-transparency'))failures.push('reduced-transparency');return{pass:failures.length===0,failures,interactiveCount:interactive.length}}
|
||||||
|
async function interactionAudit(win){const canvas=win.document.querySelector('#view'),before=win.__MANDEL_DIAG__.snapshot(),durations=[];for(let i=0;i<16;i++){const t=performance.now();canvas.dispatchEvent(new win.WheelEvent('wheel',{deltaY:0,clientX:canvas.clientWidth/2,clientY:canvas.clientHeight/2,cancelable:true}));durations.push(performance.now()-t)}const during=win.__MANDEL_DIAG__.snapshot();await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d&&!d.rendering&&!d.scheduler.wheelActive&&d.lastPass==='preview'?d:null},120000,'wheel settle preview');await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d&&!d.rendering&&d.coverage>=.999&&['COVERED','RESOLVING','REFINING','REFINED'].includes(d.drawState)?d:null},180000,'wheel settle covered');return{dispatchP95Ms:p95(durations),renderStartsDuringGesture:during.runtimeMetrics.renderStartsDuringGesture-before.runtimeMetrics.renderStartsDuringGesture,renderStartsBeforeSettle:during.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts}}
|
||||||
|
async function modeAudit(win){const select=win.document.querySelector('#processMode'),hq=win.document.querySelector('#hq');select.value='power';select.dispatchEvent(new win.Event('change',{bubbles:true}));await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d.automaticTarget==='PREVIEW'&&!d.rendering&&d.lastPass==='preview'?d:null},120000,'power preview');const powerBefore=win.__MANDEL_DIAG__.snapshot();await wait(900);const power=win.__MANDEL_DIAG__.snapshot(),powerPass=power.automaticTarget==='PREVIEW'&&power.lastPass==='preview'&&!power.rendering&&!power.scheduler.timer&&!power.detailActive&&!hq.checked&&power.screen.pixelBudget<=1048576&&power.runtimeMetrics.renderStarts===powerBefore.runtimeMetrics.renderStarts;select.value='standard';select.dispatchEvent(new win.Event('change',{bubbles:true}));await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d.automaticTarget==='COVERED'&&!d.rendering&&d.lastPass==='covered'&&d.coverage>=.999?d:null},180000,'standard covered');await wait(900);const standard=win.__MANDEL_DIAG__.snapshot(),standardPass=standard.automaticTarget==='COVERED'&&standard.lastPass==='covered'&&!standard.detailActive&&!standard.scheduler.unknownTimer&&!hq.checked&&standard.screen.pixelBudget<=4194304;return{pass:powerPass&&standardPass,power:{pass:powerPass,target:power.automaticTarget,pixelBudget:power.screen.pixelBudget,renderStartsAfterStable:power.runtimeMetrics.renderStarts-powerBefore.runtimeMetrics.renderStarts},standard:{pass:standardPass,target:standard.automaticTarget,pixelBudget:standard.screen.pixelBudget,detailActive:standard.detailActive,unknownTimer:standard.scheduler.unknownTimer}}}
|
||||||
|
async function recolorAudit(win){const select=win.document.querySelector('#palette'),before=win.__MANDEL_DIAG__.snapshot();select.value=before.palette===1?'2':'1';select.dispatchEvent(new win.Event('change',{bubbles:true}));await new Promise(resolve=>win.requestAnimationFrame(()=>win.requestAnimationFrame(resolve)));const after=win.__MANDEL_DIAG__.snapshot();return{pass:after.palette!==before.palette&&after.runtimeMetrics.renderStarts===before.runtimeMetrics.renderStarts,renderStarts:after.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts}}
|
||||||
|
async function exportAudit(win){const doc=win.document,captured=[],nativeCreate=win.URL.createObjectURL.bind(win.URL),nativeClick=win.HTMLAnchorElement.prototype.click;win.URL.createObjectURL=blob=>{const url=nativeCreate(blob);captured.push({url,blob});return url};win.HTMLAnchorElement.prototype.click=function(){};try{doc.querySelector('#exportScale').value='0';doc.querySelector('#exportWidth').value='64';doc.querySelector('#exportAA').value='1';doc.querySelector('#exportPrecision').value='balanced';doc.querySelector('#exportStart').click();await poll(()=>!win.__MANDEL_DIAG__.snapshot().exporting&&captured.length>=2,120000,'small export');const png=captured.find(x=>x.blob.type==='image/png')?.blob,json=captured.find(x=>x.blob.type==='application/json')?.blob;if(!png||!json)throw new Error('export files missing');const bytes=new Uint8Array(await png.arrayBuffer()),width=(bytes[16]<<24)|(bytes[17]<<16)|(bytes[18]<<8)|bytes[19],height=(bytes[20]<<24)|(bytes[21]<<16)|(bytes[22]<<8)|bytes[23],meta=JSON.parse(await json.text()),completed={width,height,metadataComplete:meta.determinism?.allTilesCompleted===true,sampleCount:meta.sampleCount,kernelHashes:!!meta.kernelSha256};const prior=captured.length;doc.querySelector('#exportWidth').value='256';doc.querySelector('#exportPrecision').value='validated';doc.querySelector('#exportStart').click();doc.querySelector('#exportCancel').click();await poll(()=>!win.__MANDEL_DIAG__.snapshot().exporting,120000,'export cancel');return{pass:width===64&&height===Math.round(64*win.document.querySelector('#view').height/win.document.querySelector('#view').width)&&captured.length===prior,completed,cancelledWithoutDownload:captured.length===prior}}finally{win.URL.createObjectURL=nativeCreate;win.HTMLAnchorElement.prototype.click=nativeClick}}
|
||||||
|
async function runScene(scene,viewport){frame.width=viewport.cssWidth;frame.height=viewport.cssHeight;const loaded=new Promise((resolve,reject)=>{frame.onload=resolve;frame.onerror=reject});const started=performance.now();frame.src='../index.html'+hash(scene);await loaded;const win=frame.contentWindow;const label=viewport.id+'/'+scene.id;const preview=await poll(()=>{const d=win.__MANDEL_DIAG__?.snapshot();return d&&d.drawState==='PREVIEW'&&!d.rendering&&d.lastPass==='preview'?d:null},120000,'preview '+label);const previewSnapshot=preview.value,covered=await poll(()=>{const d=win.__MANDEL_DIAG__?.snapshot();return d&&['COVERED','RESOLVING','REFINING','REFINED','VALIDATING','VALIDATED','VALIDATION_INCOMPLETE'].includes(d.drawState)&&d.coverage>=.999?d:null},180000,'covered '+label);const refined=await poll(()=>{const d=win.__MANDEL_DIAG__?.snapshot();return d&&!d.detailActive&&!d.rendering&&!d.validating&&['COVERED','REFINED','VALIDATED','VALIDATION_INCOMPLETE'].includes(d.drawState)?d:null},240000,'refined '+label);const exercise=viewport.id==='desktop'&&scene.id==='z0',interaction=exercise?await interactionAudit(win):null,modes=exercise?await modeAudit(win):null,recolor=exercise?await recolorAudit(win):null,exportResult=exercise?await exportAudit(win):null,accessibility=exercise?accessibilityAudit(win):null,visualSha256=await canvasHash(win),resources=win.performance.getEntriesByType('resource').map(e=>String(e.name)),assetRequests={deep:resources.filter(x=>/\/(deep|bla|color)-/.test(x)),allWasm:resources.filter(x=>/\.wasm(?:$|\?)/.test(x))};await wait(2000);const before=win.__MANDEL_DIAG__.snapshot();await wait(1000);const after=win.__MANDEL_DIAG__.snapshot(),idle={raf:after.scheduler.raf,timer:after.scheduler.timer,pointerSettle:after.scheduler.pointerSettle,unknownTimer:after.scheduler.unknownTimer,backgroundJobs:after.scheduler.backgroundJobs,canvasWrites:after.runtimeMetrics.canvasWrites-before.runtimeMetrics.canvasWrites,domWrites:after.runtimeMetrics.domWrites-before.runtimeMetrics.domWrites,renderStarts:after.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts};return{id:scene.id,profile:viewport.id,requestedViewport:viewport,actualViewport:{cssWidth:win.innerWidth,cssHeight:win.innerHeight},actualDevicePixelRatio:win.devicePixelRatio,totalMs:performance.now()-started,previewMs:preview.ms,coveredMs:covered.ms,refinedMs:refined.ms,preview:previewSnapshot,final:after,idle,visualSha256,assetRequests,interaction,modes,recolor,export:exportResult,accessibility}}
|
||||||
|
function evaluate(results){const exercise=results.find(x=>x.interaction),checks={idle:results.every(x=>!x.idle.raf&&!x.idle.timer&&!x.idle.pointerSettle&&!x.idle.unknownTimer&&x.idle.backgroundJobs===0&&x.idle.canvasWrites===0&&x.idle.domWrites===0&&x.idle.renderStarts===0),covered:results.every(x=>x.final.coverage>=.999&&x.final.hqTarget===x.final.frame),memory:results.every(x=>x.final.memory.managedBytes<=x.final.memory.budget),visual:results.every(x=>/^[0-9a-f]{64}$/.test(x.visualSha256)),startup:results.filter(x=>x.id==='z0').every(x=>x.assetRequests.deep.length===0),interaction:!!exercise&&exercise.interaction.dispatchP95Ms<16&&exercise.interaction.renderStartsDuringGesture===0&&exercise.interaction.renderStartsBeforeSettle===0,modes:exercise?.modes?.pass===true,recolor:exercise?.recolor?.pass===true,export:exercise?.export?.pass===true,accessibility:exercise?.accessibility?.pass===true,profileFidelity:results.every(x=>x.actualViewport.cssWidth===x.requestedViewport.cssWidth&&x.actualViewport.cssHeight===x.requestedViewport.cssHeight&&Math.abs(x.actualDevicePixelRatio-x.requestedViewport.dpr)<.01),previewBudget:results.every(x=>x.preview.lastRender<120)};const failures=Object.entries(checks).filter(([,pass])=>!pass).map(([name])=>name);return{pass:failures.length===0,checks,failures}}
|
||||||
|
document.querySelector('#run').onclick=async()=>{document.querySelector('#run').disabled=true;try{const corpus=await(await fetch('./scenes.json',{cache:'no-store'})).json(),results=[];for(const viewport of corpus.viewports)for(const scene of corpus.scenes){out.textContent='Running '+viewport.id+'/'+scene.id+'…\n'+JSON.stringify(results,null,2);results.push(await runScene(scene,viewport))}const report={format:'mandelbrot-browser-baseline-v23',generatedUtc:new Date().toISOString(),userAgent:navigator.userAgent,hostDevicePixelRatio:devicePixelRatio,profiles:corpus.viewports,results,acceptance:evaluate(results)};out.textContent=JSON.stringify(report,null,2);globalThis.__BENCHMARK_RESULT__=report}catch(error){out.textContent=String(error?.stack||error)}finally{document.querySelector('#run').disabled=false}};
|
||||||
|
</script>
|
||||||
152
tests/js-syntax.mjs
Normal file
152
tests/js-syntax.mjs
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
|
||||||
|
const root = new URL('../', import.meta.url);
|
||||||
|
const kernels = await fs.readFile(new URL('kernels.js', root), 'utf8');
|
||||||
|
let app = await fs.readFile(new URL('script.js', root), 'utf8');
|
||||||
|
const browserHarness = await fs.readFile(new URL('tests/browser-benchmark.html', root), 'utf8');
|
||||||
|
const browserModule = browserHarness.match(/<script type="module">([\s\S]*?)<\/script>/)?.[1];
|
||||||
|
if (!browserModule) throw new Error('Browser benchmark module not found.');
|
||||||
|
new vm.Script(browserModule, { filename: 'browser-benchmark-module.js' });
|
||||||
|
app = app.replace(/\}\)\(\);\s*$/, `
|
||||||
|
const __fieldProbe = makeField(2, 100);
|
||||||
|
putField(__fieldProbe, 0, 7, 16, FIELD_ESCAPED);
|
||||||
|
putField(__fieldProbe, 1, 100, 0, FIELD_UNKNOWN);
|
||||||
|
globalThis.__MANDEL_FIELD_PROBE__ = {
|
||||||
|
iterations: Array.from(__fieldProbe.iterations),
|
||||||
|
classes: Array.from(__fieldProbe.classes)
|
||||||
|
};
|
||||||
|
globalThis.__MANDEL_WORKER_SOURCES__ = {
|
||||||
|
shallow: shallowWorkerSource(),
|
||||||
|
deep: deepWorkerSource()
|
||||||
|
};
|
||||||
|
globalThis.__MANDEL_MODE_PROBE__ = {
|
||||||
|
targets: Object.fromEntries(Object.keys(MODE_TARGET).map(mode => [mode, modeTarget(mode)])),
|
||||||
|
coldDeepPreview: targetSize(RENDER_PROFILE[RENDER_PASS.PREVIEW], true)
|
||||||
|
};
|
||||||
|
globalThis.__MANDEL_REFERENCE_PROBE__ = async () => {
|
||||||
|
const bits = 256, refLen = 40, rr = new Float64Array(refLen + 1), ri = new Float64Array(refLen + 1);
|
||||||
|
const ref = { bits, re: 0n, im: 0n, rr, ri, escape: 0, version: 1, checkpointVersion: 0, checkpointBits: 0, checkpointCount: 0, checkpointMismatch: false };
|
||||||
|
const run = () => new Promise(resolve => verifyReferenceCheckpoints(ref, refLen, state.token, ok => resolve(ok)));
|
||||||
|
const agreement = await run();
|
||||||
|
ref.rr[16] = 1; ref.checkpointVersion = 0; ref.checkpointMismatch = false;
|
||||||
|
const catchesMismatch = !(await run());
|
||||||
|
return { agreement, catchesMismatch, count: ref.checkpointCount, bits: ref.checkpointBits };
|
||||||
|
};
|
||||||
|
})();`);
|
||||||
|
|
||||||
|
const controls = new Map();
|
||||||
|
function makeContext2d() {
|
||||||
|
return {
|
||||||
|
fillStyle: '', imageSmoothingEnabled: true, imageSmoothingQuality: 'high',
|
||||||
|
fillRect() {}, drawImage() {}, putImageData() {}, save() {}, restore() {},
|
||||||
|
setTransform() {}, translate() {}, scale() {},
|
||||||
|
createImageData(width, height) {
|
||||||
|
return { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
||||||
|
},
|
||||||
|
getImageData(_x, _y, width, height) {
|
||||||
|
return { width, height, data: new Uint8ClampedArray(width * height * 4) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function makeElement(id = '') {
|
||||||
|
return {
|
||||||
|
id, value: '', checked: false, disabled: false, hidden: false,
|
||||||
|
width: 800, height: 600, clientWidth: 800, clientHeight: 600,
|
||||||
|
textContent: '', innerHTML: '', style: {}, dataset: {},
|
||||||
|
classList: { add() {}, remove() {}, toggle() {} },
|
||||||
|
addEventListener() {}, setAttribute() {}, click() {}, close() {}, showModal() {},
|
||||||
|
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }),
|
||||||
|
getContext: () => makeContext2d(),
|
||||||
|
toBlob(callback) { callback(new Blob()); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function element(id) {
|
||||||
|
if (!controls.has(id)) controls.set(id, makeElement(id));
|
||||||
|
return controls.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(element('processMode'), { value: 'standard' });
|
||||||
|
Object.assign(element('palette'), { value: '0' });
|
||||||
|
Object.assign(element('cycle'), { value: '.008' });
|
||||||
|
Object.assign(element('shift'), { value: '.18' });
|
||||||
|
Object.assign(element('iters'), { value: '350' });
|
||||||
|
Object.assign(element('adaptive'), { checked: true });
|
||||||
|
Object.assign(element('hq'), { checked: true });
|
||||||
|
Object.assign(element('exportScale'), { value: '1' });
|
||||||
|
Object.assign(element('exportAA'), { value: '1' });
|
||||||
|
Object.assign(element('exportPrecision'), { value: 'balanced' });
|
||||||
|
|
||||||
|
let rafId = 0;
|
||||||
|
const sandbox = {
|
||||||
|
console, WebAssembly, BigInt, Blob, URL, URLSearchParams,
|
||||||
|
Uint8Array, Uint8ClampedArray, Uint32Array, Float32Array, Float64Array,
|
||||||
|
ArrayBuffer, Map, Set, Math, Date, JSON, Promise, performance,
|
||||||
|
atob, btoa,
|
||||||
|
innerWidth: 800, innerHeight: 600, devicePixelRatio: 1,
|
||||||
|
navigator: { hardwareConcurrency: 4, deviceMemory: 8, clipboard: { writeText: async () => {} } },
|
||||||
|
location: { protocol: 'http:', origin: 'http://localhost', hash: '', href: 'http://localhost/' },
|
||||||
|
history: { pushState() {}, replaceState() {} },
|
||||||
|
localStorage: { getItem: () => null, setItem() {} },
|
||||||
|
matchMedia: () => ({ matches: false }),
|
||||||
|
requestAnimationFrame: () => ++rafId,
|
||||||
|
cancelAnimationFrame() {}, requestIdleCallback: () => 1,
|
||||||
|
setTimeout: () => 1, clearTimeout() {}, queueMicrotask() {},
|
||||||
|
addEventListener() {}, removeEventListener() {},
|
||||||
|
document: {
|
||||||
|
hidden: false,
|
||||||
|
body: makeElement('body'),
|
||||||
|
querySelector(selector) { return element(selector.replace(/^#/, '')); },
|
||||||
|
createElement() { return makeElement(); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sandbox.window = sandbox;
|
||||||
|
sandbox.globalThis = sandbox;
|
||||||
|
|
||||||
|
const context = vm.createContext(sandbox);
|
||||||
|
new vm.Script(kernels, { filename: 'kernels.js' }).runInContext(context);
|
||||||
|
new vm.Script(app, { filename: 'script.js' }).runInContext(context);
|
||||||
|
|
||||||
|
const sources = sandbox.__MANDEL_WORKER_SOURCES__;
|
||||||
|
if (!sources?.shallow || !sources?.deep) throw new Error('Worker source extraction failed.');
|
||||||
|
new vm.Script(sources.shallow, { filename: 'shallow-worker.js' });
|
||||||
|
new vm.Script(sources.deep, { filename: 'deep-worker.js' });
|
||||||
|
|
||||||
|
let deepReady = null;
|
||||||
|
const workerSandbox = {
|
||||||
|
WebAssembly, Uint8Array, Uint8ClampedArray, Uint32Array, Float32Array, Float64Array,
|
||||||
|
ArrayBuffer, Map, Set, Math, Date, JSON, Promise, performance,
|
||||||
|
postMessage(message) { deepReady = message; }
|
||||||
|
};
|
||||||
|
workerSandbox.self = workerSandbox;
|
||||||
|
const workerContext = vm.createContext(workerSandbox);
|
||||||
|
new vm.Script(sources.deep, { filename: 'deep-worker.js' }).runInContext(workerContext);
|
||||||
|
const moduleFiles = { deep: 'deep-simd.wasm', bla: 'bla-simd.wasm', color: 'color-simd.wasm' };
|
||||||
|
const modules = {};
|
||||||
|
for (const [key, file] of Object.entries(moduleFiles)) {
|
||||||
|
const bytes = await fs.readFile(new URL(`dist/wasm/${file}`, root));
|
||||||
|
modules[key] = { module: structuredClone(await WebAssembly.compile(bytes)), simd: true };
|
||||||
|
}
|
||||||
|
await workerSandbox.onmessage({ data: { type: 'init', modules } });
|
||||||
|
if (deepReady?.type !== 'ready' || deepReady.error) throw new Error(`Deep Worker module init failed: ${deepReady?.error || 'no reply'}`);
|
||||||
|
|
||||||
|
const diagnostics = sandbox.__MANDEL_DIAG__?.snapshot();
|
||||||
|
if (diagnostics?.rendererVersion !== 23) throw new Error('Application initialization failed.');
|
||||||
|
if (diagnostics.lastPass !== 'preview') throw new Error('Discrete preview profile did not initialize.');
|
||||||
|
if (diagnostics.automaticTarget !== 'COVERED') throw new Error('Standard mode must stop automatically at Covered.');
|
||||||
|
if (JSON.stringify(sandbox.__MANDEL_MODE_PROBE__?.targets) !== JSON.stringify({ power: 'PREVIEW', standard: 'COVERED', fine: 'REFINED', validate: 'VALIDATED' })) throw new Error('Processing-mode completion targets failed.');
|
||||||
|
if (sandbox.__MANDEL_MODE_PROBE__?.coldDeepPreview?.[0] > 48) throw new Error('Cold deep Preview exceeded its conservative width cap.');
|
||||||
|
if (sandbox.__MANDEL_FIELD_PROBE__?.iterations?.join(',') !== '7,100') throw new Error('Field iteration channel failed.');
|
||||||
|
sandbox.requestAnimationFrame = callback => { queueMicrotask(() => callback(performance.now())); return ++rafId; };
|
||||||
|
const referenceCheckpoints = await sandbox.__MANDEL_REFERENCE_PROBE__();
|
||||||
|
if (!referenceCheckpoints.agreement || !referenceCheckpoints.catchesMismatch || referenceCheckpoints.bits !== 320) throw new Error('P/P+64 reference checkpoint verification failed.');
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
status: 'pass', rendererVersion: diagnostics.rendererVersion, lastPass: diagnostics.lastPass, automaticTarget: diagnostics.automaticTarget,
|
||||||
|
fieldIterations: sandbox.__MANDEL_FIELD_PROBE__.iterations,
|
||||||
|
modeProbe: sandbox.__MANDEL_MODE_PROBE__,
|
||||||
|
referenceCheckpoints,
|
||||||
|
deepWorkerModuleInit: true,
|
||||||
|
browserHarnessSyntax: true,
|
||||||
|
sources: { appBytes: app.length, shallowWorkerBytes: sources.shallow.length, deepWorkerBytes: sources.deep.length }
|
||||||
|
}));
|
||||||
68
tests/kernel-golden.mjs
Normal file
68
tests/kernel-golden.mjs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const root = path.resolve(process.argv[2] || 'build/wasm-v23');
|
||||||
|
const load = async name => {
|
||||||
|
const bytes = await fs.readFile(path.join(root, name));
|
||||||
|
return (await WebAssembly.instantiate(bytes, {})).instance.exports;
|
||||||
|
};
|
||||||
|
const assert = (ok, message) => { if (!ok) throw new Error(message); };
|
||||||
|
const close = (a, b, tolerance) => Math.abs(a - b) <= tolerance * Math.max(1, Math.abs(a), Math.abs(b));
|
||||||
|
|
||||||
|
function direct(cr, ci, limit) {
|
||||||
|
let zr=0, zi=0, zr2=0, zi2=0, n=0;
|
||||||
|
while (n < limit && zr2 + zi2 <= 4) {
|
||||||
|
zi = 2*zr*zi + ci; zr = zr2 - zi2 + cr; zr2 = zr*zr; zi2 = zi*zi; n++;
|
||||||
|
}
|
||||||
|
return [n, n < limit ? zr2 + zi2 : 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function shallowGolden() {
|
||||||
|
const cores = await Promise.all(['wasm-simd.wasm','wasm-scalar.wasm'].map(load));
|
||||||
|
const width=19, height=11, iter=320, re=-0.5, im=0, span=3.4, scale=span/width;
|
||||||
|
let baseline;
|
||||||
|
for (const [variant, ex] of [['simd',cores[0]],['scalar',cores[1]]]) {
|
||||||
|
const npx=ex.render_rows(re+scale*.5,im-scale*.5,span,width,height,0,height,iter);
|
||||||
|
assert(npx===width*height, `shallow ${variant}: output size`);
|
||||||
|
const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),npx);
|
||||||
|
const mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),npx);
|
||||||
|
const copy=Uint32Array.from(counts); if (!baseline) baseline=copy;
|
||||||
|
for(let y=0;y<height;y++)for(let x=0;x<width;x++){
|
||||||
|
const i=y*width+x,shiftedRe=re+scale*.5,shiftedIm=im-scale*.5,cr=shiftedRe+scale*(x-width*.5),ci=shiftedIm+scale*(height*.5-y),[n,m]=direct(cr,ci,iter);
|
||||||
|
assert(counts[i]===n, `shallow ${variant}: count mismatch at ${x},${y}`);
|
||||||
|
if(n<iter)assert(close(mags[i],m,1e-12),`shallow ${variant}: magnitude mismatch at ${x},${y}: wasm=${mags[i]} direct=${m}`);
|
||||||
|
assert(copy[i]===baseline[i],`shallow SIMD/scalar mismatch at ${i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function colorGolden() {
|
||||||
|
const cores=await Promise.all(['color-simd.wasm','color-scalar.wasm'].map(load));
|
||||||
|
const inputs=[4.0000001,4.1,8,32,1e4,1e100]; let baseline;
|
||||||
|
for(const [variant,ex] of [['simd',cores[0]],['scalar',cores[1]]]){
|
||||||
|
const mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),corr=new Float32Array(ex.memory.buffer,ex.corr_ptr(),65536);mags.set(inputs);assert(ex.smooth_batch(inputs.length)===inputs.length,`color ${variant}: output size`);const copy=Float32Array.from(corr.subarray(0,inputs.length));if(!baseline)baseline=copy;
|
||||||
|
for(let i=0;i<inputs.length;i++){const expected=1-Math.log2(.5*Math.log2(inputs[i]));assert(close(copy[i],expected,3e-5),`color ${variant}: correction ${i}`);assert(close(copy[i],baseline[i],1e-7),`color SIMD/scalar mismatch ${i}`)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reference(cr,ci,limit){const rr=new Float64Array(limit+1),ri=new Float64Array(limit+1);let zr=0,zi=0;for(let n=0;n<=limit;n++){rr[n]=zr;ri[n]=zi;const nr=zr*zr-zi*zi+cr;zi=2*zr*zi+ci;zr=nr}return{rr,ri}}
|
||||||
|
|
||||||
|
async function deepGolden(){
|
||||||
|
const deep=await Promise.all(['deep-simd.wasm','deep-scalar.wasm'].map(load));
|
||||||
|
const blas=await Promise.all(['bla-simd.wasm','bla-scalar.wasm'].map(load));
|
||||||
|
const width=13,height=9,iter=600,re=-.75,im=.1,span=1e-7,scale=span/width,ref=reference(re,im,iter);
|
||||||
|
let baseline;
|
||||||
|
for(const [variant,ex] of [['simd',deep[0]],['scalar',deep[1]]]){
|
||||||
|
new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ref.rr);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ref.ri);
|
||||||
|
const npx=ex.render_perturb_rebase_rect(span,0,scale*.5,-scale*.5,0,iter,width,height,width*.5,height*.5,0,0,width,height,iter,0,0,0,0,0,0,0);
|
||||||
|
assert(npx===width*height,`deep ${variant}: output size`);const counts=Uint32Array.from(new Uint32Array(ex.memory.buffer,ex.counts_ptr(),npx));if(!baseline)baseline=counts;
|
||||||
|
for(let y=0;y<height;y++)for(let x=0;x<width;x++){const i=y*width+x,cr=re+(x+.5-width*.5)*scale,ci=im+(height*.5-y-.5)*scale,[n]=direct(cr,ci,iter);assert(counts[i]===n,`deep ${variant}: count mismatch at ${x},${y}`);assert(counts[i]===baseline[i],`deep SIMD/scalar mismatch ${i}`)}
|
||||||
|
}
|
||||||
|
for(const [variant,ex] of [['simd',blas[0]],['scalar',blas[1]]]){
|
||||||
|
new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ref.rr);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ref.ri);assert(ex.build_bla(iter,Math.hypot(span*.5,span*height/(2*width)),2**-32)>0,`BLA ${variant}: build`);
|
||||||
|
const npx=ex.render_bla_rect_v2(span,scale*.5,-scale*.5,re,im,iter,width,height,0,0,width,height,iter,0,0,1);assert(npx===width*height,`BLA ${variant}: output size`);const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),npx);for(let i=0;i<npx;i++)assert(counts[i]===baseline[i]||counts[i]===0xfffffffe,`BLA ${variant}: mismatch ${i}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await shallowGolden();await colorGolden();await deepGolden();
|
||||||
|
console.log(JSON.stringify({status:'pass',generatedDirectory:root,suites:['shallow','color','deep','bla']}));
|
||||||
7
tests/kernel-golden.ps1
Normal file
7
tests/kernel-golden.ps1
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
param([Parameter(Mandatory=$true)][string]$GeneratedDirectory)
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$node = Get-Command node -ErrorAction SilentlyContinue
|
||||||
|
if (-not $node) { throw 'Node.js is required to execute generated WASM golden vectors.' }
|
||||||
|
$directory = (Resolve-Path -LiteralPath $GeneratedDirectory).Path
|
||||||
|
& $node.Source (Join-Path $PSScriptRoot 'kernel-golden.mjs') $directory
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw 'Kernel golden vectors failed.' }
|
||||||
16
tests/kernel-source-contract.ps1
Normal file
16
tests/kernel-source-contract.ps1
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
$abi = Get-Content -LiteralPath (Join-Path $workspace 'src\abi.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
$lock = Get-Content -LiteralPath (Join-Path $workspace 'toolchain.lock.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($abi.format -ne 'mandelbrot-kernel-abi-v23') { throw 'Unexpected kernel ABI version.' }
|
||||||
|
if ($lock.version -ne '17.0.6') { throw 'The pinned compiler version changed.' }
|
||||||
|
foreach ($module in $abi.modules.psobject.Properties) {
|
||||||
|
$source = Join-Path $workspace "src\$($module.Value.source)"
|
||||||
|
if (-not (Test-Path -LiteralPath $source)) { throw "Missing source: $source" }
|
||||||
|
$text = Get-Content -LiteralPath $source -Raw -Encoding UTF8
|
||||||
|
foreach ($export in $module.Value.exports) {
|
||||||
|
if ($export -eq 'memory') { continue }
|
||||||
|
if ($text -notmatch [regex]::Escape("export_name(`"$export`"") ) { throw "Missing $($module.Name) export $export" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[ordered]@{ status='pass'; modules=@($abi.modules.psobject.Properties).Count; compiler=$lock.version; pixelContract=$abi.pixelContract } | ConvertTo-Json
|
||||||
22
tests/module-clone.mjs
Normal file
22
tests/module-clone.mjs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
|
||||||
|
const root = new URL('../dist/wasm/', import.meta.url);
|
||||||
|
const assets = [
|
||||||
|
['deep-simd.wasm', ['render_perturb_rebase_rect']],
|
||||||
|
['bla-simd.wasm', ['build_bla', 'render_bla_rect_v2']],
|
||||||
|
['color-simd.wasm', ['smooth_batch']]
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = [];
|
||||||
|
for (const [name, requiredExports] of assets) {
|
||||||
|
const bytes = await fs.readFile(new URL(name, root));
|
||||||
|
const compiled = await WebAssembly.compile(bytes);
|
||||||
|
const cloned = structuredClone(compiled);
|
||||||
|
const instance = await WebAssembly.instantiate(cloned, {});
|
||||||
|
for (const symbol of requiredExports) {
|
||||||
|
if (typeof instance.exports[symbol] !== 'function') throw new Error(`${name}: missing ${symbol}`);
|
||||||
|
}
|
||||||
|
results.push({ name, bytes: bytes.length, exports: requiredExports });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify({ status: 'pass', structuredClone: true, results }));
|
||||||
62
tests/pixel-contract.mjs
Normal file
62
tests/pixel-contract.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import '../kernels.js';
|
||||||
|
|
||||||
|
const K = globalThis.MANDEL_KERNELS;
|
||||||
|
const decode = b64 => Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
||||||
|
|
||||||
|
function instantiate() {
|
||||||
|
for (const [name, payload] of [['simd', K.WASM_SIMD_B64], ['scalar', K.WASM_SCALAR_B64]]) {
|
||||||
|
try {
|
||||||
|
const instance = new WebAssembly.Instance(new WebAssembly.Module(decode(payload)), {});
|
||||||
|
return { name, ex: instance.exports };
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
throw new Error('Neither shallow WASM backend could be instantiated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsPixel(cre, cim, span, width, height, x, y, iter) {
|
||||||
|
const scale = span / width;
|
||||||
|
// Match the public ABI operation order: JavaScript shifts the center once,
|
||||||
|
// then the kernel applies the historical integer-grid formula. The
|
||||||
|
// algebraically equivalent single expression can round differently at a
|
||||||
|
// chaotic boundary and is not a useful backend-equivalence oracle.
|
||||||
|
const shiftedRe = cre + scale * 0.5;
|
||||||
|
const shiftedIm = cim - scale * 0.5;
|
||||||
|
const cr = shiftedRe + scale * (x - width * 0.5);
|
||||||
|
const ci = shiftedIm + scale * (height * 0.5 - y);
|
||||||
|
let zr = 0, zi = 0, zr2 = 0, zi2 = 0, n = 0;
|
||||||
|
while (n < iter && zr2 + zi2 <= 4) {
|
||||||
|
zi = 2 * zr * zi + ci;
|
||||||
|
zr = zr2 - zi2 + cr;
|
||||||
|
zr2 = zr * zr;
|
||||||
|
zi2 = zi * zi;
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backend = instantiate();
|
||||||
|
const scenes = [
|
||||||
|
{ cre: -0.5, cim: 0, span: 3.4, width: 31, height: 19, iter: 350 },
|
||||||
|
{ cre: -0.743643887037151, cim: 0.13182590420533, span: 4e-4, width: 37, height: 23, iter: 700 }
|
||||||
|
];
|
||||||
|
|
||||||
|
let samples = 0;
|
||||||
|
let mismatches = 0;
|
||||||
|
const mismatchDetails = [];
|
||||||
|
for (const s of scenes) {
|
||||||
|
const scale = s.span / s.width;
|
||||||
|
const npx = backend.ex.render_rows(s.cre + scale * 0.5, s.cim - scale * 0.5, s.span, s.width, s.height, 0, s.height, s.iter);
|
||||||
|
const counts = new Uint32Array(backend.ex.memory.buffer, backend.ex.counts_ptr(), npx);
|
||||||
|
for (let y = 0; y < s.height; y++) for (let x = 0; x < s.width; x++) {
|
||||||
|
samples++;
|
||||||
|
const wasmCount = counts[y * s.width + x];
|
||||||
|
const jsCount = jsPixel(s.cre, s.cim, s.span, s.width, s.height, x, y, s.iter);
|
||||||
|
if (wasmCount !== jsCount) {
|
||||||
|
mismatches++;
|
||||||
|
if (mismatchDetails.length < 12) mismatchDetails.push({ scene: scenes.indexOf(s), x, y, wasmCount, jsCount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mismatches) throw new Error(`Pixel contract mismatch: ${mismatches}/${samples} ${JSON.stringify(mismatchDetails)}`);
|
||||||
|
console.log(JSON.stringify({ status: 'pass', backend: backend.name, samples, mismatches, contract: '(x+0.5,y+0.5)' }));
|
||||||
12
tests/pixel-mapping.mjs
Normal file
12
tests/pixel-mapping.mjs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
const assert=(ok,message)=>{if(!ok)throw new Error(message)};
|
||||||
|
const close=(a,b)=>Math.abs(a-b)<=1e-15*Math.max(1,Math.abs(a),Math.abs(b));
|
||||||
|
const view={re:-.743643887037151,im:.13182590420533,span:3.4e-12,w:37,h:23};
|
||||||
|
const world=(x,y,w=view.w,h=view.h,span=view.span)=>[view.re+(x+.5-w*.5)*span/w,view.im+(h*.5-y-.5)*span/w];
|
||||||
|
const shallow=(x,y)=>{const scale=view.span/view.w,shiftedRe=view.re+scale*.5,shiftedIm=view.im-scale*.5;return[shiftedRe+(x-view.w*.5)*scale,shiftedIm+(view.h*.5-y)*scale]};
|
||||||
|
const deep=(x,y)=>{const scale=view.span/view.w,offR=scale*.5,offI=-scale*.5;return[view.re+offR+scale*(x-view.w*.5),view.im+offI+scale*(view.h*.5-y)]};
|
||||||
|
const bla=(x,y)=>{const scale=view.span/view.w,offR=scale*.5,offI=-scale*.5;return[view.re+offR+view.span*(x/view.w-.5),view.im+offI+view.span*((.5*view.h-y)/view.w)]};
|
||||||
|
let samples=0;for(let y=0;y<view.h;y++)for(let x=0;x<view.w;x++){const expected=world(x,y);for(const [name,actual]of[['shallow',shallow(x,y)],['deep',deep(x,y)],['bla',bla(x,y)]]){assert(close(expected[0],actual[0])&&close(expected[1],actual[1]),`${name} mapping mismatch ${x},${y}`)}samples++}
|
||||||
|
for(const sampleScale of[2,4]){const tile={x:7,y:5,w:11,h:9},W=view.w*sampleScale,H=view.h*sampleScale;for(let phase=0;phase<sampleScale*sampleScale;phase++){const left=(phase%sampleScale)*tile.w,top=((phase/sampleScale)|0)*tile.h;for(let yy=0;yy<tile.h;yy++)for(let xx=0;xx<tile.w;xx++){const gx=tile.x*sampleScale+left+xx,gy=tile.y*sampleScale+top+yy,expected=world(gx,gy,W,H,view.span);const direct=[view.re+(gx+.5-W*.5)*view.span/W,view.im+(H*.5-gy-.5)*view.span/W];assert(close(expected[0],direct[0])&&close(expected[1],direct[1]),`tile seam ${sampleScale}x phase ${phase}`)}}}
|
||||||
|
const first=world(0,0),last=world(view.w-1,view.h-1);assert(close((first[0]+last[0])/2,view.re),'view center moved on x');assert(close((first[1]+last[1])/2,view.im),'view center moved on y');
|
||||||
|
const result={status:'pass',samples,backends:['shallow','deep','bla'],subsampleScales:[2,4],contract:'(x+0.5,y+0.5)'};
|
||||||
|
console.log(JSON.stringify(result));export default result;
|
||||||
45
tests/precision-reference.mjs
Normal file
45
tests/precision-reference.mjs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
function fromDecimal(text, bits) {
|
||||||
|
let s = String(text).trim(), neg = s.startsWith('-');
|
||||||
|
if (neg) s = s.slice(1);
|
||||||
|
const [mantissa, exponentText] = s.toLowerCase().split('e');
|
||||||
|
const exponent = exponentText ? Number.parseInt(exponentText, 10) : 0;
|
||||||
|
const [whole = '0', fraction = ''] = mantissa.split('.');
|
||||||
|
let digits = `${whole}${fraction}`.replace(/^0+(?=\d)/, '') || '0';
|
||||||
|
let places = fraction.length - exponent;
|
||||||
|
if (places < 0) { digits += '0'.repeat(-places); places = 0; }
|
||||||
|
const value = BigInt(digits) * (1n << BigInt(bits)) / (10n ** BigInt(places));
|
||||||
|
return neg ? -value : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundShift(value, bits) {
|
||||||
|
const negative = value < 0n, absolute = negative ? -value : value;
|
||||||
|
const rounded = (absolute + (1n << (BigInt(bits) - 1n))) >> BigInt(bits);
|
||||||
|
return negative ? -rounded : rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
function iterate(reText, imText, bits, limit) {
|
||||||
|
const cr = fromDecimal(reText, bits), ci = fromDecimal(imText, bits), four = 4n << BigInt(bits);
|
||||||
|
let zr = 0n, zi = 0n;
|
||||||
|
for (let n = 0; n < limit; n++) {
|
||||||
|
const zr2 = roundShift(zr * zr, bits), zi2 = roundShift(zi * zi, bits);
|
||||||
|
if (zr2 + zi2 > four) return n;
|
||||||
|
zi = roundShift(2n * zr * zi, bits) + ci;
|
||||||
|
zr = zr2 - zi2 + cr;
|
||||||
|
}
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cases = [
|
||||||
|
{ id: 'outside', re: '1', im: '0', limit: 100 },
|
||||||
|
{ id: 'boundary-escape', re: '-0.75', im: '0.1', limit: 5000 },
|
||||||
|
{ id: 'period3-center', re: '-0.122561166876', im: '0.744861766619', limit: 4000 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = cases.map(test => {
|
||||||
|
const p = iterate(test.re, test.im, 256, test.limit);
|
||||||
|
const guarded = iterate(test.re, test.im, 320, test.limit);
|
||||||
|
return { id: test.id, p, guarded, stable: p === guarded };
|
||||||
|
});
|
||||||
|
if (results.some(result => !result.stable)) throw new Error(`Precision checkpoint mismatch: ${JSON.stringify(results)}`);
|
||||||
|
if (roundShift(-123456789n, 8) !== -roundShift(123456789n, 8)) throw new Error('Round-to-nearest lost sign symmetry.');
|
||||||
|
console.log(JSON.stringify({ status: 'pass', precisions: [256, 320], results }));
|
||||||
41
tests/runtime-budget.mjs
Normal file
41
tests/runtime-budget.mjs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { performance } from 'node:perf_hooks';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
|
||||||
|
const wasmDir=path.join(root,'build','wasm-v23');
|
||||||
|
const load=async name=>(await WebAssembly.instantiate(await fs.readFile(path.join(wasmDir,name)),{})).instance.exports;
|
||||||
|
const median=values=>values.slice().sort((a,b)=>a-b)[values.length>>1];
|
||||||
|
const assert=(ok,message)=>{if(!ok)throw new Error(message)};
|
||||||
|
|
||||||
|
async function benchmarkShallow(){
|
||||||
|
const ex=await load('wasm-simd.wasm'),cases=[
|
||||||
|
{id:'overview-350',re:-.5,im:0,span:3.4,w:640,h:400,iter:350},
|
||||||
|
{id:'boundary-900',re:-.743643887037151,im:.13182590420533,span:4e-4,w:512,h:320,iter:900}
|
||||||
|
],results=[];
|
||||||
|
for(const c of cases){const scale=c.span/c.w,rows=Math.max(1,Math.floor(65536/c.w)),run=()=>{let sum=0;for(let y=0;y<c.h;y+=rows){const n=ex.render_rows(c.re+scale*.5,c.im-scale*.5,c.span,c.w,c.h,y,Math.min(rows,c.h-y),c.iter);assert(n===c.w*Math.min(rows,c.h-y),`shallow output size: ${c.id}`);const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),n);for(let i=0;i<n;i++)sum+=counts[i]}return sum};run();const samples=[];let sum=0;for(let i=0;i<5;i++){const t=performance.now();sum=run();samples.push(performance.now()-t)}const ms=median(samples),pixels=c.w*c.h;results.push({id:c.id,width:c.w,height:c.h,iterations:c.iter,pixels,medianMs:ms,msPerMegapixel:ms*1e6/pixels,meanIterations:sum/pixels,samplesMs:samples})}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
function reference(cr,ci,limit){const rr=new Float64Array(limit+1),ri=new Float64Array(limit+1);let zr=0,zi=0;for(let n=0;n<=limit;n++){rr[n]=zr;ri[n]=zi;const nr=zr*zr-zi*zi+cr;zi=2*zr*zi+ci;zr=nr}return{rr,ri}}
|
||||||
|
async function benchmarkDeep(){
|
||||||
|
const ex=await load('bla-simd.wasm'),c={id:'seahorse-bla-2000',re:-.743643887037151,im:.13182590420533,span:3.4e-14,w:256,h:144,iter:2000},ref=reference(c.re,c.im,c.iter),scale=c.span/c.w;
|
||||||
|
new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ref.rr);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ref.ri);
|
||||||
|
const tBuild=performance.now(),entries=ex.build_bla(c.iter,Math.hypot(c.span*.5,c.span*c.h/(2*c.w)),2**-32),buildMs=performance.now()-tBuild;assert(entries>0,'BLA table build failed');
|
||||||
|
const run=()=>ex.render_bla_rect_v2(c.span,scale*.5,-scale*.5,c.re,c.im,c.iter,c.w,c.h,0,0,c.w,c.h,c.iter,0,0,1);run();const samples=[];for(let i=0;i<5;i++){const t=performance.now();assert(run()===c.w*c.h,'BLA output size');samples.push(performance.now()-t)}const ms=median(samples),counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),c.w*c.h),unresolved=counts.reduce((n,v)=>n+(v>=0xfffffffe),0);return{id:c.id,width:c.w,height:c.h,iterations:c.iter,pixels:c.w*c.h,blaEntries:entries,buildMs,medianMs:ms,msPerMegapixel:ms*1e6/(c.w*c.h),unresolved,samplesMs:samples}
|
||||||
|
}
|
||||||
|
|
||||||
|
const source=await fs.readFile(path.join(root,'script.js'),'utf8'),contracts={
|
||||||
|
previewBudget110:/budgetMs:110/.test(source),
|
||||||
|
modeTargets:/power:'PREVIEW',standard:'COVERED',fine:'REFINED',validate:'VALIDATED'/.test(source),
|
||||||
|
screenBudgets:/processMode==='power'\)return 1\*1048576/.test(source)&&/lowMemory\|\|small\?2:4/.test(source)&&/lowMemory\|\|small\?4:8/.test(source),
|
||||||
|
boundedContinuation:/deep\?384:4096/.test(source),
|
||||||
|
coldDeepCap:/deep&&!profile\.covered&&measured<=0\)\{nominal=Math\.min\(nominal,48\)/.test(source),
|
||||||
|
measuredDeepBudget:/measuredMPP=renderPerf\.deepMPP\|\|\.03/.test(source)&&/Math\.round\(1400\/measuredMPP\)/.test(source),
|
||||||
|
viewportIndependentFloor:/minDpr=Math\.min\(1,64\/Math\.max\(cssW,cssH\)\)/.test(source)
|
||||||
|
};
|
||||||
|
assert(Object.values(contracts).every(Boolean),`runtime budget contract failed: ${JSON.stringify(contracts)}`);
|
||||||
|
const report={format:'mandelbrot-node-runtime-budget-v23',generatedUtc:new Date().toISOString(),node:process.version,contracts,shallow:await benchmarkShallow(),deep:await benchmarkDeep()};
|
||||||
|
const outputArg=process.argv.indexOf('--out');if(outputArg>=0){const target=path.resolve(process.argv[outputArg+1]);await fs.writeFile(target,JSON.stringify(report,null,2)+'\n','utf8')}
|
||||||
|
console.log(JSON.stringify(report));
|
||||||
18
tests/scenes.json
Normal file
18
tests/scenes.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"format": "mandelbrot-scene-corpus-v1",
|
||||||
|
"rendererVersion": 23,
|
||||||
|
"pixelContract": "centered",
|
||||||
|
"scenes": [
|
||||||
|
{"id":"z0","re":"-0.5","im":"0","span":"3.4","tags":["shallow","overview"]},
|
||||||
|
{"id":"seahorse-z14","re":"-0.743643887037151","im":"0.13182590420533","span":"3.4e-14","tags":["deep","boundary"]},
|
||||||
|
{"id":"seahorse-z20","re":"-0.743643887037151","im":"0.13182590420533","span":"3.4e-20","tags":["deep","boundary","warm-reference"]},
|
||||||
|
{"id":"seahorse-z100","re":"-0.743643887037151","im":"0.13182590420533","span":"3.4e-100","tags":["deep","precision"]},
|
||||||
|
{"id":"period3-interior","re":"-0.122561166876","im":"0.744861766619","span":"1e-8","tags":["interior","periodic"]},
|
||||||
|
{"id":"deep-cliff-e280","re":"-0.743643887037151","im":"0.13182590420533","span":"1e-280","tags":["deep","scaled-bla-boundary"]}
|
||||||
|
],
|
||||||
|
"viewports": [
|
||||||
|
{"id":"mobile","cssWidth":390,"cssHeight":844,"dpr":3},
|
||||||
|
{"id":"desktop","cssWidth":1440,"cssHeight":900,"dpr":2},
|
||||||
|
{"id":"4k","cssWidth":3840,"cssHeight":2160,"dpr":1}
|
||||||
|
]
|
||||||
|
}
|
||||||
94
tests/source-contract.ps1
Normal file
94
tests/source-contract.ps1
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
$script = Get-Content -LiteralPath (Join-Path $workspace 'script.js') -Raw -Encoding UTF8
|
||||||
|
$html = Get-Content -LiteralPath (Join-Path $workspace 'index.html') -Raw -Encoding UTF8
|
||||||
|
$hosted = Get-Content -LiteralPath (Join-Path $workspace 'hosted-loader.js') -Raw -Encoding UTF8
|
||||||
|
$kernels = Get-Content -LiteralPath (Join-Path $workspace 'kernels.js') -Raw -Encoding UTF8
|
||||||
|
|
||||||
|
function Assert-Contains([string]$Text, [string]$Pattern, [string]$Message) {
|
||||||
|
if ($Text -notmatch $Pattern) { throw $Message }
|
||||||
|
}
|
||||||
|
function Assert-NotContains([string]$Text, [string]$Pattern, [string]$Message) {
|
||||||
|
if ($Text -match $Pattern) { throw $Message }
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert-NotContains $script 'paintFrame\(\);updateStats\(\);requestAnimationFrame\(loop\)' 'Perpetual RAF loop returned.'
|
||||||
|
Assert-NotContains $script 'setTimeout\(\(\)=>ensureDeepPool\(\),180\)' 'Deep pool is eager again.'
|
||||||
|
Assert-Contains $script '2\*x\+1-w' 'Centered BigInt pixel mapping is missing.'
|
||||||
|
Assert-Contains $script 'cre\+scale\*\.5,cim-scale\*\.5' 'Centered shallow WASM mapping is missing.'
|
||||||
|
Assert-Contains $script "\?'COVERED':'PREVIEW'" 'Covered completion state is missing.'
|
||||||
|
Assert-Contains $script 'RENDER_PASS=Object\.freeze' 'Render pass enum is missing.'
|
||||||
|
Assert-Contains $script 'if\(profile\.covered\)return null' 'Covered is allowed to reuse reprojected pixels.'
|
||||||
|
Assert-Contains $script 'const RENDER_PROFILE=Object\.freeze' 'Discrete render profiles are missing.'
|
||||||
|
Assert-Contains $script 'MODE_TARGET=Object\.freeze\(\{power:''PREVIEW'',standard:''COVERED'',fine:''REFINED'',validate:''VALIDATED''\}\)' 'Processing modes are not bound to explicit automatic completion targets.'
|
||||||
|
Assert-Contains $script 'budgetMs:110' 'Preview time budget regressed above the 80-120 ms target.'
|
||||||
|
Assert-Contains $script 'deep&&!profile\.covered&&measured<=0\)\{nominal=Math\.min\(nominal,48\)' 'Cold deep Preview has no conservative 48px first-frame cap.'
|
||||||
|
Assert-Contains $script 'deep&&!profile\.covered\?32:96' 'Cold deep Preview still inherits the 96px minimum height.'
|
||||||
|
Assert-Contains $script 'function adaptStandardDeepBudget' 'Standard deep rendering is not adapted from measured milliseconds per pixel.'
|
||||||
|
Assert-Contains $script 'deep=deepEngineNeeded\(snap,Math\.max\(1,canvas\.width\)\)' 'Frame completion still infers the deep engine from a display label or Preview width.'
|
||||||
|
Assert-Contains $script 'measuredMPP=renderPerf\.deepMPP\|\|\.03' 'Unmeasured/reprojected deep views can bypass the conservative runtime budget.'
|
||||||
|
Assert-Contains $script 'Math\.round\(1400/measuredMPP\)' 'Standard deep Covered budget is not tied to its 1.4 second target.'
|
||||||
|
Assert-Contains $script 'minDpr=Math\.min\(1,64/Math\.max\(cssW,cssH\)\)' 'Effective DPR floor still prevents 4K deep scenes from meeting the runtime budget.'
|
||||||
|
Assert-Contains $script 'state\.processMode===''power''\|\|state\.dirty' 'Power mode still advances automatically to a full Covered render.'
|
||||||
|
Assert-Contains $script 'state\.processMode!==''fine''' 'Automatic unknown-pixel continuation is not limited to Fine mode.'
|
||||||
|
Assert-Contains $script 'deep\?384:4096' 'Unknown-pixel continuation has no bounded deep/shallow sample cap.'
|
||||||
|
Assert-NotContains $script 'lastQuality' 'Legacy continuous render quality state returned.'
|
||||||
|
Assert-Contains $script 'FIELD_INTERIOR_LIKELY' 'Packed field classes are missing.'
|
||||||
|
Assert-Contains $script 'unresolved&&d\.covered' 'Preview BLA work caps are still repaired eagerly.'
|
||||||
|
Assert-NotContains $script 'likely=n===0xfffffffe' 'BLA work-cap status is still classified as interior likely.'
|
||||||
|
Assert-Contains $script 'iterations:new Uint32Array' 'Packed field escape iteration channel is missing.'
|
||||||
|
Assert-Contains $script 'iterationBuffer' 'Worker iteration buffer recycling is missing.'
|
||||||
|
Assert-Contains $script 'function fixedAnalyticInterior' 'Exact fixed-point analytic interior proof is missing.'
|
||||||
|
Assert-Contains $script 'fixedAnalyticPixelProven\(snap,fv\.w,fv\.h,x,y\)' 'Validation does not use the exact analytic proof.'
|
||||||
|
Assert-Contains $script 'classes\[i\]=likely\(cr,ci\)\?3:4' 'f64 worker interior must remain likely, not proven.'
|
||||||
|
Assert-Contains $script 'resolveSubsampleField' 'Linear-light detail resolve is missing.'
|
||||||
|
Assert-Contains $script 'sampleScale=tile\.score>=1\.15\?4:2' 'Adaptive 2x/4x AA is missing.'
|
||||||
|
Assert-NotContains $script 'n<36' 'Fixed 36-tile refinement cap returned.'
|
||||||
|
Assert-Contains $script 'detailCacheBudget\(\)' 'Byte-budget detail cache is missing.'
|
||||||
|
Assert-Contains $script 'function memoryLedger' 'Logical memory ledger is missing.'
|
||||||
|
Assert-Contains $script 'function deepWisdomStorageKey' 'Versioned per-device wisdom persistence is missing.'
|
||||||
|
Assert-Contains $script 'promoteState\(required\+32-available\);invalidateReferenceOrbit\(\)' 'Orbit-condition precision promotion does not rebuild the reference.'
|
||||||
|
Assert-Contains $script 'function verifyReferenceCheckpoints' 'P/P+64 reference-orbit checkpoint verification is missing.'
|
||||||
|
Assert-Contains $script 'state\.processMode!==''validate''' 'Reference-orbit checkpoint verification is not gated to precision-first rendering.'
|
||||||
|
Assert-Contains $script 'promoteState\(32\);invalidateReferenceOrbit\(\)' 'Reference checkpoint mismatch does not rebuild the full reference at higher precision.'
|
||||||
|
Assert-Contains $script 'deepTelemetry\.badRatio>1e-4' 'Deep-engine selection ignores measured orbit/glitch risk.'
|
||||||
|
Assert-Contains $script 'w\.postMessage\(\{type:''init'',modules:deepModuleBundle\}\)' 'Compiled deep modules are not structured-cloned to workers.'
|
||||||
|
Assert-Contains $script 'function prepareDeepModules' 'Shared deep-module compile gate is missing.'
|
||||||
|
Assert-NotContains $script 'const SIMD=\$\{JSON\.stringify\(DEEP_SIMD_B64\)\}' 'Deep payloads are duplicated into the Worker source.'
|
||||||
|
Assert-Contains $script 'highPrecisionDirectPixelAsync' 'Yielding high-precision direct verifier is missing.'
|
||||||
|
Assert-Contains $script "state\.processMode==='validate'\|\|task\.tile\.score>1\.35" 'Validated detail subsamples are not guarded-direct.'
|
||||||
|
Assert-Contains $script 'kernelSha256:globalThis\.MANDEL_KERNEL_META' 'Export kernel identity metadata is missing.'
|
||||||
|
Assert-NotContains $script 'exactDeepPixel' 'Misleading exactDeepPixel alias returned.'
|
||||||
|
Assert-Contains $script 'deepEngineNeeded' 'ULP-based engine selection is missing.'
|
||||||
|
Assert-NotContains $script 'scheduleRealWisdom' 'Default active Real Wisdom benchmark returned.'
|
||||||
|
Assert-Contains $script 'fieldBuffer' 'Worker buffer recycling is missing.'
|
||||||
|
Assert-Contains $hosted 'compileStreaming' 'Hosted shallow WASM is not streamed.'
|
||||||
|
Assert-Contains $hosted 'MANDEL_KERNEL_META' 'Hosted kernel identity injection point is missing.'
|
||||||
|
Assert-Contains $html 'aria-live="polite"' 'Live status is missing.'
|
||||||
|
Assert-Contains $html 'for="iters"' 'Form labels are not associated.'
|
||||||
|
Assert-Contains $html 'id="processMode"' 'Processing mode UI is missing.'
|
||||||
|
Assert-NotContains $html 'id="hq" type="checkbox" checked' 'Boundary AA is still enabled by default in Standard mode.'
|
||||||
|
Assert-NotContains $html 'user-scalable=no' 'Page zoom was disabled again.'
|
||||||
|
Assert-Contains $html 'button\{[^}]*min-height:44px' 'Primary controls are smaller than the 44px target.'
|
||||||
|
|
||||||
|
$manifestPath = Join-Path $workspace 'dist\wasm\manifest.json'
|
||||||
|
if (-not (Test-Path -LiteralPath $manifestPath)) { throw 'WASM checksum manifest is missing.' }
|
||||||
|
$manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
if ($manifest.payloads.Count -ne 8) { throw "Expected 8 WASM payloads, found $($manifest.payloads.Count)." }
|
||||||
|
foreach ($payload in $manifest.payloads) {
|
||||||
|
$path = Join-Path (Split-Path $manifestPath) $payload.file
|
||||||
|
$actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
if ($actual -ne $payload.sha256) { throw "Checksum mismatch: $($payload.file)" }
|
||||||
|
$metaPattern = "'$([regex]::Escape($payload.symbol))':'$([regex]::Escape($payload.sha256))'"
|
||||||
|
Assert-Contains $kernels $metaPattern "Kernel metadata mismatch: $($payload.symbol)"
|
||||||
|
}
|
||||||
|
$kernelContract = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'tests\kernel-source-contract.ps1') | ConvertFrom-Json
|
||||||
|
if ($kernelContract.status -ne 'pass') { throw 'Kernel source contract failed.' }
|
||||||
|
|
||||||
|
[ordered]@{
|
||||||
|
status = 'pass'
|
||||||
|
rendererVersion = 23
|
||||||
|
wasmPayloads = $manifest.payloads.Count
|
||||||
|
scriptBytes = (Get-Item -LiteralPath (Join-Path $workspace 'script.js')).Length
|
||||||
|
htmlBytes = (Get-Item -LiteralPath (Join-Path $workspace 'index.html')).Length
|
||||||
|
} | ConvertTo-Json
|
||||||
12
toolchain.lock.json
Normal file
12
toolchain.lock.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue