From caa90775f90aed14aed5bcc429d318134a384e3a Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Sat, 22 Aug 2026 20:55:27 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EF=BD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AUDIT.md | 206 +++++ BUILD_REPRODUCIBILITY.md | 48 + COMPLETION_AUDIT.md | 53 ++ IMPLEMENTATION_REPORT.md | 89 ++ IMPROVEMENT_PROPOSAL.md | 545 +++++++++++ audit/benchmarks.json | 245 +++++ audit/final-browser-benchmark.json | 82 ++ audit/raw/audit_v17_algos_results.json | 168 ++++ audit/raw/audit_v18_kernel.txt | 142 +++ audit/raw/dev-browser-benchmark.json | 132 +++ audit/raw/dev-verification-benchmark.json | 77 ++ audit/raw/dev-warm-cache-benchmark.json | 44 + audit/raw/interior_safety_v18.json | 62 ++ audit/raw/mb_audit_v17_results.json | 98 ++ audit/raw/pilot_fill_audit.json | 16 + audit/raw/reference_recenter_audit.json | 59 ++ audit/raw/reference_recenter_audit_z20.json | 59 ++ audit/raw/v17-final-browser-benchmark.json | 82 ++ audit/raw/v18-final-browser-benchmark.json | 82 ++ audit/raw/workcap_truth_v17.json | 12 + audit/smoke-test.json | 19 + audit/v23-browser-baseline.json | 16 + audit/v23-node-performance.json | 69 ++ audit/v23-source-baseline.json | 108 +++ build/wasm-v23/bla-scalar.wasm | Bin 0 -> 4593 bytes build/wasm-v23/bla-simd.wasm | Bin 0 -> 4959 bytes build/wasm-v23/color-scalar.wasm | Bin 0 -> 632 bytes build/wasm-v23/color-simd.wasm | Bin 0 -> 1387 bytes build/wasm-v23/deep-scalar.wasm | Bin 0 -> 5246 bytes build/wasm-v23/deep-simd.wasm | Bin 0 -> 4866 bytes build/wasm-v23/manifest.json | 85 ++ build/wasm-v23/wasm-scalar.wasm | Bin 0 -> 511 bytes build/wasm-v23/wasm-simd.wasm | Bin 0 -> 511 bytes dist/_headers | 11 + dist/hosted/_headers | 11 + dist/hosted/hosted-loader.js | 36 + dist/hosted/index.html | 69 ++ dist/hosted/script.js | 912 +++++++++++++++++++ dist/standalone/index.html | 69 ++ dist/standalone/kernels.js | 22 + dist/standalone/script.js | 912 +++++++++++++++++++ dist/wasm/bla-scalar.4f06913487044823.wasm | Bin 0 -> 4593 bytes dist/wasm/bla-scalar.wasm | Bin 0 -> 4593 bytes dist/wasm/bla-simd.f80f136e9fb676ce.wasm | Bin 0 -> 4959 bytes dist/wasm/bla-simd.wasm | Bin 0 -> 4959 bytes dist/wasm/color-scalar.aa2914ec1acacaa2.wasm | Bin 0 -> 632 bytes dist/wasm/color-scalar.wasm | Bin 0 -> 632 bytes dist/wasm/color-simd.8cf83374ed0c680b.wasm | Bin 0 -> 1387 bytes dist/wasm/color-simd.wasm | Bin 0 -> 1387 bytes dist/wasm/deep-scalar.d133bbecc8f6dbdf.wasm | Bin 0 -> 5246 bytes dist/wasm/deep-scalar.wasm | Bin 0 -> 5246 bytes dist/wasm/deep-simd.d49385f06e4a8f55.wasm | Bin 0 -> 4866 bytes dist/wasm/deep-simd.wasm | Bin 0 -> 4866 bytes dist/wasm/manifest.json | 55 ++ dist/wasm/wasm-scalar.d19b26c04e1b2f59.wasm | Bin 0 -> 511 bytes dist/wasm/wasm-scalar.wasm | Bin 0 -> 511 bytes dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm | Bin 0 -> 511 bytes dist/wasm/wasm-simd.wasm | Bin 0 -> 511 bytes hosted-headers.txt | 11 + hosted-loader.js | 36 + index.html | 69 ++ kernels.js | 22 + script.js | 912 +++++++++++++++++++ scripts/build-hosted.ps1 | 43 + scripts/build-kernels.ps1 | 50 + scripts/build-standalone.ps1 | 13 + scripts/build-wasm.ps1 | 64 ++ scripts/extract-wasm.ps1 | 56 ++ scripts/source-baseline.ps1 | 98 ++ scripts/test-all.ps1 | 32 + src/abi.json | 23 + src/bla_kernel_v18.c | 204 +++++ src/color_kernel.c | 37 + src/deep_kernel.c | 112 +++ src/shallow_kernel.c | 44 + tests/analytic-interior.mjs | 29 + tests/browser-benchmark.html | 27 + tests/js-syntax.mjs | 152 ++++ tests/kernel-golden.mjs | 68 ++ tests/kernel-golden.ps1 | 7 + tests/kernel-source-contract.ps1 | 16 + tests/module-clone.mjs | 22 + tests/pixel-contract.mjs | 62 ++ tests/pixel-mapping.mjs | 12 + tests/precision-reference.mjs | 45 + tests/runtime-budget.mjs | 41 + tests/scenes.json | 18 + tests/source-contract.ps1 | 94 ++ toolchain.lock.json | 12 + 89 files changed, 7126 insertions(+) create mode 100644 AUDIT.md create mode 100644 BUILD_REPRODUCIBILITY.md create mode 100644 COMPLETION_AUDIT.md create mode 100644 IMPLEMENTATION_REPORT.md create mode 100644 IMPROVEMENT_PROPOSAL.md create mode 100644 audit/benchmarks.json create mode 100644 audit/final-browser-benchmark.json create mode 100644 audit/raw/audit_v17_algos_results.json create mode 100644 audit/raw/audit_v18_kernel.txt create mode 100644 audit/raw/dev-browser-benchmark.json create mode 100644 audit/raw/dev-verification-benchmark.json create mode 100644 audit/raw/dev-warm-cache-benchmark.json create mode 100644 audit/raw/interior_safety_v18.json create mode 100644 audit/raw/mb_audit_v17_results.json create mode 100644 audit/raw/pilot_fill_audit.json create mode 100644 audit/raw/reference_recenter_audit.json create mode 100644 audit/raw/reference_recenter_audit_z20.json create mode 100644 audit/raw/v17-final-browser-benchmark.json create mode 100644 audit/raw/v18-final-browser-benchmark.json create mode 100644 audit/raw/workcap_truth_v17.json create mode 100644 audit/smoke-test.json create mode 100644 audit/v23-browser-baseline.json create mode 100644 audit/v23-node-performance.json create mode 100644 audit/v23-source-baseline.json create mode 100644 build/wasm-v23/bla-scalar.wasm create mode 100644 build/wasm-v23/bla-simd.wasm create mode 100644 build/wasm-v23/color-scalar.wasm create mode 100644 build/wasm-v23/color-simd.wasm create mode 100644 build/wasm-v23/deep-scalar.wasm create mode 100644 build/wasm-v23/deep-simd.wasm create mode 100644 build/wasm-v23/manifest.json create mode 100644 build/wasm-v23/wasm-scalar.wasm create mode 100644 build/wasm-v23/wasm-simd.wasm create mode 100644 dist/_headers create mode 100644 dist/hosted/_headers create mode 100644 dist/hosted/hosted-loader.js create mode 100644 dist/hosted/index.html create mode 100644 dist/hosted/script.js create mode 100644 dist/standalone/index.html create mode 100644 dist/standalone/kernels.js create mode 100644 dist/standalone/script.js create mode 100644 dist/wasm/bla-scalar.4f06913487044823.wasm create mode 100644 dist/wasm/bla-scalar.wasm create mode 100644 dist/wasm/bla-simd.f80f136e9fb676ce.wasm create mode 100644 dist/wasm/bla-simd.wasm create mode 100644 dist/wasm/color-scalar.aa2914ec1acacaa2.wasm create mode 100644 dist/wasm/color-scalar.wasm create mode 100644 dist/wasm/color-simd.8cf83374ed0c680b.wasm create mode 100644 dist/wasm/color-simd.wasm create mode 100644 dist/wasm/deep-scalar.d133bbecc8f6dbdf.wasm create mode 100644 dist/wasm/deep-scalar.wasm create mode 100644 dist/wasm/deep-simd.d49385f06e4a8f55.wasm create mode 100644 dist/wasm/deep-simd.wasm create mode 100644 dist/wasm/manifest.json create mode 100644 dist/wasm/wasm-scalar.d19b26c04e1b2f59.wasm create mode 100644 dist/wasm/wasm-scalar.wasm create mode 100644 dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm create mode 100644 dist/wasm/wasm-simd.wasm create mode 100644 hosted-headers.txt create mode 100644 hosted-loader.js create mode 100644 index.html create mode 100644 kernels.js create mode 100644 script.js create mode 100644 scripts/build-hosted.ps1 create mode 100644 scripts/build-kernels.ps1 create mode 100644 scripts/build-standalone.ps1 create mode 100644 scripts/build-wasm.ps1 create mode 100644 scripts/extract-wasm.ps1 create mode 100644 scripts/source-baseline.ps1 create mode 100644 scripts/test-all.ps1 create mode 100644 src/abi.json create mode 100644 src/bla_kernel_v18.c create mode 100644 src/color_kernel.c create mode 100644 src/deep_kernel.c create mode 100644 src/shallow_kernel.c create mode 100644 tests/analytic-interior.mjs create mode 100644 tests/browser-benchmark.html create mode 100644 tests/js-syntax.mjs create mode 100644 tests/kernel-golden.mjs create mode 100644 tests/kernel-golden.ps1 create mode 100644 tests/kernel-source-contract.ps1 create mode 100644 tests/module-clone.mjs create mode 100644 tests/pixel-contract.mjs create mode 100644 tests/pixel-mapping.mjs create mode 100644 tests/precision-reference.mjs create mode 100644 tests/runtime-budget.mjs create mode 100644 tests/scenes.json create mode 100644 tests/source-contract.ps1 create mode 100644 toolchain.lock.json diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 0000000..302d94b --- /dev/null +++ b/AUDIT.md @@ -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で必要なときだけ有効化する方がよい。 diff --git a/BUILD_REPRODUCIBILITY.md b/BUILD_REPRODUCIBILITY.md new file mode 100644 index 0000000..2016af4 --- /dev/null +++ b/BUILD_REPRODUCIBILITY.md @@ -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 +``` diff --git a/COMPLETION_AUDIT.md b/COMPLETION_AUDIT.md new file mode 100644 index 0000000..da1b09e --- /dev/null +++ b/COMPLETION_AUDIT.md @@ -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`へ保存した時点で、固定実機に対する最終受入を判定できる。 diff --git a/IMPLEMENTATION_REPORT.md b/IMPLEMENTATION_REPORT.md new file mode 100644 index 0000000..a9df8d2 --- /dev/null +++ b/IMPLEMENTATION_REPORT.md @@ -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合格は主張できるが、固定実機に対する性能目標の達成は主張しない。 diff --git a/IMPROVEMENT_PROPOSAL.md b/IMPROVEMENT_PROPOSAL.md new file mode 100644 index 0000000..e632f62 --- /dev/null +++ b/IMPROVEMENT_PROPOSAL.md @@ -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を作って確定する。 diff --git a/audit/benchmarks.json b/audit/benchmarks.json new file mode 100644 index 0000000..8d91b89 --- /dev/null +++ b/audit/benchmarks.json @@ -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 + } +} \ No newline at end of file diff --git a/audit/final-browser-benchmark.json b/audit/final-browser-benchmark.json new file mode 100644 index 0000000..d2986bf --- /dev/null +++ b/audit/final-browser-benchmark.json @@ -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 +} \ No newline at end of file diff --git a/audit/raw/audit_v17_algos_results.json b/audit/raw/audit_v17_algos_results.json new file mode 100644 index 0000000..84dd51c --- /dev/null +++ b/audit/raw/audit_v17_algos_results.json @@ -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 + } + } +] \ No newline at end of file diff --git a/audit/raw/audit_v18_kernel.txt b/audit/raw/audit_v18_kernel.txt new file mode 100644 index 0000000..4f0004d --- /dev/null +++ b/audit/raw/audit_v18_kernel.txt @@ -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 } diff --git a/audit/raw/dev-browser-benchmark.json b/audit/raw/dev-browser-benchmark.json new file mode 100644 index 0000000..fb51dba --- /dev/null +++ b/audit/raw/dev-browser-benchmark.json @@ -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 + } + ] + } +] \ No newline at end of file diff --git a/audit/raw/dev-verification-benchmark.json b/audit/raw/dev-verification-benchmark.json new file mode 100644 index 0000000..0b92e50 --- /dev/null +++ b/audit/raw/dev-verification-benchmark.json @@ -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 + } + ] + } +] \ No newline at end of file diff --git a/audit/raw/dev-warm-cache-benchmark.json b/audit/raw/dev-warm-cache-benchmark.json new file mode 100644 index 0000000..468d579 --- /dev/null +++ b/audit/raw/dev-warm-cache-benchmark.json @@ -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 + } + ] + } +] \ No newline at end of file diff --git a/audit/raw/interior_safety_v18.json b/audit/raw/interior_safety_v18.json new file mode 100644 index 0000000..d233e7d --- /dev/null +++ b/audit/raw/interior_safety_v18.json @@ -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 + } +] diff --git a/audit/raw/mb_audit_v17_results.json b/audit/raw/mb_audit_v17_results.json new file mode 100644 index 0000000..619f5c6 --- /dev/null +++ b/audit/raw/mb_audit_v17_results.json @@ -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 + } +] \ No newline at end of file diff --git a/audit/raw/pilot_fill_audit.json b/audit/raw/pilot_fill_audit.json new file mode 100644 index 0000000..58ffed3 --- /dev/null +++ b/audit/raw/pilot_fill_audit.json @@ -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 +} diff --git a/audit/raw/reference_recenter_audit.json b/audit/raw/reference_recenter_audit.json new file mode 100644 index 0000000..2d0b73c --- /dev/null +++ b/audit/raw/reference_recenter_audit.json @@ -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 + } + ] +} diff --git a/audit/raw/reference_recenter_audit_z20.json b/audit/raw/reference_recenter_audit_z20.json new file mode 100644 index 0000000..60d366b --- /dev/null +++ b/audit/raw/reference_recenter_audit_z20.json @@ -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 + } + ] +} diff --git a/audit/raw/v17-final-browser-benchmark.json b/audit/raw/v17-final-browser-benchmark.json new file mode 100644 index 0000000..857e615 --- /dev/null +++ b/audit/raw/v17-final-browser-benchmark.json @@ -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 +} \ No newline at end of file diff --git a/audit/raw/v18-final-browser-benchmark.json b/audit/raw/v18-final-browser-benchmark.json new file mode 100644 index 0000000..d2986bf --- /dev/null +++ b/audit/raw/v18-final-browser-benchmark.json @@ -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 +} \ No newline at end of file diff --git a/audit/raw/workcap_truth_v17.json b/audit/raw/workcap_truth_v17.json new file mode 100644 index 0000000..658da74 --- /dev/null +++ b/audit/raw/workcap_truth_v17.json @@ -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 +} diff --git a/audit/smoke-test.json b/audit/smoke-test.json new file mode 100644 index 0000000..845005a --- /dev/null +++ b/audit/smoke-test.json @@ -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": [] +} diff --git a/audit/v23-browser-baseline.json b/audit/v23-browser-baseline.json new file mode 100644 index 0000000..307e17e --- /dev/null +++ b/audit/v23-browser-baseline.json @@ -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"] +} diff --git a/audit/v23-node-performance.json b/audit/v23-node-performance.json new file mode 100644 index 0000000..c179c70 --- /dev/null +++ b/audit/v23-node-performance.json @@ -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 + ] + } +} diff --git a/audit/v23-source-baseline.json b/audit/v23-source-baseline.json new file mode 100644 index 0000000..bf0e79a --- /dev/null +++ b/audit/v23-source-baseline.json @@ -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." + } +} diff --git a/build/wasm-v23/bla-scalar.wasm b/build/wasm-v23/bla-scalar.wasm new file mode 100644 index 0000000000000000000000000000000000000000..5ac82b3292ab2de840001cdacc2d55aa7b069c1f GIT binary patch literal 4593 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 Zr_oJ!_eloRzghAW_10@LJZeglR4mvR6A literal 0 HcmV?d00001 diff --git a/build/wasm-v23/color-simd.wasm b/build/wasm-v23/color-simd.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ddeeaaa775b966de550a12e7068996f65717d0b9 GIT binary patch literal 1387 zcmdT^y;9pi5Z={~B`i61f(-=}t1D!J+KwhuAPo>YV2Eo%X(6-F~|gtnIWA0I+DR0`^w1hdt9W z*#lEtQT$jmU0~mSAG~BS;KkX5ZTHG=a2NM*S3-bBL?B_Q1PuMYmEM$g@(<6y6aXqw zj>NW$lp#kkp&sSzk?2MBSVzv@cATT|)G3lf1qSsKg-^+)eju~? z=I1VM_F^{@B_^g`{urg2QL2%+bpvHKqs&I4d;>*f6cLHa7$rZ+Tt=9S#O;(I92SVr z?_a!|-*`~_73L`J@Bzt{b@iQ-R3$*ebSH|QMmcNrK|eURki4zbvw(+vHvaS!o79w%lt9y96GYWZW-df-eJ`3~6@L2G=x08v2RboPBj`FG4a- zLxOn|L~Zf1i)-0dC3-&dO-wb?mW`eFjea_QhaZmFAq#Lkpv!eQI1tm6TtiVCis_8x zMhDEI>A#lhHOuTZi^wfmlsSGDHL493^UfL4RHMFsDC6?BeEX{Tb&xSn!r}jR@=r&1 V4gK@t+UgQpy2Q5ZrM2}4y+2ljd;tIe literal 0 HcmV?d00001 diff --git a/build/wasm-v23/deep-scalar.wasm b/build/wasm-v23/deep-scalar.wasm new file mode 100644 index 0000000000000000000000000000000000000000..e851c8990e8b989e77f3b3d2d9536b340cd93cbd GIT binary patch literal 5246 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 literal 0 HcmV?d00001 diff --git a/build/wasm-v23/manifest.json b/build/wasm-v23/manifest.json new file mode 100644 index 0000000..6e98a4e --- /dev/null +++ b/build/wasm-v23/manifest.json @@ -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" + } + ] +} diff --git a/build/wasm-v23/wasm-scalar.wasm b/build/wasm-v23/wasm-scalar.wasm new file mode 100644 index 0000000000000000000000000000000000000000..14840c05c5cd20fbdf32f770381da1e3e8a4c6b4 GIT binary patch literal 511 zcmYLFOG*Pl5UuL&OghsUlgV%6rg~1`LIhXIfM9Up6_TJq7|e%BM1+BvrAzSylkpH) zdKHi01q5r7XbPHouimSIelS_h5CG5@W59H*lO##Y!c;2Bb5+45B32Y4k)tuiFnc{W z)idSHrn5!-BEY>{Jk8gu(Q+LF=-K3957ETa`TaB=#f#?^2=`q|4ZNIXV0Pe*!H;l6 zDiYkiY7&CgVn8y)p$DY)&Dpo=fsuR+YGIbC8@33N+ra&>hBxb^wpaWYuCIP!J#zop zXJJp+(2xT%YY0HZ*WyylzKC`p5y9jRGuNSj~sF;`Ro literal 0 HcmV?d00001 diff --git a/build/wasm-v23/wasm-simd.wasm b/build/wasm-v23/wasm-simd.wasm new file mode 100644 index 0000000000000000000000000000000000000000..14840c05c5cd20fbdf32f770381da1e3e8a4c6b4 GIT binary patch literal 511 zcmYLFOG*Pl5UuL&OghsUlgV%6rg~1`LIhXIfM9Up6_TJq7|e%BM1+BvrAzSylkpH) zdKHi01q5r7XbPHouimSIelS_h5CG5@W59H*lO##Y!c;2Bb5+45B32Y4k)tuiFnc{W z)idSHrn5!-BEY>{Jk8gu(Q+LF=-K3957ETa`TaB=#f#?^2=`q|4ZNIXV0Pe*!H;l6 zDiYkiY7&CgVn8y)p$DY)&Dpo=fsuR+YGIbC8@33N+ra&>hBxb^wpaWYuCIP!J#zop zXJJp+(2xT%YY0HZ*WyylzKC`p5y9jRGuNSj~sF;`Ro literal 0 HcmV?d00001 diff --git a/dist/_headers b/dist/_headers new file mode 100644 index 0000000..8571cd2 --- /dev/null +++ b/dist/_headers @@ -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 diff --git a/dist/hosted/_headers b/dist/hosted/_headers new file mode 100644 index 0000000..8571cd2 --- /dev/null +++ b/dist/hosted/_headers @@ -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 diff --git a/dist/hosted/hosted-loader.js b/dist/hosted/hosted-loader.js new file mode 100644 index 0000000..9a21d2b --- /dev/null +++ b/dist/hosted/hosted-loader.js @@ -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); diff --git a/dist/hosted/index.html b/dist/hosted/index.html new file mode 100644 index 0000000..745b341 --- /dev/null +++ b/dist/hosted/index.html @@ -0,0 +1,69 @@ + + + + + + +Mandelbrot Deep Zoom v23 + + + + +
MANDELBROT DEEP ZOOM
中心
倍率  表示幅
計算 起動中… 描画
+
起動中
+
+
+
+
+
+
0.0080
+
.18
+
詳細設定・正確な座標 +
350
+
+
+
+
+
診断情報
engine: …
numeric: …
memory: …
+
+
起動中
+
ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。
+
+ +
+ +

高解像度 PNG 出力

+
+ + + + +
+ +
+
+
+ + + + diff --git a/dist/hosted/script.js b/dist/hosted/script.js new file mode 100644 index 0000000..10f7903 --- /dev/null +++ b/dist/hosted/script.js @@ -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='
Canvas 2Dを利用できません。
';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<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)} +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>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;j8)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>>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{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;inew 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{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;inew 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;ideepModuleBundle={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(refLoadedblaRefLoaded){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>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=d.iter)!==(p>=d.iter))){add(i);add(i-stride)}if(nb[0]-a[0]);if(top.length>want)top.length=want}}for(const x of top)add(x[1]);for(let k=0;picks.length=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=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=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(start0&&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{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.chunkIndexjob.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{if(token!==state.token||this.cancelled)return;const deadline=performance.now()+5;while(ktoken!==state.token||this.cancelled);if(!result)return;const[n,m]=result;putField(field,idx,n,m,nrunner.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{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;ia.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(ptoken!==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,nn+(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(itoken!==state.token||state.processMode!=='validate'); + if(!result){validationJob.active=false;return} + const[n,m]=result;if(n180){lastStatus=now;state.coverage=i/field.classes.length;invalidateStats()} + if(in+(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;yminTile||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{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=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=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>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(yyplan.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,n1.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=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=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=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=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(yMath.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=iter?0:Math.max(4.0000001,zr2+zi2),kind=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(yHI128){r*=INV256;i*=INV256;b++;m*=INV256} + while(m>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<>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<=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.nBAIL)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<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(nBAIL)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)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<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.ntoken!==state.token);if(!result)return;const[n,m]=result;putField(field,y*w+x,n,m,n{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.loadedLentoken!==state.token);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n{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(n16384||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;yexportJob.cancelled):exportSampleShallow(cre,cim,pixelScale,W,H,iter,gx,gy);if(!sample)throw new Error('cancelled');const[n,m]=sample;if(noutCanvas.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{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(); +})(); diff --git a/dist/standalone/index.html b/dist/standalone/index.html new file mode 100644 index 0000000..96f0f1b --- /dev/null +++ b/dist/standalone/index.html @@ -0,0 +1,69 @@ + + + + + + +Mandelbrot Deep Zoom v23 + + + + +
MANDELBROT DEEP ZOOM
中心
倍率  表示幅
計算 起動中… 描画
+
起動中
+
+
+
+
+
+
0.0080
+
.18
+
詳細設定・正確な座標 +
350
+
+
+
+
+
診断情報
engine: …
numeric: …
memory: …
+
+
起動中
+
ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。
+
+ +
+ +

高解像度 PNG 出力

+
+ + + + +
+ +
+
+
+ + + + diff --git a/dist/standalone/kernels.js b/dist/standalone/kernels.js new file mode 100644 index 0000000..e1a203f --- /dev/null +++ b/dist/standalone/kernels.js @@ -0,0 +1,22 @@ +'use strict'; +// Generated from dist/wasm/manifest.json. Run scripts/extract-wasm.ps1 before replacing compatibility inputs. +globalThis.MANDEL_KERNELS=Object.freeze({ + WASM_SIMD_B64:'AGFzbQEAAAABEQJgAAF/YAh8fHx/f39/fwF/AwQDAAABBAUBcAEBAQUEAQEgIAYIAX8BQYCINAsHMAQGbWVtb3J5AgAKY291bnRzX3B0cgAACG1hZ3NfcHRyAAELcmVuZGVyX3Jvd3MAAgqSAwMIAEGAiICAAAsIAEGAiJCAAAv9AgYCfwJ8AX8CfAF/B3xBACEIAkAgA0EBSA0AIARBAUgNACAGQQFIDQAgB0EBSA0AIAYgA2wiCUGAgARKDQAgAiADtyIKoyELQQAhDCAEt0QAAAAAAADgP6IhDSAKRAAAAAAAAOC/oiEOQQAhDwNAIAsgDSAPIAVqt6GiIAGgIRBEAAAAAAAAAAAhEUEAIQQDQCALIA4gEaCiIACgIRJEAAAAAAAAAAAhE0EAIQhEAAAAAAAAAAAhFEQAAAAAAAAAACECRAAAAAAAAAAAIQoCQANAIBIgFCAToaAiFSAVoiIUIAogCqAgAqIgEKAiAiACoiIToCEWIAhBAWoiCCAHTg0BIBUhCiAWRAAAAAAAABBAZQ0ACwsgDEEDdEGAiJCAAGogFkQAAAAAAAAAACAIIAdIGzkDACAMQQJ0QYCIgIAAaiAINgIAIAxBAWohDCARRAAAAAAAAPA/oCERIARBAWoiBCADRw0ACyAPQQFqIg8gBkcNAAsgCSEICyAICw==', + WASM_SCALAR_B64:'AGFzbQEAAAABEQJgAAF/YAh8fHx/f39/fwF/AwQDAAABBAUBcAEBAQUEAQEgIAYIAX8BQYCINAsHMAQGbWVtb3J5AgAKY291bnRzX3B0cgAACG1hZ3NfcHRyAAELcmVuZGVyX3Jvd3MAAgqSAwMIAEGAiICAAAsIAEGAiJCAAAv9AgYCfwJ8AX8CfAF/B3xBACEIAkAgA0EBSA0AIARBAUgNACAGQQFIDQAgB0EBSA0AIAYgA2wiCUGAgARKDQAgAiADtyIKoyELQQAhDCAEt0QAAAAAAADgP6IhDSAKRAAAAAAAAOC/oiEOQQAhDwNAIAsgDSAPIAVqt6GiIAGgIRBEAAAAAAAAAAAhEUEAIQQDQCALIA4gEaCiIACgIRJEAAAAAAAAAAAhE0EAIQhEAAAAAAAAAAAhFEQAAAAAAAAAACECRAAAAAAAAAAAIQoCQANAIBIgFCAToaAiFSAVoiIUIAogCqAgAqIgEKAiAiACoiIToCEWIAhBAWoiCCAHTg0BIBUhCiAWRAAAAAAAABBAZQ0ACwsgDEEDdEGAiJCAAGogFkQAAAAAAAAAACAIIAdIGzkDACAMQQJ0QYCIgIAAaiAINgIAIAxBAWohDCARRAAAAAAAAPA/oCERIARBAWoiBCADRw0ACyAPQQFqIg8gBkcNAAsgCSEICyAICw==', + DEEP_SIMD_B64:'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', + DEEP_SCALAR_B64:'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', + BLA_SIMD_B64:'AGFzbQEAAAABLwRgAAF/YAN/fHwBf2AQfHx8fHx/f39/f39/f39/fwF/YAt8fHx/f39/f39/fwF/AxAPAAAAAAAAAAAAAAAAAQIDBAUBcAEBAQUGAQGABIAEBgkBfwFBoL/sBwsH7AEQBm1lbW9yeQIACmNvdW50c19wdHIAAAhtYWdzX3B0cgABCnJlZnNfcl9wdHIAAgpyZWZzX2lfcHRyAAMOc3RhdF9ibGFfc3RlcHMABA5zdGF0X3B0Yl9zdGVwcwAFDHN0YXRfcmViYXNlcwAGDXN0YXRfaW50ZXJpb3IABw9zdGF0X3VucmVzb2x2ZWQACAlzdGF0X2ZhaWwACQxzdGF0X2l0ZXJfbG8ACgxzdGF0X2l0ZXJfaGkACwlidWlsZF9ibGEADBJyZW5kZXJfYmxhX3JlY3RfdjIADQ9yZW5kZXJfYmxhX3JlY3QADgqIJA8IAEGAiICAAAsIAEGAiJCAAAsIAEGAiLCAAAsIAEGQp/mAAAsLAEEAKAKYxsKBAAsLAEEAKAKcxsKBAAsLAEEAKAKgxsKBAAsLAEEAKAKkxsKBAAsLAEEAKAKoxsKBAAsLAEEAKAKsxsKBAAsLAEEAKAKwxsKBAAsLAEEAKAK0xsKBAAvuDQkCfwF8AnsDfwV7A3wBfgh/BnwCQCAAQY7sdmpBkOx2TQ0AQQAhA0EAQQE2ArjGwoEAQQAgAEF/aiIENgKQx8KBAEEAQQA2AsDGwoEAIAGaIQUCQAJAIARBAkkNACAC/RQhBiAF/RQhB0EAIQhB4NiMhgAhCSAEQX5xIgMhCgNAIAlCgYCAgBA3AwAgCEGg0OeDAGr9DAAAAAAAAPA/AAAAAAAA8D/9CwQAIAhBwJT6hABq/QwAAAAAAAAAAAAAAAAAAAAAIgv9CwQAIAhB4MfCgQBqIAhBiIiwgABq/QADACIMIAz98AEiDP0LBAAgCEGAjNWCAGogCEGYp/mAAGr9AAMAIg0gDf3wASIN/QsEACAIQfD61YYAaiALIAYgDCAM/fIBIA0gDf3yAf3wAf3vASIM/fIBIAcgDP3zAf3wASINIAsgDP0MAAAAAAAAAAAAAAAAAAAAAP3WASAM/QwAAAAAAADwfwAAAAAAAPB/Ig791gH9UCAM/Qz/////////f/////////9/Ig/9TiAO/dkB/VAiDP1NIA0gC/1J/U8gDSAP/U4gDv3YAf1O/VIgDP1S/QsEACAJQQhqIQkgCEEQaiEIIApBfmoiCg0ACyAEIANGDQELIANBA3QhCCADQX9zIABqIQogA0ECdEHg2IyGAGohCQNAIAlBATYCACAIQaDQ54MAakKAgICAgICA+D83AwAgCEHAlPqEAGpCADcDACAIQeDHwoEAaiAIQYiIsIAAaisDACIQIBCgIhE5AwAgCEGAjNWCAGogCEGYp/mAAGorAwAiECAQoCISOQMARAAAAAAAAAAAIRACQCARIBGiIBIgEqKgnyIRvSITUCATQoCAgICAgID4/wBRciATQv///////////wCDQoCAgICAgID4/wBVcg0AAkAgAiARoiAFIBGjoCIQRAAAAAAAAAAAYw0AIBC9Qv///////////wCDQoCAgICAgID4/wBTDQELRAAAAAAAAAAAIRALIAhB8PrVhgBqIBA5AwAgCUEEaiEJIAhBCGohCCAKQX9qIgoNAAsLQQEhCQJAIABBA0gNAEEAIQhBASEUA0AgCCAEaiIVIARBAWpBAXYiFmpBxagSTg0CIBRBAnQiCUGQx8KBAGogFjYCACAJQcDGwoEAaiAVNgIAAkAgBA0AQQAgFEEBaiIJNgK4xsKBAAwCC0EBIQogFkEBIBZBAUsbIRcgBEEDdCEYIAhBA3QhGSAIQQJ0QeDYjIYAaiEJIBVBAnRB4NiMhgBqIQNBACEaA0ACQAJAIAogBEgNACAZIBhqIghB4MfCgQBqIBkgGmoiG0Hgx8KBAGorAwA5AwAgCEGAjNWCAGogG0GAjNWCAGorAwA5AwAgCEGg0OeDAGogG0Gg0OeDAGorAwA5AwAgCEHAlPqEAGogG0HAlPqEAGorAwA5AwAgCEHw+tWGAGogG0Hw+tWGAGorAwA5AwAgCSgCACEIDAELIBkgGmoiCEHgx8KBAGorAwAiECAIQYiM1YIAaisDACIRoiEcIAhB6MfCgQBqKwMAIhIgCEGAjNWCAGorAwAiAqIhHSASIBCiIAIgEaKhIQUgEiAIQcCU+oQAaisDACIeoiAIQaDQ54MAaisDACIfIBGioCEgIBIgH6IgHiARoqEhEUQAAAAAAADwvyEhAkAgECAQoiACIAKioJ8iEEQAAAAAAAAAAGRFDQAgCEH4+tWGAGorAwAgHyAfoiAeIB6ioJ8gAaKhIBCjISELIB0gHKAhECAgIAhByJT6hABqKwMAoCESIAhBqNDngwBqKwMAIBGgIRECQAJAIAW9Qv///////////wCDQv/////////3/wBVDQAgEL1C////////////AINC//////////f/AFUNACARvUL///////////8Ag0L/////////9/8AVQ0AIBK9Qv///////////wCDQv/////////3/wBVDQBEAAAAAAAAAAAgCEHw+tWGAGorAwAiAiAhIAIgIWMbIgIgAkQAAAAAAAAAAGMbIgJEAAAAAAAA8H9jDQELRAAAAAAAAAAAIQILIBkgGGoiCEGAjNWCAGogEDkDACAIQeDHwoEAaiAFOQMAIAhBoNDngwBqIBE5AwAgCEHAlPqEAGogEjkDACAIQfD61YYAaiACOQMAIAlBBGooAgAgCSgCAGohCAsgAyAINgIAIAlBCGohCSAaQRBqIRogCkECaiEKIBhBCGohGCADQQRqIQMgF0F/aiIXDQALQQAgFEEBaiIJNgK4xsKBACAEQQNJDQEgFEETSSEKIBUhCCAWIQQgCSEUIAoNAAsLQQAgADYCkL/ohwAgCQ8LQQBBADYCuMbCgQBBAAvaFBIDfwF+AnwBfwF8CH8DfAF/A3wBfwJ8BH8EfAF/AXwEfwR8AXtBACEQAkAgC0EBSA0AIApBAUgNACAHQQFIDQAgBkEBSA0AQQAoApC/6IcAIAVHDQBBACgCuMbCgQAiEUEBSA0AIAsgCmwiEkGAgARKDQBCACETQQBCADcDsMbCgQBBAEEANgKoxsKBAEEAQQA2AqzGwoEAQQBBADYCpMbCgQBBAEEANgKgxsKBAEEAQQA2ApzGwoEAQQBBADYCmMbCgQAgBrchFCAHtyEVAkACQCAMQQFIDQAgEUF/aiEWIBVEAAAAAAAA4D+iIRdBACEYQQAhGUEAIRpBACEbQQAhHEEAIR1BACEeQQAhHwNAIAAgFyAZIAlqt6EgFKOiIAKgIiAgBKAiFSAVoiIhRAAAAAAAANA/okQWVueerwPSvKAhIkEAISMDQCAAICMgCGq3IBSjRAAAAAAAAOC/oKIgAaAhJAJAAkACQAJAAkACQCAPRQ0AICQgA6AiJUQAAAAAAADQv6AiFSAVoiAhoCImIBUgJqCiICJjDQEgJUQAAAAAAADwP6AiFSAVoiAhoERw//////+vP2MNAQtBACEnRAAAAAAAAPA/IShEAAAAAAAAAAAhFUQAAAAAAAAAACEmRAAAAAAAAAAAISlBACEqQQAhK0EAISxBACEtAkADQCArIAVKDQUgFSArQQN0IhBBgIiwgABqKwMAIi6gIiUgJaIgJiAQQZCn+YAAaisDACIvoCIwIDCioCIxRAAAAAAAABBAZA0BIA9BAEcgJ0EAR3EhMgJAICpBDEgNACAyRQ0AICggKKIgKSApoqAiM0QAAAAAAADwf2QgM0QAAAAAAADwf2NyQQFHDQAgM0SN7bWg98awPmNFDQBBACAdQQFqIh02AqTGwoEAQQAgEyAqrXwiEzcDsMbCgQAMBAsCQAJAAkACQAJAICtBAUgNACAxIBUgFaIgJiAmoqAiM2MNAyArIAVIDQELICsgBUgNAQwJCyArQX9qIjRoIBYgNBsiECAWIBAgEUkbIRACQCANQQFIDQAgLCANSA0AIBBBAnQhBwNAAkAgNCAQIgZ2IhAgB0GQx8KBAGooAgBODQAgB0HAxsKBAGooAgAgEGoiMkECdEHg2IyGAGooAgAiEEECSA0AIBAgK2ogBUoNACAQICpqIAxKDQAgMyAyQQN0QfD61YYAaisDACIxIDGiY0UNAEEAIB9BAWoiHzYCqMbCgQAgEyAqrHwhEwwKCyAHQXxqIQcgBkF/aiEQIAZBAEoNAAwCCwsgEEECdCEHA0ACQCA0IBAiBnYiECAHQZDHwoEAaigCAE4NACAHQcDGwoEAaigCACAQaiI1QQJ0QeDYjIYAaigCACIQQQJIDQAgECAraiI2IAVKDQAgECAqaiI3IAxKDQAgMyA1QQN0IhBB8PrVhgBqKwMAIjEgMaJjRQ0AIBBBoNDngwBqKwMAIjggJKIgEEHgx8KBAGorAwAiMSAVoiAmIBBBgIzVggBqKwMAIjmioaAgEEHAlPqEAGorAwAiOiAgoqEiO71C////////////AINC//////////f/AFUNACA6ICSiIDggIKIgMSAmoiAVIDmioKCgIji9Qv///////////wCDQv/////////3/wBVDQACQAJAIDJFDQACQCAxICiiICkgOZqioCIVvUL///////////8Ag0L/////////9/8AVQ0AQQEhJyAxICmiICggOaKgIia9Qv///////////wCDQoCAgICAgID4/wBTDQILQQAhJwsgKSEmICghFQtBACAbQQFqIhs2ApjGwoEAICxBAWohLCA2ISsgNyEqICYhKSAVISggOCEmIDshFQwECyAHQXxqIQcgBkF/aiEQIAZBAEoNAAsLAkAgDkEBSA0AIC0gDkgNAEEAIB9BAWoiHzYCqMbCgQAgEyAqrHwhEwwHCwJAAkACQCAPDQAgJyEQDAELQQEhEAJAICoNAEQAAAAAAADwPyExRAAAAAAAAAAAITAMAgsCQCAnRQ0AICUgKKIgKSAwoqEiMSAxoCIxvUL///////////8Ag0L/////////9/8AVQ0AICUgKaIgKCAwoqAiJSAloCIwvUL///////////8Ag0KAgICAgICA+P8AUw0CC0EAIRALICkhMCAoITELICQgLiAVoiAmIC+ioSIlICWgIBUgFaIgJiAmoqGgoCIlvUL///////////8Ag0L/////////9/8AVQ0HICAgLiAmoiAVIC+ioCIoICigICYgFSAVoKKgoCImvUL///////////8Ag0L/////////9/8AVQ0HQQAgHEEBaiIcNgKcxsKBACAtQQFqIS0gKkEBaiEqICtBAWohKyAQIScgMCEpIDEhKCAlIRUMAQtBACErQQAgGkEBaiIaNgKgxsKBACAwISYgJSEVCyAqIAxIDQALQQAgEyAqrHwiEzcDsMbCgQAMAgtBACATICqsfCITNwOwxsKBAAwEC0EAIB1BAWoiHTYCpMbCgQALRAAAAAAAAAAAITEgDCEqDAILQQAgEzcDsMbCgQBEAAAAAAAAAAAhMUF+ISoMAQtBACAeQQFqIh42AqzGwoEARAAAAAAAAAAAITFBfyEqCyAYQQN0QYCIkIAAaiAxOQMAIBhBAnRBgIiAgABqICo2AgAgGEEBaiEYICNBAWoiIyAKRw0ACyAZQQFqIhkgC0cNAAwCCwsCQCAPRQ0AIBVEAAAAAAAA4D+iISlBACErQQAhBUEAITQDQCAAICkgBSAJarehIBSjoiACoCAEoCIVIBWiIiVEAAAAAAAA0D+iRBZW556vA9K8oCEoICtBAnRBgIiAgABqIRAgK0EDdEGAiJCAAGohByAIIQYgCiEqA0ACQAJAIAAgBrcgFKNEAAAAAAAA4L+goiABoCADoCIwRAAAAAAAANC/oCIVIBWiICWgIiYgFSAmoKIgKGMNACAwRAAAAAAAAPA/oCIVIBWiICWgRHD//////68/Yw0AQQBCADcDsMbCgQAMAQtBACA0QQFqIjQ2AqTGwoEACyAHQgA3AwAgECAMNgIAIAZBAWohBiAQQQRqIRAgB0EIaiEHICtBAWohKyAqQX9qIioNAAsgBUEBaiIFIAtHDQAMAgsLIApBA3QhLSAKQQJ0IRggCkF+cSEyIAz9ESE8QQAhKiAKITRBgIiQgAAhBUGAiICAACEnQQAhDwNAICohKwJAAkAgCkECSQ0AICogMmohKyAFIRAgJyEHIDIhBgNAIBD9DAAAAAAAAAAAAAAAAAAAAAD9CwMAIAcgPP1bAgAAIBBBEGohECAHQQhqIQcgBkF+aiIGDQALIDIgCkYNAQsgNCArayEGICtBAnRBgIiAgABqIRAgK0EDdEGAiJCAAGohBwNAIAdCADcDACAQIAw2AgAgEEEEaiEQIAdBCGohByAGQX9qIgYNAAsLICogCmohKiA0IApqITQgBSAtaiEFICcgGGohJyAPQQFqIg8gC0cNAAsLIBIhEAsgEAs2ACAAIAEgAkQAAAAAAAAAAEQAAAAAAAAAACADIAQgBSAGIAcgCCAJIApBAEEAQQAQjYCAgAAL', + BLA_SCALAR_B64:'AGFzbQEAAAABLwRgAAF/YAN/fHwBf2AQfHx8fHx/f39/f39/f39/fwF/YAt8fHx/f39/f39/fwF/AxAPAAAAAAAAAAAAAAAAAQIDBAUBcAEBAQUGAQGABIAEBgkBfwFBoL/sBwsH7AEQBm1lbW9yeQIACmNvdW50c19wdHIAAAhtYWdzX3B0cgABCnJlZnNfcl9wdHIAAgpyZWZzX2lfcHRyAAMOc3RhdF9ibGFfc3RlcHMABA5zdGF0X3B0Yl9zdGVwcwAFDHN0YXRfcmViYXNlcwAGDXN0YXRfaW50ZXJpb3IABw9zdGF0X3VucmVzb2x2ZWQACAlzdGF0X2ZhaWwACQxzdGF0X2l0ZXJfbG8ACgxzdGF0X2l0ZXJfaGkACwlidWlsZF9ibGEADBJyZW5kZXJfYmxhX3JlY3RfdjIADQ9yZW5kZXJfYmxhX3JlY3QADgqaIQ8IAEGAiICAAAsIAEGAiJCAAAsIAEGAiLCAAAsIAEGQp/mAAAsLAEEAKAKYxsKBAAsLAEEAKAKcxsKBAAsLAEEAKAKgxsKBAAsLAEEAKAKkxsKBAAsLAEEAKAKoxsKBAAsLAEEAKAKsxsKBAAsLAEEAKAKwxsKBAAsLAEEAKAK0xsKBAAvSCgcCfwF8An8DfAF+CX8GfAJAIABBjux2akGQ7HZNDQBBACEDQQBBATYCuMbCgQBBACAAQX9qIgQ2ApDHwoEAQQBBADYCwMbCgQAgAZohBUHg2IyGACEGQQEhBwNAIAZBATYCACADQaDQ54MAakKAgICAgICA+D83AwAgA0HAlPqEAGpCADcDACADQeDHwoEAaiADQYiIsIAAaisDACIIIAigIgk5AwAgA0GAjNWCAGogA0GYp/mAAGorAwAiCCAIoCIKOQMARAAAAAAAAAAAIQgCQCAJIAmiIAogCqKgnyIJvSILUCALQoCAgICAgID4/wBRciALQv///////////wCDQoCAgICAgID4/wBVcg0AAkAgAiAJoiAFIAmjoCIIRAAAAAAAAAAAYw0AIAi9Qv///////////wCDQoCAgICAgID4/wBTDQELRAAAAAAAAAAAIQgLIANB8PrVhgBqIAg5AwAgBkEEaiEGIANBCGohAyAAIAdBAWoiB0cNAAtBASEGAkAgAEEDSA0AQQAhA0EBIQwDQCADIARqIg0gBEEBakEBdiIOakHFqBJODQIgDEECdCIGQZDHwoEAaiAONgIAIAZBwMbCgQBqIA02AgACQCAEDQBBACAMQQFqIgY2ArjGwoEADAILQQEhByAOQQEgDkEBSxshDyAEQQN0IRAgA0EDdCERIANBAnRB4NiMhgBqIQYgDUECdEHg2IyGAGohEkEAIRMDQAJAAkAgByAESA0AIBEgEGoiA0Hgx8KBAGogESATaiIUQeDHwoEAaisDADkDACADQYCM1YIAaiAUQYCM1YIAaisDADkDACADQaDQ54MAaiAUQaDQ54MAaisDADkDACADQcCU+oQAaiAUQcCU+oQAaisDADkDACADQfD61YYAaiAUQfD61YYAaisDADkDACAGKAIAIQMMAQsgESATaiIDQeDHwoEAaisDACIIIANBiIzVggBqKwMAIgmiIRUgA0Hox8KBAGorAwAiCiADQYCM1YIAaisDACICoiEWIAogCKIgAiAJoqEhBSAKIANBwJT6hABqKwMAIheiIANBoNDngwBqKwMAIhggCaKgIRkgCiAYoiAXIAmioSEJRAAAAAAAAPC/IRoCQCAIIAiiIAIgAqKgnyIIRAAAAAAAAAAAZEUNACADQfj61YYAaisDACAYIBiiIBcgF6KgnyABoqEgCKMhGgsgFiAVoCEIIBkgA0HIlPqEAGorAwCgIQogA0Go0OeDAGorAwAgCaAhCQJAAkAgBb1C////////////AINC//////////f/AFUNACAIvUL///////////8Ag0L/////////9/8AVQ0AIAm9Qv///////////wCDQv/////////3/wBVDQAgCr1C////////////AINC//////////f/AFUNAEQAAAAAAAAAACADQfD61YYAaisDACICIBogAiAaYxsiAiACRAAAAAAAAAAAYxsiAkQAAAAAAADwf2MNAQtEAAAAAAAAAAAhAgsgESAQaiIDQYCM1YIAaiAIOQMAIANB4MfCgQBqIAU5AwAgA0Gg0OeDAGogCTkDACADQcCU+oQAaiAKOQMAIANB8PrVhgBqIAI5AwAgBkEEaigCACAGKAIAaiEDCyASIAM2AgAgBkEIaiEGIBNBEGohEyAHQQJqIQcgEEEIaiEQIBJBBGohEiAPQX9qIg8NAAtBACAMQQFqIgY2ArjGwoEAIARBA0kNASAMQRNJIQcgDSEDIA4hBCAGIQwgBw0ACwtBACAANgKQv+iHACAGDwtBAEEANgK4xsKBAEEAC4gVEQN/AX4CfAF/AXwIfwN8AX8DfAF/AnwEfwR8AX8BfAR/BHxBACEQAkAgC0EBSA0AIApBAUgNACAHQQFIDQAgBkEBSA0AQQAoApC/6IcAIAVHDQBBACgCuMbCgQAiEUEBSA0AIAsgCmwiEkGAgARKDQBCACETQQBCADcDsMbCgQBBAEEANgKoxsKBAEEAQQA2AqzGwoEAQQBBADYCpMbCgQBBAEEANgKgxsKBAEEAQQA2ApzGwoEAQQBBADYCmMbCgQAgBrchFCAHtyEVAkACQCAMQQFIDQAgEUF/aiEWIBVEAAAAAAAA4D+iIRdBACEYQQAhGUEAIRpBACEbQQAhHEEAIR1BACEeQQAhHwNAIAAgFyAZIAlqt6EgFKOiIAKgIiAgBKAiFSAVoiIhRAAAAAAAANA/okQWVueerwPSvKAhIkEAISMDQCAAICMgCGq3IBSjRAAAAAAAAOC/oKIgAaAhJAJAAkACQAJAAkACQCAPRQ0AICQgA6AiJUQAAAAAAADQv6AiFSAVoiAhoCImIBUgJqCiICJjDQEgJUQAAAAAAADwP6AiFSAVoiAhoERw//////+vP2MNAQtBACEnRAAAAAAAAPA/IShEAAAAAAAAAAAhFUQAAAAAAAAAACEmRAAAAAAAAAAAISlBACEqQQAhK0EAISxBACEtAkADQCArIAVKDQUgFSArQQN0IhBBgIiwgABqKwMAIi6gIiUgJaIgJiAQQZCn+YAAaisDACIvoCIwIDCioCIxRAAAAAAAABBAZA0BIA9BAEcgJ0EAR3EhMgJAICpBDEgNACAyRQ0AICggKKIgKSApoqAiM0QAAAAAAADwf2QgM0QAAAAAAADwf2NyQQFHDQAgM0SN7bWg98awPmNFDQBBACAdQQFqIh02AqTGwoEAQQAgEyAqrXwiEzcDsMbCgQAMBAsCQAJAAkACQAJAICtBAUgNACAxIBUgFaIgJiAmoqAiM2MNAyArIAVIDQELICsgBUgNAQwJCyArQX9qIjRoIBYgNBsiECAWIBAgEUkbIRACQCANQQFIDQAgLCANSA0AIBBBAnQhBwNAAkAgNCAQIgZ2IhAgB0GQx8KBAGooAgBODQAgB0HAxsKBAGooAgAgEGoiMkECdEHg2IyGAGooAgAiEEECSA0AIBAgK2ogBUoNACAQICpqIAxKDQAgMyAyQQN0QfD61YYAaisDACIxIDGiY0UNAEEAIB9BAWoiHzYCqMbCgQAgEyAqrHwhEwwKCyAHQXxqIQcgBkF/aiEQIAZBAEoNAAwCCwsgEEECdCEHA0ACQCA0IBAiBnYiECAHQZDHwoEAaigCAE4NACAHQcDGwoEAaigCACAQaiI1QQJ0QeDYjIYAaigCACIQQQJIDQAgECAraiI2IAVKDQAgECAqaiI3IAxKDQAgMyA1QQN0IhBB8PrVhgBqKwMAIjEgMaJjRQ0AIBBBoNDngwBqKwMAIjggJKIgEEHgx8KBAGorAwAiMSAVoiAmIBBBgIzVggBqKwMAIjmioaAgEEHAlPqEAGorAwAiOiAgoqEiO71C////////////AINC//////////f/AFUNACA6ICSiIDggIKIgMSAmoiAVIDmioKCgIji9Qv///////////wCDQv/////////3/wBVDQACQAJAIDJFDQACQCAxICiiICkgOZqioCIVvUL///////////8Ag0L/////////9/8AVQ0AQQEhJyAxICmiICggOaKgIia9Qv///////////wCDQoCAgICAgID4/wBTDQILQQAhJwsgKSEmICghFQtBACAbQQFqIhs2ApjGwoEAICxBAWohLCA2ISsgNyEqICYhKSAVISggOCEmIDshFQwECyAHQXxqIQcgBkF/aiEQIAZBAEoNAAsLAkAgDkEBSA0AIC0gDkgNAEEAIB9BAWoiHzYCqMbCgQAgEyAqrHwhEwwHCwJAAkACQCAPDQAgJyEQDAELQQEhEAJAICoNAEQAAAAAAADwPyExRAAAAAAAAAAAITAMAgsCQCAnRQ0AICUgKKIgKSAwoqEiMSAxoCIxvUL///////////8Ag0L/////////9/8AVQ0AICUgKaIgKCAwoqAiJSAloCIwvUL///////////8Ag0KAgICAgICA+P8AUw0CC0EAIRALICkhMCAoITELICQgLiAVoiAmIC+ioSIlICWgIBUgFaIgJiAmoqGgoCIlvUL///////////8Ag0L/////////9/8AVQ0HICAgLiAmoiAVIC+ioCIoICigICYgFSAVoKKgoCImvUL///////////8Ag0L/////////9/8AVQ0HQQAgHEEBaiIcNgKcxsKBACAtQQFqIS0gKkEBaiEqICtBAWohKyAQIScgMCEpIDEhKCAlIRUMAQtBACErQQAgGkEBaiIaNgKgxsKBACAwISYgJSEVCyAqIAxIDQALQQAgEyAqrHwiEzcDsMbCgQAMAgtBACATICqsfCITNwOwxsKBAAwEC0EAIB1BAWoiHTYCpMbCgQALRAAAAAAAAAAAITEgDCEqDAILQQAgEzcDsMbCgQBEAAAAAAAAAAAhMUF+ISoMAQtBACAeQQFqIh42AqzGwoEARAAAAAAAAAAAITFBfyEqCyAYQQN0QYCIkIAAaiAxOQMAIBhBAnRBgIiAgABqICo2AgAgGEEBaiEYICNBAWoiIyAKRw0ACyAZQQFqIhkgC0cNAAwCCwsCQCAPRQ0AIBVEAAAAAAAA4D+iISlBACErQQAhBUEAITQDQCAAICkgBSAJarehIBSjoiACoCAEoCIVIBWiIiVEAAAAAAAA0D+iRBZW556vA9K8oCEoICtBAnRBgIiAgABqIRAgK0EDdEGAiJCAAGohByAIIQYgCiEqA0ACQAJAIAAgBrcgFKNEAAAAAAAA4L+goiABoCADoCIwRAAAAAAAANC/oCIVIBWiICWgIiYgFSAmoKIgKGMNACAwRAAAAAAAAPA/oCIVIBWiICWgRHD//////68/Yw0AQQBCADcDsMbCgQAMAQtBACA0QQFqIjQ2AqTGwoEACyAHQgA3AwAgECAMNgIAIAZBAWohBiAQQQRqIRAgB0EIaiEHICtBAWohKyAqQX9qIioNAAsgBUEBaiIFIAtHDQAMAgsLIApBAnQhMiAKQQN0IS1BACEqQQAgCkEDcSIYayErIApBf2pBA0khNSAKISdBgIiAgAAhNEGAiJCAACEFQQAhDwNAICohBwJAIBhFDQBBACEGIDQhECAFIQcDQCAHQgA3AwAgECAMNgIAIBBBBGohECAHQQhqIQcgKyAGQX9qIgZHDQALICogBmshBwsCQCA1DQAgJyAHayEGIAdBAnRBgIiAgABqIRAgB0EDdEGAiJCAAGohBwNAIAdCADcDACAQIAw2AgAgEEEEaiAMNgIAIAdBCGpCADcDACAQQQhqIAw2AgAgB0EQakIANwMAIBBBDGogDDYCACAHQRhqQgA3AwAgEEEQaiEQIAdBIGohByAGQXxqIgYNAAsLICogCmohKiAnIApqIScgNCAyaiE0IAUgLWohBSAPQQFqIg8gC0cNAAsLIBIhEAsgEAs2ACAAIAEgAkQAAAAAAAAAAEQAAAAAAAAAACADIAQgBSAGIAcgCCAJIApBAEEAQQAQjYCAgAAL', + COLOR_SIMD_B64:'AGFzbQEAAAABCgJgAAF/YAF/AX8DBAMAAAEEBQFwAQEBBQQBASAgBggBfwFBgIg0CwcvBAZtZW1vcnkCAAhtYWdzX3B0cgAACGNvcnJfcHRyAAEMc21vb3RoX2JhdGNoAAIKhgoDCABBgIiAgAALCABBgIiggAAL8QkFBX8PewF8AX4BfEEAIQECQCAAQQBIDQAgAEGAgAQgAEGAgARIGyEBIABFDQBBACECAkAgAUEBIAFBAUobIgNBAkkNAEGAiICAACEAQYCIoIAAIQQgA0H+////B3EiAiEFA0AgBP0MAAAAAAAA8D8AAAAAAADwPyIGIAD9AAQAIgf9DKf8tQYAABBAp/y1BgAAEEAgB/0MAAAAAAAAEEAAAAAAAAAQQP1K/VIiCP0M////////DwD///////8PACIJ/U79DAAAAAAAAPA/AAAAAAAA8D8iCv1QIgf9DAAAAAAAAPC/AAAAAAAA8L8iC/3wASAHIAb98AH98wEiDCAMIAwgDP3yASIH/fIBIgz9DAAAAAAAAAhAAAAAAAAACEAiDf3zAf3wASAHIAz98gEiDP0MAAAAAAAAFEAAAAAAAAAUQCIO/fMB/fABIAcgDP3yASIM/QwAAAAAAAAcQAAAAAAAABxAIg/98wH98AEgByAM/fIBIgz9DAAAAAAAACJAAAAAAAAAIkAiEP3zAf3wASAHIAz98gH9DAAAAAAAACZAAAAAAAAAJkAiEf3zAf3wASIHIAf98AH9DP6CK2VHFfc//oIrZUcV9z8iEv3yASAIQTT9zQEgB/0NAAECAwgJCgsAAQIDAAECA/0M/wcAAP8HAAD/BwAA/wcAACII/U79DAH8//8B/P//Afz//wH8//8iE/2uAf3+Af3wAf0MAAAAAAAA4D8AAAAAAADgP/3yASIUIAn9TiAK/VAiByAL/fABIAcgBv3wAf3zASIMIAwgDCAM/fIBIgf98gEiDCAN/fMB/fABIAcgDP3yASIMIA798wH98AEgByAM/fIBIgwgD/3zAf3wASAHIAz98gEiDCAQ/fMB/fABIAcgDP3yASAR/fMB/fABIgcgB/3wASAS/fIBIBRBNP3NASAH/Q0AAQIDCAkKCwABAgMAAQIDIAj9TiAT/a4B/f4B/fAB/fEBIgf9IQC2/RMgB/0hAbb9IAH9WwMAACAEQQhqIQQgAEEQaiEAIAVBfmoiBQ0ACyADIAJGDQELIAMgAmshBSACQQN0QYCIgIAAaiEAIAJBAnRBgIiggABqIQQDQCAERAAAAAAAAPA/IAArAwAiFUSn/LUGAAAQQCAVRAAAAAAAABBAZBu9IhZC/////////weDQoCAgICAgID4P4S/IhVEAAAAAAAA8L+gIBVEAAAAAAAA8D+goyIXIBcgFyAXoiIVoiIXRAAAAAAAAAhAo6AgFSAXoiIXRAAAAAAAABRAo6AgFSAXoiIXRAAAAAAAABxAo6AgFSAXoiIXRAAAAAAAACJAo6AgFSAXokQAAAAAAAAmQKOgIhUgFaBE/oIrZUcV9z+iIBZCNIinQf8PcUGBeGq3oEQAAAAAAADgP6K9IhZC/////////weDQoCAgICAgID4P4S/IhVEAAAAAAAA8L+gIBVEAAAAAAAA8D+goyIXIBcgFyAXoiIVoiIXRAAAAAAAAAhAo6AgFSAXoiIXRAAAAAAAABRAo6AgFSAXoiIXRAAAAAAAABxAo6AgFSAXoiIXRAAAAAAAACJAo6AgFSAXokQAAAAAAAAmQKOgIhUgFaBE/oIrZUcV9z+iIBZCNIinQf8PcUGBeGq3oKG2OAIAIABBCGohACAEQQRqIQQgBUF/aiIFDQALCyABCw==', + COLOR_SCALAR_B64:'AGFzbQEAAAABCgJgAAF/YAF/AX8DBAMAAAEEBQFwAQEBBQQBASAgBggBfwFBgIg0CwcvBAZtZW1vcnkCAAhtYWdzX3B0cgAACGNvcnJfcHRyAAEMc21vb3RoX2JhdGNoAAIKkwQDCABBgIiAgAALCABBgIiggAAL/gMEA38BfAF+AXxBACEBAkAgAEEASA0AIABBgIAEIABBgIAESBshASAARQ0AIAFBASABQQFKG0EDdCECQQAhAEGAiKCAACEDA0AgA0QAAAAAAADwPyAAQYCIgIAAaisDACIERKf8tQYAABBAIAREAAAAAAAAEEBkG70iBUL/////////B4NCgICAgICAgPg/hL8iBEQAAAAAAADwv6AgBEQAAAAAAADwP6CjIgYgBiAGIAaiIgSiIgZEAAAAAAAACECjoCAEIAaiIgZEAAAAAAAAFECjoCAEIAaiIgZEAAAAAAAAHECjoCAEIAaiIgZEAAAAAAAAIkCjoCAEIAaiRAAAAAAAACZAo6AiBCAEoET+gitlRxX3P6IgBUI0iKdB/w9xQYF4aregRAAAAAAAAOA/or0iBUL/////////B4NCgICAgICAgPg/hL8iBEQAAAAAAADwv6AgBEQAAAAAAADwP6CjIgYgBiAGIAaiIgSiIgZEAAAAAAAACECjoCAEIAaiIgZEAAAAAAAAFECjoCAEIAaiIgZEAAAAAAAAHECjoCAEIAaiIgZEAAAAAAAAIkCjoCAEIAaiRAAAAAAAACZAo6AiBCAEoET+gitlRxX3P6IgBUI0iKdB/w9xQYF4aregobY4AgAgA0EEaiEDIAIgAEEIaiIARw0ACwsgAQs=' +}); +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' +}); diff --git a/dist/standalone/script.js b/dist/standalone/script.js new file mode 100644 index 0000000..10f7903 --- /dev/null +++ b/dist/standalone/script.js @@ -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='
Canvas 2Dを利用できません。
';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<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)} +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>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;j8)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>>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{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;inew 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{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;inew 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;ideepModuleBundle={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(refLoadedblaRefLoaded){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>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=d.iter)!==(p>=d.iter))){add(i);add(i-stride)}if(nb[0]-a[0]);if(top.length>want)top.length=want}}for(const x of top)add(x[1]);for(let k=0;picks.length=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=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=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(start0&&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{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.chunkIndexjob.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{if(token!==state.token||this.cancelled)return;const deadline=performance.now()+5;while(ktoken!==state.token||this.cancelled);if(!result)return;const[n,m]=result;putField(field,idx,n,m,nrunner.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{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;ia.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(ptoken!==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,nn+(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(itoken!==state.token||state.processMode!=='validate'); + if(!result){validationJob.active=false;return} + const[n,m]=result;if(n180){lastStatus=now;state.coverage=i/field.classes.length;invalidateStats()} + if(in+(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;yminTile||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{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=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=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>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(yyplan.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,n1.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=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=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=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=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(yMath.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=iter?0:Math.max(4.0000001,zr2+zi2),kind=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(yHI128){r*=INV256;i*=INV256;b++;m*=INV256} + while(m>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<>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<=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.nBAIL)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<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(nBAIL)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)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<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.ntoken!==state.token);if(!result)return;const[n,m]=result;putField(field,y*w+x,n,m,n{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.loadedLentoken!==state.token);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n{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(n16384||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;yexportJob.cancelled):exportSampleShallow(cre,cim,pixelScale,W,H,iter,gx,gy);if(!sample)throw new Error('cancelled');const[n,m]=sample;if(noutCanvas.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{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(); +})(); diff --git a/dist/wasm/bla-scalar.4f06913487044823.wasm b/dist/wasm/bla-scalar.4f06913487044823.wasm new file mode 100644 index 0000000000000000000000000000000000000000..5ac82b3292ab2de840001cdacc2d55aa7b069c1f GIT binary patch literal 4593 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 Zr_oJ!_eloRzghAW_10@LJZeglR4mvR6A literal 0 HcmV?d00001 diff --git a/dist/wasm/color-scalar.wasm b/dist/wasm/color-scalar.wasm new file mode 100644 index 0000000000000000000000000000000000000000..b45806080e624ab23fabe244e7886d1815c330c5 GIT binary patch literal 632 zcmdT>y-ve05WcgWG-{~m08<$p4=f#6I;^IAXfj_x#|RfZ=s7M(tr302n<#2-Yd?j6B~D2ko6(#A2E>m*Uh zQZU11eu?*u_$#oZK(NGEbL5{ zwQ;Ky1Z&K`o^^n-!&%NKJLi=*q19KLDBRcMO?=u9b0x@q=E<*OVJB0e^YuyvONE}3 z3}>oXT>nG%wvdA@MEoGR%b|J;##z!>-c|d}{SQ0kTD>-z$<^+qyuET?lbrc(r~hs8 Zr_oJ!_eloRzghAW_10@LJZeglR4mvR6A literal 0 HcmV?d00001 diff --git a/dist/wasm/color-simd.8cf83374ed0c680b.wasm b/dist/wasm/color-simd.8cf83374ed0c680b.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ddeeaaa775b966de550a12e7068996f65717d0b9 GIT binary patch literal 1387 zcmdT^y;9pi5Z={~B`i61f(-=}t1D!J+KwhuAPo>YV2Eo%X(6-F~|gtnIWA0I+DR0`^w1hdt9W z*#lEtQT$jmU0~mSAG~BS;KkX5ZTHG=a2NM*S3-bBL?B_Q1PuMYmEM$g@(<6y6aXqw zj>NW$lp#kkp&sSzk?2MBSVzv@cATT|)G3lf1qSsKg-^+)eju~? z=I1VM_F^{@B_^g`{urg2QL2%+bpvHKqs&I4d;>*f6cLHa7$rZ+Tt=9S#O;(I92SVr z?_a!|-*`~_73L`J@Bzt{b@iQ-R3$*ebSH|QMmcNrK|eURki4zbvw(+vHvaS!o79w%lt9y96GYWZW-df-eJ`3~6@L2G=x08v2RboPBj`FG4a- zLxOn|L~Zf1i)-0dC3-&dO-wb?mW`eFjea_QhaZmFAq#Lkpv!eQI1tm6TtiVCis_8x zMhDEI>A#lhHOuTZi^wfmlsSGDHL493^UfL4RHMFsDC6?BeEX{Tb&xSn!r}jR@=r&1 V4gK@t+UgQpy2Q5ZrM2}4y+2ljd;tIe literal 0 HcmV?d00001 diff --git a/dist/wasm/color-simd.wasm b/dist/wasm/color-simd.wasm new file mode 100644 index 0000000000000000000000000000000000000000..ddeeaaa775b966de550a12e7068996f65717d0b9 GIT binary patch literal 1387 zcmdT^y;9pi5Z={~B`i61f(-=}t1D!J+KwhuAPo>YV2Eo%X(6-F~|gtnIWA0I+DR0`^w1hdt9W z*#lEtQT$jmU0~mSAG~BS;KkX5ZTHG=a2NM*S3-bBL?B_Q1PuMYmEM$g@(<6y6aXqw zj>NW$lp#kkp&sSzk?2MBSVzv@cATT|)G3lf1qSsKg-^+)eju~? z=I1VM_F^{@B_^g`{urg2QL2%+bpvHKqs&I4d;>*f6cLHa7$rZ+Tt=9S#O;(I92SVr z?_a!|-*`~_73L`J@Bzt{b@iQ-R3$*ebSH|QMmcNrK|eURki4zbvw(+vHvaS!o79w%lt9y96GYWZW-df-eJ`3~6@L2G=x08v2RboPBj`FG4a- zLxOn|L~Zf1i)-0dC3-&dO-wb?mW`eFjea_QhaZmFAq#Lkpv!eQI1tm6TtiVCis_8x zMhDEI>A#lhHOuTZi^wfmlsSGDHL493^UfL4RHMFsDC6?BeEX{Tb&xSn!r}jR@=r&1 V4gK@t+UgQpy2Q5ZrM2}4y+2ljd;tIe literal 0 HcmV?d00001 diff --git a/dist/wasm/deep-scalar.d133bbecc8f6dbdf.wasm b/dist/wasm/deep-scalar.d133bbecc8f6dbdf.wasm new file mode 100644 index 0000000000000000000000000000000000000000..e851c8990e8b989e77f3b3d2d9536b340cd93cbd GIT binary patch literal 5246 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 literal 0 HcmV?d00001 diff --git a/dist/wasm/deep-simd.wasm b/dist/wasm/deep-simd.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7858d0297006327f023b746f5b565a0426f1c805 GIT binary patch literal 4866 zcmcgwO>YxN7@pbPI3XKTDxoFLhVsr3Vw(UqA1dmpSqTD3t48A16NHdfbsIjMfT&1u zE|q#Khn{i@=Fn@^3;zIBDu1BIo=YXGJ@sI<&pW%byRl6Jp=vB=X6NIbcb@m_S&OB$ zRVjp!N1SUywyp&mt&NQq{^I|pMOU`m!Vs1T=?%%-Qp%x%l$-bz_Q{rvwx2zd{_w|z z!s^QEt>%}G7+t<~_vZTA)!XY$Ax2i0K8?F_w7K%h+SR5RcjAZ7%!518T)BB;1v4wn z^}EgMSDP!>m)2I$EUyc1^p~lj5fN>Ev$-jJ(|Ke&Kg69!-~WZqk5dQSmh-^ba39DG zw0=UYq9;1s0#H_QA(lfmMPc`#I)>%b?yR}jG>tZ%(um#C4|Yh{L{oim z9|Pb3yR;J>MvSEL7#%bj@FimPRVme{GYIHx_WZK78^HB&?Zjz1JHX9}>jxKZFdqBd zC1bKOl~iTS@o8L@X+l-t6UHOwijt04`LXHvzpl_YJ|2G;LLI=$x-i#$J9@wSv&6sW zvU`ON^X%oFgQ{vU!A1^<5>?rxB}OC$!3*u8%SKxS~vCcfc-p3PI?`{n**E<>e(2B>5j^cnz2`*u@? zDot3U%xN!FmH6_EVyMb~8qn&v!B7=NzX(-I5DVbPGx(lhU#1xv0^U&z&gYt#WeM?z zg2+Vz%A{v0M!DD;L|++G0dVD*g8}1ZeX=N$hv7TAU;r|X9VCuzJkyHmEVh+{EXZDB zLYo~X8lH!9c?==PLT+uM3wFIp4<~xj{f73UhseQY+f2EyG1h}_yYxqh}6C-wlr_gp9%YesPG&!fRM zG*uy02LSb`e*XhgK5{GUp|#X&_!lgd&o6Lgm_(xhNt$ z<*8;HdJ|vF7yu$tdAB&SZ`2z45CuZkS~t`zy|bl1doBxc73N&VRb?<1yHsHykyKi)!>}8QOqI)F;_Y)q zWHAc(yAY~^NxA5AMoz&`-Cb}oHM^^ZQCN+#1qg?ZjLR#uMkcGq3=Rk6>ah{AvmOT~ zX_*Z;7^S#6@O&=KCKf{A@e)&TQR%Y*7;-}uAk{FToC9$NwoYm(XSUrC&O+?;Xi!Gt zdXg#k8lGt_0n817jS~SFkdH#2vLTPkutpiy0MBRhQTbo2VW`bZt-*vA&JKgd)+oi+ zzyU0>0t_uXsu*C*05+6+HrQYoXmGa4uHiArCa)0}d5c^uO4wn~QNe{8DONW0N#xlq z%c?AYs?g?Wu$g?&B&8y^AJjC>)5v!oa~+u`SruZ$&>QBHDm2l^uWGSnZCUhGU>rgP z*~L<)L`4tt72S(QnAgPj$NJnt>&I`%YeEPIuK^Xwcn#bhyas&Fkg)VjBzeA~AvL+d z9#R8_gvbtk6sd3h|>LZ)m15yIky@Mi+`-{EDL7qaO8fS2q0fC&lDP@v2_8B{Vq zI2bCp!$3OG_x273QnCyr9IyqGhJk=30QexI5326t6*};#%CC`3AKFYAz!Rv>lp*n@ z1GD@NPbcCx_)r~B_&p*>e1wUUYKC1G4gYx1=3fii+h7&g#8UfsNl5)Xr2qZu`tGkD zd}>NeNC&S!N(hNcNm=kZc${eS-F*0^@1F!dt=7IQ8IH rpava(&lMPOI2Ud5fcLJ$1JL#;P4nJq8sojB`9V`L7IcWM7x@1G?)0g{ literal 0 HcmV?d00001 diff --git a/dist/wasm/manifest.json b/dist/wasm/manifest.json new file mode 100644 index 0000000..95f4b16 --- /dev/null +++ b/dist/wasm/manifest.json @@ -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" + } + ] +} diff --git a/dist/wasm/wasm-scalar.d19b26c04e1b2f59.wasm b/dist/wasm/wasm-scalar.d19b26c04e1b2f59.wasm new file mode 100644 index 0000000000000000000000000000000000000000..14840c05c5cd20fbdf32f770381da1e3e8a4c6b4 GIT binary patch literal 511 zcmYLFOG*Pl5UuL&OghsUlgV%6rg~1`LIhXIfM9Up6_TJq7|e%BM1+BvrAzSylkpH) zdKHi01q5r7XbPHouimSIelS_h5CG5@W59H*lO##Y!c;2Bb5+45B32Y4k)tuiFnc{W z)idSHrn5!-BEY>{Jk8gu(Q+LF=-K3957ETa`TaB=#f#?^2=`q|4ZNIXV0Pe*!H;l6 zDiYkiY7&CgVn8y)p$DY)&Dpo=fsuR+YGIbC8@33N+ra&>hBxb^wpaWYuCIP!J#zop zXJJp+(2xT%YY0HZ*WyylzKC`p5y9jRGuNSj~sF;`Ro literal 0 HcmV?d00001 diff --git a/dist/wasm/wasm-scalar.wasm b/dist/wasm/wasm-scalar.wasm new file mode 100644 index 0000000000000000000000000000000000000000..14840c05c5cd20fbdf32f770381da1e3e8a4c6b4 GIT binary patch literal 511 zcmYLFOG*Pl5UuL&OghsUlgV%6rg~1`LIhXIfM9Up6_TJq7|e%BM1+BvrAzSylkpH) zdKHi01q5r7XbPHouimSIelS_h5CG5@W59H*lO##Y!c;2Bb5+45B32Y4k)tuiFnc{W z)idSHrn5!-BEY>{Jk8gu(Q+LF=-K3957ETa`TaB=#f#?^2=`q|4ZNIXV0Pe*!H;l6 zDiYkiY7&CgVn8y)p$DY)&Dpo=fsuR+YGIbC8@33N+ra&>hBxb^wpaWYuCIP!J#zop zXJJp+(2xT%YY0HZ*WyylzKC`p5y9jRGuNSj~sF;`Ro literal 0 HcmV?d00001 diff --git a/dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm b/dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm new file mode 100644 index 0000000000000000000000000000000000000000..14840c05c5cd20fbdf32f770381da1e3e8a4c6b4 GIT binary patch literal 511 zcmYLFOG*Pl5UuL&OghsUlgV%6rg~1`LIhXIfM9Up6_TJq7|e%BM1+BvrAzSylkpH) zdKHi01q5r7XbPHouimSIelS_h5CG5@W59H*lO##Y!c;2Bb5+45B32Y4k)tuiFnc{W z)idSHrn5!-BEY>{Jk8gu(Q+LF=-K3957ETa`TaB=#f#?^2=`q|4ZNIXV0Pe*!H;l6 zDiYkiY7&CgVn8y)p$DY)&Dpo=fsuR+YGIbC8@33N+ra&>hBxb^wpaWYuCIP!J#zop zXJJp+(2xT%YY0HZ*WyylzKC`p5y9jRGuNSj~sF;`Ro literal 0 HcmV?d00001 diff --git a/dist/wasm/wasm-simd.wasm b/dist/wasm/wasm-simd.wasm new file mode 100644 index 0000000000000000000000000000000000000000..14840c05c5cd20fbdf32f770381da1e3e8a4c6b4 GIT binary patch literal 511 zcmYLFOG*Pl5UuL&OghsUlgV%6rg~1`LIhXIfM9Up6_TJq7|e%BM1+BvrAzSylkpH) zdKHi01q5r7XbPHouimSIelS_h5CG5@W59H*lO##Y!c;2Bb5+45B32Y4k)tuiFnc{W z)idSHrn5!-BEY>{Jk8gu(Q+LF=-K3957ETa`TaB=#f#?^2=`q|4ZNIXV0Pe*!H;l6 zDiYkiY7&CgVn8y)p$DY)&Dpo=fsuR+YGIbC8@33N+ra&>hBxb^wpaWYuCIP!J#zop zXJJp+(2xT%YY0HZ*WyylzKC`p5y9jRGuNSj~sF;`Ro literal 0 HcmV?d00001 diff --git a/hosted-headers.txt b/hosted-headers.txt new file mode 100644 index 0000000..8571cd2 --- /dev/null +++ b/hosted-headers.txt @@ -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 diff --git a/hosted-loader.js b/hosted-loader.js new file mode 100644 index 0000000..818abe6 --- /dev/null +++ b/hosted-loader.js @@ -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); diff --git a/index.html b/index.html new file mode 100644 index 0000000..96f0f1b --- /dev/null +++ b/index.html @@ -0,0 +1,69 @@ + + + + + + +Mandelbrot Deep Zoom v23 + + + + +
MANDELBROT DEEP ZOOM
中心
倍率  表示幅
計算 起動中… 描画
+
起動中
+
+
+
+
+
+
0.0080
+
.18
+
詳細設定・正確な座標 +
350
+
+
+
+
+
診断情報
engine: …
numeric: …
memory: …
+
+
起動中
+
ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。
+
+ +
+ +

高解像度 PNG 出力

+
+ + + + +
+ +
+
+
+ + + + diff --git a/kernels.js b/kernels.js new file mode 100644 index 0000000..e1a203f --- /dev/null +++ b/kernels.js @@ -0,0 +1,22 @@ +'use strict'; +// Generated from dist/wasm/manifest.json. Run scripts/extract-wasm.ps1 before replacing compatibility inputs. +globalThis.MANDEL_KERNELS=Object.freeze({ + WASM_SIMD_B64:'AGFzbQEAAAABEQJgAAF/YAh8fHx/f39/fwF/AwQDAAABBAUBcAEBAQUEAQEgIAYIAX8BQYCINAsHMAQGbWVtb3J5AgAKY291bnRzX3B0cgAACG1hZ3NfcHRyAAELcmVuZGVyX3Jvd3MAAgqSAwMIAEGAiICAAAsIAEGAiJCAAAv9AgYCfwJ8AX8CfAF/B3xBACEIAkAgA0EBSA0AIARBAUgNACAGQQFIDQAgB0EBSA0AIAYgA2wiCUGAgARKDQAgAiADtyIKoyELQQAhDCAEt0QAAAAAAADgP6IhDSAKRAAAAAAAAOC/oiEOQQAhDwNAIAsgDSAPIAVqt6GiIAGgIRBEAAAAAAAAAAAhEUEAIQQDQCALIA4gEaCiIACgIRJEAAAAAAAAAAAhE0EAIQhEAAAAAAAAAAAhFEQAAAAAAAAAACECRAAAAAAAAAAAIQoCQANAIBIgFCAToaAiFSAVoiIUIAogCqAgAqIgEKAiAiACoiIToCEWIAhBAWoiCCAHTg0BIBUhCiAWRAAAAAAAABBAZQ0ACwsgDEEDdEGAiJCAAGogFkQAAAAAAAAAACAIIAdIGzkDACAMQQJ0QYCIgIAAaiAINgIAIAxBAWohDCARRAAAAAAAAPA/oCERIARBAWoiBCADRw0ACyAPQQFqIg8gBkcNAAsgCSEICyAICw==', + WASM_SCALAR_B64:'AGFzbQEAAAABEQJgAAF/YAh8fHx/f39/fwF/AwQDAAABBAUBcAEBAQUEAQEgIAYIAX8BQYCINAsHMAQGbWVtb3J5AgAKY291bnRzX3B0cgAACG1hZ3NfcHRyAAELcmVuZGVyX3Jvd3MAAgqSAwMIAEGAiICAAAsIAEGAiJCAAAv9AgYCfwJ8AX8CfAF/B3xBACEIAkAgA0EBSA0AIARBAUgNACAGQQFIDQAgB0EBSA0AIAYgA2wiCUGAgARKDQAgAiADtyIKoyELQQAhDCAEt0QAAAAAAADgP6IhDSAKRAAAAAAAAOC/oiEOQQAhDwNAIAsgDSAPIAVqt6GiIAGgIRBEAAAAAAAAAAAhEUEAIQQDQCALIA4gEaCiIACgIRJEAAAAAAAAAAAhE0EAIQhEAAAAAAAAAAAhFEQAAAAAAAAAACECRAAAAAAAAAAAIQoCQANAIBIgFCAToaAiFSAVoiIUIAogCqAgAqIgEKAiAiACoiIToCEWIAhBAWoiCCAHTg0BIBUhCiAWRAAAAAAAABBAZQ0ACwsgDEEDdEGAiJCAAGogFkQAAAAAAAAAACAIIAdIGzkDACAMQQJ0QYCIgIAAaiAINgIAIAxBAWohDCARRAAAAAAAAPA/oCERIARBAWoiBCADRw0ACyAPQQFqIg8gBkcNAAsgCSEICyAICw==', + DEEP_SIMD_B64:'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', + DEEP_SCALAR_B64:'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', + BLA_SIMD_B64:'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HKAhECAgIAhByJT6hABqKwMAoCESIAhBqNDngwBqKwMAIBGgIRECQAJAIAW9Qv///////////wCDQv/////////3/wBVDQAgEL1C////////////AINC//////////f/AFUNACARvUL///////////8Ag0L/////////9/8AVQ0AIBK9Qv///////////wCDQv/////////3/wBVDQBEAAAAAAAAAAAgCEHw+tWGAGorAwAiAiAhIAIgIWMbIgIgAkQAAAAAAAAAAGMbIgJEAAAAAAAA8H9jDQELRAAAAAAAAAAAIQILIBkgGGoiCEGAjNWCAGogEDkDACAIQeDHwoEAaiAFOQMAIAhBoNDngwBqIBE5AwAgCEHAlPqEAGogEjkDACAIQfD61YYAaiACOQMAIAlBBGooAgAgCSgCAGohCAsgAyAINgIAIAlBCGohCSAaQRBqIRogCkECaiEKIBhBCGohGCADQQRqIQMgF0F/aiIXDQALQQAgFEEBaiIJNgK4xsKBACAEQQNJDQEgFEETSSEKIBUhCCAWIQQgCSEUIAoNAAsLQQAgADYCkL/ohwAgCQ8LQQBBADYCuMbCgQBBAAvaFBIDfwF+AnwBfwF8CH8DfAF/A3wBfwJ8BH8EfAF/AXwEfwR8AXtBACEQAkAgC0EBSA0AIApBAUgNACAHQQFIDQAgBkEBSA0AQQAoApC/6IcAIAVHDQBBACgCuMbCgQAiEUEBSA0AIAsgCmwiEkGAgARKDQBCACETQQBCADcDsMbCgQBBAEEANgKoxsKBAEEAQQA2AqzGwoEAQQBBADYCpMbCgQBBAEEANgKgxsKBAEEAQQA2ApzGwoEAQQBBADYCmMbCgQAgBrchFCAHtyEVAkACQCAMQQFIDQAgEUF/aiEWIBVEAAAAAAAA4D+iIRdBACEYQQAhGUEAIRpBACEbQQAhHEEAIR1BACEeQQAhHwNAIAAgFyAZIAlqt6EgFKOiIAKgIiAgBKAiFSAVoiIhRAAAAAAAANA/okQWVueerwPSvKAhIkEAISMDQCAAICMgCGq3IBSjRAAAAAAAAOC/oKIgAaAhJAJAAkACQAJAAkACQCAPRQ0AICQgA6AiJUQAAAAAAADQv6AiFSAVoiAhoCImIBUgJqCiICJjDQEgJUQAAAAAAADwP6AiFSAVoiAhoERw//////+vP2MNAQtBACEnRAAAAAAAAPA/IShEAAAAAAAAAAAhFUQAAAAAAAAAACEmRAAAAAAAAAAAISlBACEqQQAhK0EAISxBACEtAkADQCArIAVKDQUgFSArQQN0IhBBgIiwgABqKwMAIi6gIiUgJaIgJiAQQZCn+YAAaisDACIvoCIwIDCioCIxRAAAAAAAABBAZA0BIA9BAEcgJ0EAR3EhMgJAICpBDEgNACAyRQ0AICggKKIgKSApoqAiM0QAAAAAAADwf2QgM0QAAAAAAADwf2NyQQFHDQAgM0SN7bWg98awPmNFDQBBACAdQQFqIh02AqTGwoEAQQAgEyAqrXwiEzcDsMbCgQAMBAsCQAJAAkACQAJAICtBAUgNACAxIBUgFaIgJiAmoqAiM2MNAyArIAVIDQELICsgBUgNAQwJCyArQX9qIjRoIBYgNBsiECAWIBAgEUkbIRACQCANQQFIDQAgLCANSA0AIBBBAnQhBwNAAkAgNCAQIgZ2IhAgB0GQx8KBAGooAgBODQAgB0HAxsKBAGooAgAgEGoiMkECdEHg2IyGAGooAgAiEEECSA0AIBAgK2ogBUoNACAQICpqIAxKDQAgMyAyQQN0QfD61YYAaisDACIxIDGiY0UNAEEAIB9BAWoiHzYCqMbCgQAgEyAqrHwhEwwKCyAHQXxqIQcgBkF/aiEQIAZBAEoNAAwCCwsgEEECdCEHA0ACQCA0IBAiBnYiECAHQZDHwoEAaigCAE4NACAHQcDGwoEAaigCACAQaiI1QQJ0QeDYjIYAaigCACIQQQJIDQAgECAraiI2IAVKDQAgECAqaiI3IAxKDQAgMyA1QQN0IhBB8PrVhgBqKwMAIjEgMaJjRQ0AIBBBoNDngwBqKwMAIjggJKIgEEHgx8KBAGorAwAiMSAVoiAmIBBBgIzVggBqKwMAIjmioaAgEEHAlPqEAGorAwAiOiAgoqEiO71C////////////AINC//////////f/AFUNACA6ICSiIDggIKIgMSAmoiAVIDmioKCgIji9Qv///////////wCDQv/////////3/wBVDQACQAJAIDJFDQACQCAxICiiICkgOZqioCIVvUL///////////8Ag0L/////////9/8AVQ0AQQEhJyAxICmiICggOaKgIia9Qv///////////wCDQoCAgICAgID4/wBTDQILQQAhJwsgKSEmICghFQtBACAbQQFqIhs2ApjGwoEAICxBAWohLCA2ISsgNyEqICYhKSAVISggOCEmIDshFQwECyAHQXxqIQcgBkF/aiEQIAZBAEoNAAsLAkAgDkEBSA0AIC0gDkgNAEEAIB9BAWoiHzYCqMbCgQAgEyAqrHwhEwwHCwJAAkACQCAPDQAgJyEQDAELQQEhEAJAICoNAEQAAAAAAADwPyExRAAAAAAAAAAAITAMAgsCQCAnRQ0AICUgKKIgKSAwoqEiMSAxoCIxvUL///////////8Ag0L/////////9/8AVQ0AICUgKaIgKCAwoqAiJSAloCIwvUL///////////8Ag0KAgICAgICA+P8AUw0CC0EAIRALICkhMCAoITELICQgLiAVoiAmIC+ioSIlICWgIBUgFaIgJiAmoqGgoCIlvUL///////////8Ag0L/////////9/8AVQ0HICAgLiAmoiAVIC+ioCIoICigICYgFSAVoKKgoCImvUL///////////8Ag0L/////////9/8AVQ0HQQAgHEEBaiIcNgKcxsKBACAtQQFqIS0gKkEBaiEqICtBAWohKyAQIScgMCEpIDEhKCAlIRUMAQtBACErQQAgGkEBaiIaNgKgxsKBACAwISYgJSEVCyAqIAxIDQALQQAgEyAqrHwiEzcDsMbCgQAMAgtBACATICqsfCITNwOwxsKBAAwEC0EAIB1BAWoiHTYCpMbCgQALRAAAAAAAAAAAITEgDCEqDAILQQAgEzcDsMbCgQBEAAAAAAAAAAAhMUF+ISoMAQtBACAeQQFqIh42AqzGwoEARAAAAAAAAAAAITFBfyEqCyAYQQN0QYCIkIAAaiAxOQMAIBhBAnRBgIiAgABqICo2AgAgGEEBaiEYICNBAWoiIyAKRw0ACyAZQQFqIhkgC0cNAAwCCwsCQCAPRQ0AIBVEAAAAAAAA4D+iISlBACErQQAhBUEAITQDQCAAICkgBSAJarehIBSjoiACoCAEoCIVIBWiIiVEAAAAAAAA0D+iRBZW556vA9K8oCEoICtBAnRBgIiAgABqIRAgK0EDdEGAiJCAAGohByAIIQYgCiEqA0ACQAJAIAAgBrcgFKNEAAAAAAAA4L+goiABoCADoCIwRAAAAAAAANC/oCIVIBWiICWgIiYgFSAmoKIgKGMNACAwRAAAAAAAAPA/oCIVIBWiICWgRHD//////68/Yw0AQQBCADcDsMbCgQAMAQtBACA0QQFqIjQ2AqTGwoEACyAHQgA3AwAgECAMNgIAIAZBAWohBiAQQQRqIRAgB0EIaiEHICtBAWohKyAqQX9qIioNAAsgBUEBaiIFIAtHDQAMAgsLIApBA3QhLSAKQQJ0IRggCkF+cSEyIAz9ESE8QQAhKiAKITRBgIiQgAAhBUGAiICAACEnQQAhDwNAICohKwJAAkAgCkECSQ0AICogMmohKyAFIRAgJyEHIDIhBgNAIBD9DAAAAAAAAAAAAAAAAAAAAAD9CwMAIAcgPP1bAgAAIBBBEGohECAHQQhqIQcgBkF+aiIGDQALIDIgCkYNAQsgNCArayEGICtBAnRBgIiAgABqIRAgK0EDdEGAiJCAAGohBwNAIAdCADcDACAQIAw2AgAgEEEEaiEQIAdBCGohByAGQX9qIgYNAAsLICogCmohKiA0IApqITQgBSAtaiEFICcgGGohJyAPQQFqIg8gC0cNAAsLIBIhEAsgEAs2ACAAIAEgAkQAAAAAAAAAAEQAAAAAAAAAACADIAQgBSAGIAcgCCAJIApBAEEAQQAQjYCAgAAL', + BLA_SCALAR_B64:'AGFzbQEAAAABLwRgAAF/YAN/fHwBf2AQfHx8fHx/f39/f39/f39/fwF/YAt8fHx/f39/f39/fwF/AxAPAAAAAAAAAAAAAAAAAQIDBAUBcAEBAQUGAQGABIAEBgkBfwFBoL/sBwsH7AEQBm1lbW9yeQIACmNvdW50c19wdHIAAAhtYWdzX3B0cgABCnJlZnNfcl9wdHIAAgpyZWZzX2lfcHRyAAMOc3RhdF9ibGFfc3RlcHMABA5zdGF0X3B0Yl9zdGVwcwAFDHN0YXRfcmViYXNlcwAGDXN0YXRfaW50ZXJpb3IABw9zdGF0X3VucmVzb2x2ZWQACAlzdGF0X2ZhaWwACQxzdGF0X2l0ZXJfbG8ACgxzdGF0X2l0ZXJfaGkACwlidWlsZF9ibGEADBJyZW5kZXJfYmxhX3JlY3RfdjIADQ9yZW5kZXJfYmxhX3JlY3QADgqaIQ8IAEGAiICAAAsIAEGAiJCAAAsIAEGAiLCAAAsIAEGQp/mAAAsLAEEAKAKYxsKBAAsLAEEAKAKcxsKBAAsLAEEAKAKgxsKBAAsLAEEAKAKkxsKBAAsLAEEAKAKoxsKBAAsLAEEAKAKsxsKBAAsLAEEAKAKwxsKBAAsLAEEAKAK0xsKBAAvSCgcCfwF8An8DfAF+CX8GfAJAIABBjux2akGQ7HZNDQBBACEDQQBBATYCuMbCgQBBACAAQX9qIgQ2ApDHwoEAQQBBADYCwMbCgQAgAZohBUHg2IyGACEGQQEhBwNAIAZBATYCACADQaDQ54MAakKAgICAgICA+D83AwAgA0HAlPqEAGpCADcDACADQeDHwoEAaiADQYiIsIAAaisDACIIIAigIgk5AwAgA0GAjNWCAGogA0GYp/mAAGorAwAiCCAIoCIKOQMARAAAAAAAAAAAIQgCQCAJIAmiIAogCqKgnyIJvSILUCALQoCAgICAgID4/wBRciALQv///////////wCDQoCAgICAgID4/wBVcg0AAkAgAiAJoiAFIAmjoCIIRAAAAAAAAAAAYw0AIAi9Qv///////////wCDQoCAgICAgID4/wBTDQELRAAAAAAAAAAAIQgLIANB8PrVhgBqIAg5AwAgBkEEaiEGIANBCGohAyAAIAdBAWoiB0cNAAtBASEGAkAgAEEDSA0AQQAhA0EBIQwDQCADIARqIg0gBEEBakEBdiIOakHFqBJODQIgDEECdCIGQZDHwoEAaiAONgIAIAZBwMbCgQBqIA02AgACQCAEDQBBACAMQQFqIgY2ArjGwoEADAILQQEhByAOQQEgDkEBSxshDyAEQQN0IRAgA0EDdCERIANBAnRB4NiMhgBqIQYgDUECdEHg2IyGAGohEkEAIRMDQAJAAkAgByAESA0AIBEgEGoiA0Hgx8KBAGogESATaiIUQeDHwoEAaisDADkDACADQYCM1YIAaiAUQYCM1YIAaisDADkDACADQaDQ54MAaiAUQaDQ54MAaisDADkDACADQcCU+oQAaiAUQcCU+oQAaisDADkDACADQfD61YYAaiAUQfD61YYAaisDADkDACAGKAIAIQMMAQsgESATaiIDQeDHwoEAaisDACIIIANBiIzVggBqKwMAIgmiIRUgA0Hox8KBAGorAwAiCiADQYCM1YIAaisDACICoiEWIAogCKIgAiAJoqEhBSAKIANBwJT6hABqKwMAIheiIANBoNDngwBqKwMAIhggCaKgIRkgCiAYoiAXIAmioSEJRAAAAAAAAPC/IRoCQCAIIAiiIAIgAqKgnyIIRAAAAAAAAAAAZEUNACADQfj61YYAaisDACAYIBiiIBcgF6KgnyABoqEgCKMhGgsgFiAVoCEIIBkgA0HIlPqEAGorAwCgIQogA0Go0OeDAGorAwAgCaAhCQJAAkAgBb1C////////////AINC//////////f/AFUNACAIvUL///////////8Ag0L/////////9/8AVQ0AIAm9Qv///////////wCDQv/////////3/wBVDQAgCr1C////////////AINC//////////f/AFUNAEQAAAAAAAAAACADQfD61YYAaisDACICIBogAiAaYxsiAiACRAAAAAAAAAAAYxsiAkQAAAAAAADwf2MNAQtEAAAAAAAAAAAhAgsgESAQaiIDQYCM1YIAaiAIOQMAIANB4MfCgQBqIAU5AwAgA0Gg0OeDAGogCTkDACADQcCU+oQAaiAKOQMAIANB8PrVhgBqIAI5AwAgBkEEaigCACAGKAIAaiEDCyASIAM2AgAgBkEIaiEGIBNBEGohEyAHQQJqIQcgEEEIaiEQIBJBBGohEiAPQX9qIg8NAAtBACAMQQFqIgY2ArjGwoEAIARBA0kNASAMQRNJIQcgDSEDIA4hBCAGIQwgBw0ACwtBACAANgKQv+iHACAGDwtBAEEANgK4xsKBAEEAC4gVEQN/AX4CfAF/AXwIfwN8AX8DfAF/AnwEfwR8AX8BfAR/BHxBACEQAkAgC0EBSA0AIApBAUgNACAHQQFIDQAgBkEBSA0AQQAoApC/6IcAIAVHDQBBACgCuMbCgQAiEUEBSA0AIAsgCmwiEkGAgARKDQBCACETQQBCADcDsMbCgQBBAEEANgKoxsKBAEEAQQA2AqzGwoEAQQBBADYCpMbCgQBBAEEANgKgxsKBAEEAQQA2ApzGwoEAQQBBADYCmMbCgQAgBrchFCAHtyEVAkACQCAMQQFIDQAgEUF/aiEWIBVEAAAAAAAA4D+iIRdBACEYQQAhGUEAIRpBACEbQQAhHEEAIR1BACEeQQAhHwNAIAAgFyAZIAlqt6EgFKOiIAKgIiAgBKAiFSAVoiIhRAAAAAAAANA/okQWVueerwPSvKAhIkEAISMDQCAAICMgCGq3IBSjRAAAAAAAAOC/oKIgAaAhJAJAAkACQAJAAkACQCAPRQ0AICQgA6AiJUQAAAAAAADQv6AiFSAVoiAhoCImIBUgJqCiICJjDQEgJUQAAAAAAADwP6AiFSAVoiAhoERw//////+vP2MNAQtBACEnRAAAAAAAAPA/IShEAAAAAAAAAAAhFUQAAAAAAAAAACEmRAAAAAAAAAAAISlBACEqQQAhK0EAISxBACEtAkADQCArIAVKDQUgFSArQQN0IhBBgIiwgABqKwMAIi6gIiUgJaIgJiAQQZCn+YAAaisDACIvoCIwIDCioCIxRAAAAAAAABBAZA0BIA9BAEcgJ0EAR3EhMgJAICpBDEgNACAyRQ0AICggKKIgKSApoqAiM0QAAAAAAADwf2QgM0QAAAAAAADwf2NyQQFHDQAgM0SN7bWg98awPmNFDQBBACAdQQFqIh02AqTGwoEAQQAgEyAqrXwiEzcDsMbCgQAMBAsCQAJAAkACQAJAICtBAUgNACAxIBUgFaIgJiAmoqAiM2MNAyArIAVIDQELICsgBUgNAQwJCyArQX9qIjRoIBYgNBsiECAWIBAgEUkbIRACQCANQQFIDQAgLCANSA0AIBBBAnQhBwNAAkAgNCAQIgZ2IhAgB0GQx8KBAGooAgBODQAgB0HAxsKBAGooAgAgEGoiMkECdEHg2IyGAGooAgAiEEECSA0AIBAgK2ogBUoNACAQICpqIAxKDQAgMyAyQQN0QfD61YYAaisDACIxIDGiY0UNAEEAIB9BAWoiHzYCqMbCgQAgEyAqrHwhEwwKCyAHQXxqIQcgBkF/aiEQIAZBAEoNAAwCCwsgEEECdCEHA0ACQCA0IBAiBnYiECAHQZDHwoEAaigCAE4NACAHQcDGwoEAaigCACAQaiI1QQJ0QeDYjIYAaigCACIQQQJIDQAgECAraiI2IAVKDQAgECAqaiI3IAxKDQAgMyA1QQN0IhBB8PrVhgBqKwMAIjEgMaJjRQ0AIBBBoNDngwBqKwMAIjggJKIgEEHgx8KBAGorAwAiMSAVoiAmIBBBgIzVggBqKwMAIjmioaAgEEHAlPqEAGorAwAiOiAgoqEiO71C////////////AINC//////////f/AFUNACA6ICSiIDggIKIgMSAmoiAVIDmioKCgIji9Qv///////////wCDQv/////////3/wBVDQACQAJAIDJFDQACQCAxICiiICkgOZqioCIVvUL///////////8Ag0L/////////9/8AVQ0AQQEhJyAxICmiICggOaKgIia9Qv///////////wCDQoCAgICAgID4/wBTDQILQQAhJwsgKSEmICghFQtBACAbQQFqIhs2ApjGwoEAICxBAWohLCA2ISsgNyEqICYhKSAVISggOCEmIDshFQwECyAHQXxqIQcgBkF/aiEQIAZBAEoNAAsLAkAgDkEBSA0AIC0gDkgNAEEAIB9BAWoiHzYCqMbCgQAgEyAqrHwhEwwHCwJAAkACQCAPDQAgJyEQDAELQQEhEAJAICoNAEQAAAAAAADwPyExRAAAAAAAAAAAITAMAgsCQCAnRQ0AICUgKKIgKSAwoqEiMSAxoCIxvUL///////////8Ag0L/////////9/8AVQ0AICUgKaIgKCAwoqAiJSAloCIwvUL///////////8Ag0KAgICAgICA+P8AUw0CC0EAIRALICkhMCAoITELICQgLiAVoiAmIC+ioSIlICWgIBUgFaIgJiAmoqGgoCIlvUL///////////8Ag0L/////////9/8AVQ0HICAgLiAmoiAVIC+ioCIoICigICYgFSAVoKKgoCImvUL///////////8Ag0L/////////9/8AVQ0HQQAgHEEBaiIcNgKcxsKBACAtQQFqIS0gKkEBaiEqICtBAWohKyAQIScgMCEpIDEhKCAlIRUMAQtBACErQQAgGkEBaiIaNgKgxsKBACAwISYgJSEVCyAqIAxIDQALQQAgEyAqrHwiEzcDsMbCgQAMAgtBACATICqsfCITNwOwxsKBAAwEC0EAIB1BAWoiHTYCpMbCgQALRAAAAAAAAAAAITEgDCEqDAILQQAgEzcDsMbCgQBEAAAAAAAAAAAhMUF+ISoMAQtBACAeQQFqIh42AqzGwoEARAAAAAAAAAAAITFBfyEqCyAYQQN0QYCIkIAAaiAxOQMAIBhBAnRBgIiAgABqICo2AgAgGEEBaiEYICNBAWoiIyAKRw0ACyAZQQFqIhkgC0cNAAwCCwsCQCAPRQ0AIBVEAAAAAAAA4D+iISlBACErQQAhBUEAITQDQCAAICkgBSAJarehIBSjoiACoCAEoCIVIBWiIiVEAAAAAAAA0D+iRBZW556vA9K8oCEoICtBAnRBgIiAgABqIRAgK0EDdEGAiJCAAGohByAIIQYgCiEqA0ACQAJAIAAgBrcgFKNEAAAAAAAA4L+goiABoCADoCIwRAAAAAAAANC/oCIVIBWiICWgIiYgFSAmoKIgKGMNACAwRAAAAAAAAPA/oCIVIBWiICWgRHD//////68/Yw0AQQBCADcDsMbCgQAMAQtBACA0QQFqIjQ2AqTGwoEACyAHQgA3AwAgECAMNgIAIAZBAWohBiAQQQRqIRAgB0EIaiEHICtBAWohKyAqQX9qIioNAAsgBUEBaiIFIAtHDQAMAgsLIApBAnQhMiAKQQN0IS1BACEqQQAgCkEDcSIYayErIApBf2pBA0khNSAKISdBgIiAgAAhNEGAiJCAACEFQQAhDwNAICohBwJAIBhFDQBBACEGIDQhECAFIQcDQCAHQgA3AwAgECAMNgIAIBBBBGohECAHQQhqIQcgKyAGQX9qIgZHDQALICogBmshBwsCQCA1DQAgJyAHayEGIAdBAnRBgIiAgABqIRAgB0EDdEGAiJCAAGohBwNAIAdCADcDACAQIAw2AgAgEEEEaiAMNgIAIAdBCGpCADcDACAQQQhqIAw2AgAgB0EQakIANwMAIBBBDGogDDYCACAHQRhqQgA3AwAgEEEQaiEQIAdBIGohByAGQXxqIgYNAAsLICogCmohKiAnIApqIScgNCAyaiE0IAUgLWohBSAPQQFqIg8gC0cNAAsLIBIhEAsgEAs2ACAAIAEgAkQAAAAAAAAAAEQAAAAAAAAAACADIAQgBSAGIAcgCCAJIApBAEEAQQAQjYCAgAAL', + COLOR_SIMD_B64:'AGFzbQEAAAABCgJgAAF/YAF/AX8DBAMAAAEEBQFwAQEBBQQBASAgBggBfwFBgIg0CwcvBAZtZW1vcnkCAAhtYWdzX3B0cgAACGNvcnJfcHRyAAEMc21vb3RoX2JhdGNoAAIKhgoDCABBgIiAgAALCABBgIiggAAL8QkFBX8PewF8AX4BfEEAIQECQCAAQQBIDQAgAEGAgAQgAEGAgARIGyEBIABFDQBBACECAkAgAUEBIAFBAUobIgNBAkkNAEGAiICAACEAQYCIoIAAIQQgA0H+////B3EiAiEFA0AgBP0MAAAAAAAA8D8AAAAAAADwPyIGIAD9AAQAIgf9DKf8tQYAABBAp/y1BgAAEEAgB/0MAAAAAAAAEEAAAAAAAAAQQP1K/VIiCP0M////////DwD///////8PACIJ/U79DAAAAAAAAPA/AAAAAAAA8D8iCv1QIgf9DAAAAAAAAPC/AAAAAAAA8L8iC/3wASAHIAb98AH98wEiDCAMIAwgDP3yASIH/fIBIgz9DAAAAAAAAAhAAAAAAAAACEAiDf3zAf3wASAHIAz98gEiDP0MAAAAAAAAFEAAAAAAAAAUQCIO/fMB/fABIAcgDP3yASIM/QwAAAAAAAAcQAAAAAAAABxAIg/98wH98AEgByAM/fIBIgz9DAAAAAAAACJAAAAAAAAAIkAiEP3zAf3wASAHIAz98gH9DAAAAAAAACZAAAAAAAAAJkAiEf3zAf3wASIHIAf98AH9DP6CK2VHFfc//oIrZUcV9z8iEv3yASAIQTT9zQEgB/0NAAECAwgJCgsAAQIDAAECA/0M/wcAAP8HAAD/BwAA/wcAACII/U79DAH8//8B/P//Afz//wH8//8iE/2uAf3+Af3wAf0MAAAAAAAA4D8AAAAAAADgP/3yASIUIAn9TiAK/VAiByAL/fABIAcgBv3wAf3zASIMIAwgDCAM/fIBIgf98gEiDCAN/fMB/fABIAcgDP3yASIMIA798wH98AEgByAM/fIBIgwgD/3zAf3wASAHIAz98gEiDCAQ/fMB/fABIAcgDP3yASAR/fMB/fABIgcgB/3wASAS/fIBIBRBNP3NASAH/Q0AAQIDCAkKCwABAgMAAQIDIAj9TiAT/a4B/f4B/fAB/fEBIgf9IQC2/RMgB/0hAbb9IAH9WwMAACAEQQhqIQQgAEEQaiEAIAVBfmoiBQ0ACyADIAJGDQELIAMgAmshBSACQQN0QYCIgIAAaiEAIAJBAnRBgIiggABqIQQDQCAERAAAAAAAAPA/IAArAwAiFUSn/LUGAAAQQCAVRAAAAAAAABBAZBu9IhZC/////////weDQoCAgICAgID4P4S/IhVEAAAAAAAA8L+gIBVEAAAAAAAA8D+goyIXIBcgFyAXoiIVoiIXRAAAAAAAAAhAo6AgFSAXoiIXRAAAAAAAABRAo6AgFSAXoiIXRAAAAAAAABxAo6AgFSAXoiIXRAAAAAAAACJAo6AgFSAXokQAAAAAAAAmQKOgIhUgFaBE/oIrZUcV9z+iIBZCNIinQf8PcUGBeGq3oEQAAAAAAADgP6K9IhZC/////////weDQoCAgICAgID4P4S/IhVEAAAAAAAA8L+gIBVEAAAAAAAA8D+goyIXIBcgFyAXoiIVoiIXRAAAAAAAAAhAo6AgFSAXoiIXRAAAAAAAABRAo6AgFSAXoiIXRAAAAAAAABxAo6AgFSAXoiIXRAAAAAAAACJAo6AgFSAXokQAAAAAAAAmQKOgIhUgFaBE/oIrZUcV9z+iIBZCNIinQf8PcUGBeGq3oKG2OAIAIABBCGohACAEQQRqIQQgBUF/aiIFDQALCyABCw==', + COLOR_SCALAR_B64:'AGFzbQEAAAABCgJgAAF/YAF/AX8DBAMAAAEEBQFwAQEBBQQBASAgBggBfwFBgIg0CwcvBAZtZW1vcnkCAAhtYWdzX3B0cgAACGNvcnJfcHRyAAEMc21vb3RoX2JhdGNoAAIKkwQDCABBgIiAgAALCABBgIiggAAL/gMEA38BfAF+AXxBACEBAkAgAEEASA0AIABBgIAEIABBgIAESBshASAARQ0AIAFBASABQQFKG0EDdCECQQAhAEGAiKCAACEDA0AgA0QAAAAAAADwPyAAQYCIgIAAaisDACIERKf8tQYAABBAIAREAAAAAAAAEEBkG70iBUL/////////B4NCgICAgICAgPg/hL8iBEQAAAAAAADwv6AgBEQAAAAAAADwP6CjIgYgBiAGIAaiIgSiIgZEAAAAAAAACECjoCAEIAaiIgZEAAAAAAAAFECjoCAEIAaiIgZEAAAAAAAAHECjoCAEIAaiIgZEAAAAAAAAIkCjoCAEIAaiRAAAAAAAACZAo6AiBCAEoET+gitlRxX3P6IgBUI0iKdB/w9xQYF4aregRAAAAAAAAOA/or0iBUL/////////B4NCgICAgICAgPg/hL8iBEQAAAAAAADwv6AgBEQAAAAAAADwP6CjIgYgBiAGIAaiIgSiIgZEAAAAAAAACECjoCAEIAaiIgZEAAAAAAAAFECjoCAEIAaiIgZEAAAAAAAAHECjoCAEIAaiIgZEAAAAAAAAIkCjoCAEIAaiRAAAAAAAACZAo6AiBCAEoET+gitlRxX3P6IgBUI0iKdB/w9xQYF4aregobY4AgAgA0EEaiEDIAIgAEEIaiIARw0ACwsgAQs=' +}); +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' +}); diff --git a/script.js b/script.js new file mode 100644 index 0000000..10f7903 --- /dev/null +++ b/script.js @@ -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='
Canvas 2Dを利用できません。
';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<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)} +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>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;j8)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>>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{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;inew 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{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;inew 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;ideepModuleBundle={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(refLoadedblaRefLoaded){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>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=d.iter)!==(p>=d.iter))){add(i);add(i-stride)}if(nb[0]-a[0]);if(top.length>want)top.length=want}}for(const x of top)add(x[1]);for(let k=0;picks.length=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=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=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(start0&&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{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.chunkIndexjob.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{if(token!==state.token||this.cancelled)return;const deadline=performance.now()+5;while(ktoken!==state.token||this.cancelled);if(!result)return;const[n,m]=result;putField(field,idx,n,m,nrunner.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{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;ia.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(ptoken!==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,nn+(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(itoken!==state.token||state.processMode!=='validate'); + if(!result){validationJob.active=false;return} + const[n,m]=result;if(n180){lastStatus=now;state.coverage=i/field.classes.length;invalidateStats()} + if(in+(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;yminTile||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{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=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=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>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(yyplan.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,n1.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=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=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=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=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(yMath.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=iter?0:Math.max(4.0000001,zr2+zi2),kind=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(yHI128){r*=INV256;i*=INV256;b++;m*=INV256} + while(m>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<>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<=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.nBAIL)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<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(nBAIL)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)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<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.ntoken!==state.token);if(!result)return;const[n,m]=result;putField(field,y*w+x,n,m,n{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.loadedLentoken!==state.token);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n{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(n16384||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;yexportJob.cancelled):exportSampleShallow(cre,cim,pixelScale,W,H,iter,gx,gy);if(!sample)throw new Error('cancelled');const[n,m]=sample;if(noutCanvas.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{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(); +})(); diff --git a/scripts/build-hosted.ps1 b/scripts/build-hosted.ps1 new file mode 100644 index 0000000..f22e8a5 --- /dev/null +++ b/scripts/build-hosted.ps1 @@ -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, '\s*', '') +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 diff --git a/scripts/build-kernels.ps1 b/scripts/build-kernels.ps1 new file mode 100644 index 0000000..7b98c06 --- /dev/null +++ b/scripts/build-kernels.ps1 @@ -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 diff --git a/scripts/build-standalone.ps1 b/scripts/build-standalone.ps1 new file mode 100644 index 0000000..2eae81e --- /dev/null +++ b/scripts/build-standalone.ps1 @@ -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 diff --git a/scripts/build-wasm.ps1 b/scripts/build-wasm.ps1 new file mode 100644 index 0000000..6628256 --- /dev/null +++ b/scripts/build-wasm.ps1 @@ -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 diff --git a/scripts/extract-wasm.ps1 b/scripts/extract-wasm.ps1 new file mode 100644 index 0000000..73e8bc1 --- /dev/null +++ b/scripts/extract-wasm.ps1 @@ -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\(\{(?[\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 diff --git a/scripts/source-baseline.ps1 b/scripts/source-baseline.ps1 new file mode 100644 index 0000000..96584be --- /dev/null +++ b/scripts/source-baseline.ps1 @@ -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 diff --git a/scripts/test-all.ps1 b/scripts/test-all.ps1 new file mode 100644 index 0000000..703e97d --- /dev/null +++ b/scripts/test-all.ps1 @@ -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 diff --git a/src/abi.json b/src/abi.json new file mode 100644 index 0000000..211c233 --- /dev/null +++ b/src/abi.json @@ -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"] + } + } +} diff --git a/src/bla_kernel_v18.c b/src/bla_kernel_v18.c new file mode 100644 index 0000000..eae2b59 --- /dev/null +++ b/src/bla_kernel_v18.c @@ -0,0 +1,204 @@ +#include + +#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>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;j0.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>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=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(nref_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 && m0){ + 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 || !(z20 && 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 + +#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; +} diff --git a/src/deep_kernel.c b/src/deep_kernel.c new file mode 100644 index 0000000..9eb2ee5 --- /dev/null +++ b/src/deep_kernel.c @@ -0,0 +1,112 @@ +#include + +#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; +} diff --git a/src/shallow_kernel.c b/src/shallow_kernel.c new file mode 100644 index 0000000..badbacc --- /dev/null +++ b/src/shallow_kernel.c @@ -0,0 +1,44 @@ +#include + +#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; +} diff --git a/tests/analytic-interior.mjs b/tests/analytic-interior.mjs new file mode 100644 index 0000000..ea5a68a --- /dev/null +++ b/tests/analytic-interior.mjs @@ -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' })); diff --git a/tests/browser-benchmark.html b/tests/browser-benchmark.html new file mode 100644 index 0000000..e0ecc27 --- /dev/null +++ b/tests/browser-benchmark.html @@ -0,0 +1,27 @@ + + +Mandelbrot v23 browser benchmark + +

v23 browser benchmark

+

HTTP(S)でこのworkspaceを配信して実行します。iframeの実寸と実DPRを結果へ記録します。

+ +
Ready.
+ + diff --git a/tests/js-syntax.mjs b/tests/js-syntax.mjs new file mode 100644 index 0000000..51cd857 --- /dev/null +++ b/tests/js-syntax.mjs @@ -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(/ + + - diff --git a/dist/hosted/script.js b/dist/hosted/script.js index 10f7903..44822b7 100644 --- a/dist/hosted/script.js +++ b/dist/hosted/script.js @@ -1,912 +1,248 @@ (()=>{'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='
Canvas 2Dを利用できません。
';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{} -} +const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません'); +const $=s=>document.querySelector(s),canvas=$('#view'); +const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n; +const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3; +const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false}; +let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash=''; +const viewHistory=[];let viewHistoryIndex=-1; +const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0}; +// ── exact fixed-point view state ───────────────────────────────────────── function one(bits=state.bits){return 1n<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 fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v} +function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)} +function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length} +function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<>BigInt(-d)} +function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x} function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits} function log2Fixed(v){return log2FixedAt(v,state.bits)} -function 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)} -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>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;j8)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>>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{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;inew 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{const raw=atob(b64),bytes=new Uint8Array(raw.length);for(let i=0;inew 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;ideepModuleBundle={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(refLoadedblaRefLoaded){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>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=d.iter)!==(p>=d.iter))){add(i);add(i-stride)}if(nb[0]-a[0]);if(top.length>want)top.length=want}}for(const x of top)add(x[1]);for(let k=0;picks.length=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=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=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(start0&&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{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.chunkIndexjob.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{if(token!==state.token||this.cancelled)return;const deadline=performance.now()+5;while(ktoken!==state.token||this.cancelled);if(!result)return;const[n,m]=result;putField(field,idx,n,m,nrunner.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>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}} -function 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;ia.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(ptoken!==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,nn+(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(itoken!==state.token||state.processMode!=='validate'); - if(!result){validationJob.active=false;return} - const[n,m]=result;if(n180){lastStatus=now;state.coverage=i/field.classes.length;invalidateStats()} - if(in+(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;yminTile||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{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=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=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>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(yyplan.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,n1.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=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=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=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=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(yMath.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=iter?0:Math.max(4.0000001,zr2+zi2),kind=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(yHI128){r*=INV256;i*=INV256;b++;m*=INV256} - while(m>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<>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<=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.nBAIL)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<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(nBAIL)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)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<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.ntoken!==state.token);if(!result)return;const[n,m]=result;putField(field,y*w+x,n,m,n{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.loadedLentoken!==state.token);if(!result)return;const[n,m]=result;putField(field,idx,n,m,n{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(n16384||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;yexportJob.cancelled):exportSampleShallow(cre,cim,pixelScale,W,H,iter,gx,gy);if(!sample)throw new Error('cancelled');const[n,m]=sample;if(noutCanvas.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 zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))} +function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e} +function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}} +function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)} +function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){const stepLog=log2FixedAt(snap.span,snap.bits)-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp)));return !Number.isFinite(ratio)||ratio<96||stepLog<-120} function 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 recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()} +function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()} + +// ── iteration / quality policy ─────────────────────────────────────────── +function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))} +function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)} +function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}} + +// ── high precision reference worker ───────────────────────────────────── +function referenceWorkerSource(){return String.raw` +'use strict'; +const MAX_REF=150001,MAX_LEVELS=20; +function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length} +function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q} +function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n} +function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}} +function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<BAIL)escape=n+1}return{mismatch,checked}} +function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf} +self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} +`} +class ReferenceService{ + constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=null;this.failed=false} + ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache=d;p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} + request(snap,iter){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.1'].join(':');if(this.cache&&this.cache.key===key)return Promise.resolve(this.cache);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})} + cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} + destroy(){this.cancelPending('destroyed');if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} +} +const refs=new ReferenceService(); + +// ── WebGPU renderer ────────────────────────────────────────────────────── +function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})} +function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}} +function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a} +class WebGpuRenderer{ + constructor(adapter,device){ + this.adapter=adapter;this.device=device; + const ai=adapter.info||{}; + this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''}; + this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)}; + this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat(); + this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'}); + device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)}); + this.ready=this.initPipelines(); + device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()}); + } + configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})} + async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m} + async initPipelines(){ + this.device.pushErrorScope?.('validation'); + try{ + const [dm,xm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]); + this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}}); + this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}}); + this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}}); + this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}}); + this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}}); + this.context=canvas.getContext('webgpu'); + if(!this.context)throw new Error('WebGPU canvas contextを取得できません'); + webgpuCanvasClaimed=true;this.configure(); + }finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}} + } + frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null} + ensureFrame(w,h){ + const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage; + this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,64,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame; + } + ensureExportWorkspace(){ + if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S; + this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved'),pbufs:Array.from({length:4},(_,i)=>buf(d,64,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+16,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs; + } + exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','unresolved','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null} + setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}} + destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null} + directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})} + deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){const se=spanMantExp(snap);return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,0,0,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,Math.fround(se.mant),true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true)})} + colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})} + async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){ + await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(4)); + d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap)); + d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'}); + if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();} + else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();} + const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end(); + d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true; + } + async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true} + presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}} + presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])} + async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolved,pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(4)); + if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),p=encoder.beginComputePass();p.setPipeline(this.deep);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();} + else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),p=encoder.beginComputePass();p.setPipeline(this.direct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();} + const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end(); + const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolved,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);d.queue.writeBuffer(unresolved,0,new Uint32Array(4)); + for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si];if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();} + const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end(); + const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise} +function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx} +function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')} +function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()} +function schedule(){if(!raf)raf=requestAnimationFrame(loop)} +async function renderFrame(){const token=++state.token,snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,canvas.width),t0=performance.now();state.rendering=true;state.dirty=false;state.drawState='COVERING';runtime.renderStarts++;updateStats();try{const r=renderer||await initRenderer();if(token!==state.token)return;if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}let ctx=null;if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded rescaled perturbation'}else state.lastEngine='WebGPU · f32 direct';const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false;state.unresolved=0;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}else r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();if(deep)r.readUnresolved().then(q=>{if(token===state.token){state.unresolved=q||0;updateStats()}}).catch(()=>{})}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}} +function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false}); +canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}}); +canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}}); +function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer); + +// ── URL / controls ─────────────────────────────────────────────────────── +function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}} function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}} -function 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()}} +function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)} +function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1} +function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()} +function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)} +function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')} +$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()}; +$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()}; $('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}}; -$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);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('コピーできませんでした')}}; +$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}}; +$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}}; $('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex{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(); +$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()}; +$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()}; +$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}}; +addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}}); +addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}}); + +// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ────── +const exportJob={active:false,cancelled:false}; +function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)} +const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})(); +function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0} +function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out} +class StreamingPng{ + constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()} + async rows(filteredRows){await this.writer.write(filteredRows)} + async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})} + async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}} +} +function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}} +async function runExport(){ + if(exportJob.active)return; + const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return} + const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w),strict=$('#exportPrecision').value==='strict'; + let ctx=null; + if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}} + const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1; + exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true; + let done=0,unresolvedSamples=0; + try{ + for(let y=0;y{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png'); + +// ── diagnostics ────────────────────────────────────────────────────────── +function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(deepNeeded()?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx;$('#diagEngine').textContent='engine: '+state.lastEngine+' | WebGPU '+(renderer?'ready':'unavailable')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height} + +globalThis.__MANDEL_TEST__={ + async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}}, + async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)}, + state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}), + async probeMeta({w,h,strict=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict}))}, + async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}} +}; +globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:deepNeeded(),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})}; + +// ── boot / teardown ────────────────────────────────────────────────────── +addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true}); +try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule(); })(); diff --git a/dist/standalone/gpu-kernels.js b/dist/standalone/gpu-kernels.js new file mode 100644 index 0000000..93b7e33 --- /dev/null +++ b/dist/standalone/gpu-kernels.js @@ -0,0 +1,258 @@ +(()=>{'use strict'; +const COMMON=String.raw` +const FIELD_UNKNOWN:u32=0u; +const FIELD_ESCAPED:u32=1u; +const FIELD_INTERIOR_LIKELY:u32=2u; +const FIELD_INTERIOR_PROVEN:u32=3u; +const ITER_MASK:u32=0x0fffffffu; +const STATUS_UNRESOLVED:u32=0xfffffffeu; + +fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); } +fn cmul(a:vec2, b:vec2)->vec2{ + return vec2(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x); +} +fn maxabs(v:vec2)->f32 { return max(abs(v.x),abs(v.y)); } +const F32_U:f32=5.960464477539063e-8; +fn pow2_safe(e:i32)->f32 { + if(e < -126){ return 0.0; } + if(e > 126){ return 8.507059e37; } + return ldexp(1.0,e); +} +fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2, delta:vec2)->f32{ + let propagated=abs(errScaled*pow2_safe(scaleExp)); + let reconstruction=64.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30); + return propagated+reconstruction; +} +fn scaled_to_f32(v:vec2, e:i32)->vec2{ + if(e < -126){ return vec2(0.0); } + if(e > 126){ return vec2(8.507059e37); } + return ldexp(v,vec2(e)); +} +fn smooth_escape(n:u32, mag2:f32)->f32{ + let u=log2(max(4.0000005,mag2)); + return f32(n)+1.0-log2(max(1.0e-20,0.5*u)); +} +`; + +const DIRECT_F32_WGSL=COMMON+String.raw` +struct Params{ + tileW:u32, tileH:u32, fullW:u32, fullH:u32, + tileX:u32, tileY:u32, maxIter:u32, strict:u32, + centerRe:f32, centerIm:f32, span:f32, sampleX:f32, + sampleY:f32, _p0:f32, _p1:f32, _p2:f32, +}; +@group(0) @binding(0) var p:Params; +@group(0) @binding(1) var fieldMeta:array; +@group(0) @binding(2) var fieldSmooth:array; +fn analytic(cr:f32,ci:f32)->bool{ + let y2=ci*ci; let x=cr-0.25; let q=x*x+y2; + let lhs=q*(q+x); let rhs=0.25*y2; + let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0); + if(lhs){ + if(gid.x>=p.tileW||gid.y>=p.tileH){return;} + let out=gid.y*p.tileW+gid.x; + let gx=f32(p.tileX+gid.x)+p.sampleX; + let gy=f32(p.tileY+gid.y)+p.sampleY; + let scale=p.span/f32(p.fullW); + let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale; + let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale; + if(analytic(cr,ci)){ + fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return; + } + var zr=0.0; var zi=0.0; var n=0u; + loop{ + if(n>=p.maxIter){break;} + let zr2=zr*zr; let zi2=zi*zi; + zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u; + let mag=zr*zr+zi*zi; + if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;} + } + fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0; +} +`; + +// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation. +const DEEP_PERTURB_WGSL=COMMON+String.raw` +struct Params{ + tileW:u32, tileH:u32, fullW:u32, fullH:u32, + tileX:u32, tileY:u32, maxIter:u32, refLen:u32, + strict:u32, _numeric0:u32, _numeric1:u32, _numeric2:u32, + spanMant:f32, spanExp:i32, sampleX:f32, sampleY:f32, +}; +struct RefPoint{ hi:vec2, lo:vec2 }; +struct UnresolvedHead{ remaining:atomic, _p0:u32, _p1:u32, _p2:u32 }; +@group(0) @binding(0) var p:Params; +@group(0) @binding(1) var refs:array; +@group(0) @binding(2) var fieldMeta:array; +@group(0) @binding(3) var fieldSmooth:array; +@group(0) @binding(4) var unresolved:UnresolvedHead; + +fn mark_unresolved(out:u32,n:u32){ + fieldMeta[out]=pack_meta(n,FIELD_UNKNOWN); fieldSmooth[out]=0.0; + atomicAdd(&unresolved.remaining,1u); +} +fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ + let dx=(gx-0.5*f32(p.fullW))/f32(p.fullW); + let dy=(0.5*f32(p.fullH)-gy)/f32(p.fullW); + // dc = d * 2^scaleExp. Keep d and w in one shared scale. + var d=vec2(p.spanMant*dx,p.spanMant*dy); + var w=vec2(0.0); + var scaleExp=p.spanExp; + var n=0u; var m=0u; var operations=0u; + var errScaled=64.0*F32_U*maxabs(d); + loop{ + if(n>=p.maxIter){ + let rpEnd=refs[min(m,p.refLen)]; + let deltaEnd=scaled_to_f32(w,scaleExp); + let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd); + let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd); + let limit=select(1.0e-3,1.0e-4,strictMode); + if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n);} + return; + } + if(m>p.refLen){mark_unresolved(out,n);return;} + let rp=refs[m]; + let delta=scaled_to_f32(w,scaleExp); + let z=rp.hi+(rp.lo+delta); + let mag=dot(z,z); + if(mag>4.0){ + let errAbs=safe_abs_error(errScaled,scaleExp,z,delta); + if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} + mark_unresolved(out,n);return; + } + + // Rebase only when dc remains numerically representable in the new scale. + if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMant*dx,p.spanMant*dy),p.spanExp); scaleExp=0; m=0u; + errScaled+=64.0*F32_U*maxabs(d); + continue; + } + + if(m>=p.refLen){mark_unresolved(out,n);return;} + let r=refs[m]; + let refAbs=maxabs(r.hi)+maxabs(r.lo); + let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp)); + let gain=2.0*refAbs+2.0*wAbs*p2; + let roundErr=64.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30); + errScaled=gain*errScaled+roundErr; + let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); + // delta^2 / 2^scaleExp = w^2 * 2^scaleExp + let sq=cmul(w,w)*pow2_safe(scaleExp); + w=linear+sq+d; m+=1u; n+=1u; operations+=1u; + if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n);return;} + let mm=max(maxabs(w),maxabs(d)); + if(mm>65536.0){ + w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16; + }else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){ + w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16; + } + if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n);return;} + if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n);return;} + } +} +@compute @workgroup_size(8,8) +fn main(@builtin(global_invocation_id) gid:vec3){ + if(gid.x>=p.tileW||gid.y>=p.tileH){return;} + let out=gid.y*p.tileW+gid.x; + let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; + render_pixel(out,gx,gy,p.strict!=0u); +} +`; + +const COLOR_WGSL=String.raw` +struct Params{ + width:u32,height:u32,palette:u32,edgeAA:u32, + cycle:f32,shift:f32,_p0:f32,_p1:f32, +}; +@group(0) @binding(0) var p:Params; +@group(0) @binding(1) var fieldMeta:array; +@group(0) @binding(2) var fieldSmooth:array; +@group(0) @binding(3) var outTex:texture_storage_2d; +fn hsv(h:f32,s:f32,v:f32)->vec3{ + let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f)); + if(i==0){return vec3(v,t,pp);} if(i==1){return vec3(q,v,pp);} if(i==2){return vec3(pp,v,t);} if(i==3){return vec3(pp,q,v);} if(i==4){return vec3(t,pp,v);} return vec3(v,pp,q); +} +fn current_palette(t0:f32)->vec3{ + let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5); + if(t<0.11){return mix(vec3(4,10,27),vec3(12,53,79),smoothstep(0.0,0.11,t))/255.0;} + if(t<0.25){return mix(vec3(12,53,79),vec3(31,156,184),smoothstep(0.11,0.25,t))/255.0;} + if(t<0.38){return mix(vec3(31,156,184),vec3(91,226,234),smoothstep(0.25,0.38,t))/255.0;} + if(t<0.50){return mix(vec3(91,226,234),vec3(66,53,151),smoothstep(0.38,0.50,t))/255.0;} + if(t<0.62){return mix(vec3(66,53,151),vec3(139,49,170),smoothstep(0.50,0.62,t))/255.0;} + if(t<0.73){return mix(vec3(139,49,170),vec3(232,72,145),smoothstep(0.62,0.73,t))/255.0;} + if(t<0.84){return mix(vec3(232,72,145),vec3(255,137,64),smoothstep(0.73,0.84,t))/255.0;} + if(t<0.93){return mix(vec3(255,137,64),vec3(255,211,99),smoothstep(0.84,0.93,t))/255.0;} + return mix(vec3(255,211,99),vec3(255,250,223),smoothstep(0.93,1.0,t))/255.0; +} +fn base_color(i:u32)->vec3{ + let m=fieldMeta[i]; let cls=(m>>28u)&3u; + if(cls==0u){return vec3(20,22,30)/255.0;} if(cls!=1u){return vec3(0.0);} + let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3(0.0); + if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3(g);}else{c=current_palette(phase);} + let n=f32(m&0x0fffffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38); + let floorc=select(vec3(2,5,15)/255.0,vec3(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv); +} +fn linearize(c:vec3)->vec3{return pow(c,vec3(2.2));} +fn delinearize(c:vec3)->vec3{return pow(max(c,vec3(0.0)),vec3(1.0/2.2));} +@compute @workgroup_size(8,8) +fn main(@builtin(global_invocation_id) gid:vec3){ + if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i); + if(p.edgeAA!=0u){ + let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0; + let x=i32(gid.x); let y=i32(gid.y); + for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x0fffffffu)-i32(m&0x0fffffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}} + if(boundary){c=delinearize(sum/cnt);} + } + textureStore(outTex,vec2(gid.xy),vec4(c,1.0)); +} +`; + + +const AA_RESOLVE_WGSL=String.raw` +@group(0) @binding(0) var a:texture_2d; +@group(0) @binding(1) var b:texture_2d; +@group(0) @binding(2) var c:texture_2d; +@group(0) @binding(3) var d:texture_2d; +@group(0) @binding(4) var outTex:texture_storage_2d; +fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);} +fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);} +fn lin3(v:vec3)->vec3{return vec3(to_linear(v.x),to_linear(v.y),to_linear(v.z));} +fn srgb3(v:vec3)->vec3{return vec3(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));} +@compute @workgroup_size(8,8) +fn main(@builtin(global_invocation_id) gid:vec3){ + let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;} + let q=vec2(gid.xy); + let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb); + textureStore(outTex,q,vec4(srgb3(sum*0.25),1.0)); +} +`; + +const PRESENT_WGSL=String.raw` +struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32}; +@group(0) @binding(0) var samp:sampler; +@group(0) @binding(1) var tex:texture_2d; +@group(0) @binding(2) var p:Params; +struct VSOut{@builtin(position) pos:vec4,@location(0) uv:vec2}; +@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{ + var pos=array,3>(vec2(-1.0,-1.0),vec2(3.0,-1.0),vec2(-1.0,3.0)); + var uv=array,3>(vec2(0.0,1.0),vec2(2.0,1.0),vec2(0.0,-1.0)); + var o:VSOut;o.pos=vec4(pos[i],0.0,1.0);o.uv=uv[i];return o; +} +@fragment fn fs(in:VSOut)->@location(0) vec4{ + let uv=vec2(0.5)+(in.uv-vec2(0.5))*vec2(p.scaleX,p.scaleY)+vec2(p.offsetX,p.offsetY); + if(any(uv(0.0))||any(uv>vec2(1.0))){return vec4(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0); +} +`; + +globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({ + version:'24.1.3',DIRECT_F32_WGSL,DEEP_PERTURB_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL +}); +})(); diff --git a/dist/standalone/index.html b/dist/standalone/index.html index 96f0f1b..186da65 100644 --- a/dist/standalone/index.html +++ b/dist/standalone/index.html @@ -4,7 +4,7 @@ -Mandelbrot Deep Zoom v23 +Mandelbrot Deep Zoom v24.1.3 WebGPU -

v23 browser benchmark

-

HTTP(S)でこのworkspaceを配信して実行します。iframeの実寸と実DPRを結果へ記録します。

- +Mandelbrot v23 browser acceptance runner + +

v23 browser acceptance runner

+

+ HTTP(S)で実行します。1回のrunは1 profile・1 buildだけを検査します。 + 例: ?profile=desktop&target=hosted。 + iframe寸法ではDPRを模擬できないため、要求DPRを持つbrowser contextごとに実行してください。 +

+
Ready.
diff --git a/tests/document-contract.ps1 b/tests/document-contract.ps1 new file mode 100644 index 0000000..02d0c10 --- /dev/null +++ b/tests/document-contract.ps1 @@ -0,0 +1,133 @@ +$ErrorActionPreference = 'Stop' +$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path + +function Assert-Contract([bool]$Condition, [string]$Message) { + if (-not $Condition) { throw $Message } +} + +function Decimal-Fraction([string]$Text) { + if ($Text -notmatch '^([+-]?)(\d+)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$') { + throw "Invalid decimal: $Text" + } + $negative = $Matches[1] -eq '-' + $fraction = if ($null -eq $Matches[3]) { '' } else { $Matches[3] } + $digits = ($Matches[2] + $fraction).TrimStart('0') + if (-not $digits) { $digits = '0' } + $numerator = [System.Numerics.BigInteger]::Parse($digits) + if ($negative) { $numerator = -$numerator } + $exponent = if ($Matches[4]) { [int]$Matches[4] } else { 0 } + $places = $fraction.Length - $exponent + if ($places -ge 0) { + $denominator = [System.Numerics.BigInteger]::Pow([System.Numerics.BigInteger]10, $places) + } else { + $numerator *= [System.Numerics.BigInteger]::Pow([System.Numerics.BigInteger]10, -$places) + $denominator = [System.Numerics.BigInteger]1 + } + [pscustomobject]@{ numerator=$numerator; denominator=$denominator } +} + +function Assert-ExactCoordinate($Scene, [string]$Axis) { + $decimal = Decimal-Fraction ([string]$Scene.$Axis) + $prefix = if ($Axis -eq 're') { 're' } else { 'im' } + $numerator = [System.Numerics.BigInteger]::Parse([string]$Scene.coordinate.($prefix + 'Numerator')) + $denominator = [System.Numerics.BigInteger]::Parse([string]$Scene.coordinate.($prefix + 'Denominator')) + Assert-Contract ($denominator -gt 0) "$($Scene.id): $Axis denominator must be positive." + Assert-Contract ($decimal.numerator * $denominator -eq $numerator * $decimal.denominator) "$($Scene.id): $Axis decimal does not match its exact rational." +} + +$proposal = Get-Content -LiteralPath (Join-Path $workspace 'IMPROVEMENT_PROPOSAL.md') -Raw -Encoding UTF8 +Assert-Contract ($proposal -match 'Document status: historical-v22 / implemented-v23') 'Proposal status/provenance is missing.' +Assert-Contract ($proposal -match 'sample coverage.+UNRESOLVED.+resolved coverage') 'Coverage semantics are not separated.' +Assert-Contract ($proposal -match 'Balanced Export.+Validated Export') 'Export precision tiers are not separated.' +Assert-Contract ($proposal -match 'exact half.+away-from-zero') 'Fixed-point tie rounding is not specified.' +Assert-Contract ($proposal -match 'Phase 0A' -and $proposal -match 'Phase 0B') 'Browserless and browser gates are not separated.' +Assert-Contract ($proposal -match 'threshold.+pass') 'Undefined acceptance thresholds must fail.' + +$scenePath = Join-Path $workspace 'tests\scenes.json' +$scenes = Get-Content -LiteralPath $scenePath -Raw -Encoding UTF8 | ConvertFrom-Json +$policy = Get-Content -LiteralPath (Join-Path $workspace 'tests\numeric-policy-v23.json') -Raw -Encoding UTF8 | ConvertFrom-Json +Assert-Contract ($scenes.format -eq 'mandelbrot-scene-corpus-v2') 'Unexpected scene corpus version.' +Assert-Contract ($scenes.pixelContract -eq 'centered-rational-ties-away-from-zero') 'Unexpected scene pixel contract.' +Assert-Contract ($scenes.numericPolicy -eq $policy.id) 'Scene corpus and numeric policy IDs differ.' +Assert-Contract ($policy.validated.falseEscapedMax -eq 0 -and $policy.validated.falseInteriorProvenMax -eq 0 -and $policy.validated.unresolvedMax -eq 0) 'Validated zero-error thresholds changed.' + +$ids = @{} +$maxViewportPixels = ($scenes.viewports | ForEach-Object { [double]$_.cssWidth * [double]$_.dpr } | Measure-Object -Maximum).Maximum +$viewportDigits = [Math]::Ceiling([Math]::Log10($maxViewportPixels)) +foreach ($scene in $scenes.scenes) { + Assert-Contract (-not $ids.ContainsKey($scene.id)) "Duplicate scene ID: $($scene.id)" + $ids[$scene.id] = $true + Assert-Contract ($null -ne $scene.coordinate -and $null -ne $scene.oracle) "$($scene.id): coordinate/oracle metadata missing." + if ($scene.coordinate.kind -eq 'exact-rational') { + Assert-ExactCoordinate $scene 're' + Assert-ExactCoordinate $scene 'im' + } elseif ($scene.coordinate.kind -eq 'decimal') { + $reDigits = ([regex]::Replace([string]$scene.re, '[^0-9]', '')).TrimStart('0').Length + $imDigits = ([regex]::Replace([string]$scene.im, '[^0-9]', '')).TrimStart('0').Length + $actualDigits = [Math]::Min($reDigits, $imDigits) + Assert-Contract ($actualDigits -eq [int]$scene.coordinate.significantDigits) "$($scene.id): significantDigits metadata is stale." + $span = [Math]::Abs([double]::Parse([string]$scene.span, [Globalization.CultureInfo]::InvariantCulture)) + $depthDigits = [Math]::Ceiling(-[Math]::Log10($span)) + $guard = $actualDigits - $depthDigits - $viewportDigits + Assert-Contract ($guard -eq [int]$scene.coordinate.guardDigitsAtViewport -and $guard -ge 4) "$($scene.id): coordinate guard digits are insufficient or stale." + } else { + throw "$($scene.id): unsupported coordinate kind $($scene.coordinate.kind)" + } +} +foreach ($required in @('z0','period2-cusp-z14','swirly-seahorses-z12','period2-cusp-z20','period2-cusp-z100','period3-interior','period2-cusp-e280')) { + Assert-Contract ($ids.ContainsKey($required)) "Required scene missing: $required" +} +$runtimeTest = Get-Content -LiteralPath (Join-Path $workspace 'tests\runtime-budget.mjs') -Raw -Encoding UTF8 +Assert-Contract ($runtimeTest -match "sceneById\('swirly-seahorses-z12'\)" -and $runtimeTest -match "acceptance:false") 'Runtime characterization must use the published mixed-boundary scene and remain non-acceptance evidence.' + +$runner = Get-Content -LiteralPath (Join-Path $workspace 'tests\browser-benchmark.html') -Raw -Encoding UTF8 +Assert-Contract ($runner -match "target==='hosted'.+dist/hosted/index\.html") 'Browser runner does not target the Hosted build.' +Assert-Contract ($runner -match "selectedProfile=query\.get\('profile'\)" -and $runner -notmatch 'for\s*\(const profile of corpus\.viewports\)') 'Browser runner must execute one real-DPR profile per run.' +Assert-Contract ($runner -notmatch 'deltaY\s*:\s*0') 'Wheel audit still uses a zero-delta event.' +Assert-Contract ($runner -match 'previewTrials>=30' -and $runner -match "drawState==='REFINED'") 'Preview p95 or actual Refined checks are missing.' +Assert-Contract ($runner -match 'visualGoldens' -and $runner -match 'hashValue===expected') 'Visual acceptance lacks a golden comparison.' +Assert-Contract ($runner -match 'interaction\.longTasks===0') 'Long tasks are not part of browser acceptance.' +Assert-Contract ($runner -match 'completed&&noEarlyDownload&&deterministic&&cancelledWithoutDownload') 'Export evidence is not part of the pass condition.' +Assert-Contract ($runner -match 'pendingExternal' -and $runner -match 'return\{pass:false,automatedPass') 'Browser-only/manual residual gates can be falsely marked complete.' + +$baseline = Get-Content -LiteralPath (Join-Path $workspace 'audit\v23-source-baseline.json') -Raw -Encoding UTF8 | ConvertFrom-Json +Assert-Contract ($baseline.hosted.deepRequestMeasurementStatus -eq 'not-run') 'Static deep-request contract is mislabeled as a measurement.' +$records = @($baseline.hosted.firstViewFiles) + @($baseline.standalone.files) + @($baseline.sourceBuild.files) +foreach ($record in $records) { + $path = Join-Path $workspace ([string]$record.file).Replace('/', [System.IO.Path]::DirectorySeparatorChar) + Assert-Contract (Test-Path -LiteralPath $path) "Baseline file missing: $($record.file)" + $item = Get-Item -LiteralPath $path + $hash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant() + Assert-Contract ($item.Length -eq [long]$record.bytes -and $hash -eq [string]$record.sha256) "Baseline bytes/hash mismatch: $($record.file)" +} + +$implementation = Get-Content -LiteralPath (Join-Path $workspace 'IMPLEMENTATION_REPORT.md') -Raw -Encoding UTF8 +Assert-Contract ($implementation -match 'Hosted first view ([\d,]+) bytes') 'Hosted implementation size statement missing.' +$hostedDoc = [long](($Matches[1] -replace ',', '')) +Assert-Contract ($implementation -match 'Standalone ([\d,]+) bytes') 'Standalone implementation size statement missing.' +$standaloneDoc = [long](($Matches[1] -replace ',', '')) +Assert-Contract ($hostedDoc -eq [long]$baseline.hosted.firstViewUncompressedBytes -and $standaloneDoc -eq [long]$baseline.standalone.uncompressedBytes) 'Implementation size statement is stale.' + +$completion = Get-Content -LiteralPath (Join-Path $workspace 'COMPLETION_AUDIT.md') -Raw -Encoding UTF8 +Assert-Contract ($completion -notmatch '0\.91|36\.6') 'Completion audit cites performance values absent from the saved v23 JSON.' +$buildDoc = Get-Content -LiteralPath (Join-Path $workspace 'BUILD_REPRODUCIBILITY.md') -Raw -Encoding UTF8 +Assert-Contract ($buildDoc -match 'Provenance status: tool archive hash not retained') 'Toolchain provenance boundary is overstated.' + +$browserless = Get-Content -LiteralPath (Join-Path $workspace 'audit\v23-browserless-baseline.json') -Raw -Encoding UTF8 | ConvertFrom-Json +Assert-Contract ($browserless.status -eq 'pass' -and $browserless.scope -eq 'browserless-current-artifacts' -and -not $browserless.fullAcceptance) 'Browserless audit scope/status is misleading.' +Assert-Contract ($browserless.node.version -eq 'v22.18.0' -and $browserless.node.archive.sha256 -eq 'c95d8a7e1c99e669cc08c9f1176e068c1f50847c37908fcb8c35b62482366511') 'Pinned Node provenance is missing.' +foreach ($test in $browserless.executableTests.psobject.Properties) { + Assert-Contract ($test.Value.status -eq 'pass' -or $test.Name -eq 'runtime-budget.mjs') "Browserless executable gate failed: $($test.Name)" +} +Assert-Contract ($browserless.runtime.status -eq 'measured-not-acceptance' -and $browserless.v22Comparison.status -eq 'not-verifiable' -and $browserless.browserAcceptance.status -eq 'not-run') 'Residual gates are overstated.' + +[ordered]@{ + status='pass' + proposalContract='revised-v23' + scenes=@($scenes.scenes).Count + viewports=@($scenes.viewports).Count + numericPolicy=$policy.id + hashedFiles=$records.Count + browserRunner='static-contract-pass' + browserlessEvidence='pass-with-explicit-limits' +} | ConvertTo-Json diff --git a/tests/js-syntax.mjs b/tests/js-syntax.mjs index 51cd857..006bf46 100644 --- a/tests/js-syntax.mjs +++ b/tests/js-syntax.mjs @@ -3,7 +3,8 @@ 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 appSource = await fs.readFile(new URL('script.js', root), 'utf8'); +let app = appSource; const browserHarness = await fs.readFile(new URL('tests/browser-benchmark.html', root), 'utf8'); const browserModule = browserHarness.match(/'))throw new Error('legacy kernels.js loaded');console.log(JSON.stringify({status:'pass',checks:req.length+1},null,2)); diff --git a/tests/v24-png-stream-model.mjs b/tests/v24-png-stream-model.mjs new file mode 100644 index 0000000..f2d29b2 --- /dev/null +++ b/tests/v24-png-stream-model.mjs @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import {inflateSync} from 'node:zlib'; + +const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})(); +function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0} +function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out} +class StreamingPng{ + constructor(w,h){this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()} + async rows(filteredRows){await this.writer.write(filteredRows)} + async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})} + async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}} +} + +assert.equal(typeof CompressionStream,'function','CompressionStream required for this model'); +const p=new StreamingPng(2,2); +const row0=Uint8Array.from([255,0,0,255, 0,255,0,255]); +const row1=Uint8Array.from([0,0,255,255, 255,255,255,128]); +const filtered=new Uint8Array(18);filtered.set(row0,1);filtered.set(row1,10);await p.rows(filtered); +const bytes=new Uint8Array(await (await p.finish()).arrayBuffer()); +assert.deepEqual(Array.from(bytes.subarray(0,8)),[137,80,78,71,13,10,26,10]); +let off=8, width=0, height=0, idats=[], seenIend=false; +while(off=1); assert.ok(seenIend); +const packed=Buffer.concat(idats.map(x=>Buffer.from(x))); +const raw=new Uint8Array(inflateSync(packed)); +assert.equal(raw.length,2*(1+2*4)); +assert.equal(raw[0],0); assert.deepEqual(Array.from(raw.subarray(1,9)),Array.from(row0)); +assert.equal(raw[9],0); assert.deepEqual(Array.from(raw.subarray(10,18)),Array.from(row1)); + +const aborted=new StreamingPng(1,1); await aborted.rows(Uint8Array.from([0,0,0,0,255])); await aborted.abort(new Error('cancelled')); +console.log(JSON.stringify({status:'pass',png:{width,height,idatChunks:idats.length,rawBytes:raw.length},abort:'settled'},null,2)); diff --git a/tests/v24-reference-worker.mjs b/tests/v24-reference-worker.mjs new file mode 100644 index 0000000..bcc6cb6 --- /dev/null +++ b/tests/v24-reference-worker.mjs @@ -0,0 +1,3 @@ +import fs from 'node:fs/promises';import vm from 'node:vm';import {performance} from 'node:perf_hooks'; +const source=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'),prefix="function referenceWorkerSource(){return String.raw`",start=source.indexOf(prefix)+prefix.length,end=source.indexOf("`}\nclass ReferenceService",start);if(start{message=m},onmessage:null};const ctx={self,postMessage:(...a)=>self.postMessage(...a),performance,Math,Number,BigInt,ArrayBuffer,DataView,Float32Array,Float64Array,Uint32Array,Set,String,Error};vm.createContext(ctx);vm.runInContext(workerSource,ctx,{timeout:5000});if(typeof self.onmessage!=='function')throw new Error('worker handler missing'); +self.onmessage({data:{type:'build',id:1,key:'test',bits:256,re:(-3n*(1n<<256n)/4n).toString(),im:'0',iter:800}});if(!message)throw new Error('worker produced no message');if(message.type==='error')throw new Error(message.error);if(message.checkpointMismatch)throw new Error('reference +64 guard checkpoint mismatch');if(message.refLen<3||message.refs.byteLength!==(message.refLen+1)*16)throw new Error('reference packing invalid');for(const forbidden of ['nodes','levels','nodeCount','levelCount','invalidNodes'])if(forbidden in message)throw new Error('BLA payload leaked from quarantined reference worker: '+forbidden);const primary=message;message=null;const b=1536,R=(-3n*(1n<{if(!ok)throw new Error(msg)}; +must(/const VERSION=24/.test(script),'renderer version is not 24'); +must(/navigator\.gpu\.requestAdapter/.test(script),'WebGPU adapter path missing'); +must(/guarded rescaled perturbation/.test(script),'guarded deep WebGPU path missing'); +must(/referenceWorkerSource/.test(script)&&/BigInt\(d\.re\)/.test(script),'BigInt reference worker missing'); +must(!/function buildBla\(/.test(script)&&!/BlaNode/.test(kernels)&&!/useBla/.test(kernels),'unsafe BLA path is not fully quarantined'); +must(/F32_U/.test(kernels)&&/safe_abs_error/.test(kernels)&&/errAbs/.test(kernels),'deep error-bound guard missing'); +must(/struct UnresolvedHead/.test(kernels)&&/atomicAdd\(&unresolved\.remaining/.test(kernels),'unresolved counter missing'); +must(!/strict_queue|QUEUE_FINALIZE_WGSL/.test(kernels)&&!/dispatchWorkgroupsIndirect|qitems|strict-indirect/.test(script),'redundant strict retry queue remains'); +must(/texture_storage_2d/.test(kernels),'GPU color field path missing'); +must(/AA_RESOLVE_WGSL/.test(kernels)&&/renderTileRGBA2x/.test(script),'GPU 2x2 export resolve missing'); +must(/class StreamingPng/.test(script)&&/CompressionStream\('deflate'\)/.test(script)&&/async abort\(reason\)/.test(script),'streaming PNG export/abort missing'); +must(/PRESENT_WGSL/.test(kernels),'GPU reprojection/presentation missing'); +must(/recolorPending/.test(script)&&!/const token=\+\+state\.token,stateSnap/.test(script),'recolor race fix missing'); +must(/this\.context=null/.test(script)&&/webgpuCanvasClaimed=true;this\.configure\(\)/.test(script)&&/if\(webgpuCanvasClaimed\)return null/.test(script),'delayed WebGPU canvas claim/fallback guard missing'); +must(/このズーム深度はWebGPUが必要です/.test(script),'deep fallback rejection missing'); +must(!/Math\.min\(340/.test(script),'deep exact-coordinate formatter still capped at 340 digits'); +must(/uncapturederror/.test(script)&&/pushErrorScope/.test(script),'WebGPU error capture missing'); +must(/gpu-kernels\.js/.test(html)&&!/src="kernels\.js"/.test(html),'index is not WebGPU-only'); +must(/numericParams/.test(script)&&/colorParams/.test(script),'persistent frame parameter buffers missing'); +must(/ensureExportWorkspace/.test(script)&&/exportWorkspaceDestroy/.test(script)&&/export tile exceeds reusable workspace/.test(script),'reusable export GPU workspace missing'); +must(/if\(!adapter\)adapter=await navigator\.gpu\.requestAdapter\(\)/.test(script),'adapter fallback request missing'); +must(/gpuInitFailed/.test(script)&&/WebGPU初期化失敗/.test(script),'shader/pipeline failure guard missing'); +must(/if\(state\.gpuInitFailed\)return null/.test(script),'failed shader initialization can be retried on every render'); +must(/if\(!r\)\{if\(state\.gpuInitFailed\)/.test(script),'shader failure still enters CPU fallback'); +must(/return 524288/.test(script)&&/1572864/.test(script),'responsive screen pixel budgets missing'); +must(/gpuUnavailable/.test(script)&&/return 262144/.test(script),'lightweight no-adapter CPU fallback budget missing'); +for(const banned of ['DEEP_SIMD_B64','DEEP_SCALAR_B64','BLA_SIMD_B64','BLA_SCALAR_B64','deepPool','deepWasm','highPrecisionDirectPixelAsync','renderDeepAdaptive','render_perturb_rebase_rect','WebAssembly.Instance'])must(!script.includes(banned),'legacy production deep dependency remains: '+banned); +must(!/Validated direct/.test(html),'obsolete Validated UI remains'); +console.log(JSON.stringify({status:'pass',rendererVersion:24,shaderVersion:'24.1.3',checks:28},null,2)); diff --git a/tests/v24-tree-contract.mjs b/tests/v24-tree-contract.mjs new file mode 100644 index 0000000..9fcc0c5 --- /dev/null +++ b/tests/v24-tree-contract.mjs @@ -0,0 +1,5 @@ +import fs from 'node:fs/promises'; +const root=new URL('../',import.meta.url);const exists=async p=>{try{await fs.stat(new URL(p,root));return true}catch{return false}}; +for(const banned of ['kernels.js','src/deep_kernel.c','src/bla_kernel_v18.c','src/color_kernel.c','src/shallow_kernel.c','build/wasm-v23'])if(await exists(banned))throw new Error('legacy renderer asset remains: '+banned); +for(const required of ['index.html','script.js','gpu-kernels.js','tests/webgpu-acceptance.html','tests/webgpu-acceptance.js'])if(!await exists(required))throw new Error('required v24 asset missing: '+required); +console.log(JSON.stringify({status:'pass',legacyDeepAssets:0,requiredAssets:5},null,2)); diff --git a/tests/v24-wgsl-reserved.mjs b/tests/v24-wgsl-reserved.mjs new file mode 100644 index 0000000..21682ee --- /dev/null +++ b/tests/v24-wgsl-reserved.mjs @@ -0,0 +1,21 @@ +import fs from 'node:fs/promises'; +import vm from 'node:vm'; +const root=new URL('../',import.meta.url); +const src=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'); +const context={};context.globalThis=context;vm.runInNewContext(src,context,{filename:'gpu-kernels.js'}); +const kernels=context.MANDEL_WEBGPU_KERNELS; +if(!kernels)throw new Error('kernel bundle did not initialize'); +// WGSL 16.2 Reserved Words. A module must not contain one of these tokens. +const reserved=`NULL Self abstract active alignas alignof as asm asm_fragment async attribute auto await become cast catch class co_await co_return co_yield coherent column_major common compile compile_fragment concept const_cast consteval constexpr constinit crate debugger decltype delete demote demote_to_helper do dynamic_cast enum explicit export extends extern external fallthrough filter final finally friend from fxgroup get goto groupshared highp impl implements import inline instanceof interface layout lowp macro macro_rules match mediump meta mod module move mut mutable namespace new nil noexcept noinline nointerpolation non_coherent noncoherent noperspective null nullptr of operator package packoffset partition pass patch pixelfragment precise precision premerge priv protected pub public readonly ref regardless register reinterpret_cast require resource restrict self set shared sizeof smooth snorm static static_assert static_cast std subroutine super target template this thread_local throw trait try type typedef typeid typename typeof union unless unorm unsafe unsized use using varying virtual volatile wgsl where with writeonly yield`.split(/\s+/); +const strip=s=>s.replace(/\/\*[\s\S]*?\*\//g,' ').replace(/\/\/.*$/gm,' '); +const failures=[]; +for(const [name,code] of Object.entries(kernels)){ + if(typeof code!=='string'||!name.endsWith('_WGSL'))continue; + const clean=strip(code); + for(const word of reserved){ + const re=new RegExp(`\\b${word}\\b`); + if(re.test(clean))failures.push(`${name}: reserved token ${word}`); + } +} +if(failures.length)throw new Error(failures.join('\n')); +console.log(JSON.stringify({status:'pass',check:'wgsl-reserved-words',shaderVersion:kernels.version,kernels:Object.keys(kernels).filter(k=>k.endsWith('_WGSL')).length},null,2)); diff --git a/tests/webgpu-acceptance.html b/tests/webgpu-acceptance.html new file mode 100644 index 0000000..26f8542 --- /dev/null +++ b/tests/webgpu-acceptance.html @@ -0,0 +1,4 @@ +v24 WebGPU acceptance + +

v24 WebGPU acceptance

starting…
+ diff --git a/tests/webgpu-acceptance.js b/tests/webgpu-acceptance.js new file mode 100644 index 0000000..d9955ac --- /dev/null +++ b/tests/webgpu-acceptance.js @@ -0,0 +1,35 @@ +(()=>{'use strict'; +const out=document.querySelector('#out'),frame=document.querySelector('#app'); +const scenes=[ + ['z0','-0.5','0','3.4',120], + ['period2-cusp-z14','-0.75','0','3.4e-14',1200], + ['swirly-seahorses-z12','-0.7453983606667815','0.1125046349959942','3.52e-12',2000], + ['period2-cusp-z20','-0.75','0','3.4e-20',1800], + ['period2-cusp-z100','-0.75','0','3.4e-100',2400], + ['period3-interior','-0.122561166876653619975245551820735654052','0.744861766619744236593170428604392367240','1e-8',1200], + ['period2-cusp-e280','-0.75','0','1e-280',2400], + ['period2-cusp-e400','-0.75','0','1e-400',1600] +]; +function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q} +function align(v,a,b){const d=b-a;return d===0?v:d>0?v<>BigInt(-d)} +function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q} +function pixel(st,x,y,bits=st.bits){const re=align(BigInt(st.re),st.bits,bits),im=align(BigInt(st.im),st.bits,bits),span=align(BigInt(st.span),st.bits,bits),den=BigInt(2*st.width);return[re+roundDiv(span*BigInt(2*x+1-st.width),den),im+roundDiv(span*BigInt(st.height-2*y-1),den)]} +function orbit(cr,ci,bits,limit){const bail=4n<bail)return n+1}return limit} +function points(w,h){const xs=[0,.25,.5,.75,1].map(t=>Math.min(w-1,Math.max(0,Math.round(t*(w-1))))),ys=[0,.25,.5,.75,1].map(t=>Math.min(h-1,Math.max(0,Math.round(t*(h-1)))));return ys.flatMap(y=>xs.map(x=>[x,y]))} +async function waitApp(){if(frame.contentWindow&&frame.contentWindow.__MANDEL_TEST__)return;await new Promise(r=>frame.addEventListener('load',r,{once:true}));const t=performance.now();while(!frame.contentWindow.__MANDEL_TEST__&&performance.now()-t<15000)await new Promise(r=>setTimeout(r,50));if(!frame.contentWindow.__MANDEL_TEST__)throw new Error('test hook not available')} +async function checkScene(api,id,re,im,span,iter,mode){ + const r=await api.setView({re,im,span,baseIter:iter,adaptive:false,processMode:mode}),diag=r.diag; + if(diag.backend!=='webgpu'||diag.webgpuError)throw new Error(id+'/'+mode+': WebGPU backend unavailable/error: '+(diag.webgpuError||diag.backend)); + const st=api.state(),ps=points(st.width,st.height),meta=await api.sampleMeta(ps);let guardMismatch=0,falseEscaped=0,falseBounded=0,escapeIterationMismatch=0,unknown=0; + for(let i=0;i>>28)&3,n=m&0x0fffffff;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a>>28)&3,n=m&0x0fffffff;if(cls===1&&a===denseState.iter)throw new Error('dense false escape '+JSON.stringify({x,y,a,cls,n}));if(cls!==0&&cls!==1&&a{const result={status:'fail',error:String(e&&e.stack||e)};out.textContent=JSON.stringify(result,null,2);document.title='FAIL v24.1 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result}); +})();