From 0e1c1f04fd94115943521cd17610dd3a7031fc70 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Mon, 24 Aug 2026 20:30:26 +0900 Subject: [PATCH 1/3] slim --- AUDIT.md | 206 --- AUDIT_V24.md | 182 --- BUILD_REPRODUCIBILITY.md | 51 - COMPLETION_AUDIT.md | 55 - CROSSOVER_BENCHMARK.md | 62 - FIXED96_EXPERIMENT.md | 70 - HYBRID_EXPERIMENT.md | 69 - IMPLEMENTATION_REPORT.md | 118 -- IMPROVEMENT_PROPOSAL.md | 566 -------- MIGRATION_V24.md | 157 --- README.md | 136 -- audit/benchmarks.json | 245 ---- audit/final-browser-benchmark.json | 82 -- audit/legacy-v23/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 | 18 - audit/v23-browserless-baseline.json | 267 ---- audit/v23-browserless-runtime.json | 73 -- audit/v23-node-performance.json | 69 - audit/v23-source-baseline.json | 109 -- audit/v24-backend-policy.json | 9 - audit/v24-bla-status.json | 6 - audit/v24-cpu-numeric-model.json | 308 ----- audit/v24-crossover-gpu-status.json | 25 - audit/v24-crossover-hotfix.json | 34 - audit/v24-crossover-quality.json | 766 ----------- audit/v24-fixed96-hotfix.json | 19 - audit/v24-fixed96-model.json | 182 --- audit/v24-fixed96-preparation.json | 31 - audit/v24-fixed96-sparse-queue.json | 40 - audit/v24-hybrid-preparation.json | 63 - audit/v24-hybrid-risk-model.json | 56 - audit/v24-hybrid-ui-hotfix.json | 29 - audit/v24-real-webgpu-status.json | 43 - audit/v24-release-manifest.json | 246 ---- audit/v24-release-verification.json | 25 - audit/v24-sparse-correction-model.json | 26 - audit/v24-test-summary.json | 48 - build/wasm-v23/bla-scalar.wasm | Bin 4593 -> 0 bytes build/wasm-v23/bla-simd.wasm | Bin 4959 -> 0 bytes build/wasm-v23/color-scalar.wasm | Bin 632 -> 0 bytes build/wasm-v23/color-simd.wasm | Bin 1387 -> 0 bytes build/wasm-v23/deep-scalar.wasm | Bin 5246 -> 0 bytes build/wasm-v23/deep-simd.wasm | Bin 4866 -> 0 bytes build/wasm-v23/manifest.json | 85 -- build/wasm-v23/wasm-scalar.wasm | Bin 511 -> 0 bytes build/wasm-v23/wasm-simd.wasm | Bin 511 -> 0 bytes dist/_headers | 11 - dist/hosted/gpu-kernels-v24.2.6.js | 598 --------- dist/hosted/gpu-kernels.js | 708 +++++----- dist/hosted/hosted-loader.js | 36 - dist/hosted/index.html | 51 +- dist/hosted/script-v24.2.6.js | 454 ------- dist/hosted/script.js | 399 +++--- dist/standalone/gpu-kernels-v24.2.6.js | 598 --------- dist/standalone/gpu-kernels.js | 598 --------- dist/standalone/index.html | 1160 ++++++++--------- dist/standalone/kernels.js | 22 - dist/standalone/script-v24.2.6.js | 454 ------- dist/standalone/script.js | 454 ------- dist/wasm/bla-scalar.4f06913487044823.wasm | Bin 4593 -> 0 bytes dist/wasm/bla-scalar.wasm | Bin 4593 -> 0 bytes dist/wasm/bla-simd.f80f136e9fb676ce.wasm | Bin 4959 -> 0 bytes dist/wasm/bla-simd.wasm | Bin 4959 -> 0 bytes dist/wasm/color-scalar.aa2914ec1acacaa2.wasm | Bin 632 -> 0 bytes dist/wasm/color-scalar.wasm | Bin 632 -> 0 bytes dist/wasm/color-simd.8cf83374ed0c680b.wasm | Bin 1387 -> 0 bytes dist/wasm/color-simd.wasm | Bin 1387 -> 0 bytes dist/wasm/deep-scalar.d133bbecc8f6dbdf.wasm | Bin 5246 -> 0 bytes dist/wasm/deep-scalar.wasm | Bin 5246 -> 0 bytes dist/wasm/deep-simd.d49385f06e4a8f55.wasm | Bin 4866 -> 0 bytes dist/wasm/deep-simd.wasm | Bin 4866 -> 0 bytes dist/wasm/manifest.json | 55 - dist/wasm/wasm-scalar.d19b26c04e1b2f59.wasm | Bin 511 -> 0 bytes dist/wasm/wasm-scalar.wasm | Bin 511 -> 0 bytes dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm | Bin 511 -> 0 bytes dist/wasm/wasm-simd.wasm | Bin 511 -> 0 bytes docs/CHANGELOG.md | 15 + docs/README.md | 117 ++ docs/VALIDATION_REPORT.md | 101 ++ gpu-kernels-v24.2.6.js | 598 --------- gpu-kernels.js | 708 +++++----- hosted-headers.txt | 11 - hosted-loader.js | 36 - index.external.html | 51 +- index.html | 1160 ++++++++--------- kernels.js | 22 - package.json | 2 +- script-v24.2.6.js | 454 ------- script.js | 399 +++--- scripts/browserless-audit.ps1 | 90 -- scripts/build-hosted.ps1 | 43 - scripts/build-kernels.ps1 | 50 - scripts/build-standalone.ps1 | 13 - scripts/build-wasm.ps1 | 64 - scripts/build.mjs | 2 +- scripts/extract-wasm.ps1 | 56 - scripts/source-baseline.ps1 | 99 -- scripts/test-all.mjs | 2 +- scripts/test-all.ps1 | 41 - 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 | 323 ----- tests/document-contract.ps1 | 133 -- tests/js-syntax.mjs | 157 --- tests/kernel-golden.mjs | 68 - tests/kernel-golden.ps1 | 7 - tests/kernel-source-contract.ps1 | 16 - tests/module-clone.mjs | 22 - tests/numeric-policy-v23.json | 30 - tests/pixel-contract.mjs | 62 - tests/pixel-mapping.mjs | 79 -- tests/precision-reference.mjs | 45 - tests/runtime-budget.mjs | 47 - tests/source-contract.ps1 | 97 -- tests/v24-acceptance-contract.mjs | 14 +- tests/v24-backend-policy.mjs | 10 - tests/v24-baseline-idle-refinement-model.mjs | 46 + tests/v24-color-relative-model.mjs | 14 + tests/v24-crossover-contract.mjs | 16 - tests/v24-crossover-quality.mjs | 106 -- tests/v24-deep-sparse-queue-model.mjs | 78 ++ tests/v24-fast-precision-extension-model.mjs | 11 + tests/v24-fixed96-contract.mjs | 24 - tests/v24-fixed96-limb-model.mjs | 15 - tests/v24-fixed96-limb-orbit-model.mjs | 24 - tests/v24-fixed96-model.mjs | 39 - tests/v24-fixed96-queue-model.mjs | 22 - tests/v24-fixed96-shader-range-model.mjs | 24 - tests/v24-hybrid-contract.mjs | 15 - tests/v24-hybrid-risk-model.mjs | 39 - tests/v24-hybrid-ui-runtime.mjs | 28 - tests/v24-index-contract.mjs | 8 +- tests/v24-pan-reuse-model.mjs | 16 + tests/v24-primary-post-stats-model.mjs | 30 + tests/v24-reference-recovery-model.mjs | 15 + tests/v24-source-contract.mjs | 51 +- tests/v24-standalone-ui-contract.mjs | 8 +- tests/v24-ui-cacheproof.mjs | 13 - tests/webgpu-acceptance.js | 40 +- toolchain.lock.json | 12 - 158 files changed, 2578 insertions(+), 15117 deletions(-) delete mode 100644 AUDIT.md delete mode 100644 AUDIT_V24.md delete mode 100644 BUILD_REPRODUCIBILITY.md delete mode 100644 COMPLETION_AUDIT.md delete mode 100644 CROSSOVER_BENCHMARK.md delete mode 100644 FIXED96_EXPERIMENT.md delete mode 100644 HYBRID_EXPERIMENT.md delete mode 100644 IMPLEMENTATION_REPORT.md delete mode 100644 IMPROVEMENT_PROPOSAL.md delete mode 100644 MIGRATION_V24.md delete mode 100644 README.md delete mode 100644 audit/benchmarks.json delete mode 100644 audit/final-browser-benchmark.json delete mode 100644 audit/legacy-v23/final-browser-benchmark.json delete mode 100644 audit/raw/audit_v17_algos_results.json delete mode 100644 audit/raw/audit_v18_kernel.txt delete mode 100644 audit/raw/dev-browser-benchmark.json delete mode 100644 audit/raw/dev-verification-benchmark.json delete mode 100644 audit/raw/dev-warm-cache-benchmark.json delete mode 100644 audit/raw/interior_safety_v18.json delete mode 100644 audit/raw/mb_audit_v17_results.json delete mode 100644 audit/raw/pilot_fill_audit.json delete mode 100644 audit/raw/reference_recenter_audit.json delete mode 100644 audit/raw/reference_recenter_audit_z20.json delete mode 100644 audit/raw/v17-final-browser-benchmark.json delete mode 100644 audit/raw/v18-final-browser-benchmark.json delete mode 100644 audit/raw/workcap_truth_v17.json delete mode 100644 audit/smoke-test.json delete mode 100644 audit/v23-browser-baseline.json delete mode 100644 audit/v23-browserless-baseline.json delete mode 100644 audit/v23-browserless-runtime.json delete mode 100644 audit/v23-node-performance.json delete mode 100644 audit/v23-source-baseline.json delete mode 100644 audit/v24-backend-policy.json delete mode 100644 audit/v24-bla-status.json delete mode 100644 audit/v24-cpu-numeric-model.json delete mode 100644 audit/v24-crossover-gpu-status.json delete mode 100644 audit/v24-crossover-hotfix.json delete mode 100644 audit/v24-crossover-quality.json delete mode 100644 audit/v24-fixed96-hotfix.json delete mode 100644 audit/v24-fixed96-model.json delete mode 100644 audit/v24-fixed96-preparation.json delete mode 100644 audit/v24-fixed96-sparse-queue.json delete mode 100644 audit/v24-hybrid-preparation.json delete mode 100644 audit/v24-hybrid-risk-model.json delete mode 100644 audit/v24-hybrid-ui-hotfix.json delete mode 100644 audit/v24-real-webgpu-status.json delete mode 100644 audit/v24-release-manifest.json delete mode 100644 audit/v24-release-verification.json delete mode 100644 audit/v24-sparse-correction-model.json delete mode 100644 audit/v24-test-summary.json delete mode 100644 build/wasm-v23/bla-scalar.wasm delete mode 100644 build/wasm-v23/bla-simd.wasm delete mode 100644 build/wasm-v23/color-scalar.wasm delete mode 100644 build/wasm-v23/color-simd.wasm delete mode 100644 build/wasm-v23/deep-scalar.wasm delete mode 100644 build/wasm-v23/deep-simd.wasm delete mode 100644 build/wasm-v23/manifest.json delete mode 100644 build/wasm-v23/wasm-scalar.wasm delete mode 100644 build/wasm-v23/wasm-simd.wasm delete mode 100644 dist/_headers delete mode 100644 dist/hosted/gpu-kernels-v24.2.6.js delete mode 100644 dist/hosted/hosted-loader.js delete mode 100644 dist/hosted/script-v24.2.6.js delete mode 100644 dist/standalone/gpu-kernels-v24.2.6.js delete mode 100644 dist/standalone/gpu-kernels.js delete mode 100644 dist/standalone/kernels.js delete mode 100644 dist/standalone/script-v24.2.6.js delete mode 100644 dist/standalone/script.js delete mode 100644 dist/wasm/bla-scalar.4f06913487044823.wasm delete mode 100644 dist/wasm/bla-scalar.wasm delete mode 100644 dist/wasm/bla-simd.f80f136e9fb676ce.wasm delete mode 100644 dist/wasm/bla-simd.wasm delete mode 100644 dist/wasm/color-scalar.aa2914ec1acacaa2.wasm delete mode 100644 dist/wasm/color-scalar.wasm delete mode 100644 dist/wasm/color-simd.8cf83374ed0c680b.wasm delete mode 100644 dist/wasm/color-simd.wasm delete mode 100644 dist/wasm/deep-scalar.d133bbecc8f6dbdf.wasm delete mode 100644 dist/wasm/deep-scalar.wasm delete mode 100644 dist/wasm/deep-simd.d49385f06e4a8f55.wasm delete mode 100644 dist/wasm/deep-simd.wasm delete mode 100644 dist/wasm/manifest.json delete mode 100644 dist/wasm/wasm-scalar.d19b26c04e1b2f59.wasm delete mode 100644 dist/wasm/wasm-scalar.wasm delete mode 100644 dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm delete mode 100644 dist/wasm/wasm-simd.wasm create mode 100644 docs/CHANGELOG.md create mode 100644 docs/README.md create mode 100644 docs/VALIDATION_REPORT.md delete mode 100644 gpu-kernels-v24.2.6.js delete mode 100644 hosted-headers.txt delete mode 100644 hosted-loader.js delete mode 100644 kernels.js delete mode 100644 script-v24.2.6.js delete mode 100644 scripts/browserless-audit.ps1 delete mode 100644 scripts/build-hosted.ps1 delete mode 100644 scripts/build-kernels.ps1 delete mode 100644 scripts/build-standalone.ps1 delete mode 100644 scripts/build-wasm.ps1 delete mode 100644 scripts/extract-wasm.ps1 delete mode 100644 scripts/source-baseline.ps1 delete mode 100644 scripts/test-all.ps1 delete mode 100644 src/abi.json delete mode 100644 src/bla_kernel_v18.c delete mode 100644 src/color_kernel.c delete mode 100644 src/deep_kernel.c delete mode 100644 src/shallow_kernel.c delete mode 100644 tests/analytic-interior.mjs delete mode 100644 tests/browser-benchmark.html delete mode 100644 tests/document-contract.ps1 delete mode 100644 tests/js-syntax.mjs delete mode 100644 tests/kernel-golden.mjs delete mode 100644 tests/kernel-golden.ps1 delete mode 100644 tests/kernel-source-contract.ps1 delete mode 100644 tests/module-clone.mjs delete mode 100644 tests/numeric-policy-v23.json delete mode 100644 tests/pixel-contract.mjs delete mode 100644 tests/pixel-mapping.mjs delete mode 100644 tests/precision-reference.mjs delete mode 100644 tests/runtime-budget.mjs delete mode 100644 tests/source-contract.ps1 delete mode 100644 tests/v24-backend-policy.mjs create mode 100644 tests/v24-baseline-idle-refinement-model.mjs create mode 100644 tests/v24-color-relative-model.mjs delete mode 100644 tests/v24-crossover-contract.mjs delete mode 100644 tests/v24-crossover-quality.mjs create mode 100644 tests/v24-deep-sparse-queue-model.mjs create mode 100644 tests/v24-fast-precision-extension-model.mjs delete mode 100644 tests/v24-fixed96-contract.mjs delete mode 100644 tests/v24-fixed96-limb-model.mjs delete mode 100644 tests/v24-fixed96-limb-orbit-model.mjs delete mode 100644 tests/v24-fixed96-model.mjs delete mode 100644 tests/v24-fixed96-queue-model.mjs delete mode 100644 tests/v24-fixed96-shader-range-model.mjs delete mode 100644 tests/v24-hybrid-contract.mjs delete mode 100644 tests/v24-hybrid-risk-model.mjs delete mode 100644 tests/v24-hybrid-ui-runtime.mjs create mode 100644 tests/v24-pan-reuse-model.mjs create mode 100644 tests/v24-primary-post-stats-model.mjs create mode 100644 tests/v24-reference-recovery-model.mjs delete mode 100644 tests/v24-ui-cacheproof.mjs delete mode 100644 toolchain.lock.json diff --git a/AUDIT.md b/AUDIT.md deleted file mode 100644 index 302d94b..0000000 --- a/AUDIT.md +++ /dev/null @@ -1,206 +0,0 @@ -# v18 数値・性能監査 - -> **Archive notice:** この監査はv18時点の履歴資料です。現行renderer v23の受入値ではありません。v23のsource/配布baselineは`audit/v23-source-baseline.json`、実行時browser baselineは`audit/v23-browser-baseline.json`へ分離します。 - -## 目的 - -v7〜v17で積み重なった補正処理を「残っているから残す」のではなく、正しさ・速度・相互作用を測定して再判定した。 - -絶対時間は実行環境に依存するため、**誤判定率・同条件での比率・処理の計算量**を主な判断材料とする。 - -## 結論 - -| 処理 | v18判断 | 理由 | -|---|---|---| -| Arbitrary precision + Reference Orbit | 維持 | deep zoomの座標精度と基準軌道に必須 | -| Perturbation + Rebasing | 維持 | 全pixelをBigIntで直接計算するより圧倒的に有利 | -| BLA | 維持・監視強化 | deep pixel kernelの主高速化手段。ただし適切なεは画面依存 | -| Persistent Worker / Reference | 維持 | warm stateの価値が大きい | -| 固定距離によるReference維持 | 改廃 | 距離だけではrecenterの損得を判定できない | -| Pilot render | 縮小して維持 | コスト予測専用なら有用。画素の代用には不正確 | -| 数学的iteration上限を途中で切るwork cap | **廃止** | 黒/外部分類を壊す | -| Pilotのnearest-neighborで未解決pixelを補完 | **廃止** | 境界で誤差が大きい | -| coarse periodic interior mask | **廃止** | 効く画面が偏り、pilotコストも増える | -| 全strip strict verification | **廃止** | deep zoomで隠れた巨大ボトルネック | -| 1点の誤差で全画面strict再描画 | **廃止** | 局所誤差に対して再計算範囲が過大 | -| 複数の解像度feedback controller | **統合** | controller同士が競合して粗いまま固定されることがあった | -| Dither | **廃止** | detail detectorの勾配を人工的に増やし、余計な高精細tileを誘発 | -| Adaptive detail refinement/cache | 維持 | idle時へ遅延すれば費用対効果が良い | -| Autoの黒/色境界追従 | 維持 | kernelと独立で軽く、誤航行抑制に有用 | -| Synthetic Worker Wisdom | **廃止** | 実際のWASM+BLA負荷と相関しにくい | -| Real BLA Wisdom | 維持 | 実kernelを使ってWorker数を選べる | - -## 1. v17のiteration work capは数学的に不正確 - -v17はdeep base描画で、表示用global iterationより小さい`computeIter`を使い、そこまで発散しなければ事実上黒候補として扱っていた。 - -Seahorse付近、48×30、global 3490 / cap 2870をBigInt直接計算で照合した。 - -- capまで残ったpixel: **1440 / 1440** -- その後3490までに発散したpixel: **1066** -- cap survivor中のlate escape: **74.03%** - -つまり「2870で未発散だから黒」とみなすのは、この画面では4分の3近くを誤分類する。 - -**v18:** `computeIter = colorIter`へ戻した。負荷制限は数学的iteration数ではなく、BLA operation / perturbation operationの回数に対して行う。上限到達は`UNRESOLVED`であり黒ではない。少数なら局所再計算、多ければそのstripだけ無制限BLAへ戻す。 - -Raw: `audit/raw/workcap_truth_v17.json` - -## 2. Pilot画像による未解決pixel補完は不正確 - -128×78のfull BLAと56×34 pilotのnearest-neighbor補完を比較。 - -- 黒/外部分類の不一致: **451 / 9984 = 4.52%** -- escape iteration差が8超: **43.71%** - -境界付近では低解像度pilotを「計算結果」として使えない。 - -**v18:** pilotはコスト推定にだけ使う。画面へ表示せず、未解決pixelの色・黒判定にも使わない。 - -Raw: `audit/raw/pilot_fill_audit.json` - -## 3. Interior detectionは「追加反復」ではなく早期終了で入れる - -以前の黒専用look-aheadは、黒pixelへ追加iterationを課すため重かった。v18では逆に、内部と判定できたpixelを**早く終了**する。 - -実装: - -- main cardioid / period-2 bulbの解析判定 -- 軌道微分の収縮判定 -- BLA jump時はBLAの線形係数で微分も更新 - -96×72、2500 iterationでInterior detection OFF/ONを比較。黒/外部分類不一致は全テスト0。 - -| 場所 | OFF | ON | 分類不一致 | -|---|---:|---:|---:| -| period-3 wide | 766.7 ms | 6.0 ms | 0 | -| period-3 mid | 438.7 ms | 2.3 ms | 0 | -| period-2近傍 | 364.7 ms | 1.0 ms | 0 | -| mixed interior/exterior | 282.8 ms | 16.2 ms | 0 | -| cardioid cusp近傍 | 329.9 ms | 1.8 ms | 0 | - -これは「黒を正しくするために重くする処理」ではなく、黒の多い画面を軽くする処理として残す価値が高い。 - -Raw: `audit/raw/interior_safety_v18.json` - -## 4. BLA εは単一固定値にできない - -Seahorse z≈14、160×98で保守的BLAを基準に比較。 - -| ε | 時間 | class mismatch | iteration差>1 | -|---|---:|---:|---:| -| 2^-23 | 約299 ms | 1.00% | 5.23% | -| 2^-32 | 約786 ms | 0.383% | 1.16% | -| 2^-40 | 約1218 ms | 0.083% | 0.198% | - -一方、同じSeahorseでもz≈20では2^-23が基準と一致したテストがある。したがって「常に厳密」「常に緩い」の両方が非効率。 - -**v18:** fast BLAを使い、1フレームあたり固定少数点だけsafe BLAと照合する。誤差が出たstripだけsafe BLAで再計算する。 - -- 通常の検証はstrict perturbationではない -- 1stripごとに何点もstrict計算しない -- 1点の不一致で全画面をstrict再描画しない - -Raw: `audit/raw/audit_v18_kernel.txt` - -## 5. per-strip strict verificationは隠れたボトルネックだった - -旧設計では各stripのBLA後にstrict perturbationを複数点実行していた。非常に深い場所ではBLA本体が数msでも、strict検証が毎strip積み重なりwall timeを支配する。 - -v18の設計: - -1. fast BLAでstrip計算 -2. **フレーム全体で固定個数**だけsafe BLAと比較 -3. mismatchがあれば**そのstripだけ**safe BLAで再計算 -4. status failureの少数pixelだけexact fallback - -これで検証コストが「strip数 × deep iteration」に増えにくくなった。 - -## 6. Reference recenterは距離だけで決めない - -z≈14ではrecenterが有利なケースが多かった。 - -- 0.8画面横移動: first-frameで約**91 ms節約** -- 0.8横+0.5縦: 約**40 ms節約** - -しかしz≈20では、0.5画面横移動でrecenterが約**14.5 ms損**するケースもあった。 - -同じ「0.5画面離れた」でも損得が逆転するため、固定距離閾値は合理的でない。 - -**v18:** - -- Referenceは基本的に維持 -- 実測kernel MPPが基準より悪化したときだけ、予測損失とReference rebuild EMAを比較 -- rebuildの方が安いと予測できる場合だけrecenter -- 極端に離れた場合だけhard safety limitを使う - -Raw: `audit/raw/reference_recenter_audit.json`, `audit/raw/reference_recenter_audit_z20.json` - -## 7. Resolution controlを一本化 - -過去版では以下が同時に解像度へ作用していた。 - -- pilot推定 -- renderPerf MPP -- deepScale -- Device WisdomのresScale -- 品質段階ごとのmin/max width - -複数feedbackが同時に動くため、負荷変化後に過剰縮小したり、逆に最低幅に張り付いたりしやすかった。 - -**v18:** deep base resolutionは一つの時間予算モデルで決定する。pilotまたは最近の実測MPPを入力にし、detail tileは別のidle品質レイヤーとして扱う。 - -## 8. Ditherをdetail判定の前に入れない - -Ditherはグラデーションbandingには効くが、画像のRGB勾配を人工的に増やす。detail detectorの入力へ先に適用すると「存在しない細部」を検出して追加tileを計算する。 - -**v18:** runtime ditherは削除。Canvasのhigh-quality scalingは維持。再導入するならdetail選定後の表示/export段階だけにする。 - -## 9. Real BLA Wisdomは残す - -旧Synthetic Wisdomは単純なJS浮動小数点loopでWorker数を選んでいた。実際の負荷はWASM、BLA table、reference memory、postMessageを含むため別物。 - -**v18:** 長時間idle時のみ実際のBLA mini renderを1〜4 Workerで測定し、実スループットからWorker数を決める。解像度feedbackとは切り離した。 - -## 10. v17→v18 end-to-end確認 - -同一のclean Chromium 144 headless、780×441 viewportで、UI操作に相当するdeep base passを比較した。動的解像度を含む**実際のbase-frame pipeline比較**であり、pixel-for-pixel kernel benchmarkではない。 - -| Scene | v17 | v18 | 改善 | -|---|---:|---:|---:| -| Seahorse z≈14 | 1059 ms | 288 ms | **3.68×** | -| Seahorse z≈20 | 1226 ms | 238 ms | **5.15×** | -| Seahorse z≈100 | 983 ms | 474 ms | **2.07×** | -| period-3 z≈20 | 1931 ms | 781 ms | **2.47×** | - -v18のclean runではruntime exception 0。 - -Raw: `audit/raw/v17-final-browser-benchmark.json`, `audit/raw/v18-final-browser-benchmark.json`, `audit/benchmarks.json` - -## 11. 残した処理 - -次はデータ上、削る理由が弱いので維持した。 - -- BigInt fixed-point coordinate -- Reference Orbit cache + precision promotion -- Perturbation / Rebasing -- BLA -- Series Approximation(主にfallback側) -- Persistent Worker -- smooth coloring LUT -- Auto black/colour boundary tracking + safe rollback -- world-space HQ tile cache -- progressive detail refinement -- high-quality Canvas scaling - -## 12. 今後のボトルネック - -v18ではpixel hot-loopの補正処理を大幅に整理した。さらに深い地点で残る主候補はReference OrbitのBigInt生成である。 - -特にReferenceが長く、かつ高bit精度になるケースでは、次の候補は: - -1. Reference Orbit専用Worker -2. 次Referenceの先読み / double buffer -3. Reference orbitの圧縮・周期利用 - -ただしz≈20程度ではReference生成が常に主因ではないため、常時複雑化するよりtelemetryで必要なときだけ有効化する方がよい。 diff --git a/AUDIT_V24.md b/AUDIT_V24.md deleted file mode 100644 index 604abaf..0000000 --- a/AUDIT_V24.md +++ /dev/null @@ -1,182 +0,0 @@ -# v24.2.3 crossover-benchmark audit - -## 判定 - -静的contract、Node数値モデル、build/reproducibility gateはPASS。実WebGPU adapterでのv24.2.3 compile/dispatchはこのコンテナではadapterを取得できないため未実行であり、`tests/webgpu-acceptance.html`を外部実機gateとして残す。 - -## 主な修正 - -1. 深部の黒面積/黒点 - - UNKNOWN reasonを保持。 - - guarded f32 primary後にUNKNOWN限定double-single correctionを追加。 - - interior黒とUNKNOWN表示を分離。 -2. 標準描画範囲 - - direct/deep overlapを導入。 - - current-view quality/speed probeを本描画前に実行。 - - deepが15%以上高速な場合だけdeepを採用。 -3. Multi-reference - - 全画面モデルで費用対効果が低いため撤去。 -4. Export - - 1×/2×2 deep ExportにもDS correctionを統合。 - - 2×2はsample別unresolved counterを使用。 -5. 旧BLA - - membership反転の既知反例があるため引き続き未搭載。 - -## CPU numeric model results - -### Primary guarded perturbation - -canonical 17×11 corpus: - -- z14: false escaped 0 / false bounded 0 / UNKNOWN 0 -- z20: 0 / 0 / 0 -- z100: 0 / 0 / 0 -- period3: 0 / 0 / 0 -- e280: 0 / 0 / 0 -- e400: 0 / 0 / 0 -- swirly: false escaped 0 / false bounded 0 / UNKNOWN 49 of 187 - -Dense known regression pixels `(25,12)`, `(26,15)`, `(27,25)`はprimaryでは誤分類せずUNKNOWNへ退避。 - -### Sparse double-single correction - -`swirly-seahorses-z12`, 61×39, 2000 iter: - -- oracle guard mismatch: 0 -- primary UNKNOWN: 478 / 2379 -- primary false escaped: 0 -- primary false bounded: 0 -- corrected: 478 -- remaining: 0 -- correction false escaped: 0 -- correction false bounded: 0 -- escape iteration mismatch: 5 -- max escape iteration difference: 374 - -DS passはfinite-iteration visual classification recoveryとして有効だが、escape iteration equalityは保証しない。 - -## Router contract - -- direct coordinate floor: ratio 8 -- no-probe direct margin: ratio 128 -- overlap: 8–128 -- deep required speed advantage: 15% -- corrected deep probe UNKNOWN max: 0.5% -- class disagreement tolerance: 0 -- current view probe required; depth profile aloneではrouteしない - -## Performance/迂遠処理監査 - -- full-frame high-precision CPU renderer: なし -- multi-reference reference build loop: なし -- redundant primary→same-f32 strict retry: なし -- BLA precompute/evaluate: なし -- deep correction: full dispatchだがKNOWN pixelは即return。追加reference生成なし。 -- Export: reusable 512² workspace、1 readback/tile。 -- 2×2 Export: 4 numerical samplesを1 command stream内で処理し、1 resolve/1 readback。 -- presentation/recolor: GPU field常駐。 - -## Real WebGPU acceptance - -`tests/webgpu-acceptance.html`は以下を要求する。 - -- production 6 WGSL + benchmark DS Direct 1本の計7 WGSL modules compile/pipeline creation -- stable corpus corrected UNKNOWN = 0 on sampled pixels -- BigInt P/P+64 guard mismatch = 0 -- false escaped / false bounded = 0 -- dense 3 regression pixelsをDS correctionでKNOWNへ回収 -- overlap view-specific routerがquality gateを通過し、15% speed ruleどおりroute -- 1× / 2×2 Export unresolved = 0 -- uncaptured WebGPU validation/runtime error = 0 - -この環境では`navigator.gpu`/adapterを実利用できないため、上記hardware gateのみ未判定。 - - -## v24.2.3 crossover追加監査 - -- DS Direct WGSLはproduction routerへ未接続。 -- Round-depth benchmark: z0,2,4,6,8,10,12,14,16,20,30,40。 -- CPU BigInt oracle sweep: `audit/v24-crossover-quality.json`。 -- Seahorse / 1600×900相当ではf32 material degradation ≈ z6、DS ≈ z10。 -- corrected Deep CPU modelはz8で1/315 false escapeを検出したため、依然membership certificateではない。 -- 実GPU timingはこのcontainerではWebGPU adapter不成立のため未測定。実機では診断情報→Backend比較で取得する。 - - -## v24.2.3 z12 benchmark hang hotfix - -ユーザー実機でBackend比較が `z12 · deep` から終了しない事象を確認。原因はproduction rendererの無限loopではなく、benchmarkが現在の全画面をDeep primary + DS correctionでwarmup 1回 + 2〜5回反復し、各回を単一の大規模GPU jobとして待っていたこと。1600×900級・z12では1測定だけでも最大10億回規模の反復候補となり、queue completion待ちが極端に長くなり得た。 - -修正: - -- speed benchmarkを代表3tile(最大256×144)へ縮小。 -- actual full-resolution座標を維持したtileX/tileYで測定。 -- aggregate `ms/pixel` からcurrent canvasのfull-frame時間を換算。 -- quality gateは従来どおり5 spatial tiles。 -- GPU batch/reference buildに15 s watchdog。timeout時は明示的errorとし、GPU deviceを破棄して再初期化可能にする。 -- progress表示を `deep sample 2/3` のように細分化。 -- crossover JSON formatをv2へ更新し、`speedTiles`, `speedPixels`, `msPerPixel`, `fullMedianMs`を記録。 - -このhotfixはproduction backend routing/rendering mathを変更しない。 - - -## v24.2.4 Hybrid experiment preparation - -- Added benchmark-only `DIRECT_DS_GUARDED_WGSL`; production router is unchanged. -- Guard metric: approximate `|dz/dc|`; threshold sweep `1e12 / 1e13 / 1e14`. -- Added `benchmarkHybrid()` / `renderTileMetaHybrid()` using the existing Deep double-single correction only for DS-marked UNKNOWN pixels. -- Added z6/z8/z10/z12 experiment UI and JSON export. -- Added CPU BigInt P/P+64 risk-model gate. At risk=1e13, observed DS class errors are all caught at z6/z8/z10 with correction rates 2.8% / 11.1% / 34.6%; z12 correction rate is 91.2%, intentionally indicating the Full-Deep crossover. -- Added real-WebGPU acceptance smoke for the Hybrid diagnostic path. - - -## v24.2.5 Sparse Direct experiment (Audit) - -- The previous sparse-Full-Deep diagnostic is superseded and is not exposed in the UI. -- Added `DIRECT_TS_CORRECT_WGSL`, a reference-free three-term f32 expansion correction shader that only processes DS sensitivity UNKNOWN pixels. -- Added `benchmarkSparseDirect()` / `renderTileMetaSparseDirect()` and a z6/z8/z10/z12 risk sweep. -- Full Deep remains the quality/timing baseline; the Sparse Direct candidate itself does not build or bind a reference orbit. -- Production backend routing is unchanged. -- Root `index.html` is now a single-file bundle with no external script dependency, specifically to keep diagnostic UI controls functional when the HTML is opened from an archive/temp extraction. -- Static single-file UI contract and real-browser click smoke are separate gates; real WebGPU arithmetic still requires a WebGPU-capable browser. - -- Sparse Direct coordinate packing uses exact residual subtraction from BigInt fixed-point into three f32 components; this removed the CPU TD-model class errors caused by the earlier host-Number split. - -## v24.2.6 Fixed96 experiment audit - -- v24.2.5 three-term f32 expansion correction is removed from the active experiment because the real-GPU quality gate produced no viable candidate at z6/z8/z10/z12. -- New correction shader: `DIRECT_FIXED96_CORRECT_WGSL`. -- Arithmetic is signed Q8.88 in three `u32` limbs; no floating-point expansion is used by the correction pass. -- Exact BigInt ViewState coordinates are converted directly to Q8.88 tile base + pixel step on the host; no JS `Number` coordinate round-trip is used. -- `tests/v24-fixed96-limb-model.mjs` verifies the limb multiplication/round-shift algorithm against BigInt over 20,000 deterministic products. -- `tests/v24-fixed96-model.mjs` verifies the DS sensitivity mask + Q8.88 correction against the BigInt P/P+64 oracle on a 1600×900-equivalent Seahorse grid. Safe thresholds exist at z6/z8/z10/z12 in the CPU preflight. -- Experiment output reports every risk threshold, including rejected candidates, with correction rate, remaining UNKNOWN, class mismatch and timing. The former `best risk = none` information loss is removed. -- Production routing/rendering remains unchanged. -- Root `index.html` remains a single-file bundle; the Fixed96 button handler is embedded and has no sibling-JS dependency when opened from an extracted ZIP. -- Real-GPU WGSL compilation/timing remains the decisive acceptance gate. - - -## v24.2.7 Fixed96 range hotfix audit - -- Root cause: `fx_bad_range()` incorrectly required the Q8.88 top integer byte to be `0x00` or `0xff`, rejecting valid values with magnitude >= 1. -- Symptom match: real-GPU experiment reported `correction ~= remain` across z6-z12. -- Fix: remove the invalid sign-extension test; use a certain-escape component test at `|component| > 2` before fixed-point magnitude squaring. -- Added BigInt coordinate range/step-underflow checks before GPU packing. -- Added regression tests for valid Q8.88 values above 1, 20,000 limb multiplication cases, and full limb-orbit iteration equality against exact BigInt Q8.88. - - -## v24.2.8 Fixed96 true sparse queue audit - -Purpose: remove the v24.2.7 full-screen Fixed96 dispatch and make the diagnostic correction genuinely proportional to risky-pixel count. - -Implementation checks: - -- DS risk shader appends local pixel indices to a storage queue with atomics. -- queue capacity equals tile pixel count; overflow is still counted and treated as fatal. -- a dedicated finalize shader writes indirect dispatch arguments from the GPU-written queue count. -- Fixed96 uses `@workgroup_size(64)` and `dispatchWorkgroupsIndirect`; it no longer launches 8x8 workgroups over the full tile. -- Fixed96 validates queue indices and verifies the queued pixel is still UNKNOWN(reason 7). -- benchmark and quality paths both run `fixed96QueueIntegrity()` and fail closed on any counter/invariant mismatch. -- static queue-integrity model covers zero, 20, 65-entry, overflow, wrong-indirect, processed mismatch and stale-entry cases. -- Q8.88 limb multiplication/orbit tests remain unchanged and passing; this revision changes scheduling only. - -Real-GPU timing remains the decisive performance gate. diff --git a/BUILD_REPRODUCIBILITY.md b/BUILD_REPRODUCIBILITY.md deleted file mode 100644 index 7cd2033..0000000 --- a/BUILD_REPRODUCIBILITY.md +++ /dev/null @@ -1,51 +0,0 @@ -# WASM build reproducibility - -v23では、`src/`を監査可能なkernel source、`src/abi.json`を公開ABI、 -`toolchain.lock.json`を固定toolchain契約として扱います。 - -## 互換payloadを展開する - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File scripts/extract-wasm.ps1 -``` - -現在配布している8 payloadを`dist/wasm/`へ展開し、byte数とSHA-256を -`manifest.json`へ記録します。COLOR SIMD/scalarがbyte単位で同一ならStandaloneでは -Base64文字列を1本へ自動集約し、異なる場合は各payloadを保持します。どちらの場合もmanifest上の2 ABI名は維持します。 - -## sourceから再生成する - -clang 17.0.6をPATHへ置き、次を実行します。 - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-wasm.ps1 -``` - -生成先は`build/wasm-v23/`です。`src/shallow_kernel.c`、 -`src/deep_kernel.c`、`src/bla_kernel_v18.c`、`src/color_kernel.c`をそれぞれ -SIMD/scalarでcompileし、timestampを含まないsource manifestを生成します。 - -配布payloadへ昇格するときだけ`-Promote`を指定します。昇格前に -`tests/kernel-golden.mjs`が、画素中心座標、escape iteration、magnitude、 -smooth補正、SIMD/scalar同値性を検査します。golden失敗時はコピーも -`kernels.js`再生成も行いません。 - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-wasm.ps1 -Promote -``` - -## 信頼境界 - -clang 17.0.6とNode.js v22.18.0を作業領域内で使用し、復元sourceのcompile、 -generated-WASM golden、SIMD/scalar同値性、画素中心契約を実行済みです。tool配布archive自体の -URL/SHA-256は現repositoryに保持されていないため、「公式archiveのhash照合」は再監査可能な証拠には -含めません。再監査できる信頼境界はversion/flags、source hash、生成manifest、golden結果です。 -Provenance status: tool archive hash not retained. -golden合格後に8 payloadと`kernels.js`を昇格し、Hosted/Standalone成果物も再生成しました。 -実測結果と生成manifestのSHA-256は`audit/v23-source-baseline.json`へ記録しています。 - -省略なしの再検証は、toolchainをPATHへ追加した状態で次を実行します。 - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File scripts/test-all.ps1 -RequireToolchain -``` diff --git a/COMPLETION_AUDIT.md b/COMPLETION_AUDIT.md deleted file mode 100644 index c634e32..0000000 --- a/COMPLETION_AUDIT.md +++ /dev/null @@ -1,55 +0,0 @@ -# Mandelbrot Deep Zoom v23 完了監査 - -## 判定 - -`IMPROVEMENT_PROPOSAL.md`のうち、Phase 0Aと計測結果に依存しないPhase 1〜3Aの実装項目はコード、build、非ブラウザ試験へ反映した。source/ABI/数値/pixel mapping/WASM Module clone/配布物の保存済みgateは合格している。v22成果物が残っていないため、v22比較項目は`not-verifiable`でありpassには含めない。 - -ただし、固定実機を必要とするbrowser acceptanceは実行環境にin-app Browserが存在しないため未実施である。性能修正後に再開した接続監査でも3回連続のgoal turnで利用可能Browser 0件となり、2026-08-22T10:48:56.1197927Zに外部状態待ちのblockerとして再確定した。Node固定sceneではdeep BLAの高いMPPを確認し、標準deepを約1.4秒の実測時間予算へ変更済みだが、これはCanvas、Worker、入力、downloadを含むbrowser gateの代替ではない。またPhase 3BとPhase 4は、提案書自身がbrowser telemetryで支配コストまたは誤差を確認した場合だけ実施すると定義した条件付き項目であり、trigger判定も未実施である。したがって本監査は「実装・非ブラウザgate合格、実機受入blocked」と判定する。 - -## 要件トレーサビリティ - -| 提案 | 実装結果 | 主な証拠 | -|---|---|---| -| 6.1 イベント駆動 | 常時RAFを廃止。paint/statsをinvalidate時だけ予約し、非表示時はscheduler・render・refine・Exportを停止 | `script.js`の`scheduleFrame`、`requestScheduler`、`stopSchedulers`、`visibilitychange`。`tests/source-contract.ps1` | -| 6.2 Canvas/計算grid分離 | 処理モード別Screen pixel budgetとeffective DPRを導入。Previewは110ms予算、cold deepは幅48px以下。標準shallowは2〜4MP、標準deepは実測MPPから約1.4秒予算へ調整、精細は4〜8MP | `screenPixelBudget`、`adaptStandardDeepBudget`、`resize`、`targetSize`、`RENDER_PROFILE` | -| 6.3 gesture/取消 | wheel 110ms、pointer 90ms settle。操作中は再投影。省電力はPreviewで停止し、標準はCovered後の追加処理なし。精細の追加反復はescape境界候補だけをdeep 384/shallow 4,096点まで実行。非共有環境は短いtile/chunk | gesture handlers、`nextQualityDue`、`scheduleUnknownContinuation` | -| 6.4 deep遅延生成 | main deep WASM、Module、Worker、referenceを数値条件付近まで生成しない。1 Workerから実測増員。実job wisdomを端末・kernel keyで永続化 | `prewarmDeepAssets`、`prepareDeepModules`、`addDeepWorker`、`deepWisdomStorageKey` | -| 6.5 WASM/memory | compile済みdeep/BLA/color Moduleを一度だけ作りWorkerへclone。field buffer返却、byte ledger、Worker退役を実装。1 module/1 shared memory統合はPhase 3Bの条件付き項目 | `compileKernelPair`、Worker `init`、`memoryLedger`、`retireDeepAssets`、`tests/module-clone.mjs` | -| 6.6 byte cache | fixed countを廃止し、mobile 96 MiB/desktop 192 MiBのmanaged byte ledgerとLRUへ変更 | `rendererMemoryBudget`、`detailCacheBudget`、`trimDetailCache` | -| 6.7 Hosted/Standalone | content hash外部WASM+streaming compileのHosted、3ファイル直開きのStandaloneを生成。deep requestは深部到達前0の契約 | `hosted-loader.js`、`scripts/build-hosted.ps1`、`scripts/build-standalone.ps1`、`dist/` | -| 6.8 再現build | 4 kernel source、ABI、固定clang flags、SHA-256 manifest、SIMD/scalar goldenを整備 | `src/`、`toolchain.lock.json`、`BUILD_REPRODUCIBILITY.md`、`build/wasm-v23/manifest.json` | -| 7.1 画素中心 | JS、BigInt、shallow/deep/BLA WASM、detail、Exportを`x+0.5,y+0.5`へ統一しrenderer v23化 | `tests/pixel-mapping.mjs` 851 sample、`tests/pixel-contract.mjs` 1,440 sample | -| 7.2 adaptive AA | 全域sample後、数値fieldの分類・分散・confidenceでquadtree 2×/4× subsample。linear RGBでresolve | `scoreDetailTiles`、`resolveSubsampleField` | -| 7.3 field/color分離 | state、smooth、iteration、confidenceを保持。境界subsample fieldも保持。palette変更は再着色だけ | `makeField`、`colorizeField`、`recolorCurrentField` | -| 7.4 BLA/detail精度 | Covered/detailで同一precision profile。高scoreまたはValidated detailはguarded direct。同一world座標の数値gateと視覚gateを分離 | `blaProfile`、`sendDeepDetailRect`、pixel tests | -| 7.5 未確定分離 | `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を分離。反復を局所継続し、未確定数を表示・Export metadataへ記録 | field constants、`scheduleUnknownContinuation`、`runValidation` | -| 7.6 誤差ベース精度 | pixel step/ULP/orbit telemetry+hysteresisでengine選択。round-to-nearest fixed point。P/P+64 directとreference checkpointを照合し、不一致時はglobal reference再構築 | `deepEngineNeeded`、`roundShift`、`highPrecisionDirectPixelAsync`、`verifyReferenceCheckpoints` | -| 7.7 超深部 | 上限と未確定を診断表示。scaled BLA、multi-reference、可変参照長はPhase 4のtelemetry条件付き研究 | 診断UI、下記「条件付き項目」 | -| 7.8 Export | Quick snapshotと独立tiled Exportを分離。1×/2×/4×/custom、AA、Balanced/Validated、進捗、取消、全tile後encode、JSON sidecar | `runExport`、`tests/browser-benchmark.html` | -| 8/9 改廃・UX | 連続`q`、固定DPR/HQ幅、RGB detail、固定deep閾値、eager pool、件数cacheを廃止。ViewSpec、history、正確座標、初回UI、mobile status、Advanced、accessibilityを実装 | `index.html`、`script.js`、`tests/source-contract.ps1` | - -## Gate結果 - -| Gate | 結果 | -|---|---| -| JavaScript本体/生成Worker/browser harness構文、mock DOM初期化 | pass | -| deep/BLA/color `WebAssembly.Module` structured clone・instantiate | pass | -| 256/320-bit direct agreement、analytic interior整数証明 | pass | -| reference P/P+64 checkpoint agreement・故意の不一致検出 | pass | -| Node固定scene計測/予算式契約 | pass。shallow 2 sceneと、出典付きのswirly-seahorses混在境界sceneでdeep BLAを測定し、deep標準を固定画素数から実測時間適応へ変更。保存値は端末負荷に依存する特性値であり、時間上限そのものをassertするgateではない | -| 全backend pixel mapping・2×/4× tile seam | pass、851 sample | -| 配布shallow SIMD pixel contract | pass、1,440 sample、mismatch 0 | -| kernel source/ABI、payload checksum、Hosted/Standalone build | pass | -| 実ブラウザ performance/visual/interaction/Export/accessibility | not-run。Browser bindingなし | - -機械可読なsource基準値は`audit/v23-source-baseline.json`、未実施browser gateは`audit/v23-browser-baseline.json`に記録した。 - -追加のbrowser非依存監査は`audit/v23-browserless-baseline.json`へ保存した。固定Node.js v22.18.0で7 executable tests、source/document contract、配布hashを再実行してpassしたが、scopeは`browserless-current-artifacts`、`fullAcceptance: false`である。v22比較は`not-verifiable`、source-WASM buildはprior evidence reused、runtimeは測定値であり性能合格ではない。 - -## 条件付き項目 - -Phase 3Bの1 shared memory統合、immutable reference/BLA table共有、WASM hot loopのatomic cancel、およびPhase 4のdouble-double、interval/error ball、scaled BLA、multi-reference、WebGPUは無条件要件ではない。`IMPROVEMENT_PROPOSAL.md` 3.3、7.7、Phase 3B/4の規定どおり、固定browser計測でcopy、memory、10^-280以深、glitch、reference生成のどれが支配的か判明した場合だけ着手する。現在はModule clone、buffer pool、短いchunk、guarded directで安全な前段を実装済みだが、telemetryがないため条件成立を主張しない。 - -## Browser受入の再開条件 - -`tests/browser-benchmark.html`をHTTPで開けるin-app Browserを接続し、mobile/desktop/4Kを要求DPRの別contextで、Hosted/Standalone別に実行する。runnerはraw pixel golden、30回以上のPreview、Covered/実Refined、idle write、long task、managed/取得可能ならUA memory、deep request、非zero wheel paint、再着色、Export完了/取消/再現性、keyboard操作を採取する。observed GPU memory、visible focus、page zoom/pinch、screen-reader順序の外部証拠と統合し、全profile・全buildが揃った時点で固定実機の最終受入を判定できる。 diff --git a/CROSSOVER_BENCHMARK.md b/CROSSOVER_BENCHMARK.md deleted file mode 100644 index 8c4b6fe..0000000 --- a/CROSSOVER_BENCHMARK.md +++ /dev/null @@ -1,62 +0,0 @@ -# v24.2.3 Crossover Benchmark - -## 目的 - -標準 `f32 Direct` が十分に速いのにBigInt referenceを伴うDeepへ早く切り替わっている可能性を検証する。Deepを速度目的ではなく「品質上必要になった地点」でのみ使えるよう、`f32 Direct / experimental DS Direct / corrected Deep`を同じ深度列で比較する。 - -## 品質モデル - -- Seahorse center: `-0.7453983606667815 + 0.1125046349959942i` -- viewport: 1600×900。21×15点を画面全域から間引き、隣接21 pixelの座標分解能も別途検査。 -- 深度: z0,2,4,6,8,10,12,14,16,20,30,40。`span = 3.4×10^-z`。 -- oracle: BigInt fixed-point P / P+64一致。 -- Deep: guarded rescaled f32 perturbation + UNKNOWNのみDS correction。 -- DS Direct: GPU候補と同じdouble-single arithmeticのCPU `Math.fround`モデル。 - -| z | f32 class error | f32 adjacent unique | DS class error | DS adjacent unique | Deep error/UNKNOWN | -|---:|---:|---:|---:|---:|---:| -| 0 | 0.00% | 21/21 | 0.00% | 21/21 | 0 / 0 | -| 2 | 0.63% | 21/21 | 0.00% | 21/21 | 0 / 0 | -| 4 | 0.63% | 21/21 | 0.00% | 21/21 | 0 / 0 | -| 6 | 1.59% | 2/21 | 0.32% | 21/21 | 0 / 0 | -| 8 | 1.27% | 1/21 | 0.63% | 21/21 | 1 / 0 | -| 10 | 3.17% | 1/21 | 3.81% | 21/21 | 0 / 0 | -| 12 | 17.14% | 1/21 | 8.57% | 21/21 | 0 / 0 | -| 14 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 | -| 16 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 | -| 20 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 | -| 30 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 | -| 40 | 100.00% | 1/21 | 0.00% | 1/21 | 0 / 0 | - -## 暫定結論 - -1. **f32 Directはz4付近までは実用候補。z6では隣接21 pixelが2値まで潰れ、品質上の限界が明確。** 現在の `ratio < 8` 強制Deepは1600px幅・この中心ではおおむねz4前後で発動するため、少なくとも「Deepを速度で選ぶ」には早すぎる。 -2. **DS Directはz8まで中間backendとして有望。z10ではclass errorが約3.8%に増え、本命Deepへ移るべき。** 隣接pixel分解能自体はz12まで維持するが、反復誤差が先に品質を制限する。 -3. **Deepは廃止できない。** f32はz6、DSもz10付近から品質が崩れる。Deepは極端な固定費を持つが、より深い領域では唯一の実用候補。 -4. Deep CPUモデルにはz8で1/315のfalse escapeが残るため、Deepもmembership certificateではない。これは既存の`membershipCertified:false`方針と整合する。 - -## 実GPU速度ベンチ - -このコンテナではWebGPU adapterを取得できないため、GPU時間は測定できない。代わりに本体の診断情報へ **Backend比較** を追加した。 - -v24.2.2では各深度を現在のcanvas全画面でwarmup+複数回測定していたため、z12 Deepで巨大dispatchが長時間GPU queueを占有するケースがあった。v24.2.3では速度計測を現在のview座標上の代表3タイル(各最大256×144)に限定し、合計時間から `ms/pixel` を算出して現在の全画面へ換算する。品質は別の5タイルで比較する。各GPU batch/reference buildには15秒watchdogがあり、永久待ちはしない。 - -実機では同じ深度列を次の3方式により測定する。 - -- `f32`: 標準numeric kernel。 -- `DS`: experimental whole-frame double-single direct kernel。production routerには未接続。 -- `Deep warm`: reference cache済みのDeep primary + DS correction。 -- `Deep cold`: `Deep warm + verified BigInt reference build`。 -各候補のqualityはactual full-resolution座標上の5領域でcorrected Deepと比較し、class mismatch / escape-iteration mismatch / UNKNOWN率をJSONへ記録する。速度JSONにはspeedTiles / speedPixels / msPerPixel / fullMedianMsを保存する。Deep自体の絶対品質は上記BigInt CPU oracle reportを参照する。 - -## 次のrouter候補 - -実GPU結果が得られたら、backendは「速度だけ」ではなく次で選ぶ。 - -```text -f32 quality OK → f32/DS/Deep のうち最速 -f32 NG, DS quality OK → DS/Deep のうち最速 -DS NG → Deep -``` - -DeepにBLAがない現在、同じiteration数で`f32 Direct`よりDeep numeric kernelが速くなる理由はほぼない。Deepの役割は速度ではなく座標/反復精度であり、速度比較の本命は **DS cold cost vs Deep cold/warm cost** になる。 \ No newline at end of file diff --git a/FIXED96_EXPERIMENT.md b/FIXED96_EXPERIMENT.md deleted file mode 100644 index 288ac57..0000000 --- a/FIXED96_EXPERIMENT.md +++ /dev/null @@ -1,70 +0,0 @@ -# v24.2.8 DS + True Sparse Fixed96 Direct Experiment - -## Purpose - -The previous three-term f32 expansion correction failed the real-GPU quality gate on every tested depth. v24.2.6 keeps the DS sensitivity gate but replaces the correction arithmetic with deterministic integer fixed-point operations. - -Pipeline: - -```text -DS Direct + |dz/dc| risk gate - -> risky pixel indices compacted into GPU u32 queue - -> GPU finalize writes indirect workgroup count - -> dispatchWorkgroupsIndirect - -> signed Q8.88 Direct runs only queued pixels - -> compare against corrected Full Deep -``` - -The Fixed96 correction does not bind or build a deep reference. - -## Arithmetic - -- width: 96 bits -- format: signed Q8.88 two's complement -- storage: 3 × `u32` -- multiplication: exact 32×32→64 decomposition from 16-bit partial products, 96×96 accumulation into six 32-bit limbs, then round-and-shift by 88 bits -- coordinates: packed directly from the BigInt ViewState into Q8.88; JavaScript `Number` is not used as an intermediate - -Q8.88 is intended only for the z6–z12 transition experiment. It is not proposed as an unlimited deep renderer. - -## Measurements - -For z6 / z8 / z10 / z12 and risk `1e11 / 1e12 / 1e13 / 1e14`, the UI reports every candidate row: - -- extrapolated Fixed96 full-frame time, -- Full Deep cold estimate, -- speedup, -- correction rate, -- remaining UNKNOWN, -- escaped/bounded class mismatch, -- actual Fixed96 dispatch rate, -- queue integrity status, -- status/failure reason. - -A failed candidate is never collapsed into a single `none` row. - -## CPU preflight - -`tests/v24-fixed96-model.mjs` compares the DS risk gate + exact Q8.88 Direct model with the BigInt P/P+64 oracle on a 1600×900-equivalent Seahorse sample grid. `tests/v24-fixed96-limb-model.mjs` separately verifies the six-limb multiplication algorithm against BigInt for 20,000 deterministic cases. - -These are preflight tests. Real-GPU shader compilation, timing and class comparison remain decisive. - -## Production status - -Diagnostic only. The production router remains unchanged. - - -## v24.2.7 range hotfix - -v24.2.6 contained a shader bug in `fx_bad_range()`: the top Q8.88 integer byte was treated as if it had to be pure sign-extension. Valid values such as `+1.2` were therefore rejected. Escaping Mandelbrot orbits commonly pass through component magnitudes above 1, so selected correction pixels were counted as `remaining` instead of being written back. - -v24.2.7 removes that invalid test. While the prior state is inside the bailout disk, Q8.88 multiplication is bounded. After each iteration the shader first tests `|Re(z)| > 2 || |Im(z)| > 2` as a mathematically certain escape, and only squares components when both are within ±2. This prevents both the false range rejection and large-value fixed-point wraparound. - - -## v24.2.8 true sparse queue - -v24.2.7 still launched a 2D Fixed96 workgroup over the whole benchmark tile and returned immediately for non-risk pixels. On SIMD/SIMT hardware this was not sparse enough: a few risky lanes could keep most of a workgroup occupied by the long integer loop. - -v24.2.8 changes the execution topology, not the Q8.88 arithmetic. The guarded DS pass atomically appends each risky local pixel index to a queue. A one-invocation finalize shader writes `ceil(queueCount/64),1,1` to an indirect-argument buffer. The Fixed96 kernel is one-dimensional with `@workgroup_size(64)` and reads exactly one queue entry per invocation. No CPU map/readback occurs between these passes. - -Every timing and quality tile validates queue invariants before its result is accepted. Overflow, wrong indirect arguments, invalid/stale indices or any disagreement between selected/enqueued/processed/corrected counts aborts the experiment rather than producing a misleading timing row. diff --git a/HYBRID_EXPERIMENT.md b/HYBRID_EXPERIMENT.md deleted file mode 100644 index 63a7c1c..0000000 --- a/HYBRID_EXPERIMENT.md +++ /dev/null @@ -1,69 +0,0 @@ -# v24.2.6 DS + sparse Deep Hybrid Experiment - -## 目的 - -v24.2.3実機計測では、Seahorse trackでDS Directはz10/z12でもFull Deepより速い一方、分類品質が不足した。次の実験ではDS Direct全画面を捨てず、数値的に危険なpixelだけDeep補正へ送ることで、z6〜z10の中間帯を高速化できるか測る。 - -production routerは変更しない。本ファイルの実験結果が採用条件を満たした場合にのみ次版で `f32 → DS/Hybrid → Full Deep` のrouterを検討する。 - -## 実験backend - -`DS guarded direct → UNKNOWN only → existing Deep DS correction` - -DS guarded directはorbitと同時に複素微分 `D = dz/dc` を近似追跡する。 - -``` -D[n+1] = 2 z[n] D[n] + 1 -``` - -`|D| > riskLimit` になったpixelはDS Directの分類を採用せず `FIELD_UNKNOWN / ds-sensitivity` とする。そのpixelだけ既存のBigInt reference + double-single perturbation correctionで再評価する。 - -この判定はmembership証明ではなく、DS Directの不安定化を早期に検出する実験用heuristicである。 - -## sweep - -- 深度: z6 / z8 / z10 / z12 -- riskLimit: `1e12`, `1e13`, `1e14` -- speed: actual full-resolution座標上の代表3tile(各最大256×144) -- quality: 5 spatial tiles -- quality reference: corrected Full Deep -- cold time: verified BigInt reference buildをHybrid / Full Deep双方へ加算 - -診断情報 → **Hybrid実験を開始** を押すとダイアログ表示後に自動開始する。ダイアログ内の **再実行** で同じ設定を再度測定でき、結果はJSON保存できる。 - -## CPU事前モデル - -1600×900相当、Seahorse、31×21点、BigInt P/P+64 oracle。`riskLimit=1e13`: - -| depth | raw DS class error | Deep補正対象 | DS class error捕捉率 | -|---:|---:|---:|---:| -| z6 | 4 / 651 | 18 / 651 = 2.8% | 100% | -| z8 | 4 / 651 | 72 / 651 = 11.1% | 100% | -| z10 | 10 / 651 | 225 / 651 = 34.6% | 100% | -| z12 | 51 / 651 | 594 / 651 = 91.2% | 100% | - -このため事前仮説は次の通り。 - -- z6: Hybrid有望。 -- z8: Hybrid有望。 -- z10: 補正率約35%でもFull Deepより速いかが主要な計測点。 -- z12: 約91%をDeepへ送るためHybridの意味が薄く、Full Deepへ切り替える候補。 - -## 採用gate - -Hybridをproduction候補にするのは、各viewで以下を全て満たす場合のみ。 - -1. corrected Full Deepとのescape/bounded class mismatch = 0。 -2. correction後remaining UNKNOWN = 0。 -3. Hybrid cold estimateがFull Deep coldより10%以上高速。 -4. correction対象率 < 60%。60%以上ならFull Deepを優先。 -5. 実GPU acceptanceでuncaptured validation/runtime error = 0。 - -最終production routerへ昇格する前には、BigInt oracleでdense boundary corpusを別途再検証する。 - -## 注意 - -- `riskLimit=1e13`はCPU事前モデルから得た候補で、固定仕様ではない。 -- `|dz/dc|`はcondition proxyであり厳密なroundoff error boundではない。 -- Hybrid correctionはreference buildを必要とする。ただしv24.2.3実機値ではreference buildはDeep全時間の小部分だったため、主要比較はGPU numeric costとなる。 -- z12でHybrid補正率が高いことを「失敗」とは扱わない。Full Deepへ切り替えるべき境界を検出することも実験目的である。 diff --git a/IMPLEMENTATION_REPORT.md b/IMPLEMENTATION_REPORT.md deleted file mode 100644 index dd43bc4..0000000 --- a/IMPLEMENTATION_REPORT.md +++ /dev/null @@ -1,118 +0,0 @@ -# v24.2.3 crossover benchmark implementation report - -## ファイルの役割 - -- `index.html`: UI、canvas、Export dialog。WebGPU実装ロジックは持たない。 -- `script.js`: BigInt ViewState、reference Worker、WebGPU resource orchestration、backend router、screen/Export pipeline。 -- `gpu-kernels.js`: production 6 WGSL source + benchmark-only `DS Direct` 1本。DS Directはproduction routerから参照しない。 -- `tests/v24-sparse-correction-model.mjs`: WGSLと同じf32/double-single式をCPU `Math.fround`で再現し、BigInt oracleと比較する品質gate。 -- `tests/v24-backend-policy.mjs`: view-specific route policyの純粋ロジック検査。 -- `tests/webgpu-acceptance.*`: 実GPU compile/dispatch/quality/export gate。 -- `AUDIT_V24.md`: 現releaseの検査結果と未検証事項。 -- `MIGRATION_V24.md`: v23からの設計変更と、廃案を含む理由。 - -## 今回作成/変更した理由 - -深部primaryのUNKNOWNを黒として残すことが閲覧品質を下げていたため、追加referenceではなく同じreference上のdouble-single correctionへ変更した。またdirect利用範囲を固定閾値だけで狭めず、actual view上のqualityとGPU速度を測ってdeep採用を限定するためrouterを変更した。 - -## 機能検査 - -`npm test`で以下を自動実行する。 - -- JS syntax -- WGSL reserved identifiers -- source/tree/index contracts -- reference Worker -- direct f32 model -- backend router -- guarded deep model -- sparse DS correction model -- BLA disabled contract -- geometry/export contract -- exact deep coordinate formatter -- streaming PNG -- real-GPU acceptance source contract - -## 性能設計 - -- overlap probeは本描画前に1回/current-view。 -- speed probeは最大224×160。 -- quality probeは最大72×54を5領域。 -- backend profileは補助診断であり、現在viewのquality probeを省略しない。 -- DS correctionはUNKNOWNのみ数値ループへ入り、KNOWN pixelは即return。 -- Export workspaceは512×512固定で再利用。 - -## 既知の制約 - -- double-singleはWGSLの実装差を含めmembership certificateとして扱わない。 -- 実GPU acceptanceはWebGPU adapterを取得できる環境で別途実行が必要。 - - -## v24.2.3追加作業 - -- `DIRECT_DS_WGSL`: 全画面double-single Directの速度/品質候補。 -- `runCrossoverBenchmark()`: z0〜z40のround-depth自動比較。 -- `Backend比較` dialog: 実GPUでf32 / DS / Deep warm / Deep coldを同一解像度で計時しJSON保存。 -- `tests/v24-crossover-quality.mjs`: 1600×900相当のBigInt P/P+64 oracle品質掃引。 -- `CROSSOVER_BENCHMARK.md`: 実装意図、CPU品質結果、次routerの判定基準。 - -現時点ではDS Directは診断専用であり、本体のbackend選択は変更していない。実GPU速度結果なしにproductionへ昇格させないための措置。 - - -## v24.2.4 Hybrid experiment preparation - -- Added benchmark-only `DIRECT_DS_GUARDED_WGSL`; production router is unchanged. -- Guard metric: approximate `|dz/dc|`; threshold sweep `1e12 / 1e13 / 1e14`. -- Added `benchmarkHybrid()` / `renderTileMetaHybrid()` using the existing Deep double-single correction only for DS-marked UNKNOWN pixels. -- Added z6/z8/z10/z12 experiment UI and JSON export. -- Added CPU BigInt P/P+64 risk-model gate. At risk=1e13, observed DS class errors are all caught at z6/z8/z10 with correction rates 2.8% / 11.1% / 34.6%; z12 correction rate is 91.2%, intentionally indicating the Full-Deep crossover. -- Added real-WebGPU acceptance smoke for the Hybrid diagnostic path. - - -## v24.2.5 Sparse Direct experiment (Implementation) - -- The previous sparse-Full-Deep diagnostic is superseded and is not exposed in the UI. -- Added `DIRECT_TS_CORRECT_WGSL`, a reference-free three-term f32 expansion correction shader that only processes DS sensitivity UNKNOWN pixels. -- Added `benchmarkSparseDirect()` / `renderTileMetaSparseDirect()` and a z6/z8/z10/z12 risk sweep. -- Full Deep remains the quality/timing baseline; the Sparse Direct candidate itself does not build or bind a reference orbit. -- Production backend routing is unchanged. -- Root `index.html` is now a single-file bundle with no external script dependency, specifically to keep diagnostic UI controls functional when the HTML is opened from an archive/temp extraction. -- Static single-file UI contract and real-browser click smoke are separate gates; real WebGPU arithmetic still requires a WebGPU-capable browser. - -- Sparse Direct coordinate packing uses exact residual subtraction from BigInt fixed-point into three f32 components; this removed the CPU TD-model class errors caused by the earlier host-Number split. - -## v24.2.6 Fixed96 experiment implementation - -Purpose: test a deterministic reference-free correction tier between DS Direct and Full Deep without relying on WGSL float-expansion behavior. - -Implemented: -- `DIRECT_FIXED96_CORRECT_WGSL`: signed Q8.88 Direct iteration using 3×u32 values. -- exact 32×32→64 multiplication from 16-bit partial products; -- 96×96→192 accumulation and rounded Q8.88 reduction; -- BigInt→Q8.88 tile coordinate packing; -- DS risk gate + Fixed96 UNKNOWN-only correction benchmark; -- per-risk failure reporting instead of collapsing all failures to `none`; -- single-file ZIP UI contract; -- CPU oracle and limb-arithmetic tests. - -Not implemented: production router integration. The experiment must first pass on a real WebGPU adapter. - - -## v24.2.7 Fixed96 hotfix - -The Fixed96 experiment's real-GPU `remaining` counter exposed an implementation bug rather than an arithmetic-quality limitation. The correction shader's range check confused the 8-bit Q8.88 integer field with redundant sign-extension. The hotfix removes that check and makes bailout ordering overflow-safe: component-certain escape first, exact fixed-point magnitude only inside the ±2 component box. - - -## v24.2.8 — Fixed96 GPU compact queue - -### Files changed - -- `gpu-kernels.js`: DS risk queue output, indirect-args finalize shader, 1D queue-indexed Fixed96 correction kernel. -- `script.js`: queue/indirect buffers, indirect dispatch encoding, per-sample queue integrity verification, queue/dispatch diagnostics in the experiment result. -- `tests/v24-fixed96-contract.mjs`: static true-sparse contract. -- `tests/v24-fixed96-queue-model.mjs`: fail-closed queue counter/indirect argument model. -- `tests/webgpu-acceptance.js`: real-GPU Fixed96 smoke now requires queue integrity and dispatch rate equality. - -### Performance intent - -The previous implementation's numerical correction fraction was sparse, but its GPU invocation topology was not. v24.2.8 makes integer iteration work proportional to the actual queue length. The DS guard remains a full-tile pass because it is the cheap classifier; only the expensive Q8.88 loop is sparse. diff --git a/IMPROVEMENT_PROPOSAL.md b/IMPROVEMENT_PROPOSAL.md deleted file mode 100644 index 8022b96..0000000 --- a/IMPROVEMENT_PROPOSAL.md +++ /dev/null @@ -1,566 +0,0 @@ -# マンデルブロ集合ビューワー 軽量・高精細化 改善提案 - -- 対象baseline: Mandelbrot ∞ Zoom v22 -- 文書status: v22に対する実装前提案。採用項目はrenderer v23へ実装済みであり、現行状態と残存gateは`IMPLEMENTATION_REPORT.md`と`COMPLETION_AUDIT.md`を正とする。 -- Document status: historical-v22 / implemented-v23 -- 証拠境界: v22成果物とGit snapshotは現workspaceに残っていないため、v22の行番号・bytesは履歴記録であり再監査可能なbaselineではない。以後の比較基準はversion付きJSONとSHA-256を必須とする。 -- 作成日: 2026-08-22 -- 対象ファイル: `index.html`, `script.js`, `kernels.js`, `src/bla_kernel_v18.c` -- 性格: 設計判断を保存する履歴文書兼、修正版の品質契約。効果値は、version付き監査JSONの引用を除き検証すべき目標または仮説である。 - -## 1. 結論 - -現行実装は、任意精度座標、基準軌道、摂動法、リベース、BLA、SIMD、局所フォールバックまで備えている。数値エンジンの骨格を捨てて全面的にGPU化するより、次の順で周辺設計を改める方が、軽量性と高精細を両立しやすい。 - -1. 待機中も続く全画面再描画を止め、イベント駆動にする。 -2. 描画品質を曖昧な数値 `q` ではなく、Reprojected / Preview / Covered / Refined / Validated の完了条件で管理する。 -3. 画面全域を、確定した表示pixel gridまでタイル描画してから、境界だけ適応的アンチエイリアスする。通常はnative DPRとし、画素予算でDPRを制限した場合は明示する。 -4. 計算結果と彩色を分離し、色変更でフラクタル計算をやり直さない。 -5. 深部WorkerとWASMを遅延生成し、固定最大長・Workerごとの重複メモリを減らす。 -6. 画素中心、BLA精度、未解決画素、出力完了条件を明文化し、「高精細」の意味を計測・監査できる状態へ変える。 - -最優先はカーネルの細かな命令最適化ではない。現状の配布物は合計138,831 bytesとすでに小さい一方、待機中の常時描画、巨大なCanvas、Workerごとの固定配列は継続的なCPU/GPU・メモリ負荷になるためである。 - -## 2. 「軽量」と「高精細」の定義 - -本提案では、目標を次のように分ける。 - -| 軸 | 意味 | 主な指標 | -|---|---|---| -| 配布の軽さ | 初回ロード・解析・WASMコンパイルが小さい | 転送bytes、起動時に取得/compileする資産 | -| 実行の軽さ | 操作中と待機中にCPU/GPUを浪費しない | long task、idle CPU、Canvas draw回数、消費電力 | -| メモリの軽さ | DPR・Worker数・参照長が増えても予算内に収める | peak memory、cache bytes、Workerごとの固定領域 | -| ラスター精細度 | 表示ピクセルを実サンプルで覆い、境界を適切にAAする | sample coverage、resolved coverage、samples/pixel、継ぎ目 | -| 数値精度 | 座標・反復・BLA近似の誤差を検出し、未解決を黒と混同しない | 高精度referenceとの差、unresolved数、検証モード | -| 出力精細度 | 表示プレビューとは独立して、指定寸法を完了後に保存する | 出力寸法、AA、再現性、未完了pixel数 | - -「Canvasが4Kサイズである」だけでは高精細とはみなさない。低解像度フレームを4K Canvasへ拡大した状態と、4K分のサンプルを実計算した状態を区別する。 - -本書のcoverageは次の2値へ分ける。sample coverageは宣言したiteration/precision policyで新ViewSpecのsampleを計算済みのtarget pixel比率であり、`UNRESOLVED`も計算済みsampleとして含む。resolved coverageは`ESCAPED`またはpolicy上の`INTERIOR_PROVEN`へ確定した比率である。Coveredはsample coverage 100%、Validatedはsample coverage 100%に加えてunresolved 0を要求する。 - -## 3. 現状評価 - -### 3.1 残す価値が高い部分 - -- BigInt固定小数点による中心・表示幅と、段階的な精度昇格 (`script.js:58-109`) -- 可変精度BigInt固定小数で生成し、Float64表現を摂動/BLAへ渡す基準軌道 (`script.js:608-705`) -- SIMDからscalar、さらにJS/BigIntへのフォールバック (`script.js:208-227`, `script.js:551-685`) -- 常駐Workerのwarm cache、少数検証、strip単位の局所補修 (`script.js:296-390`) -- main cardioid / period-2 bulbと収縮判定による内部早期終了 (`src/bla_kernel_v18.c:94-172`) -- 操作中の既存フレーム再投影 (`script.js:154-184`, `script.js:393-424`) -- smooth coloring、world-space detail cacheという考え方 (`script.js:187-206`, `script.js:393-531`) - -既存のv18監査では、v17比のbase-frame改善が2.07〜5.15倍で、テストした内部早期終了は黒/外部分類不一致0だった (`AUDIT.md:60-82`, `AUDIT.md:163-176`)。この実績があるため、BLAや内部早期終了を一律に廃止する理由はない。 - -提案作成時の現行コードはv22だったのに、性能監査と最終ベンチの表題・データはv18だった。既存値は設計判断の参考には使えるが、v22の受入基準にはできない。当初は同じsceneのv22再計測を必要条件としたが、現在は不変なv22成果物が残っていないため比較を`not-verifiable`とし、version固定したv23以後のbaselineだけを回帰判定へ使う。 - -### 3.2 主要なギャップ - -| 優先度 | ギャップ | 根拠 | 影響 | -|---|---|---|---| -| P0 | 静止中も永久に`requestAnimationFrame`を回す | `script.js:722-729` | idle CPU/GPU、電池、発熱 | -| P0 | 1フレームで全Canvasと全detail cacheを再合成し、DOM統計も更新する | `script.js:413-423`, `script.js:713-716` | 画面が変わらなくても継続負荷。ドラッグ中は同一RAF内で二重paintになる | -| P0 | 解像度制御が端末実測を使っていない | `renderPerf`は記録のみ。`targetSize()`は固定tier (`script.js:42-44`, `script.js:426-439`, `script.js:534-537`) | 小画面では過剰計算、大画面では不足 | -| P0 | HQ全画面は最大2100×2200で、選抜領域だけ2倍 | `script.js:429-461` | 4Kでは要求gridを全域計算しない。細線を基礎画像で見失うと精細化されない | -| P0 | 確認できるJS/direct/BLA経路が、画素中心ではなく整数格子点をサンプルする | `script.js:516-518`, `script.js:570-573`, `script.js:672-674`, `src/bla_kernel_v18.c:184-190` | 非対称なsample grid、解像度間対応の非標準化、backend差のリスク | -| P0 | detail fast BLAがbase HQより緩い | base HQはe-32、detailは通常e-28。`strictBoundary`は現在常にfalse (`script.js:350-359`, `script.js:504-525`) | 「精細化した領域の方が数値的に粗い」可能性 | -| P0 | PNGが現在の表示Canvasを即時保存する | `script.js:739` | 再投影中・低解像度・detail未完了でも高解像度出力に見える | -| P1 | 起動180ms後にdeep poolを必ず作る | `script.js:296-307`, `script.js:750` | 浅部だけ見る場合もWorker、参照配列、LUTを確保 | -| P1 | Workerごとに基準軌道とBLA表を重複保持・構築する | `script.js:249-259`, `src/bla_kernel_v18.c:3-23` | 深部使用後のpeak memoryが大きい | -| P1 | WorkerでstripごとにRGBAを新規確保し、transfer後にmain全画面配列とImageDataへ再コピーする | `script.js:265-269`, `script.js:378-382`, `script.js:398-400` | memory bandwidth・一時領域。transfer自体はzero-copy | -| P1 | palette/cycle/shift変更で数値計算まで再実行する | `script.js:397`, `script.js:468`, `script.js:744-747` | 深部の色調整が不必要に重い | -| P1 | deep切替が固定ズーム指数11.5 | `script.js:20`, `script.js:707-709` | 中心値、DPR、1画素幅に対し過剰または不足 | -| P1 | 有限反復表示として`maxIter`到達を黒にするため、精度優先時も証明済み内部と未確定を区別できない | `script.js:116-119`, `script.js:561-564`, `script.js:666-670` | Validated/Exportで確定度を説明できない。v17の途中work cap不具合とは別問題 | -| P2 | BLAが通常doubleのspanに依存し、約10^-280以深で無効になる | `script.js:364`, `script.js:508`, `src/bla_kernel_v18.c:175-190` | 超深部で性能が崖状に低下 | - -### 3.3 メモリ上の具体例 - -`src/bla_kernel_v18.c`の固定配列だけを合計すると、1つのBLA WASM instanceあたり約15.6 MiBになる。 - -- 出力: `counts[65536]` + `mags[65536]` ≈ 0.75 MiB -- 参照軌道: `refs_r[150001]` + `refs_i[150001]` ≈ 2.29 MiB -- BLA double配列5本 × 300100 ≈ 11.45 MiB -- BLA length配列 × 300100 ≈ 1.14 MiB - -深部job後に4 WorkerがBLA instanceを持てば、この固定領域だけで約62.5 MiBとなる。さらに各WorkerにはJS側の`RR/RI`、3パレットのLUT、deep/color WASM、出力scratchがある。 - -また、Worker内部の`RR/RI`とLUTはWASMより先に生成されるため、起動直後に4 Workerを作るだけでもtyped arrayだけで概算13 MiB超を確保する。固定最大配列を現在の`refLen`へ合わせる効果は大きい。 - -### 3.4 配布サイズ上の具体例 - -- `index.html`: 6,618 bytes -- `script.js`: 80,077 bytes -- `kernels.js`: 52,136 bytes -- 実行に必要な3ファイル合計: 138,831 bytes -- Base64部分: 51,808文字、復号後WASM合計38,845 bytes - -絶対量はすでに小さい。Base64と復号後の約13KB差は非圧縮表現上の値であり、Brotli転送量が同量減るわけではない。外出しの主効果は独立cache、遅延取得、streaming compileであるため、先にidle負荷とメモリ重複を直す方がよい。なお`COLOR_SIMD_B64`と`COLOR_SCALAR_B64`は同一内容であり、現状は同じ2,510-byteバイナリを二重に内包している。 - -## 4. 推奨する新しい品質仕様 - -内部の連続値`q`を、利用者と実装の双方が判定できる状態機械へ置き換える。現状は初回`render(.3)`に対し分岐が`q < .3`なので、意図した最軽量tierではなく次のtierへ入る (`script.js:426-437`, `script.js:751`)。enum化はこの種の境界ずれも防ぐ。 - -| 状態 | 開始条件 | 解像度・計算 | 完了条件 | UI表示例 | -|---|---|---|---|---| -| Reprojected | wheel / pinch / pan継続中 | 既存フレームのtransformのみ | 入力停止待ち | プレビュー | -| Preview | 入力停止80〜120ms後 | 時間予算内の低解像度。近似BLA可 | 最優先tileを表示 | 高速描画 | -| Covered | Preview後 | 選択した処理モードのtarget gridを全域tile計算 | sample coverage 100%、再投影pixel 0。未確定数は別表示 | 全域描画 68% | -| Refined | Covered後のidle | 境界・不確実tileだけ2×/4× adaptive AA | 対象tile完了 | 境界AA完了 | -| Validated | 精度優先時またはValidated Export | 宣言した有限反復・精度policyを局所照合 | checks合格かつunresolved 0。残る場合は別状態「検証未完了・未確定N」 | 検証完了 / 検証未完了 | - -重要な契約は次の通り。 - -- Previewは粗くてよいが、新しい未解決画素を「内部」と断定して黒にしない。 -- Coveredはズーム深度に関係なく、確定したScreen Canvas gridを新しいViewSpecの実sampleだけで全域計算する。再投影した旧frameは描画中のpresentationにだけ使い、sample coverageへ数えない。計算済み`UNRESOLVED`はsample coverageへ数えるがresolved coverageへは数えない。 -- native DPRは上限であり、標準・精細とも明示したpixel/memory budget内で使う。精細は時間適応で解像度を下げないが、memory/pixel上限は維持する。DPRを制限した場合はeffective DPRと実解像度を表示する。 -- Refinedは単なる2倍補間ではなく、複数の実サンプルをlinear-lightでresolveする。 -- Validatedは数学的なマンデルブロ集合所属証明ではなく、宣言した有限反復・precision policyの照合完了を表す。全sampleへ誤差境界または証明手続きを適用する将来モードだけを`Certified`と呼ぶ。 -- 「高精細」と「数値検証完了」を同じチェックボックスにしない。 -- Exportは表示Canvasとは独立したjobであり、view、寸法、反復方針、AA、precision tier、乱数seedを固定してから開始する。Balanced ExportとValidated Exportを区別し、前者は未確定数をmetadataへ記録して完了可能、後者だけをunresolved 0必須とする。 - -処理モードと描画状態は分ける。 - -| 処理モード | Coveredのtarget | 自動到達点 | -|---|---|---| -| 省電力 | 自動Coveredなし。Previewは最大約1MP | Preview | -| 標準 | shallowはmobile/低memory約2MP、desktop約4MP。deepはPreview実測から約1.4秒のCovered予算へeffective DPRを下げる | Covered。AA・未確定追加反復は自動実行しない | -| 精細 | mobile/低memory約4MP、desktop約8MP。上限時は実解像度を明示 | Refined。未確定追加反復は境界候補だけを上限付きで行う | -| 精度優先 | 指定gridとprecision policy | Validated。将来はCertifiedを選択可 | - -Exportは処理モードではなく独立jobである。Balanced / Validatedのprecision tierを選び、それぞれRefined相当 / Validated相当の完了条件を適用する。 - -## 5. 推奨アーキテクチャ - -```text -入力 - ↓ -ViewState(BigInt座標・履歴・version) - ↓ -Render Scheduler(gesture、優先度、時間/メモリ予算、取消) - ├─ f64 SIMD backend - ├─ deep BLA / perturbation backend - └─ high-precision verifier / fallback - ↓ -Field Cache(escaped / interior likely / interior proven / unresolved、smooth value、confidence) - ├─ Colorizer → Screen Compositor - └─ Adaptive AA / Export -``` - -責務を次の単位へ分ける。 - -- `view-model`: BigInt座標、pixel-to-world規約、URL version、戻る/進む -- `scheduler`: gesture settle、tile priority、generation cancel、visibility、品質状態 -- `backend`: f64 / deep / verifierを同じtile protocolで実装 -- `field-cache`: palette非依存のpacked数値結果、未解決tileだけの継続状態、byte-budget LRU -- `colorizer`: palette、tone mapping、最終ditherだけを担当 -- `presenter`: 再投影、tile合成、coverage表示 -- `exporter`: 指定寸法の独立tile render、進捗、取消、metadata - -Workerへ送るjobは、最低限`viewVersion`, `tile`, `samplePattern`, `iterationPolicy`, `precisionPolicy`を持たせる。古いversionの結果は合成せず、1 tileを8〜12ms程度へ収めて取消後の滞留を限定する。 - -## 6. 軽量化の具体策 - -### 6.1 イベント駆動描画へ変更する — P0 - -現在の`loop()`は静止中も毎フレーム`paintFrame()`、`updateStats()`、次のRAF予約を行う。次へ変更する。 - -- `needsPaint` / `needsStats` / `scheduled`を設け、invalidate時だけRAFを1回予約する。 -- pointermove/wheelはイベントごとに描かず、最新transformを1 RAFに集約する。 -- render/tile完了時だけ該当rectを合成する。 -- statsは状態変化時のみ、操作中でも最大4〜10Hzに制限する。 -- `document.hidden`ではdetail、Wisdom、未表示tileを停止する。 -- 静止完了後はRAF、Canvas write、DOM writeを0回/秒にする。 - -これは実装難度が低く、画質を落とさず、待機時負荷を大きく下げる最初の変更である。 - -### 6.2 表示Canvasと計算解像度を分離する — P0 - -現状は表示Canvasを`innerWidth × min(DPR, 2.5)`で確保する一方、frameは最大2100×2200である。4K・高DPRでは巨大な表示バッファへ小さいframeを拡大するだけになり得る。 - -極端な例として、3840×2160 CSS px・DPR 2.5では表示Canvasだけで9600×5400×4 bytes、約198 MiBになる。画素数上限がないため、source frameより大幅に大きいsurfaceを常時再描画し得る。 - -- Screen Canvasには処理モード別に約1〜8MPのbyte/pixel budgetを設ける。標準shallowは2〜4MP、精細・精度優先は4〜8MPを初期値とする。標準deepはcold Previewを極小gridで測り、実測MPPから約1.4秒以内を狙うpixel budgetへ下げる。effective DPRと実寸を表示し、精細を選んだ場合はこの時間適応を使わない。 -- `effectiveDpr = min(deviceDpr, pixelBudget由来上限)`とする。 -- Covered計算はタイルで選択モードのtarget Canvas gridを100%覆う。 -- 画面より大きい4K/8K出力は表示Canvasを巨大化せず、ExporterのOffscreenCanvasまたはtile encoderで作る。 -- `renderPerf`を本当に使い、Previewだけ`targetPixels = budgetMs / EMA(msPerPixel)`で決める。 -- EMAはshallow/deepだけでなく、iteration帯、BLA有無、warm/coldを区別する。 - -Previewの目安は80〜120ms、停止後baseは400〜700msとし、その後はtileで漸進する。端末差を吸収するため、固定pixel幅ではなく時間予算から決める。 - -### 6.3 操作中の無駄な計算を止める — P0 - -- wheelをpointerと同じgestureとして扱い、入力停止80〜120msまでは再投影だけにする。 -- pinch / wheel / +−を同じsettle処理へ統合する。 -- SharedArrayBufferを使えるHosted版では、WASM hot loopが定期的に読むatomic cancel flagを設ける。 -- SharedArrayBufferが使えない構成では、tile/chunkを小さくして取消後の最悪滞留時間を制限する。 -- Preview中にwork capへ到達した画素は`UNRESOLVED`のまま残す。現状は低品質passで未解決が2%を超えると同stripを即時無制限再計算する (`script.js:265-266`) が、field/statusを導入するPhase 2以降はCoveredで継続し、Validatedでは必要に応じ高精度経路へ上げる。 -- 浅部の`.58 → .84 → 1.0`という最大3回の全面再計算は、実測が遅い端末ではPreview → Coveredの2段へ減らす。 - -### 6.4 deep資産を遅延生成する — P0/P1 - -- Hosted版の初期表示ではshallow SIMDだけを取得・compileする。Standalone版は全Base64文字列をparseするが、deep payloadの復号・compileとWorker生成は遅延する。 -- deep main-thread WASM、deep worker source、Worker poolは、数値条件が閾値へ近づいた時点で準備する。 -- Workerは最初1〜2個だけ作り、実jobのthroughputから必要時だけ増やす。 -- 現在のReal Wisdomは深部HQへ到達した各ページセッションのidle時に1回、最大4 Workerなら合計10回のmini renderを追加する (`script.js:333-348`, `script.js:698`)。通常jobから受動学習し、能動ベンチは診断時または不確実時だけにする。 -- 学習結果はkernel versionと最小限の端末帯域特性をkeyに永続化する。schema versionと保存時刻を持たせ、既定TTL 30日で失効させる。storage拒否時はsession内学習だけで動作し、raw timing履歴や過剰なfingerprint属性を保存しない。 - -### 6.5 WASMとメモリを統合・動的化する — P1 - -- deep、BLA、smooth/colorを1 module・1 memoryへ統合する。 -- `MAX_REF`、`MAX_BLA`の固定最大配列をやめ、現在の`refLen`に合わせて初期容量を決め、必要時だけ`memory.grow`する。linear memoryは縮まないため、大scene後の回収には容量bucket別instance poolの退役・再生成を使う。 -- JS `RR/RI` → deep WASM → BLA WASMという重複copyを1回へ減らす。 -- BLA tableは参照長`n`に対して概ね`2n`要素を動的確保する。 -- Hosted版ではCOOP/COEPを設定する。共有対象はbuild完了後immutableなreference、key別BLA table、tileごとに非重複なfield領域へ限定する。table公開にはready barrierを設け、scratch・統計はWorker別offsetまたは別memoryにしてdata raceを避ける。 -- 非共有版ではbuffer poolを使い、transferしたArrayBufferをWorkerへ返却して再利用する。 -- `mags`をcolor WASMへcopyする経路も統合し、kernelからpacked fieldまたはRGBAを一度だけ出す。 - -### 6.6 キャッシュを件数ではなくbytesで管理する — P1 - -`DETAIL_TILE_LIMIT=96`は、tile寸法、DPR、Canvas実装の裏側を考慮しない。次へ置き換える。 - -- mobile 96 MiB、desktop 192 MiBを、JSから管理できるtyped array / WASM / cacheの暫定論理予算として計測開始する。 -- frame、numeric field、tile canvasの推定bytes、reference、BLA、scratchをmemory ledgerへ登録する。ブラウザ内部のCanvas/GPU backing storeは正確に台帳化できないため、固定環境のprocess/UA memory観測を別指標にする。 -- LRUはbytes、現viewportからの距離、再利用見込みでevictする。 -- pan開始時の全frame cloneを避け、gesture中はimmutableな現frameを直接transformする。 -- 論理予算超過時はcacheを捨てる。ブラウザ固有のmemory-pressure通知へ依存せず、最終解像度を黙って落とさない。 - -### 6.7 配布をHosted版とStandalone版へ分ける — P2 - -推奨する標準はHosted版である。 - -- `.wasm`とmodule Workerをhash付き外部資産にする。 -- `compileStreaming` / `instantiateStreaming`、Brotli、immutable cacheを使う。 -- SIMD版を先に取得し、失敗時だけscalar版を取得する。 -- deep/BLA/colorは深部到達時だけ取得する。 -- compile済み`WebAssembly.Module`をWorkerへstructured cloneする。 - -既存の「3ファイルを直接開ける」はStandalone互換buildとして残し、build時にWASMをinlineする。これにより、軽量な通常配信とオフライン可搬性を両立できる。 - -WebGPUは最初から必須にしない。まずpalette適用、linear-light resolve、tile合成へ使い、数値演算は低ズームPreviewだけの任意backendとして比較する。fp32を深部の最終結果へ使わない。 - -### 6.8 WASM buildを再現可能にする — P1 - -現状の`src/`にはBLA C sourceしかなく、他の埋込WASMに対応するsource、build command、toolchain manifestが見当たらない。バイナリだけでは、画素座標、丸め、SIMD/scalarの同値性を十分に監査できない。 - -- 全kernel sourceと生成scriptを保存する。 -- clang等のversion、compile flags、exports ABIを固定する。 -- 生成物のSHA-256をmanifestへ記録する。 -- SIMD/scalarについて同じgolden vectorを実行する。 -- `kernels.js`またはStandalone bundleは生成物とし、手編集しない。 - -## 7. 高精細・高精度化の具体策 - -### 7.1 pixel-to-world規約を画素中心へ統一する — P0 - -確認できるJS/direct/BLA実装では整数格子式が使われている。埋込WASMを含む全backend、detail、exportで、次の規約を共有する。 - -```text -cr = centerRe + span * ((x + 0.5) / width - 0.5) -ci = centerIm + span * ((height / 2 - (y + 0.5)) / width) -``` - -BigIntでは次の有理式にし、早期のdouble化を避ける。 - -```text -cr = centerRe + span * (2*x + 1 - width) / (2*width) -ci = centerIm + span * (height - 2*y - 1) / (2*width) -``` - -canonical座標は上の有理数とする。fixed-point化では分子の符号と対称なround-to-nearest、exact halfはaway-from-zeroへ丸める`roundDiv`をABI契約とし、積を先に計算してから1回だけ除算する。JS f64、shallow WASM、deep WASM、BLA、direct fallback、detail、exportについて、同一pixelがcanonical座標を各backendの表現可能範囲で再現するcross-backend testを追加する。「backend差0」は整数のfixed-point表現または分類・iteration契約に対して用い、f64座標は宣言ULP以内を基準にする。整数格子式だけで現在のtile seamが実証されたわけではないが、規約を統一して将来のbackend差を防ぐ。この変更は既存URLのview中心を変えない一方、画像sampleは半画素分変化し得るため、renderer/golden image versionを上げる。 - -### 7.2 Covered完了後にadaptive AAする — P0/P1 - -現行のRGBA勾配選別は、基礎サンプルに現れなかった細いフィラメントや微小島を検出できない。また、Canvasの`imageSmoothingQuality`は再拡大補間であり、真のフラクタルAAではない。 - -新しい流れは次の通り。 - -1. 画面全域を1 sample / target screen pixelで計算する。 -2. kernelからescape iteration、smooth potential、内部/外部/未解決、confidence、可能なら距離推定値を出す。 -3. 分類境界、値の分散、距離推定、tile中央・四隅probeからquadtreeを細分化する。 -4. 必要なpixelだけ4/8/16 sampleへ増やす。 -5. sampleをlinear RGBで平均し、最後にsRGB/P3へencodeする。 - -これにより、全面4倍SSAAより少ない計算量で、ジャギー、細線、微小島を改善できる。detail選定はpalette非依存となる。ただし中央・四隅probeや距離推定も、任意に細い未サンプル構造を完全には保証しない。Refinedは誤差推定に基づくadaptive AAであり、全pixel面積の数学的被覆証明ではない。Certifiedを名乗る場合は、別途保守的なdistance/error boundが必要になる。 - -### 7.3 数値fieldと色を分離する — P1 - -全画面について保持するのは、少なくとも次のpacked fieldに限定する。 - -- state: `ESCAPED`, `INTERIOR_LIKELY`, `INTERIOR_PROVEN`, `UNRESOLVED` -- smooth escape valueまたは固定小数へpackした値 -- confidence / precision tier - -`zr/zi`、参照位置、微分などの継続状態は全画面に持たず、未解決pixelまたは未解決tileだけに保持する。8MP全域へ完全な継続状態を持つと、memory budgetを容易に超えるためである。 - -adaptive AAを再着色可能にするには、境界pixelのsubsampleごとのstate/smooth値とsample patternを保持する。単一の平均smooth値だけでは、非線形paletteを正しく再resolveできない。palette、cycle、shift変更時はこのfieldから安価なcolor/AA resolveだけを再実行し、フラクタル反復はやり直さない。detail cache keyからstyleを外し、geometry/field cacheとcolor cacheを分離する。 - -現状の3 palette × 2048 phase × 64 mix LUTはmainと各Workerで約1.1 MiBずつ使う。1D palette LUT + 計算式または補間へ変えれば小さくできる。色はlinear RGBまたはOKLabで生成し、最後だけ8bit化する。 - -「昼夜」paletteは周期の先頭色と末尾色が一致していない (`script.js:189-197`)。phase wrap位置に筋が出ないよう、循環paletteはendpointを一致させ、非循環paletteはping-pong mappingにする。banding対策のditherを戻す場合は、detail/AA選定後の最終表示またはExportだけに固定seedで適用する。 - -### 7.4 BLA/detailの精度逆転をなくす — P0 - -- Covered/Refinedのfast εは、少なくともbase HQと同等にする。 -- 境界scoreまたはconfidenceが悪いtileではεを厳しくするか、BLAを切ってstrict perturbation / double-doubleへ上げる。 -- 現行detail検証の「黒分類不一致3.5%以下、平均RGB差48以下」 (`script.js:476-487`) は、品質保証ではなく視覚的coherence gateにすぎない。視覚gateと数値correctness gateを分離し、後者は同一world座標を高精度referenceで再評価して比較する。low/high rasterの異なるsample位置を直接0 mismatch要件にしない。 -- fast BLAとsafe BLAは同じdouble参照軌道を使うため、「safe」を数学的厳密性の意味で使わない。 -- Balancedでは少数probe方式を残せるが、Validatedでは疑わしいtileを独立した高精度reference実装またはdouble-doubleと比較する。少数probe合格だけで未検査pixelを保証済みとは呼ばない。 - -### 7.5 未解決を黒から分離し、反復を継続可能にする — P1 - -通常の有限反復表示で`maxIter`到達を黒にすること自体は一般的な表示規約であり、v17の「global iterationより手前で打ち切ったwork cap」とは別である。本提案では、Validated/Exportの確定度を説明するため、内部表現を次へ分ける。 - -- `INTERIOR_PROVEN`: 誤差境界付き解析判定、または周期吸引を証明できたもの -- `INTERIOR_LIKELY`: 現行のf64 cardioid/bulb判定や収縮heuristicなど、高信頼だが証明扱いしないもの -- `ESCAPED`: 発散が確認されたもの -- `UNRESOLVED`: iteration/work/precision上限へ達したもの - -Coveredの描画中は未解決領域へ中立色または直前結果をpresentationとして重ねられる。旧frameの再投影はsample coverageへ数えないが、新ViewSpecでpolicy上限まで計算した`UNRESOLVED`はsample coverageへ数え、resolved coverageと未確定数で別に監査する。精細では未解決の境界tileだけ反復を段階的に継続する。これなら、旧work capを復活させずに計算量を局所化できる。 - -収縮による内部早期終了は既存監査で有効だったためBalancedでは`INTERIOR_LIKELY`として維持する。Validatedでは判定根拠をconfidenceへ記録し、policyが要求する場合は誤差境界付き解析判定・周期証明へ上げられないものを`UNRESOLVED`へ戻す。 - -### 7.6 engine選択とguard bitsを誤差ベースにする — P1 - -固定`DEEP_ZOOM_THRESHOLD=11.5`ではなく、少なくとも軸ごとに次を評価する。 - -- 1 pixelのworld-space幅 -- `centerRe`, `centerIm`付近のf64 ULP -- 隣接pixel座標が別のdoubleになるか -- 直近orbitの誤差・glitch率 - -`pixelStep > safety × ulp(center)`を満たす間はf64を使い、境界付近にはhysteresisを置く。これにより、不要なdeep移行を減らしつつ量子化を防げる。 - -座標のBigInt精度と基準軌道の計算精度も分ける。基準軌道をP bitとP+32/64 bitのcheckpointで比較する。単一global referenceを使う間はreference単位で精度を上げる。tile単位で上げるにはlocal referenceを新設するか、そのtileだけdirect/double-double摂動へ昇格する。全画面を常時多倍長化しない。 - -`exactDeepPixel`は固定小数積を右shiftで量子化する高精度direct計算であり、数学的なexactではない (`script.js:602-605`)。正値は切り捨て、負値は算術shiftによる負方向丸めになる。`highPrecisionDirectPixel`へ改名し、round-to-nearestを使う。Certifiedでは区間演算でescapeを証明し、内部には別の周期吸引証明を使う。 - -### 7.7 超深部の性能崖を段階的に解消する — P2 - -- BLA APIのspan、offset、cMaxを通常doubleではなくmantissa + exponent bucketへする。 -- 係数も必要に応じてscaled complexまたはdouble-doubleへ昇格する。 -- glitch tileを分割し、tile中心にlocal referenceを作るmulti-reference perturbationを研究する。 -- reference orbitをchunk化し、固定150,001点・最大140,000反復の上限をmemory budgetへ置き換える。 -- 上限へ達した場合は「∞」を暗黙に名乗らず、現在の反復/参照/精度上限と未解決数を表示する。 - -multi-referenceやdouble-doubleは高難度である。version固定した現rendererのbrowser計測で、reference生成、glitch fallback、10^-280以深のどれが実際に支配的か確認してから実装する。v22欠落分を現renderer値で遡及的に代用しない。 - -### 7.8 Exportを独立pipelineにする — P0/P1 - -`PNG`を次の2操作へ分ける。 - -- Quick snapshot: 現在見えているCanvasを保存。Previewである可能性を明示する。 -- High-quality export: 1× / 2× / 4× / custom、AA、Balanced / Validated、進捗、取消を指定する。Balancedは未確定sampleをmetadataへ記録して完了可能、Validatedはchecks合格かつunresolved 0だけをfinalとして出す。Certifiedは誤差境界経路を実装した後に追加する。 - -High-quality exportでは、固定したViewSpecをtile描画し、全tile完了後にだけencodeする。開始前に最大辺、sample数、予約bytesをpreflightし、予算超過・allocation失敗は解像度を黙って下げず明示失敗する。座標、span、bits、iteration policy、palette、AA、precision tier、backend、kernel hash、SIMD/scalar、色空間、encoder、renderer versionをPNG metadataまたはsidecar JSONへ記録する。同じ条件では規定許容差内で再現し、bit単位再現が必要な場合はscalar mathとencoderまで固定する。 - -## 8. 既存仕様の改廃 - -| 現行仕様 | 判断 | 新仕様 | -|---|---|---| -| 自動ズームを削除 | 維持 | 自動航法は戻さず、手動探索を軽く保つ | -| 深度だけで解像度を下げない | 趣旨を維持 | Coveredは深度非依存でtarget grid 100%。Previewだけ時間予算で縮小可 | -| `q`による固定4段階解像度 | 廃止 | 描画状態をReprojected / Preview / Covered / Refined / Validatedへ変更 | -| DPR 2.5、HQ 2100×2200、direct fallback幅390/620 | 置換 | 処理モード別pixel budgetを表示。Covered/Exportは選択したgridをtileで完遂 | -| 整数格子pixel座標 | 破壊的変更 | `(x+0.5, y+0.5)`へ統一し、renderer versionとgolden imageを更新 | -| RGB勾配で最大36枚だけ2× | 置換 | 数値field/confidence/distanceベースのadaptive quadtree AA | -| HQチェック1個 | 分割 | 表示品質、省電力、数値検証を別設定へ | -| palette変更で全再計算 | 廃止 | field cacheから即時再着色 | -| 固定z=11.5でdeep切替 | 廃止 | pixel step / ULP / orbit errorで切替 | -| 起動直後にdeep pool生成 | 廃止 | 数値的必要時に段階生成 | -| 深部HQ後、各ページセッションでReal Wisdomを能動測定 | 既定では廃止 | 実jobから受動学習。診断時だけ能動測定 | -| detail cache 96件固定 | 廃止 | 管理可能領域のbyte budget LRU | -| 表示Canvasの即時PNGだけ | 変更 | Quick snapshot + 独立High-quality export | -| Base64 WASM 3ファイル直開き | 互換buildへ | Hosted版を標準、Standalone版を併売 | -| SIMD → scalar → JS/BigInt fallback | 維持 | 全backendを同じtile/pixel-center contractへ統一 | -| Reference / perturbation / rebase / BLA | 維持・強化 | 局所精度昇格、動的memory、将来multi-reference | -| world-space cache | 維持・再設計 | style非依存field cache、quadtree key、byte budget | -| base iteration + adaptive slider | 変更 | Auto policyは維持。生値はAdvancedへ移し、未確定数を表示 | -| view hash / URL共有 | 強化 | version付きViewSpec、gesture settle保存、戻る/進む復元 | -| 「∞ Zoom」表記 | 廃止 | 有限の参照長・iteration・precision上限を持つため「Deep Zoom」とし、現在の上限と未確定数を併記 | -| UI初期非表示 | 変更 | 初回だけ操作ヒントを表示し、以後は利用者設定を復元 | - -低優先度の整理対象として、未使用の`MILLION`、`last`、`sx/sy`、未使用`pilot`引数、常にfalseの`strictBoundary`、v22時点で同一内容だったCOLOR SIMD/scalar payloadがある。性能改善というより、仕様とコードのずれを減らすためにテスト後に削除する。実装後の再現buildではCOLOR variantsが異なるため、generatorは同一時だけ自動deduplicateする。 - -## 9. UX・再現性の変更 - -- 一般statusは「プレビュー / 全域描画 68% / 境界AA中 / 完了 / 未確定あり」とする。Covered等の内部用語は診断drawerだけに出す。 -- bit数、BLA ε、Worker数、MPPは診断drawerへ移す。 -- 深部座標表示を17桁固定 (`script.js:114`) にせず、spanから必要桁数を求める。画面上は短縮し、コピー時は正確な値を出す。 -- URL hashをversion付きにし、見た目を再現するiteration profileも保存する。端末依存の解像度やWorker数は保存しない。 -- gesture settleごとに`replaceState`、明示shareで`pushState`、`popstate/hashchange`でviewを復元する。座標/spanの正確値入力・コピーとUndo/Redoも同じViewSpecを使う。 -- `user-scalable=no`を外し、bodyはpage zoom可能にする。Canvasだけに必要なtouch-actionを限定し、アプリのpinch zoomとブラウザpage zoomの範囲を分ける。 -- rangeとlabelを関連付け、toast/statusへ`aria-live`、UI toggleへ`aria-expanded`、visible focus、44px以上のtarget、`prefers-reduced-motion/transparency`対応を付ける。 -- 初回は操作ヒントを表示し、2回目以降はUI表示状態を保存する。 -- 通常利用者には「省電力 / 標準 / 精細」を提示し、生の反復回数はAdvancedへ移す。 -- 700px以下でも進捗、未確定数、Export状態へアクセスできるcompact status sheetを出す。現行のようにstatsとhintをすべて消さない (`index.html:21`)。 - -## 10. 実装ロードマップ - -P0/P1/P2は利用者影響のseverityであり、実装順ではない。各項目はphase、依存先、mandatory / telemetry-conditional、未通過gateを追跡する。後段実装を先行できても、前段gateを通過したとは扱わない。 - -### Phase 0A — browser非依存の証拠基準化 - -- shallow/deep/colorを含む全kernel sourceを回収または復元し、固定toolchain、compile flags、生成script、payload checksumを揃える。 -- z0 / z14 / z20 / z100 / period-3 interior / boundary / 10^-280付近をversion付き固定sceneにする。中心座標はspan相応のguard digitsを持つdecimalかexact rationalとし、precision kind、oracle、期待分類、iteration policyをscene schemaへ保存する。 -- v22成果物が残っている場合は全対象hashとbaselineをimmutableに保存する。残っていない場合は「v22比較不能」と機械可読に記録し、後からv23値で代用しない。 -- 現行`exactDeepPixel`とは独立した、複数精度一致と誤差上限を持つ高精度reference実装で、分類、iteration、smooth valueを比較する。 -- source、ABI、pixel mapping、tile seam、SIMD/scalar、解析的内部証明、Module clone、配布物hash、文書内の生成値を同じcommandで検証する。 -- v18監査はarchiveと明記し、現renderer baselineを別JSONにする。 - -Exit gate: 全kernelをsourceから再現buildでき、version付きsceneとoracleで数値baselineを再生成でき、全成果物hashと文書内の生成値が一致する。過去baseline欠落は明示的な`not-verifiable`でありpassにしない。 - -### Phase 0B — 固定browser baseline - -- desktop、mobile相当、4Kでcold / warmを測る。 -- first preview、Covered完了、Refined完了、idle job/draw、peak memory、long taskを記録する。 -- Hosted / Standalone、SIMD / scalar、Worker / Workerなしを対象構成matrixに従って分けて実行する。 -- viewportのCSS寸法だけでDPRを模擬しない。mobile / desktop / 4Kは要求DPRを持つ別browser contextまたは別実機runとし、各runの実viewport・実DPR・browser build・OS・電源・試行数を保存する。 -- visualはraw pixelまたはlinear-light fieldのversion付きgoldenと比較し、hash文字列が生成できただけではpassにしない。 - -Exit gate: 対象profileごとのbrowser acceptanceが合格し、cold / warm、操作p95、visual、idle、long task、observed memory、Export、accessibilityのraw結果を保存する。binding不在などで未実施の場合、後続実装は進められるがbrowser性能・操作・表示の達成を主張しない。 - -### Phase 1A — Runtime quick wins - -- event-driven paint/stats -- wheel gesture settle -- `visibilitychange`停止 -- deep pool遅延生成 -- Screen Canvas pixel budget -- 初回`q=.3`境界の修正と、既存`q`のままPreview時間予算へ`renderPerf`を接続 -- Quick snapshotに品質状態を表示し、描画中の誤解を防止 -- 固定件数cacheをbyte budgetへ置換 - -Exit gate: idle時に予約RAF/timer/background job 0、wheel中full render 0、浅部だけではdeep compile 0、Canvas/管理可能memoryが予算内。 - -### Phase 1B — Pixel contract - -- 全kernelを画素中心規約へ変更し、cross-backend testを追加する。 -- renderer/golden image versionを上げる。 -- detail BLAをbase HQ以上へ修正する。 - -Exit gate: canonical rationalに対するfixed-point整数一致とf64の宣言ULP基準、tile境界回帰0、既存URLのview中心不変を確認する。 - -### Phase 2 — Tile/field pipeline - -- packed field形式、subsample形式、論理byte budgetを先に固定する。 -- buffer poolを導入し、field追加による一時的なpeak memory悪化を防ぐ。 -- shallowもWorker化し、全backendを共通tile protocolへする。 -- Reprojected / Preview / Covered / Refined状態機械を導入する。 -- palette非依存field bufferと即時再着色を導入する。 -- 処理モード別sample coverage 100%とresolved coverage/未確定数の分離を実装する。 -- ULPベースのshallow/deep切替を導入する。 -- `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を導入し、局所反復継続を実装する。 - -Exit gate: sample coverage 100%、再投影pixelの誤加算0、未確定数の独立計上、false `INTERIOR_PROVEN` 0、logical memory budget内。 - -### Phase 3A — Refine / Export - -- Hosted / Standalone buildを分ける。 -- 独立High-quality export、進捗、取消、metadataを実装する。 -- adaptive AAとlinear-light resolveを導入する。 - -Exit gate: 指定寸法、完了待ち、取消、metadata、許容差内の再現性、Refinedのsample accountingが合格する。 - -### Phase 3B — 計測で必要ならWASM memory再編 - -- deep/BLA/color WASM統合と動的初期容量 -- immutable reference / key別BLA table共有、Worker別scratch -- shared framebufferまたはbuffer poolによるcopy削減 -- 大容量instanceの退役・再生成 - -Exit gate: 対象deep sceneで速度改善を確認し、data race 0、logical/observed peak memoryが予算内。 - -### Phase 4 — 計測後の高精度研究 - -- reference orbit専用Workerと次referenceの先読み -- double-double局所昇格 -- error ball / interval verification -- scaled BLA -- multi-reference perturbation -- WebGPUによるcolor/resolve/composition、必要なら浅部Preview演算 - -Phase 4は、telemetryで支配コストまたは誤差が確認された項目だけ実施する。 - -## 11. 受入基準 - -browser時間値は、profileごとに固定したbrowser build・OS・viewport・実DPR・電源条件で、warmup後30回以上を採取してp95を算出する。単発値、同期event handler時間、異なるsceneを混ぜた分位点は代用しない。Visual golden、数値oracle、threshold policyには独立したversion IDを与え、threshold欠落時はpassにしない。 - -| 分野 | 基準案 | -|---|---| -| Idle | 最終更新2秒後に予約RAF/timer/background job 0、Canvas/DOM write 0回/秒。CPUは同環境blank baselineとの差を補助指標にする | -| 操作 | 最後の実入力eventから、その入力を反映した再投影frameのpaint完了までのp95 < 16ms、計測区間のmain thread 50ms超long task 0、継続wheel中に新規full renderを開始しない | -| Preview | settle期限到達からPreview paint完了まで、profile・sceneごとの30回以上でp95 < 120ms | -| Covered | target Screen Canvasのsample coverage 100%、effective DPRを表示し、再投影pixelをsample coverageへ数えない。resolved coverageとunresolved数を別記録。scene別時間budgetはPhase 0B後に確定 | -| Refined | Fine modeで実際に`REFINED`へ到達し、対象pixelは記録済み実sampleのlinear-light resolve。subsample accountingとversion付きgoldenを比較し、Canvas補間だけをAAと数えない | -| 数値 | version付きcorpus/oracle/threshold policyを必須とする。Validatedではcorpus上のfalse `ESCAPED` 0、false `INTERIOR_PROVEN` 0、unresolved 0。escape iteration差・smooth誤差・`INTERIOR_LIKELY` mismatchはpolicyの明示閾値で判定し、閾値未定義ならfail | -| Pixel mapping | canonical rationalとties-away-from-zeroの`roundDiv`を基準に、fixed-point backendは期待整数と一致、f64 backendは宣言ULP以内。tile境界に1px/半pxの回帰がない | -| Export | 指定寸法どおり、全tile完了前はfinal fileを出さない、取消可能、metadata必須項目とsample countを検査。Balancedはunresolved数を記録、Validatedは0。宣言したbackend/encoder条件とversion付きgoldenの許容差内で再現 | -| Memory | managed typed array/WASM/cacheは暫定mobile 96 MiB、desktop 192 MiB以内。Canvas/GPUを含むobserved peakは固定環境で別記録 | -| Startup | Hosted shallow表示ではdeep取得/compile 0。Standaloneはdeep復号/compileとWorker生成0 | -| 配布 | v22 baseline欠落のため初回比較は`not-verifiable`とする。v23の同一request集合・Content-Encoding・cache条件を新baselineとして固定し、以後の取得bytes・compile timeを回帰判定する。Hostedではdeep到達前のdeep request 0 | -| Accessibility | keyboard-onlyで全操作を実行し、visible focus、44px target、label/name、live status、page zoomとCanvas pinchの共存、reduced motion/transparencyの実動作が合格。静的属性の存在だけではpassにしない | -| Regression | version付き現行scene corpusをCIで実行し、速度・memory・分類・例外を履歴化する。各metricのwarn/fail閾値をpolicyへ保存する | - -端末依存の時間値は、対象端末を固定して初めて合否に使う。絶対時間だけでなく、同一scene・同一pixel数の比率とcorrectnessを主要指標にする。browser非依存gateはsource/ABI/hash/数値oracle/pixel mapping/scene精度/document consistencyまでを証明し、paint、DPR、GPU、入力、download、実accessibilityの代替とはみなさない。 - -## 12. 推奨する最初の実装単位 - -最初の変更セットは、次の範囲に限定すると効果を測りやすい。 - -1. v22 baselineの有無を機械可読に確定し、現renderer baselineと全WASMの再現buildを用意する。 -2. 常時RAFと毎frame DOM更新を廃止する。 -3. wheelをgestureとしてデバウンスし、非表示時のjobを止める。 -4. Screen Canvasへpixel budgetを設ける。 -5. deep poolを必要時まで作らない。 -6. 既存`q`のままPreview時間予算へ`renderPerf`を接続する。 -7. version付き現renderer用のidle・gesture・startup・memory回帰テストを追加する。 - -この段階ではBLAカーネルやpixel contractをまだ変えない。まずidle負荷、入力応答、Canvas/初期memory、Preview時間を改善する。画素中心変更は、全kernelを再現buildできるPhase 1Bで行う。 - -## 付録A: 根拠データ - -- 現行仕様: `README.md` -- 数値・性能監査: `AUDIT.md` -- end-to-end記録: `audit/final-browser-benchmark.json`, `audit/benchmarks.json` -- UI/スケジューラ/renderer: `index.html`, `script.js` -- BLA固定配列・演算: `src/bla_kernel_v18.c` - -既存監査から今後も守るべき知見は次の通り。 - -- 数学的iterationを途中で切って黒扱いしない。 -- pilot画像を未解決pixelの計算結果として使わない。 -- 全stripを毎回strict検証しない。 -- 1点の不一致で全画面を再計算せず、tile/strip単位で補修する。 -- ditherはdetail検出前へ入れず、最終表示段だけにする。 -- Reference recenterは固定距離でなく、実測損失とbuild費を比較する。 - -## 付録B: 用語 - -| 用語 | 本文での意味 | -|---|---| -| BLA | Bilinear Approximation。基準軌道上の複数反復を近似的にまとめて進める高速化 | -| perturbation | 高精度な基準軌道との差分だけを低精度で追跡する摂動法 | -| rebase | 差分が不安定になったとき、基準軌道上の別位置へ基準を移す処理 | -| DPR / effective DPR | OS/browserが示すdevice pixel比 / pixel budget適用後に実際に使う比率 | -| sample / resolved coverage | 選択したScreen Canvas gridのうち、新ViewSpecでpolicy上限まで実計算済みの割合 / `ESCAPED`またはpolicy上の`INTERIOR_PROVEN`へ確定した割合 | -| AA | Anti-Aliasing。1 pixel内の複数sampleをresolveして境界のジャギーを減らす処理 | -| SIMD | 1命令で複数データを処理するWASM最適化 | -| ULP | その浮動小数値付近で表現できる隣接値の間隔 | -| EMA / MPP | 指数移動平均 / millisecond per pixel。時間予算の推定に使う | -| cold / warm | 初回compile・cache未構築 / Worker・reference・BLA cache構築後 | -| confidence | 近似・検証・内部判定の根拠と確からしさを表すmetadata | -| Validated / Certified | 宣言した有限policyを照合済み / 全sampleへ誤差境界または証明手続きを適用済み | - -## 付録C: 互換構成 - -| 構成 | 動作方針 | 主な制限 | -|---|---|---| -| Hosted + SIMD + Worker | 推奨構成。外部WASM、遅延取得、並列tile | Shared memoryは別途COOP/COEPが必要 | -| Hosted、SIMDなし | scalar WASMを必要時に取得 | 速度低下。数値policyは同一 | -| SharedArrayBufferなし | Transferable buffer poolと短いchunkを使用 | hot loop中のatomic cancel、read-only table共有なし | -| Workerなし | main-thread WASMを時間sliceして使用 | 並列なし。Covered/Exportの完了が遅い | -| OffscreenCanvasなし | Workerはfield/RGBA bufferを返し、main Canvasへ合成 | Export encodeと合成の一部がmain thread | -| Standalone / `file://` | 埋込WASM互換build。deepの復号・compileは遅延 | streaming、HTTP cache、COOP/COEP、Service Workerなし | - -最低対応browser versionとoffline cache方針は、Phase 0Bで実機matrixを作って確定する。 diff --git a/MIGRATION_V24.md b/MIGRATION_V24.md deleted file mode 100644 index 0cf0571..0000000 --- a/MIGRATION_V24.md +++ /dev/null @@ -1,157 +0,0 @@ -# v23 → v24.2.3 WebGPU migration / crossover study - -## 目的 - -v23のCPU/WASM deep pixel rendererとWorker poolを廃止し、画素単位のmassively parallel処理をWebGPUへ集約する。CPU高精度処理はBigInt ViewStateとreference orbit生成に限定する。 - -## 最終構成 - -```text -BigInt ViewState - │ - ├─ Direct安全域 ─────────────── WebGPU f32 direct - │ - └─ Deep/overlap - │ - ├─ overlap: current-view GPU quality/speed probe - │ ├─ direct比較 - │ └─ corrected deep比較 - │ - └─ deep: BigInt reference Worker - ↓ - guarded rescaled f32 perturbation - ↓ - UNKNOWN? - / \ - no yes - │ ↓ - │ double-single correction - └────────────┘ - ↓ - GPU field - ↓ - color / AA / present -``` - -## v24.1系からの品質修正 - -v24.1のguarded f32 perturbationは誤分類を避けるためUNKNOWNへ保守退避する設計だった。境界sceneではUNKNOWNが大きく、interiorに近い暗色表示と相まって黒面積増大/黒点として見えた。 - -### Multi-reference評価 - -最初にUNKNOWN macro tileへsecondary BigInt referenceを追加する方式をモデル化した。しかし複数referenceを追加しても`swirly-seahorses-z12`全画面モデルのUNKNOWN回収率が低く、reference生成・転送・再dispatchのコストに見合わなかった。そのためproductionへは採用しない。 - -### Sparse double-single correction - -代わりにUNKNOWNだけをdouble-single complex perturbationで再評価する。reference orbitはprimaryと共用するため追加BigInt orbitは不要。 - -61×39 dense model / 2000 iter: - -- primary UNKNOWN 478 -- primary membership reversal 0 -- correction recovered 478 -- remaining UNKNOWN 0 -- correction membership reversal 0 -- escape iteration mismatch 5(最大374) - -したがってcorrectionは表示品質passとして採用し、membership certificateには使わない。 - -## Backend router再設計 - -旧設計のzoom/ULP単一閾値はdeepを早く使いすぎる一方、閾値を下げるだけではdirectの境界誤分類リスクがある。 - -v24.2.1は三領域に分ける。 - -- ratio < 8: deep必須 -- ratio >= 128: direct -- 8 <= ratio < 128: overlap - -Overlapは本描画前にcurrent-view probeを行う。probeは中心のspeed tileと、画面5領域のquality tileをactual full-resolution pixel座標で評価する。directとDS補正済みdeepのescape/bounded分類差が0、補正後deep UNKNOWN率0.5%以下であることを要求する。そのうえでreference buildを含むdeep予測時間がdirectの85%未満ならdeep、それ以外はdirect。 - -深度binの過去profileだけでbackendを決定しない。 - -## BLAを戻さない理由 - -f64 BLAをf32係数へ量子化した旧試作は、dense swirly回帰でbounded/escaped membership反転を起こした。量子化誤差を含む安全な受入半径が未確立のため、BLA builder/evaluator/nodeはproduction treeに存在しない。 - -## Export - -v24.2.1ではscreenだけでなくExportにもdeep correctionを適用する。 - -1×: - -```text -primary deep → clear stats → DS correction → color → 1 readback -``` - -2×2: - -```text -4 samples × (primary deep → DS correction → color) -→ GPU linear-light resolve -→ 1 readback / tile -``` - -4 sampleのunresolved counterを分離し、sidecarの`unresolvedSamples`へ合計する。 - -## 削除済み - -- deep WASM renderer / Worker pool -- BLA WASM / GPU BLA試作 -- BigInt direct production pixel renderer -- multi-reference production試作 -- redundant strict retry queue -- Canvas2D deep hot path -- giant full-image Canvas Export -- Validated direct UI/Export - -## Validation contract - -productionは`membershipCertified:false`。Strictはprimary error-bound閾値を厳しくするpolicyで、任意精度direct証明ではない。DS correctionもvisual-quality recovery passである。 - - -## v24.2.3 crossover study - -BigInt Deepを廃止するのではなく、使用開始点を後ろへ送れるか検証するためDS Direct候補を追加した。DSはreference構築が不要なためcold latency削減の可能性があるが、WGSL double-singleのコストと品質はGPU依存である。したがってこのreleaseではbenchmark専用とし、実GPU結果を得てからrouterへの昇格を判断する。 - -## v24.2.4 diagnostic Hybrid tier - -Crossover実機値でDS Directはz10/z12でもFull Deepより高速だったが、単独では分類品質が不足した。そこでproduction routerを変更する前に、DS Directのcondition proxy `|dz/dc|` を追跡し、危険pixelだけ既存Deep double-single correctionへ送るHybridをbenchmark専用で実装した。 - -候補routerはまだ採用しない。実験で `class mismatch=0`, `remaining UNKNOWN=0`, `Hybrid cold < 0.9 × Full Deep cold`, `correction rate < 60%` を満たすviewだけを将来のHybrid overlap候補とする。z12のCPU事前モデルではrisk=1e13で約91%が補正対象となるため、Full Deepへ切り替える境界を検出できる見込み。 - - -## v24.2.5 Sparse Direct experiment (Migration) - -- The previous sparse-Full-Deep diagnostic is superseded and is not exposed in the UI. -- Added `DIRECT_TS_CORRECT_WGSL`, a reference-free three-term f32 expansion correction shader that only processes DS sensitivity UNKNOWN pixels. -- Added `benchmarkSparseDirect()` / `renderTileMetaSparseDirect()` and a z6/z8/z10/z12 risk sweep. -- Full Deep remains the quality/timing baseline; the Sparse Direct candidate itself does not build or bind a reference orbit. -- Production backend routing is unchanged. -- Root `index.html` is now a single-file bundle with no external script dependency, specifically to keep diagnostic UI controls functional when the HTML is opened from an archive/temp extraction. -- Static single-file UI contract and real-browser click smoke are separate gates; real WebGPU arithmetic still requires a WebGPU-capable browser. - -- Sparse Direct coordinate packing uses exact residual subtraction from BigInt fixed-point into three f32 components; this removed the CPU TD-model class errors caused by the earlier host-Number split. - -## v24.2.6 experiment direction - -The float-expansion sparse correction branch is superseded by an integer fixed-point experiment. The transition strategy under test is now: - -```text -f32 Direct - -> DS Direct - -> DS risk gate + sparse Q8.88 Direct - -> Full Deep -``` - -Only the diagnostic branch is changed. Production remains on the existing direct/deep router until real-GPU timing and class gates justify promotion. - - -## v24.2.7 experiment correction - -Do not interpret v24.2.6 Fixed96 `remaining-unknown` measurements as a Fixed96 feasibility result. They were dominated by an invalid Q8.88 range guard. v24.2.7 is the first build in this branch whose real-GPU Fixed96 correction measurements are suitable for the crossover experiment. - - -## v24.2.8 experiment scheduling correction - -Fixed96 remains diagnostic and is not part of the production backend router. The correction execution changed from full-tile 2D dispatch with early returns to GPU compact-queue + indirect 1D dispatch. This isolates the experiment's arithmetic feasibility from an avoidable SIMD divergence/scheduling penalty. Adoption decisions must use v24.2.8-or-later timings; v24.2.7 Fixed96 timings are not representative of a sparse implementation. diff --git a/README.md b/README.md deleted file mode 100644 index a10fdb5..0000000 --- a/README.md +++ /dev/null @@ -1,136 +0,0 @@ -# Mandelbrot Deep Zoom v24.2.8 — Fixed96 True Sparse Queue Experiment - -v24.2.8は、v24.2.7で数値バグを修正したFixed96実験を**真のsparse GPU実行**へ変更した診断版です。DS guarded passが危険pixel indexをGPU queueへcompactし、1-thread finalize passがqueue件数からindirect dispatchを生成、Fixed96 Q8.88 kernelはqueueに入ったpixelだけを処理します。guard→queue→indirect Fixed96の間にCPU readbackはありません。production routerはまだ変更していません。 - -## 数値backend - -- **Direct**: WebGPU `f32` direct iteration。 -- **Deep primary**: BigInt reference orbit + WebGPU guarded rescaled `f32` perturbation。 -- **Deep correction**: primaryで`FIELD_UNKNOWN`になったpixelだけ、同じreferenceを用いるdouble-single perturbationで再評価。 -- **BLA**: productionでは未搭載。旧f32量子化BLAでmembership反転を再現したため再導入していません。 -- **Multi-reference**: v24.2開発中に全画面モデルを評価したが、追加referenceの生成/dispatchコストに対してUNKNOWN回収が小さかったためproductionから撤去しました。 - -Deep primaryはroundoff上界を追跡し、escape/bounded判定を安全に受理できないpixelを`UNKNOWN`にします。double-single correctionは表示品質回復用であり、任意精度membership証明ではありません。PNG sidecarの`membershipCertified`は常に`false`です。 - -## Direct / Deep router - -単一のzoom閾値でdeepへ切り替えません。 - -1. `pixel step / f32 ULP < 8`ならdirect座標分解能が不足するためdeep必須。 -2. 比率が128以上ならdirectを即採用。 -3. 8以上128未満のoverlap帯では、本描画前に**現在のview専用GPU probe**を実行。 -4. probeではactual full-resolution座標上の複数領域をdirectと「DS補正済みdeep」で比較。 -5. deep側UNKNOWN率が0.5%以下、escape/bounded分類差0を満たした場合だけ速度比較へ進む。 -6. reference生成時間を含めてもdeepがdirectより15%以上高速な場合だけdeepを採用。それ以外はdirect。 - -過去の同深度profileだけで現在viewをdeepへ振ることはありません。profileは診断/平滑化用で、backend選択には現在viewのquality probeを要求します。 - - -## Crossover benchmark - -診断情報の **Backend比較** から、現在のGPUで z0 / 2 / 4 / 6 / 8 / 10 / 12 / 14 / 16 / 20 / 30 / 40 を自動測定できます。 - -速度測定は全画面を反復実行しません。現在のview座標上の代表3タイル(最大256×144)を測定して `ms/pixel` を求め、現在のcanvas全体へ換算します。品質比較は別の5領域で実施します。これによりz12以降で全画面Deepを何度も実行してGPU queueが長時間塞がる問題を避けます。reference生成または1回のGPU測定が15秒を超えた場合は明示的に中断します。 - -比較するbackend: - -- `f32`: 現行WebGPU標準。 -- `DS`: experimental whole-frame double-single Direct。**production routerには未接続**。 -- `Deep warm`: BigInt referenceをcache済みとしたDeep primary + DS correction。 -- `Deep cold`: verified BigInt reference構築 + Deep warm。 - -結果の時間欄は代表tileからの**全画面換算値**です。画面表示に加えJSON保存できます。品質のBigInt oracle掃引結果は `CROSSOVER_BENCHMARK.md` と `audit/v24-crossover-quality.json` を参照してください。CPUモデルではSeahorse trackでf32がz6付近、DS Directがz10付近からmaterial degradationを示し、DSを中間backendとして試す価値があるという結果です。 - -## Fixed96 Direct experiment - -診断情報の **Fixed96 Direct実験** から z6 / z8 / z10 / z12 を測定できます。DS Directで `|dz/dc|` を追跡し、`1e11 / 1e12 / 1e13 / 1e14` の各risk thresholdを超えたpixelだけ、reference不要の**96-bit符号付きQ8.88固定小数点Direct**で再評価します。 - -v24.2.8では補正対象を`u32` pixel-index queueへcompactします。queue lengthから`dispatchWorkgroupsIndirect()`用の1D workgroup数をGPU内で生成し、Fixed96は**queued pixel数だけ**実行します。従来の「全画面dispatchして非対象laneをreturn」は廃止しました。 - -実験システムは各GPU sampleで次をfail-closed検証します。 - -- `selected == enqueued == dispatchCount == processed` -- queue overflow = 0 -- indirect workgroup数 = `ceil(enqueued / 64)` -- invalid/stale queue entry = 0 -- `corrected + remain == processed` - -1項目でも崩れれば測定値を採用せず実験をエラー停止します。結果表には`correction`と実際の`dispatch`率を別々に表示し、`queue=ok`を確認できます。速度は代表3tileから全画面換算し、品質は5 spatial tilesでcorrected Full Deepと比較します。production routerへは未接続です。 - -### 単一ファイル起動 - -rootの `index.html` は `gpu-kernels.js` と `script.js` を内包した単一HTMLです。ZIP内のHTMLをOSが一時展開して開く場合でも、隣接JSファイルに依存しません。 - -## Deep correction - -primary deepでUNKNOWNが残った場合、通常/精細/Strictではsparse double-single correctionを1回実行します。KNOWN pixelはshader冒頭で即returnするため、数値反復はUNKNOWNだけです。省電力モードはUNKNOWNが一定量を超えた場合のみ補正します。 - -`swirly-seahorses-z12`の61×39 CPU `Math.fround`モデルでは: - -- primary UNKNOWN: 478 / 2379 -- primary false escape: 0 -- primary false bounded: 0 -- DS correction回収: 478 / 478 -- correction後UNKNOWN: 0 -- correction false escape / false bounded: 0 / 0 - -escape iterationは5 pixelでBigInt oracleと差があり、最大差374 iterationでした。このためDS correctionは「見た目と有限iterationの分類回復」であり、Validated相当の保証には使用しません。 - -## UNKNOWN診断 - -内部metadataではUNKNOWN理由を分離しています。 - -- error-bound -- escape-uncertain -- reference-end -- rebase-gap -- range -- operation-limit - -未補正UNKNOWNはinteriorの完全な黒と別色で表示され、診断欄に理由別件数が出ます。 - -## Export - -- 512×512 reusable GPU workspace。 -- deep Exportでもprimary後にDS correctionを実行。 -- 2×2 AAは各4 sampleをGPUで数値計算→補正→彩色し、GPU resolve後に1回だけreadback。 -- 最終画像全体の巨大Canvasは作らず、scanline bandを`CompressionStream('deflate')`へ流すstreaming PNG。 -- 16384px級の片辺をtile処理可能。 -- sidecarに`unresolvedSamples`を記録。 - -## Fallback - -WebGPUそのものが利用できない場合だけ軽量JavaScript f64 fallbackを使います。deep zoomはNumber underflow等で誤画像を出さず、「このズーム深度はWebGPUが必要です」と表示して停止します。shader/pipeline failureではCPU全画面rendererへ自動fallbackしません。 - -## Build / test - -```bash -npm test -npm run build -``` - -`npm test`には以下を含みます。 - -- WGSL reserved-word scan(9 shader。DS Direct / guarded DS / Fixed96 Direct correctionは実験専用) -- production source/tree contract -- BigInt reference Worker + P/P+64 checkpoint -- f32 direct CPU model -- view固有adaptive backend router policy -- guarded primary deep CPU model -- sparse double-single correction CPU model -- BLA disabled contract -- pixel/export geometry -- e400/e1000 exact coordinate formatting -- streaming PNG CRC/deflate/abort -- real-WebGPU acceptance harnessの静的contract - -実GPU受入は `tests/webgpu-acceptance.html` をWebGPU対応ブラウザで開いて実行します。これはdirect/deep/DS/Fixed96 Direct shader compile、DS correction、Fixed96 smoke、dense既知反例、view-specific router、1×/2×2 Export readback、uncaptured WebGPU errorを検査します。 - -## 配布 - -`npm run build`で以下を生成します。 - -- `dist/standalone/` -- `dist/hosted/` - -`dist/standalone/` は単一 `index.html`、`dist/hosted/` は `index.html`, `gpu-kernels.js`, `script.js` です。root `index.html` も単一ファイルで、ZIPから展開してそのまま開けます。 diff --git a/audit/benchmarks.json b/audit/benchmarks.json deleted file mode 100644 index 8d91b89..0000000 --- a/audit/benchmarks.json +++ /dev/null @@ -1,245 +0,0 @@ -{ - "environment_note": "Absolute timings are container/Chromium specific. Use ratios and correctness rates as the main evidence.", - "end_to_end_dynamic_base_comparison": [ - { - "scene": "seahorse_z14", - "v17_base_ms": 1059.0, - "v18_base_ms": 288.0, - "speedup_x": 3.677 - }, - { - "scene": "seahorse_z20", - "v17_base_ms": 1226.0, - "v18_base_ms": 238.0, - "speedup_x": 5.151 - }, - { - "scene": "seahorse_z100", - "v17_base_ms": 983.0, - "v18_base_ms": 474.0, - "speedup_x": 2.074 - }, - { - "scene": "period3_z20", - "v17_base_ms": 1931.0, - "v18_base_ms": 781.0, - "speedup_x": 2.472 - } - ], - "v17_work_cap_correctness": { - "w": 48, - "h": 30, - "pixels": 1440, - "globalIter": 3490, - "capIter": 2870, - "capped": 1440, - "late": 1066, - "insideGlobal": 374, - "lateEscapeRateAmongCapped": 0.7402777777777778, - "elapsedMs": 6344.968723999999 - }, - "pilot_nearest_fill_accuracy": { - "full": [ - 128, - 78 - ], - "pilot": [ - 56, - 34 - ], - "fullMs": 1356.671812, - "pilotMs": 169.93951399999992, - "classMismatch": 451, - "classRate": 0.04517227564102564, - "escapeIterDiffGt8": 4364, - "diffRate": 0.437099358974359 - }, - "intrinsic_interior_detection": [ - { - "name": "period3 wide", - "re": "-0.1225611668766536", - "im": "0.7448617666197442", - "span": 0.1, - "offMs": 766.7149390000001, - "onMs": 5.955513999999994, - "interior": 6912, - "classMismatch": 0, - "diffGt1": 0, - "total": 6912 - }, - { - "name": "period3 mid", - "re": "-0.1225611668766536", - "im": "0.7448617666197442", - "span": 0.03, - "offMs": 438.65803500000015, - "onMs": 2.334043000000065, - "interior": 6912, - "classMismatch": 0, - "diffGt1": 0, - "total": 6912 - }, - { - "name": "period2 boundary neighborhood", - "re": "-0.75", - "im": "0", - "span": 0.1, - "offMs": 364.72781799999984, - "onMs": 1.0177550000000792, - "interior": 6806, - "classMismatch": 0, - "diffGt1": 0, - "total": 6912 - }, - { - "name": "mixed exterior/interior", - "re": "-0.5", - "im": "0.5", - "span": 0.1, - "offMs": 282.849144, - "onMs": 16.219244000000117, - "interior": 5338, - "classMismatch": 0, - "diffGt1": 0, - "total": 6912 - }, - { - "name": "cardioid cusp neighborhood", - "re": "0.25", - "im": "0", - "span": 0.04, - "offMs": 329.88844100000006, - "onMs": 1.7636029999998755, - "interior": 6403, - "classMismatch": 0, - "diffGt1": 0, - "total": 6912 - } - ], - "reference_recenter_z14": { - "oldRefBuildMs": 29.320617, - "tests": [ - { - "sx": 0.5, - "sy": 0, - "oldMs": 691.804547, - "newMs": 640.2659849999995, - "newRefBuildMs": 9.508285999999998, - "firstFrameNetSavingMs": 42.03027600000041, - "steadySavingMs": 51.53856200000041 - }, - { - "sx": 0.8, - "sy": 0, - "oldMs": 756.002915, - "newMs": 655.9519419999997, - "newRefBuildMs": 8.81105800000023, - "firstFrameNetSavingMs": 91.23991500000011, - "steadySavingMs": 100.05097300000034 - }, - { - "sx": 0, - "sy": 0.8, - "oldMs": 730.6417070000007, - "newMs": 671.6106240000008, - "newRefBuildMs": 8.391966000001048, - "firstFrameNetSavingMs": 50.639116999998805, - "steadySavingMs": 59.03108299999985 - }, - { - "sx": 0.8, - "sy": 0.5, - "oldMs": 787.9931480000014, - "newMs": 740.047842, - "newRefBuildMs": 7.513370000000577, - "firstFrameNetSavingMs": 40.43193600000086, - "steadySavingMs": 47.94530600000144 - }, - { - "sx": 1.2, - "sy": 0, - "oldMs": 705.7068249999975, - "newMs": 689.7542820000017, - "newRefBuildMs": 7.101795999999013, - "firstFrameNetSavingMs": 8.850746999996773, - "steadySavingMs": 15.952542999995785 - }, - { - "sx": 1.5, - "sy": 0.8, - "oldMs": 793.3721589999986, - "newMs": 733.093237000001, - "newRefBuildMs": 7.395623000000342, - "firstFrameNetSavingMs": 52.88329899999735, - "steadySavingMs": 60.27892199999769 - } - ] - }, - "reference_recenter_z20": { - "oldRefBuildMs": 23.925890999999993, - "tests": [ - { - "sx": 0.5, - "sy": 0, - "oldMs": 100.18383699999998, - "newMs": 105.04222300000004, - "newRefBuildMs": 9.637189000000006, - "firstFrameNetSavingMs": -14.49557500000006, - "steadySavingMs": -4.858386000000053 - }, - { - "sx": 0.8, - "sy": 0, - "oldMs": 106.89347099999986, - "newMs": 93.24754299999995, - "newRefBuildMs": 8.222010999999952, - "firstFrameNetSavingMs": 5.42391699999996, - "steadySavingMs": 13.645927999999913 - }, - { - "sx": 0, - "sy": 0.8, - "oldMs": 110.3922050000001, - "newMs": 98.40486799999985, - "newRefBuildMs": 7.7547400000000835, - "firstFrameNetSavingMs": 4.232597000000169, - "steadySavingMs": 11.987337000000252 - }, - { - "sx": 0.8, - "sy": 0.5, - "oldMs": 138.4319349999996, - "newMs": 102.46810800000003, - "newRefBuildMs": 8.225470999999743, - "firstFrameNetSavingMs": 27.73835599999984, - "steadySavingMs": 35.96382699999958 - }, - { - "sx": 1.2, - "sy": 0, - "oldMs": 111.11085400000002, - "newMs": 93.51345999999967, - "newRefBuildMs": 8.248458000000028, - "firstFrameNetSavingMs": 9.348936000000322, - "steadySavingMs": 17.59739400000035 - }, - { - "sx": 1.5, - "sy": 0.8, - "oldMs": 126.42733499999986, - "newMs": 95.50466299999971, - "newRefBuildMs": 8.381738999999925, - "firstFrameNetSavingMs": 22.540933000000223, - "steadySavingMs": 30.922672000000148 - } - ] - }, - "code_metrics": { - "v17_script_lines": 1067, - "v17_script_bytes": 143149, - "v18_script_lines": 805, - "v18_script_bytes": 83427, - "v18_kernels_lines": 12, - "v18_kernels_bytes": 52136 - } -} \ No newline at end of file diff --git a/audit/final-browser-benchmark.json b/audit/final-browser-benchmark.json deleted file mode 100644 index d2986bf..0000000 --- a/audit/final-browser-benchmark.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "environment": "Chromium 144 headless, 780x441 viewport, container-local", - "scenes": [ - { - "name": "seahorse_z14", - "bits": 288, - "first": { - "render": "789 ms / 3490 回 · BLA 54.395ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.7424271106719971 - }, - "base": { - "render": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.28958845138549805 - }, - "engine": "WASM SIMD ×2 · BLA e-28 + リベース / 288 bit", - "renderText": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "seahorse_z20", - "bits": 320, - "first": { - "render": "401 ms / 4509 回 · BLA 5.023ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.4155466556549072 - }, - "base": { - "render": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.26917600631713867 - }, - "engine": "WASM SIMD ×2 · BLA e-23 + リベース / 320 bit", - "renderText": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "seahorse_z100", - "bits": 576, - "first": { - "render": "482 ms / 17085 回 · BLA 0.076ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.5742058753967285 - }, - "base": { - "render": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.5136466026306152 - }, - "engine": "WASM SIMD ×2 · BLA e-23 + リベース / 576 bit", - "renderText": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "period3_z20", - "bits": 320, - "first": { - "render": "1420 ms / 4509 回 · BLA 0.395ms/kpx · 内部省略 100%", - "badge": "深部リベース", - "saw": true, - "wall": 1.5150198936462402 - }, - "base": { - "render": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%", - "badge": "深部リベース", - "saw": true, - "wall": 0.8927633762359619 - }, - "engine": "WASM SIMD ×2 · BLA e-23 + リベース · 内部早期終了 100% / 320 bit", - "renderText": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%", - "canvas": "780x441" - } - ], - "errorCount": 0 -} \ No newline at end of file diff --git a/audit/legacy-v23/final-browser-benchmark.json b/audit/legacy-v23/final-browser-benchmark.json deleted file mode 100644 index d2986bf..0000000 --- a/audit/legacy-v23/final-browser-benchmark.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "environment": "Chromium 144 headless, 780x441 viewport, container-local", - "scenes": [ - { - "name": "seahorse_z14", - "bits": 288, - "first": { - "render": "789 ms / 3490 回 · BLA 54.395ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.7424271106719971 - }, - "base": { - "render": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.28958845138549805 - }, - "engine": "WASM SIMD ×2 · BLA e-28 + リベース / 288 bit", - "renderText": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "seahorse_z20", - "bits": 320, - "first": { - "render": "401 ms / 4509 回 · BLA 5.023ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.4155466556549072 - }, - "base": { - "render": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.26917600631713867 - }, - "engine": "WASM SIMD ×2 · BLA e-23 + リベース / 320 bit", - "renderText": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "seahorse_z100", - "bits": 576, - "first": { - "render": "482 ms / 17085 回 · BLA 0.076ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.5742058753967285 - }, - "base": { - "render": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.5136466026306152 - }, - "engine": "WASM SIMD ×2 · BLA e-23 + リベース / 576 bit", - "renderText": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "period3_z20", - "bits": 320, - "first": { - "render": "1420 ms / 4509 回 · BLA 0.395ms/kpx · 内部省略 100%", - "badge": "深部リベース", - "saw": true, - "wall": 1.5150198936462402 - }, - "base": { - "render": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%", - "badge": "深部リベース", - "saw": true, - "wall": 0.8927633762359619 - }, - "engine": "WASM SIMD ×2 · BLA e-23 + リベース · 内部早期終了 100% / 320 bit", - "renderText": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%", - "canvas": "780x441" - } - ], - "errorCount": 0 -} \ No newline at end of file diff --git a/audit/raw/audit_v17_algos_results.json b/audit/raw/audit_v17_algos_results.json deleted file mode 100644 index 84dd51c..0000000 --- a/audit/raw/audit_v17_algos_results.json +++ /dev/null @@ -1,168 +0,0 @@ -[ - { - "z": 40, - "global": 7752, - "comp": 4300, - "refBuildMs": 18.262529999999998, - "refLen": 3246, - "fullBlaMs": 5.190414000000004, - "blaBuildMs": 2.432442000000009, - "strictMs": 0, - "blaStrict": { - "valid": 41340, - "classMismatch": 0, - "classRate": 0, - "diffGt1": 0, - "diffRate": 0, - "bad": 0, - "maxDiff": 0 - }, - "work": { - "unresolved": 0, - "unresolvedRate": 0, - "lateEscape": 0, - "lateEscapeRate": 0, - "pilotClassMismatch": 0, - "pilotClassMismatchRate": 0, - "meanIterErr": 0, - "maxIterErr": 0 - }, - "safe": { - "safeRatio": 0, - "periodicMs": 0.08044799999998986, - "blackRatio": 0, - "interiorRatio": 0 - }, - "safeAcc": { - "skipped": 0, - "ratio": 0, - "falseEscaped": 0, - "falseRate": 0 - }, - "safeRun": { - "buildMs": 0.15029499999999985, - "renderMs": 1.6046879999999817, - "pixels": 41340, - "tasks": 34 - }, - "eps": [ - { - "e": 18, - "ms": 1.3821410000000185, - "build": 0.14597599999999034 - }, - { - "e": 20, - "ms": 1.4494080000000054, - "build": 0.09218199999997978 - }, - { - "e": 23, - "ms": 1.6922059999999988, - "build": 0.09046299999999974 - }, - { - "e": 27, - "ms": 1.456985000000003, - "build": 0.09038200000000529 - }, - { - "e": 32, - "ms": 1.3537540000000092, - "build": 0.0893779999999822 - } - ], - "recenter": { - "oldRefLen": 3246, - "newRefLen": 3246, - "newBuildMs": 10.052744000000018, - "oldRenderMs": 1.324105999999972, - "newRenderMs": 1.4144129999999961, - "benefitMs": -0.09030700000002412, - "netFirstFrame": -10.143051000000042 - } - }, - { - "z": 100, - "global": 17085, - "comp": 7600, - "refBuildMs": 9.734955000000014, - "refLen": 3246, - "fullBlaMs": 1.3740439999999978, - "blaBuildMs": 0.10230300000000625, - "strictMs": 0, - "blaStrict": { - "valid": 41340, - "classMismatch": 0, - "classRate": 0, - "diffGt1": 0, - "diffRate": 0, - "bad": 0, - "maxDiff": 0 - }, - "work": { - "unresolved": 0, - "unresolvedRate": 0, - "lateEscape": 0, - "lateEscapeRate": 0, - "pilotClassMismatch": 0, - "pilotClassMismatchRate": 0, - "meanIterErr": 0, - "maxIterErr": 0 - }, - "safe": { - "safeRatio": 0, - "periodicMs": 0.06870000000000687, - "blackRatio": 0, - "interiorRatio": 0 - }, - "safeAcc": { - "skipped": 0, - "ratio": 0, - "falseEscaped": 0, - "falseRate": 0 - }, - "safeRun": { - "buildMs": 0.11969400000000974, - "renderMs": 1.3758139999999912, - "pixels": 41340, - "tasks": 34 - }, - "eps": [ - { - "e": 18, - "ms": 1.3486680000000035, - "build": 0.08054500000000075 - }, - { - "e": 20, - "ms": 1.3468280000000163, - "build": 0.09072599999998943 - }, - { - "e": 23, - "ms": 1.3401210000000106, - "build": 0.0852779999999882 - }, - { - "e": 27, - "ms": 1.6213699999999847, - "build": 0.08418900000000917 - }, - { - "e": 32, - "ms": 1.4460180000000094, - "build": 0.08714600000001838 - } - ], - "recenter": { - "oldRefLen": 3246, - "newRefLen": 3246, - "newBuildMs": 8.042026000000021, - "oldRenderMs": 1.3196079999999881, - "newRenderMs": 1.3709360000000004, - "benefitMs": -0.05132800000001225, - "netFirstFrame": -8.093354000000033 - } - } -] \ No newline at end of file diff --git a/audit/raw/audit_v18_kernel.txt b/audit/raw/audit_v18_kernel.txt deleted file mode 100644 index 4f0004d..0000000 --- a/audit/raw/audit_v18_kernel.txt +++ /dev/null @@ -1,142 +0,0 @@ - - seahorse-z14 iter 3490 ref 3246 build 21.53 baseline 1807.84 -{ e: 23, b: 0, p: 0, int: 0 } ms 299.14 { - bla: 3043732, - ptb: 11197405, - rebase: 72197, - interior: 0, - unresolved: 0, - fail: 0 -} cmp { - cls: 157, - clsRate: 0.010012755102040817, - diff: 820, - diffRate: 0.05229591836734694, - bad: 0 -} -{ e: 23, b: 1600, p: 2200, int: 1 } ms 373.69 { - bla: 3043732, - ptb: 11197405, - rebase: 72197, - interior: 0, - unresolved: 0, - fail: 0 -} cmp { - cls: 157, - clsRate: 0.010012755102040817, - diff: 820, - diffRate: 0.05229591836734694, - bad: 0 -} -{ e: 23, b: 800, p: 1200, int: 1 } ms 398.19 { - bla: 3043729, - ptb: 11189659, - rebase: 71797, - interior: 0, - unresolved: 107, - fail: 0 -} cmp { - cls: 131, - clsRate: 0.008354591836734693, - diff: 747, - diffRate: 0.04764030612244898, - bad: 107 -} -{ e: 32, b: 1600, p: 2200, int: 1 } ms 785.73 { - bla: 3016859, - ptb: 26792628, - rebase: 72089, - interior: 0, - unresolved: 54, - fail: 0 -} cmp { - cls: 60, - clsRate: 0.003826530612244898, - diff: 182, - diffRate: 0.011607142857142858, - bad: 54 -} -{ e: 40, b: 0, p: 0, int: 1 } ms 1217.85 { - bla: 1091101, - ptb: 44427881, - rebase: 72170, - interior: 0, - unresolved: 0, - fail: 0 -} cmp { - cls: 13, - clsRate: 0.0008290816326530612, - diff: 31, - diffRate: 0.0019770408163265305, - bad: 0 -} - - seahorse-z20 iter 4509 ref 3246 build 14.47 baseline 383.51 -{ e: 23, b: 0, p: 0, int: 0 } ms 52.83 { - bla: 605601, - ptb: 2085586, - rebase: 0, - interior: 0, - unresolved: 0, - fail: 0 -} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 23, b: 1600, p: 2200, int: 1 } ms 60.84 { - bla: 605601, - ptb: 2085586, - rebase: 0, - interior: 0, - unresolved: 0, - fail: 0 -} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 23, b: 800, p: 1200, int: 1 } ms 61.15 { - bla: 605601, - ptb: 2085586, - rebase: 0, - interior: 0, - unresolved: 0, - fail: 0 -} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 32, b: 1600, p: 2200, int: 1 } ms 138.45 { - bla: 1253140, - ptb: 4763652, - rebase: 0, - interior: 0, - unresolved: 0, - fail: 0 -} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 40, b: 0, p: 0, int: 1 } ms 253.84 { - bla: 2443606, - ptb: 8374654, - rebase: 0, - interior: 0, - unresolved: 0, - fail: 0 -} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } - - period2-z20 iter 4509 ref 4509 build 4.04 baseline 1552.21 -{ e: 23, b: 0, p: 0, int: 0 } ms 1507.19 { - bla: 0, - ptb: 70701120, - rebase: 0, - interior: 0, - unresolved: 0, - fail: 0 -} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 23, b: 1600, p: 2200, int: 1 } ms 0.20 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 23, b: 800, p: 1200, int: 1 } ms 0.12 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 32, b: 1600, p: 2200, int: 1 } ms 0.12 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 40, b: 0, p: 0, int: 1 } ms 0.16 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } - - main-z20 iter 4509 ref 4509 build 9.77 baseline 5.36 -{ e: 23, b: 0, p: 0, int: 0 } ms 2.83 { - bla: 141120, - ptb: 15680, - rebase: 0, - interior: 0, - unresolved: 0, - fail: 0 -} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 23, b: 1600, p: 2200, int: 1 } ms 0.15 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 23, b: 800, p: 1200, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 32, b: 1600, p: 2200, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } -{ e: 40, b: 0, p: 0, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 } diff --git a/audit/raw/dev-browser-benchmark.json b/audit/raw/dev-browser-benchmark.json deleted file mode 100644 index fb51dba..0000000 --- a/audit/raw/dev-browser-benchmark.json +++ /dev/null @@ -1,132 +0,0 @@ -[ - { - "name": "seahorse-z14", - "results": [ - { - "r": false, - "last": 693.8000000007451, - "e": "WASM SIMD ×2 · BLA e-28 + rebase · 局所補修 6", - "f": 1, - "px": 12288, - "k": 690.1000000052154, - "rep": 0.3, - "un": 0, - "int": 0, - "bla": 162.36328125, - "ptb": 1876.836181640625, - "bits": 512, - "q": 0.58 - }, - { - "r": false, - "last": 1999.9000000022352, - "e": "WASM SIMD ×2 · BLA e-32 + rebase · 局所補修 12", - "f": 2, - "px": 36300, - "k": 2172.9000000059605, - "rep": 0.36363636363636365, - "un": 0, - "int": 0, - "bla": 135.99895316804407, - "ptb": 2132.273305785124, - "bits": 512, - "q": 1 - } - ] - }, - { - "name": "seahorse-z20", - "results": [ - { - "r": false, - "last": 388.79999999701977, - "e": "WASM SIMD ×2 · BLA e-23 + rebase", - "f": 3, - "px": 72463, - "k": 330.9000000022352, - "rep": 0, - "un": 0, - "int": 0, - "bla": 38.68238963333011, - "ptb": 133.7588424437299, - "bits": 512, - "q": 0.58 - }, - { - "r": false, - "last": 1732.699999999255, - "e": "WASM SIMD ×2 · BLA e-32 + rebase", - "f": 4, - "px": 134091, - "k": 1531.5999999977648, - "rep": 0, - "un": 0, - "int": 0, - "bla": 82.81766114056872, - "ptb": 306.63208567316224, - "bits": 512, - "q": 1 - } - ] - }, - { - "name": "period3-z20", - "results": [ - { - "r": false, - "last": 174.90000000223517, - "e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%", - "f": 5, - "px": 202800, - "k": 67.5, - "rep": 0, - "un": 0, - "int": 1, - "bla": 2, - "ptb": 8, - "bits": 512, - "q": 0.58 - } - ] - }, - { - "name": "period2-z20", - "results": [ - { - "r": false, - "last": 49.19999999925494, - "e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%", - "f": 6, - "px": 202800, - "k": 4.399999998509884, - "rep": 0, - "un": 0, - "int": 1, - "bla": 0, - "ptb": 0, - "bits": 512, - "q": 0.58 - } - ] - }, - { - "name": "cardioid-z20", - "results": [ - { - "r": false, - "last": 33, - "e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%", - "f": 7, - "px": 43200, - "k": 0.5999999977648258, - "rep": 0, - "un": 0, - "int": 1, - "bla": 0, - "ptb": 0, - "bits": 512, - "q": 0.58 - } - ] - } -] \ No newline at end of file diff --git a/audit/raw/dev-verification-benchmark.json b/audit/raw/dev-verification-benchmark.json deleted file mode 100644 index 0b92e50..0000000 --- a/audit/raw/dev-verification-benchmark.json +++ /dev/null @@ -1,77 +0,0 @@ -[ - { - "z": 14, - "runs": [ - { - "r": false, - "last": 528.9000000022352, - "e": "WASM SIMD \u00d72 \u00b7 BLA e-28 + rebase \u00b7 \u5c40\u6240\u88dc\u4fee 1", - "f": 1, - "px": 12288, - "k": 531.7000000104308, - "pilot": 111.89999999850988, - "rep": 0.05 - }, - { - "r": false, - "last": 332.6000000014901, - "e": "WASM SIMD \u00d72 \u00b7 BLA e-28 + rebase", - "f": 2, - "px": 12288, - "k": 472.99999998882413, - "pilot": 111.89999999850988, - "rep": 0 - } - ] - }, - { - "z": 20, - "runs": [ - { - "r": false, - "last": 322.1000000014901, - "e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase", - "f": 3, - "px": 56444, - "k": 340.30000000447035, - "pilot": 6.699999999254942, - "rep": 0 - }, - { - "r": false, - "last": 216.40000000223517, - "e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase", - "f": 4, - "px": 45510, - "k": 218.69999999925494, - "pilot": 6.699999999254942, - "rep": 0 - } - ] - }, - { - "z": 100, - "runs": [ - { - "r": false, - "last": 33.70000000298023, - "e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase", - "f": 5, - "px": 202800, - "k": 16.599999994039536, - "pilot": 0.09999999776482582, - "rep": 0 - }, - { - "r": false, - "last": 46.099999997764826, - "e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase", - "f": 6, - "px": 202800, - "k": 10.899999998509884, - "pilot": 0.09999999776482582, - "rep": 0 - } - ] - } -] \ No newline at end of file diff --git a/audit/raw/dev-warm-cache-benchmark.json b/audit/raw/dev-warm-cache-benchmark.json deleted file mode 100644 index 468d579..0000000 --- a/audit/raw/dev-warm-cache-benchmark.json +++ /dev/null @@ -1,44 +0,0 @@ -[ - { - "z": 20, - "runs": [ - { - "r": false, - "last": 1422.0999999977648, - "f": 1, - "px": 35371, - "k": 170.7000000141561, - "pilot": 10.700000002980232 - }, - { - "r": false, - "last": 1080.1000000014901, - "f": 2, - "px": 12288, - "k": 57.79999999701977, - "pilot": 10.700000002980232 - } - ] - }, - { - "z": 100, - "runs": [ - { - "r": false, - "last": 3249.7000000029802, - "f": 3, - "px": 202800, - "k": 17.099999982863665, - "pilot": 0.19999999925494194 - }, - { - "r": false, - "last": 1368.800000000745, - "f": 4, - "px": 16170, - "k": 2.2000000029802322, - "pilot": 0.19999999925494194 - } - ] - } -] \ No newline at end of file diff --git a/audit/raw/interior_safety_v18.json b/audit/raw/interior_safety_v18.json deleted file mode 100644 index d233e7d..0000000 --- a/audit/raw/interior_safety_v18.json +++ /dev/null @@ -1,62 +0,0 @@ -[ - { - "name": "period3 wide", - "re": "-0.1225611668766536", - "im": "0.7448617666197442", - "span": 0.1, - "offMs": 766.7149390000001, - "onMs": 5.955513999999994, - "interior": 6912, - "classMismatch": 0, - "diffGt1": 0, - "total": 6912 - }, - { - "name": "period3 mid", - "re": "-0.1225611668766536", - "im": "0.7448617666197442", - "span": 0.03, - "offMs": 438.65803500000015, - "onMs": 2.334043000000065, - "interior": 6912, - "classMismatch": 0, - "diffGt1": 0, - "total": 6912 - }, - { - "name": "period2 boundary neighborhood", - "re": "-0.75", - "im": "0", - "span": 0.1, - "offMs": 364.72781799999984, - "onMs": 1.0177550000000792, - "interior": 6806, - "classMismatch": 0, - "diffGt1": 0, - "total": 6912 - }, - { - "name": "mixed exterior/interior", - "re": "-0.5", - "im": "0.5", - "span": 0.1, - "offMs": 282.849144, - "onMs": 16.219244000000117, - "interior": 5338, - "classMismatch": 0, - "diffGt1": 0, - "total": 6912 - }, - { - "name": "cardioid cusp neighborhood", - "re": "0.25", - "im": "0", - "span": 0.04, - "offMs": 329.88844100000006, - "onMs": 1.7636029999998755, - "interior": 6403, - "classMismatch": 0, - "diffGt1": 0, - "total": 6912 - } -] diff --git a/audit/raw/mb_audit_v17_results.json b/audit/raw/mb_audit_v17_results.json deleted file mode 100644 index 619f5c6..0000000 --- a/audit/raw/mb_audit_v17_results.json +++ /dev/null @@ -1,98 +0,0 @@ -[ - { - "exp": 14, - "setup": { - "bits": 768, - "z": 14, - "iter": 3490 - }, - "base": { - "wall": 7315.300000000745, - "lastRender": 7292.300000000745, - "w": 260, - "h": 159, - "pilotMs": 0, - "kernelMPP": 0, - "interiorSkip": 0, - "black": 0, - "bad": 0, - "engine": "WASM SIMD · main-thread rebase · skip 998", - "z": 14, - "iter": 3490, - "compute": 2870 - }, - "safe": null - }, - { - "exp": 20, - "setup": { - "bits": 768, - "z": 20.00000000000001, - "iter": 4509 - }, - "base": { - "wall": 3576.60000000149, - "lastRender": 3555.39999999851, - "w": 260, - "h": 159, - "pilotMs": 0, - "kernelMPP": 0, - "interiorSkip": 0, - "black": 0, - "bad": 0, - "engine": "WASM SIMD · main-thread rebase · skip 998", - "z": 20.00000000000001, - "iter": 4509, - "compute": 3200 - }, - "safe": null - }, - { - "exp": 40, - "setup": { - "bits": 768, - "z": 40.00000000000002, - "iter": 7752 - }, - "base": { - "wall": 3800.199999999255, - "lastRender": 3782.599999997765, - "w": 260, - "h": 159, - "pilotMs": 0, - "kernelMPP": 0, - "interiorSkip": 0, - "black": 0, - "bad": 0, - "engine": "WASM SIMD · main-thread rebase · skip 998", - "z": 40.00000000000002, - "iter": 7752, - "compute": 4300 - }, - "safe": null - }, - { - "exp": 100, - "setup": { - "bits": 768, - "z": 100, - "iter": 17085 - }, - "base": { - "wall": 3633.7999999970198, - "lastRender": 3622, - "w": 260, - "h": 159, - "pilotMs": 0, - "kernelMPP": 0, - "interiorSkip": 0, - "black": 0, - "bad": 0, - "engine": "WASM SIMD · main-thread rebase · skip 998", - "z": 100, - "iter": 17085, - "compute": 7600 - }, - "safe": null - } -] \ No newline at end of file diff --git a/audit/raw/pilot_fill_audit.json b/audit/raw/pilot_fill_audit.json deleted file mode 100644 index 58ffed3..0000000 --- a/audit/raw/pilot_fill_audit.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "full": [ - 128, - 78 - ], - "pilot": [ - 56, - 34 - ], - "fullMs": 1356.671812, - "pilotMs": 169.93951399999992, - "classMismatch": 451, - "classRate": 0.04517227564102564, - "escapeIterDiffGt8": 4364, - "diffRate": 0.437099358974359 -} diff --git a/audit/raw/reference_recenter_audit.json b/audit/raw/reference_recenter_audit.json deleted file mode 100644 index 2d0b73c..0000000 --- a/audit/raw/reference_recenter_audit.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "oldRefBuildMs": 29.320617, - "tests": [ - { - "sx": 0.5, - "sy": 0, - "oldMs": 691.804547, - "newMs": 640.2659849999995, - "newRefBuildMs": 9.508285999999998, - "firstFrameNetSavingMs": 42.03027600000041, - "steadySavingMs": 51.53856200000041 - }, - { - "sx": 0.8, - "sy": 0, - "oldMs": 756.002915, - "newMs": 655.9519419999997, - "newRefBuildMs": 8.81105800000023, - "firstFrameNetSavingMs": 91.23991500000011, - "steadySavingMs": 100.05097300000034 - }, - { - "sx": 0, - "sy": 0.8, - "oldMs": 730.6417070000007, - "newMs": 671.6106240000008, - "newRefBuildMs": 8.391966000001048, - "firstFrameNetSavingMs": 50.639116999998805, - "steadySavingMs": 59.03108299999985 - }, - { - "sx": 0.8, - "sy": 0.5, - "oldMs": 787.9931480000014, - "newMs": 740.047842, - "newRefBuildMs": 7.513370000000577, - "firstFrameNetSavingMs": 40.43193600000086, - "steadySavingMs": 47.94530600000144 - }, - { - "sx": 1.2, - "sy": 0, - "oldMs": 705.7068249999975, - "newMs": 689.7542820000017, - "newRefBuildMs": 7.101795999999013, - "firstFrameNetSavingMs": 8.850746999996773, - "steadySavingMs": 15.952542999995785 - }, - { - "sx": 1.5, - "sy": 0.8, - "oldMs": 793.3721589999986, - "newMs": 733.093237000001, - "newRefBuildMs": 7.395623000000342, - "firstFrameNetSavingMs": 52.88329899999735, - "steadySavingMs": 60.27892199999769 - } - ] -} diff --git a/audit/raw/reference_recenter_audit_z20.json b/audit/raw/reference_recenter_audit_z20.json deleted file mode 100644 index 60d366b..0000000 --- a/audit/raw/reference_recenter_audit_z20.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "oldRefBuildMs": 23.925890999999993, - "tests": [ - { - "sx": 0.5, - "sy": 0, - "oldMs": 100.18383699999998, - "newMs": 105.04222300000004, - "newRefBuildMs": 9.637189000000006, - "firstFrameNetSavingMs": -14.49557500000006, - "steadySavingMs": -4.858386000000053 - }, - { - "sx": 0.8, - "sy": 0, - "oldMs": 106.89347099999986, - "newMs": 93.24754299999995, - "newRefBuildMs": 8.222010999999952, - "firstFrameNetSavingMs": 5.42391699999996, - "steadySavingMs": 13.645927999999913 - }, - { - "sx": 0, - "sy": 0.8, - "oldMs": 110.3922050000001, - "newMs": 98.40486799999985, - "newRefBuildMs": 7.7547400000000835, - "firstFrameNetSavingMs": 4.232597000000169, - "steadySavingMs": 11.987337000000252 - }, - { - "sx": 0.8, - "sy": 0.5, - "oldMs": 138.4319349999996, - "newMs": 102.46810800000003, - "newRefBuildMs": 8.225470999999743, - "firstFrameNetSavingMs": 27.73835599999984, - "steadySavingMs": 35.96382699999958 - }, - { - "sx": 1.2, - "sy": 0, - "oldMs": 111.11085400000002, - "newMs": 93.51345999999967, - "newRefBuildMs": 8.248458000000028, - "firstFrameNetSavingMs": 9.348936000000322, - "steadySavingMs": 17.59739400000035 - }, - { - "sx": 1.5, - "sy": 0.8, - "oldMs": 126.42733499999986, - "newMs": 95.50466299999971, - "newRefBuildMs": 8.381738999999925, - "firstFrameNetSavingMs": 22.540933000000223, - "steadySavingMs": 30.922672000000148 - } - ] -} diff --git a/audit/raw/v17-final-browser-benchmark.json b/audit/raw/v17-final-browser-benchmark.json deleted file mode 100644 index 857e615..0000000 --- a/audit/raw/v17-final-browser-benchmark.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "environment": "Chromium 144 headless, 780x441 viewport, container-local", - "scenes": [ - { - "name": "seahorse_z14", - "bits": 288, - "first": { - "render": "835 ms / 3490 回 · BLA 8.034ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.9532754421234131 - }, - "base": { - "render": "1059 ms / 3490 回 · BLA 9.226ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 1.098907470703125 - }, - "engine": "WASM SIMD ×2 常駐Worker · BLA e-23 + リベース · スキップ 998 / 288 bit", - "renderText": "1059 ms / 3490 回 · BLA 9.226ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "seahorse_z20", - "bits": 320, - "first": { - "render": "1574 ms / 4509 回 · BLA 1.891ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 1.7402434349060059 - }, - "base": { - "render": "1226 ms / 4509 回 · BLA 2.126ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 1.3763623237609863 - }, - "engine": "WASM SIMD ×2 常駐Worker · BLA e-25 + リベース · スキップ 3231 / 320 bit", - "renderText": "1226 ms / 4509 回 · BLA 2.126ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "seahorse_z100", - "bits": 576, - "first": { - "render": "1224 ms / 17085 回 · BLA 0.104ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 1.4651060104370117 - }, - "base": { - "render": "983 ms / 17085 回 · BLA 0.097ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 1.1331260204315186 - }, - "engine": "WASM SIMD ×2 常駐Worker · BLA e-23 + リベース · スキップ 3245 / 576 bit", - "renderText": "983 ms / 17085 回 · BLA 0.097ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "period3_z20", - "bits": 320, - "first": { - "render": "1675 ms / 4509 回 · BLA 53.047ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 1.8480825424194336 - }, - "base": { - "render": "1931 ms / 4509 回 · BLA 52.519ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 2.0063276290893555 - }, - "engine": "WASM SIMD ×2 常駐Worker · BLA e-25 + リベース · スキップ 4508 / 320 bit", - "renderText": "1931 ms / 4509 回 · BLA 52.519ms/kpx · 内部省略 0%", - "canvas": "780x441" - } - ], - "errorCount": 0 -} \ No newline at end of file diff --git a/audit/raw/v18-final-browser-benchmark.json b/audit/raw/v18-final-browser-benchmark.json deleted file mode 100644 index d2986bf..0000000 --- a/audit/raw/v18-final-browser-benchmark.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "environment": "Chromium 144 headless, 780x441 viewport, container-local", - "scenes": [ - { - "name": "seahorse_z14", - "bits": 288, - "first": { - "render": "789 ms / 3490 回 · BLA 54.395ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.7424271106719971 - }, - "base": { - "render": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.28958845138549805 - }, - "engine": "WASM SIMD ×2 · BLA e-28 + リベース / 288 bit", - "renderText": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "seahorse_z20", - "bits": 320, - "first": { - "render": "401 ms / 4509 回 · BLA 5.023ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.4155466556549072 - }, - "base": { - "render": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.26917600631713867 - }, - "engine": "WASM SIMD ×2 · BLA e-23 + リベース / 320 bit", - "renderText": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "seahorse_z100", - "bits": 576, - "first": { - "render": "482 ms / 17085 回 · BLA 0.076ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.5742058753967285 - }, - "base": { - "render": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%", - "badge": "深部リベース", - "saw": true, - "wall": 0.5136466026306152 - }, - "engine": "WASM SIMD ×2 · BLA e-23 + リベース / 576 bit", - "renderText": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%", - "canvas": "780x441" - }, - { - "name": "period3_z20", - "bits": 320, - "first": { - "render": "1420 ms / 4509 回 · BLA 0.395ms/kpx · 内部省略 100%", - "badge": "深部リベース", - "saw": true, - "wall": 1.5150198936462402 - }, - "base": { - "render": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%", - "badge": "深部リベース", - "saw": true, - "wall": 0.8927633762359619 - }, - "engine": "WASM SIMD ×2 · BLA e-23 + リベース · 内部早期終了 100% / 320 bit", - "renderText": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%", - "canvas": "780x441" - } - ], - "errorCount": 0 -} \ No newline at end of file diff --git a/audit/raw/workcap_truth_v17.json b/audit/raw/workcap_truth_v17.json deleted file mode 100644 index 658da74..0000000 --- a/audit/raw/workcap_truth_v17.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "w": 48, - "h": 30, - "pixels": 1440, - "globalIter": 3490, - "capIter": 2870, - "capped": 1440, - "late": 1066, - "insideGlobal": 374, - "lateEscapeRateAmongCapped": 0.7402777777777778, - "elapsedMs": 6344.968723999999 -} diff --git a/audit/smoke-test.json b/audit/smoke-test.json deleted file mode 100644 index 845005a..0000000 --- a/audit/smoke-test.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "values": { - "title": "Mandelbrot ∞ Zoom v18", - "engine": "WebAssembly f64 / 256 bit", - "render": "281 ms / 350 回", - "badge": "準備完了", - "canvas": "780x441", - "black": { - "n": 6035, - "b": 1358, - "uniq": 1215 - }, - "autoText": "❚❚ 自動", - "badge2": "自動 · 境界追従", - "render2": "53 ms / 381 回" - }, - "errorCount": 0, - "errors": [] -} diff --git a/audit/v23-browser-baseline.json b/audit/v23-browser-baseline.json deleted file mode 100644 index 7b33914..0000000 --- a/audit/v23-browser-baseline.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "format": "mandelbrot-browser-baseline-v23", - "status": "not-run", - "reason": "No in-app browser binding was available in this environment.", - "lastCheckedUtc": "2026-08-22T10:48:56.1197927Z", - "connectionAudit": { - "consecutiveGoalTurns": 3, - "auditCycle": "resumed-after-performance-fix", - "availableBrowsers": 0, - "outcome": "blocked", - "recovery": "Connect an in-app Browser, then rerun tests/browser-benchmark.html. Unrelated browser-control backends are intentionally not substituted. Node runtime-budget coverage is available in audit/v23-node-performance.json but does not replace the browser gate." - }, - "runner": "tests/browser-benchmark.html?profile=&target=", - "executionModel": "one real-DPR profile and one build target per run; iframe sizing does not emulate DPR", - "requiredTargets": ["hosted", "standalone"], - "requiredProfiles": ["mobile", "desktop", "4k"], - "acceptancePending": ["visual output", "requested viewport and actual DPR fidelity", "interaction p95", "Preview/Covered/Refined timing", "Power stops at Preview", "Standard stops at Covered without AA/continuation", "idle writes", "long tasks", "observed browser/GPU peak memory", "export download/cancel", "keyboard/focus/target-size/live-status/page-zoom accessibility"] -} diff --git a/audit/v23-browserless-baseline.json b/audit/v23-browserless-baseline.json deleted file mode 100644 index fc6c2a3..0000000 --- a/audit/v23-browserless-baseline.json +++ /dev/null @@ -1,267 +0,0 @@ -{ - "format": "mandelbrot-browserless-audit-v23", - "generatedUtc": "2026-08-22T12:35:37.5161338Z", - "status": "pass", - "scope": "browserless-current-artifacts", - "fullAcceptance": false, - "rendererVersion": 23, - "node": { - "version": "v22.18.0", - "archive": { - "file": ".tmp-node-v22.18.0-win-x64.zip", - "sha256": "c95d8a7e1c99e669cc08c9f1176e068c1f50847c37908fcb8c35b62482366511", - "source": "https://nodejs.org/dist/v22.18.0/node-v22.18.0-win-x64.zip" - } - }, - "contracts": { - "kernel": { - "status": "pass", - "modules": 4, - "compiler": "17.0.6", - "pixelContract": "sample centers at x + 0.5, y + 0.5" - }, - "source": { - "status": "pass", - "rendererVersion": 23, - "wasmPayloads": 8, - "scriptBytes": 129807, - "htmlBytes": 11395 - }, - "document": { - "status": "pass", - "proposalContract": "revised-v23", - "scenes": 7, - "viewports": 3, - "numericPolicy": "numeric-policy-v23", - "hashedFiles": 13, - "browserRunner": "static-contract-pass", - "browserlessEvidence": "pass-with-explicit-limits" - } - }, - "executableTests": { - "js-syntax.mjs": { - "status": "pass", - "rendererVersion": 23, - "lastPass": "preview", - "automaticTarget": "COVERED", - "fieldIterations": [ - 7, - 100 - ], - "modeProbe": { - "targets": { - "power": "PREVIEW", - "standard": "COVERED", - "fine": "REFINED", - "validate": "VALIDATED" - }, - "coldDeepPreview": [ - 48, - 36 - ] - }, - "referenceCheckpoints": { - "agreement": true, - "catchesMismatch": true, - "count": 17, - "bits": 320 - }, - "deepWorkerModuleInit": true, - "browserHarnessSyntax": true, - "sources": { - "appBytes": 129807, - "shallowWorkerCharacters": 1509, - "deepWorkerCharacters": 8788 - } - }, - "module-clone.mjs": { - "status": "pass", - "structuredClone": true, - "results": [ - { - "name": "deep-simd.wasm", - "bytes": 4866, - "exports": [ - "render_perturb_rebase_rect" - ] - }, - { - "name": "bla-simd.wasm", - "bytes": 4959, - "exports": [ - "build_bla", - "render_bla_rect_v2" - ] - }, - { - "name": "color-simd.wasm", - "bytes": 1387, - "exports": [ - "smooth_batch" - ] - } - ] - }, - "precision-reference.mjs": { - "status": "pass", - "precisions": [ - 256, - 320 - ], - "results": [ - { - "id": "outside", - "p": 3, - "guarded": 3, - "stable": true - }, - { - "id": "boundary-escape", - "p": 33, - "guarded": 33, - "stable": true - }, - { - "id": "period3-center", - "p": 4000, - "guarded": 4000, - "stable": true - } - ] - }, - "analytic-interior.mjs": { - "status": "pass", - "checked": 14, - "precisions": [ - 256, - 320 - ], - "proof": "integer cardioid/period-2 inequalities" - }, - "pixel-mapping.mjs": { - "status": "pass", - "samples": 851, - "backends": [ - "shallow", - "deep", - "bla" - ], - "subsampleScales": [ - 2, - 4 - ], - "contract": "centered-rational", - "float64UlpMax": 4, - "fixedPointRounding": "nearest-ties-away-from-zero" - }, - "pixel-contract.mjs": { - "status": "pass", - "backend": "simd", - "samples": 1440, - "mismatches": 0, - "contract": "(x+0.5,y+0.5)" - }, - "runtime-budget.mjs": { - "format": "mandelbrot-node-runtime-budget-v23", - "generatedUtc": "2026-08-22T12:35:33.428Z", - "node": "v22.18.0", - "status": "measured", - "acceptance": false, - "sceneCorpus": "mandelbrot-scene-corpus-v2", - "measurementNote": "Runtime characterization only; browser acceptance has separate thresholds and evidence.", - "contracts": { - "previewBudget110": true, - "modeTargets": true, - "screenBudgets": true, - "boundedContinuation": true, - "coldDeepCap": true, - "measuredDeepBudget": true, - "viewportIndependentFloor": true - }, - "shallow": [ - { - "id": "overview-350", - "width": 640, - "height": 400, - "iterations": 350, - "pixels": 256000, - "medianMs": 85.583, - "msPerMegapixel": 334.30859375, - "meanIterations": 78.3253515625, - "samplesMs": [ - 85.583, - 88.88799999999998, - 84.14580000000001, - 84.10900000000004, - 92.09339999999997 - ] - }, - { - "id": "boundary-900", - "width": 512, - "height": 320, - "iterations": 900, - "pixels": 163840, - "medianMs": 113.91780000000006, - "msPerMegapixel": 695.2990722656253, - "meanIterations": 154.49669189453124, - "samplesMs": [ - 115.31709999999998, - 113.91780000000006, - 112.1979, - 112.90809999999988, - 117.25350000000003 - ] - } - ], - "deep": { - "id": "swirly-seahorses-z12-bla-2000", - "width": 256, - "height": 144, - "iterations": 2000, - "pixels": 36864, - "blaEntries": 12, - "buildMs": 0.27119999999990796, - "medianMs": 395.4109000000003, - "msPerMegapixel": 10726.207139756953, - "unresolved": 0, - "samplesMs": [ - 388.1887999999999, - 394.21360000000004, - 413.6929, - 395.4109000000003, - 396.3185000000003 - ] - } - } - }, - "runtime": { - "status": "measured-not-acceptance", - "report": "audit/v23-browserless-runtime.json", - "reason": "Runtime values are environment/load dependent and no browser paint/input/GPU threshold is asserted." - }, - "sourceWasmBuild": { - "status": "prior-evidence-reused", - "manifest": { - "file": "build/wasm-v23/manifest.json", - "bytes": 4193, - "sha256": "cc388d664f642181d45a110ea5b44c47524050f8bf904ebd115e8e24c0f58a18" - }, - "reason": "Verified generated manifest reused; fixed toolchain was removed after the successful gate." - }, - "v22Comparison": { - "status": "not-verifiable", - "reason": "No immutable v22 artifacts, hashes, or versioned baseline are present." - }, - "browserAcceptance": { - "status": "not-run", - "report": "audit/v23-browser-baseline.json", - "nonSubstitutable": [ - "actual DPR and visual output", - "transform-to-paint and Preview p95", - "long tasks and observed browser/GPU memory", - "download/cancel behavior", - "keyboard/focus/page-zoom/screen-reader behavior" - ] - } -} diff --git a/audit/v23-browserless-runtime.json b/audit/v23-browserless-runtime.json deleted file mode 100644 index 97d2c34..0000000 --- a/audit/v23-browserless-runtime.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "format": "mandelbrot-node-runtime-budget-v23", - "generatedUtc": "2026-08-22T12:35:33.428Z", - "node": "v22.18.0", - "status": "measured", - "acceptance": false, - "sceneCorpus": "mandelbrot-scene-corpus-v2", - "measurementNote": "Runtime characterization only; browser acceptance has separate thresholds and evidence.", - "contracts": { - "previewBudget110": true, - "modeTargets": true, - "screenBudgets": true, - "boundedContinuation": true, - "coldDeepCap": true, - "measuredDeepBudget": true, - "viewportIndependentFloor": true - }, - "shallow": [ - { - "id": "overview-350", - "width": 640, - "height": 400, - "iterations": 350, - "pixels": 256000, - "medianMs": 85.583, - "msPerMegapixel": 334.30859375, - "meanIterations": 78.3253515625, - "samplesMs": [ - 85.583, - 88.88799999999998, - 84.14580000000001, - 84.10900000000004, - 92.09339999999997 - ] - }, - { - "id": "boundary-900", - "width": 512, - "height": 320, - "iterations": 900, - "pixels": 163840, - "medianMs": 113.91780000000006, - "msPerMegapixel": 695.2990722656253, - "meanIterations": 154.49669189453124, - "samplesMs": [ - 115.31709999999998, - 113.91780000000006, - 112.1979, - 112.90809999999988, - 117.25350000000003 - ] - } - ], - "deep": { - "id": "swirly-seahorses-z12-bla-2000", - "width": 256, - "height": 144, - "iterations": 2000, - "pixels": 36864, - "blaEntries": 12, - "buildMs": 0.27119999999990796, - "medianMs": 395.4109000000003, - "msPerMegapixel": 10726.207139756953, - "unresolved": 0, - "samplesMs": [ - 388.1887999999999, - 394.21360000000004, - 413.6929, - 395.4109000000003, - 396.3185000000003 - ] - } -} diff --git a/audit/v23-node-performance.json b/audit/v23-node-performance.json deleted file mode 100644 index c179c70..0000000 --- a/audit/v23-node-performance.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "format": "mandelbrot-node-runtime-budget-v23", - "generatedUtc": "2026-08-22T10:48:04.607Z", - "node": "v24.16.0", - "contracts": { - "previewBudget110": true, - "modeTargets": true, - "screenBudgets": true, - "boundedContinuation": true, - "coldDeepCap": true, - "measuredDeepBudget": true, - "viewportIndependentFloor": true - }, - "shallow": [ - { - "id": "overview-350", - "width": 640, - "height": 400, - "iterations": 350, - "pixels": 256000, - "medianMs": 86.21379999999999, - "msPerMegapixel": 336.77265624999995, - "meanIterations": 78.3253515625, - "samplesMs": [ - 83.7123, - 87.21029999999999, - 86.21379999999999, - 87.21250000000003, - 85.40730000000002 - ] - }, - { - "id": "boundary-900", - "width": 512, - "height": 320, - "iterations": 900, - "pixels": 163840, - "medianMs": 109.8057, - "msPerMegapixel": 670.2008056640625, - "meanIterations": 154.49669189453124, - "samplesMs": [ - 109.8057, - 110.24979999999994, - 109.15139999999997, - 108.90229999999997, - 110.59719999999993 - ] - } - ], - "deep": { - "id": "seahorse-bla-2000", - "width": 256, - "height": 144, - "iterations": 2000, - "pixels": 36864, - "blaEntries": 12, - "buildMs": 0.4078999999999269, - "medianMs": 722.5364, - "msPerMegapixel": 19600.054253472223, - "unresolved": 0, - "samplesMs": [ - 692.4027999999998, - 695.0712000000003, - 773.1455000000001, - 722.5364, - 863.2840000000006 - ] - } -} diff --git a/audit/v23-source-baseline.json b/audit/v23-source-baseline.json deleted file mode 100644 index b131b5f..0000000 --- a/audit/v23-source-baseline.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "format": "mandelbrot-source-baseline-v23", - "generatedUtc": "2026-08-22T12:30:30.4538464Z", - "rendererVersion": 23, - "contract": { - "status": "pass", - "rendererVersion": 23, - "wasmPayloads": 8, - "scriptBytes": 129807, - "htmlBytes": 11395 - }, - "hosted": { - "firstViewFiles": [ - { - "file": "dist/hosted/index.html", - "bytes": 11388, - "sha256": "e92c68d4b3c36a438675aee5fff4349c75ebd8ba8bdbc85369bfb3f55d07fd73" - }, - { - "file": "dist/hosted/hosted-loader.js", - "bytes": 2338, - "sha256": "43804e07f0603d57a4c4d210cfbfb02e5fa7adc13e2dc53c1a15e05d1d74f4c1" - }, - { - "file": "dist/hosted/script.js", - "bytes": 129807, - "sha256": "6ffebe51532fb8a141e9b1939e586d76b6c79db18786c06713e67ffe8d4ca78a" - }, - { - "file": "dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm", - "bytes": 511, - "sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b" - } - ], - "firstViewUncompressedBytes": 144044, - "deepRequestContractBeforeDeepView": 0, - "deepRequestMeasurementStatus": "not-run", - "note": "The zero is a static loader contract, not a measured request count. Transfer compression, request count, parse, compile, and runtime timings require the fixed Hosted browser benchmark." - }, - "standalone": { - "files": [ - { - "file": "dist/standalone/index.html", - "bytes": 11395, - "sha256": "b2017e3aac73d22d1178e2a28bb5cb1ea0035cbcae5a4ed422dbe4ff7eb951fa" - }, - { - "file": "dist/standalone/script.js", - "bytes": 129807, - "sha256": "6ffebe51532fb8a141e9b1939e586d76b6c79db18786c06713e67ffe8d4ca78a" - }, - { - "file": "dist/standalone/kernels.js", - "bytes": 31389, - "sha256": "e97726c09af138da92b331c376766c1128c10aa3c361b83750356824262ea1a0" - } - ], - "uncompressedBytes": 172591 - }, - "sourceBuild": { - "files": [ - { - "file": "src/shallow_kernel.c", - "bytes": 1512, - "sha256": "ec1067a7a425e180eef7bebf250730de1144e63783735c9ed22084856da096d5" - }, - { - "file": "src/deep_kernel.c", - "bytes": 5076, - "sha256": "c484aa9255a7a33eb4ce4ab90c74ec45865885b9a04fd263ce230838d3ce5137" - }, - { - "file": "src/bla_kernel_v18.c", - "bytes": 8744, - "sha256": "66724b9afee313899110380f427ba6abbf5340af5c9af1b715eb24b8cff901a9" - }, - { - "file": "src/color_kernel.c", - "bytes": 1284, - "sha256": "a40c7bd511dba94b096bf5e075f1fb95bb9f98b5154bac712f5e2bdeb1432a09" - }, - { - "file": "src/abi.json", - "bytes": 899, - "sha256": "747f6dd6a0240684e9b502ec19a90458956e2d4fc583084bfe0b3ac0e879a0da" - }, - { - "file": "toolchain.lock.json", - "bytes": 381, - "sha256": "d51122cff57d4304880db660be37988403c44770e209e51fe33e5ba30c9a7f38" - } - ], - "compiler": "clang 17.0.6", - "node": "v22.18.0", - "compiledInThisEnvironment": true, - "goldenStatus": "pass", - "generatedManifest": { - "file": "build/wasm-v23/manifest.json", - "bytes": 4193, - "sha256": "cc388d664f642181d45a110ea5b44c47524050f8bf904ebd115e8e24c0f58a18" - }, - "reason": "Verified generated manifest reused; fixed toolchain was removed after the successful gate." - }, - "browserBenchmark": { - "status": "not-run", - "report": "audit/v23-browser-baseline.json", - "reason": "No in-app browser binding was available in this environment." - } -} diff --git a/audit/v24-backend-policy.json b/audit/v24-backend-policy.json deleted file mode 100644 index 94b7d06..0000000 --- a/audit/v24-backend-policy.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "status": "pass", - "kind": "view-specific-adaptive-backend-router", - "directRequiredRatio": 8, - "directDefaultRatio": 128, - "deepSpeedMargin": 0.85, - "deepProbeUnknownMax": 0.005, - "cases": 10 -} diff --git a/audit/v24-bla-status.json b/audit/v24-bla-status.json deleted file mode 100644 index f0a4265..0000000 --- a/audit/v24-bla-status.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "status": "pass", - "kind": "bla-disabled-contract", - "reason": "dense swirly regression demonstrated unsafe f32-quantized BLA classifications", - "productionBla": false -} diff --git a/audit/v24-cpu-numeric-model.json b/audit/v24-cpu-numeric-model.json deleted file mode 100644 index bdc0d80..0000000 --- a/audit/v24-cpu-numeric-model.json +++ /dev/null @@ -1,308 +0,0 @@ -{ - "status": "pass", - "kind": "guarded-production-equation-cpu-f32-model-not-real-gpu", - "thresholds": { - "balanced": 0.001, - "strict": 0.0001 - }, - "report": [ - { - "mode": "balanced", - "id": "period2-cusp-z14", - "bits": 285, - "limit": 1200, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "balanced", - "id": "swirly-seahorses-z12", - "bits": 279, - "limit": 2000, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 49, - "known": 138, - "exactEscape": 130, - "mismatch": [ - { - "x": 5, - "y": 1, - "oracle": 675, - "gpu": { - "kind": "escaped", - "n": 674, - "err": 0.06557769039826006 - } - }, - { - "x": 6, - "y": 1, - "oracle": 636, - "gpu": { - "kind": "escaped", - "n": 635, - "err": 0.12117791268198698 - } - } - ] - }, - { - "mode": "balanced", - "id": "period2-cusp-z20", - "bits": 305, - "limit": 1800, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "balanced", - "id": "period2-cusp-z100", - "bits": 571, - "limit": 2400, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "balanced", - "id": "period3-interior", - "bits": 267, - "limit": 1200, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "balanced", - "id": "period2-cusp-e280", - "bits": 1171, - "limit": 2400, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "balanced", - "id": "period2-cusp-e400", - "bits": 1569, - "limit": 1600, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "strict", - "id": "period2-cusp-z14", - "bits": 285, - "limit": 1200, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "strict", - "id": "swirly-seahorses-z12", - "bits": 279, - "limit": 2000, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 49, - "known": 138, - "exactEscape": 130, - "mismatch": [ - { - "x": 5, - "y": 1, - "oracle": 675, - "gpu": { - "kind": "escaped", - "n": 674, - "err": 0.06557769039826006 - } - }, - { - "x": 6, - "y": 1, - "oracle": 636, - "gpu": { - "kind": "escaped", - "n": 635, - "err": 0.12117791268198698 - } - } - ] - }, - { - "mode": "strict", - "id": "period2-cusp-z20", - "bits": 305, - "limit": 1800, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "strict", - "id": "period2-cusp-z100", - "bits": 571, - "limit": 2400, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "strict", - "id": "period3-interior", - "bits": 267, - "limit": 1200, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "strict", - "id": "period2-cusp-e280", - "bits": 1171, - "limit": 2400, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - }, - { - "mode": "strict", - "id": "period2-cusp-e400", - "bits": 1569, - "limit": 1600, - "guardMismatch": 0, - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "known": 187, - "exactEscape": 0, - "mismatch": [] - } - ], - "denseRegression": [ - { - "mode": "balanced", - "x": 25, - "y": 12, - "oracle": 2000, - "gpu": { - "kind": "unknown", - "n": 2000, - "reason": "error-bound", - "err": 34362354130585895000 - } - }, - { - "mode": "strict", - "x": 25, - "y": 12, - "oracle": 2000, - "gpu": { - "kind": "unknown", - "n": 2000, - "reason": "error-bound", - "err": 34362354130585895000 - } - }, - { - "mode": "balanced", - "x": 26, - "y": 15, - "oracle": 2000, - "gpu": { - "kind": "unknown", - "n": 2000, - "reason": "error-bound", - "err": 121379814641826710000 - } - }, - { - "mode": "strict", - "x": 26, - "y": 15, - "oracle": 2000, - "gpu": { - "kind": "unknown", - "n": 2000, - "reason": "error-bound", - "err": 121379814641826710000 - } - }, - { - "mode": "balanced", - "x": 27, - "y": 25, - "oracle": 1957, - "gpu": { - "kind": "unknown", - "n": 2000, - "reason": "error-bound", - "err": 32570060988.51048 - } - }, - { - "mode": "strict", - "x": 27, - "y": 25, - "oracle": 1957, - "gpu": { - "kind": "unknown", - "n": 2000, - "reason": "error-bound", - "err": 32570060988.51048 - } - } - ] -} diff --git a/audit/v24-crossover-gpu-status.json b/audit/v24-crossover-gpu-status.json deleted file mode 100644 index d8d9601..0000000 --- a/audit/v24-crossover-gpu-status.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "status": "not-run", - "reason": "container Chromium does not expose a working WebGPU adapter / GPU process initialization fails", - "benchmark": "UI Diagnostics > Backend比較", - "depths": [ - 0, - 2, - 4, - 6, - 8, - 10, - 12, - 14, - 16, - 20, - 30, - 40 - ], - "timings": [ - "f32 numeric", - "DS direct numeric", - "Deep warm numeric", - "Deep cold = verified BigInt reference + warm numeric" - ] -} \ No newline at end of file diff --git a/audit/v24-crossover-hotfix.json b/audit/v24-crossover-hotfix.json deleted file mode 100644 index 147e999..0000000 --- a/audit/v24-crossover-hotfix.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "version": "24.2.3", - "issue": "Backend comparison could appear to hang at z12 deep because the benchmark repeated corrected Deep over the entire current canvas and waited for monolithic GPU queue completion.", - "fix": { - "speedTile": [256, 144], - "speedTileCount": 3, - "timingModel": "sum representative tile GPU queue completion time, derive msPerPixel, extrapolate to current full canvas", - "qualityTileCount": 5, - "gpuTimeoutMs": 15000, - "referenceTimeoutMs": 15000, - "productionRendererChanged": false, - "benchmarkFormat": "mandelbrot-webgpu-crossover-v2" - }, - "verification": { - "wgslReserved": "pass", - "sourceContract": "pass", - "indexContract": "pass", - "treeContract": "pass", - "crossoverQuality": "pass", - "crossoverContract": "pass", - "referenceWorker": "pass", - "directModel": "pass", - "backendPolicy": "pass", - "cpuNumericModel": "pass (individual run)", - "sparseCorrectionModel": "pass (individual run)", - "blaDisabledContract": "pass", - "geometryContract": "pass", - "coordinateFormat": "pass", - "pngStream": "pass", - "acceptanceContract": "pass", - "build": "pass", - "realWebGPU": "not available in this container" - } -} diff --git a/audit/v24-crossover-quality.json b/audit/v24-crossover-quality.json deleted file mode 100644 index 82fdda4..0000000 --- a/audit/v24-crossover-quality.json +++ /dev/null @@ -1,766 +0,0 @@ -{ - "format": "mandelbrot-crossover-quality-v1", - "kind": "cpu-f32-model + BigInt P/P+64 oracle; not real GPU", - "viewport": [ - 1600, - 900 - ], - "sampleGrid": [ - 21, - 15 - ], - "depths": [ - 0, - 2, - 4, - 6, - 8, - 10, - 12, - 14, - 16, - 20, - 30, - 40 - ], - "tracks": [ - "cusp", - "seahorse" - ], - "rows": [ - { - "track": "cusp", - "z": 0, - "limit": 350, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 21, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 188 - }, - { - "track": "cusp", - "z": 2, - "limit": 478, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 21, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1107 - }, - { - "track": "cusp", - "z": 4, - "limit": 550, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 21, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 851 - }, - { - "track": "cusp", - "z": 6, - "limit": 611, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 761 - }, - { - "track": "cusp", - "z": 8, - "limit": 667, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 15 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 920 - }, - { - "track": "cusp", - "z": 10, - "limit": 721, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 15 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 998 - }, - { - "track": "cusp", - "z": 12, - "limit": 772, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 15 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1005 - }, - { - "track": "cusp", - "z": 14, - "limit": 821, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 15 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1099 - }, - { - "track": "cusp", - "z": 16, - "limit": 870, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 15 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1134 - }, - { - "track": "cusp", - "z": 20, - "limit": 963, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 15 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1271 - }, - { - "track": "cusp", - "z": 30, - "limit": 1183, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 15 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1589 - }, - { - "track": "cusp", - "z": 40, - "limit": 1392, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 15 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1856 - }, - { - "track": "seahorse", - "z": 0, - "limit": 350, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 21, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 103 - }, - { - "track": "seahorse", - "z": 2, - "limit": 478, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 21, - "ds": 21 - }, - "f32": { - "falseEscaped": 2, - "falseBounded": 0, - "escapeIterationMismatch": 2, - "uniqueCoords": 315 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 366 - }, - { - "track": "seahorse", - "z": 4, - "limit": 550, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 21, - "ds": 21 - }, - "f32": { - "falseEscaped": 0, - "falseBounded": 2, - "escapeIterationMismatch": 36, - "uniqueCoords": 315 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 2, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 1 - }, - "modelMs": 161 - }, - { - "track": "seahorse", - "z": 6, - "limit": 611, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 2, - "ds": 21 - }, - "f32": { - "falseEscaped": 5, - "falseBounded": 0, - "escapeIterationMismatch": 104, - "uniqueCoords": 315 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 1, - "escapeIterationMismatch": 2, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 230 - }, - { - "track": "seahorse", - "z": 8, - "limit": 667, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 4, - "falseBounded": 0, - "escapeIterationMismatch": 307, - "uniqueCoords": 8 - }, - "ds": { - "falseEscaped": 1, - "falseBounded": 1, - "escapeIterationMismatch": 14, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 1, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 2 - }, - "modelMs": 376 - }, - { - "track": "seahorse", - "z": 10, - "limit": 721, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 10, - "falseBounded": 0, - "escapeIterationMismatch": 305, - "uniqueCoords": 2 - }, - "ds": { - "falseEscaped": 6, - "falseBounded": 6, - "escapeIterationMismatch": 34, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 512 - }, - { - "track": "seahorse", - "z": 12, - "limit": 772, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 21 - }, - "f32": { - "falseEscaped": 54, - "falseBounded": 0, - "escapeIterationMismatch": 261, - "uniqueCoords": 1 - }, - "ds": { - "falseEscaped": 14, - "falseBounded": 13, - "escapeIterationMismatch": 103, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 3 - }, - "modelMs": 919 - }, - { - "track": "seahorse", - "z": 14, - "limit": 821, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 1 - }, - "f32": { - "falseEscaped": 315, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 1 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 315 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1158 - }, - { - "track": "seahorse", - "z": 16, - "limit": 870, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 1 - }, - "f32": { - "falseEscaped": 315, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 1 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 1 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1211 - }, - { - "track": "seahorse", - "z": 20, - "limit": 963, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 1 - }, - "f32": { - "falseEscaped": 315, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 1 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 1 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1374 - }, - { - "track": "seahorse", - "z": 30, - "limit": 1183, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 1 - }, - "f32": { - "falseEscaped": 315, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 1 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 1 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 1968 - }, - { - "track": "seahorse", - "z": 40, - "limit": 1392, - "tested": 315, - "guardMismatch": 0, - "adjacentUniqueX": { - "f32": 1, - "ds": 1 - }, - "f32": { - "falseEscaped": 315, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 1 - }, - "ds": { - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 0, - "uniqueCoords": 1 - }, - "deep": { - "falseEscaped": 0, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 0 - }, - "modelMs": 2066 - } - ], - "summary": { - "firstMaterialF32DegradationDepth": 6, - "firstMaterialDsDegradationDepth": 10, - "deepIssueRows": [ - { - "z": 8, - "falseEscaped": 1, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 2 - } - ], - "interpretation": "On the Seahorse track at a 1600x900 viewport, f32 direct becomes materially unsuitable around z6; DS direct remains a plausible bridge through roughly z8 and degrades by z10. Corrected deep is the best of the modeled backends, but one z8 false escape means it is not a membership certificate." - } -} \ No newline at end of file diff --git a/audit/v24-fixed96-hotfix.json b/audit/v24-fixed96-hotfix.json deleted file mode 100644 index 1745b16..0000000 --- a/audit/v24-fixed96-hotfix.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": "24.2.7-fixed96-hotfix", - "rootCause": "fx_bad_range treated the Q8.88 integer byte as redundant sign extension and rejected valid values such as +1.2", - "symptom": "correction rate approximately equaled remaining-unknown rate on the real GPU", - "fix": [ - "removed invalid fx_bad_range check", - "classify |Re(z)|>2 or |Im(z)|>2 as certain escape before squaring", - "square for exact |z|^2>4 only while both components are within +/-2", - "validate Q8.88 coordinate range and pixel-step underflow before packing" - ], - "regressions": [ - "v24-fixed96-contract.mjs", - "v24-fixed96-limb-model.mjs", - "v24-fixed96-limb-orbit-model.mjs", - "v24-fixed96-shader-range-model.mjs", - "v24-fixed96-model.mjs" - ], - "realWebGPU": "pending user rerun" -} diff --git a/audit/v24-fixed96-model.json b/audit/v24-fixed96-model.json deleted file mode 100644 index 2598b19..0000000 --- a/audit/v24-fixed96-model.json +++ /dev/null @@ -1,182 +0,0 @@ -{ - "format": "mandelbrot-fixed96-q88-model-v1", - "kind": "Exact BigInt model of signed Q8.88 direct + DS risk gate + BigInt P/P+64 oracle; integer GPU arithmetic should be deterministic modulo implementation bugs", - "viewport": [ - 1600, - 900 - ], - "sampleGrid": [ - 31, - 21 - ], - "rows": [ - { - "z": 6, - "limit": 611, - "variants": [ - { - "risk": 100000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 43, - "uncertainRate": 0.06605222734254992, - "wrongBefore": 4, - "wrongAfter": 0 - }, - { - "risk": 1000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 23, - "uncertainRate": 0.03533026113671275, - "wrongBefore": 4, - "wrongAfter": 0 - }, - { - "risk": 10000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 18, - "uncertainRate": 0.027649769585253458, - "wrongBefore": 4, - "wrongAfter": 0 - }, - { - "risk": 100000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 10, - "uncertainRate": 0.015360983102918587, - "wrongBefore": 4, - "wrongAfter": 1 - } - ] - }, - { - "z": 8, - "limit": 667, - "variants": [ - { - "risk": 100000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 202, - "uncertainRate": 0.3102918586789555, - "wrongBefore": 4, - "wrongAfter": 0 - }, - { - "risk": 1000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 126, - "uncertainRate": 0.1935483870967742, - "wrongBefore": 4, - "wrongAfter": 0 - }, - { - "risk": 10000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 72, - "uncertainRate": 0.11059907834101383, - "wrongBefore": 4, - "wrongAfter": 0 - }, - { - "risk": 100000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 46, - "uncertainRate": 0.0706605222734255, - "wrongBefore": 4, - "wrongAfter": 0 - } - ] - }, - { - "z": 10, - "limit": 721, - "variants": [ - { - "risk": 100000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 648, - "uncertainRate": 0.9953917050691244, - "wrongBefore": 10, - "wrongAfter": 0 - }, - { - "risk": 1000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 398, - "uncertainRate": 0.6113671274961597, - "wrongBefore": 10, - "wrongAfter": 0 - }, - { - "risk": 10000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 225, - "uncertainRate": 0.3456221198156682, - "wrongBefore": 10, - "wrongAfter": 0 - }, - { - "risk": 100000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 153, - "uncertainRate": 0.2350230414746544, - "wrongBefore": 10, - "wrongAfter": 0 - } - ] - }, - { - "z": 12, - "limit": 772, - "variants": [ - { - "risk": 100000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 602, - "uncertainRate": 0.9247311827956989, - "wrongBefore": 51, - "wrongAfter": 0 - }, - { - "risk": 1000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 600, - "uncertainRate": 0.9216589861751152, - "wrongBefore": 51, - "wrongAfter": 0 - }, - { - "risk": 10000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 594, - "uncertainRate": 0.9124423963133641, - "wrongBefore": 51, - "wrongAfter": 0 - }, - { - "risk": 100000000000000, - "tested": 651, - "guardMismatch": 0, - "uncertain": 488, - "uncertainRate": 0.7496159754224271, - "wrongBefore": 51, - "wrongAfter": 0 - } - ] - } - ] -} \ No newline at end of file diff --git a/audit/v24-fixed96-preparation.json b/audit/v24-fixed96-preparation.json deleted file mode 100644 index 89ff547..0000000 --- a/audit/v24-fixed96-preparation.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "status": "prepared", - "version": "24.2.6-fixed96-experiment", - "purpose": "DS risk gate -> sparse signed Q8.88 u32-limb direct correction -> compare with Full Deep", - "productionRouterChanged": false, - "standaloneUi": { - "rootIndexSingleFile": true, - "externalScriptTags": 0, - "button": "Fixed96 Direct実験", - "allRiskRowsVisible": true - }, - "fixedPoint": { - "bits": 96, - "fractionBits": 88, - "format": "signed Q8.88", - "limbs": 3, - "limbType": "u32" - }, - "cpuPreflight": { - "status": "pass", - "report": "audit/v24-fixed96-model.json", - "limbArithmetic": "tests/v24-fixed96-limb-model.mjs", - "safeCandidateAtDepths": [ - 6, - 8, - 10, - 12 - ] - }, - "realWebGPU": "pending: run tests/webgpu-acceptance.html and Diagnostics > Fixed96 Direct実験 on a WebGPU-capable browser" -} \ No newline at end of file diff --git a/audit/v24-fixed96-sparse-queue.json b/audit/v24-fixed96-sparse-queue.json deleted file mode 100644 index 809db86..0000000 --- a/audit/v24-fixed96-sparse-queue.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "version": "24.2.8-fixed96-sparse-queue", - "purpose": "Make the diagnostic Fixed96 correction truly sparse on GPU and fail closed on experiment-system counter/dispatch inconsistencies.", - "productionRouterChanged": false, - "execution": { - "guard": "full-tile DS sensitivity pass; risky local pixel indices atomically compacted", - "finalize": "1 invocation writes indirect args from GPU queue length", - "correction": "1D @workgroup_size(64), dispatchWorkgroupsIndirect, one queued pixel per invocation", - "cpuReadbackBetweenPasses": false - }, - "integrityGate": [ - "selected == enqueued == dispatchCount == processed", - "overflow == 0", - "indirectWorkgroups == ceil(enqueued/64)", - "invalidIndex == 0", - "staleEntry == 0", - "corrected + remain == processed" - ], - "tests": { - "fixed96Contract": "pass", - "queueIntegrityModel": "pass (7 cases)", - "wgslReserved": "pass (10 shaders)", - "fixed96LimbMultiply": "pass (20000 cases)", - "fixed96LimbOrbit": "pass", - "fixed96RangeRegression": "pass", - "singleFileUiContract": "pass", - "sourceContract": "pass", - "acceptanceContract": "pass", - "realWebGPU": "pending external run", - "samplingConsistencyGate": "pass (speed queue rate vs quality correction rate)" - }, - "sourceSha256": { - "script.js": "0104d437471b9ad1c3a9bc69206e1562b6841846e0e2343cde2c9ce5632d0a4d", - "gpu-kernels.js": "364e2babe5e986dc02d17c3e8897891e34d73498cc4c84d93c7f09cc2b6f3bd6", - "index.external.html": "23527ec2708d162bfe40c285952e170ce783cfafb63f37a5c7b5c7ecbfdec80c", - "tests/v24-fixed96-contract.mjs": "a85f569764a5623720614c79503398e5993846fa28d0a8aec45979e0edc225ce", - "tests/v24-fixed96-queue-model.mjs": "e29f84189314a9a15ae3e08da85f5bcf2d990ec9c6ab2e9c731ae4639d8f3d22", - "tests/webgpu-acceptance.js": "e5580f7cfc402aa5b533169cda4a088d3f10e0086002c508bf989b1ffff19cb5" - } -} diff --git a/audit/v24-hybrid-preparation.json b/audit/v24-hybrid-preparation.json deleted file mode 100644 index 574bbca..0000000 --- a/audit/v24-hybrid-preparation.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "format": "mandelbrot-v24.2.4-hybrid-preparation-v1", - "createdAt": "2026-08-23T11:36:39Z", - "shaderVersion": "24.2.4-hybrid-experiment", - "productionRouterUnchanged": true, - "hybridDepths": [ - 6, - 8, - 10, - 12 - ], - "riskLimits": [ - 1000000000000.0, - 10000000000000.0, - 100000000000000.0 - ], - "cpuPriorRisk1e13": { - "z6": { - "correctionRate": 0.027649769585253458, - "caughtClassErrors": "4/4" - }, - "z8": { - "correctionRate": 0.11059907834101383, - "caughtClassErrors": "4/4" - }, - "z10": { - "correctionRate": 0.3456221198156682, - "caughtClassErrors": "10/10" - }, - "z12": { - "correctionRate": 0.9124423963133641, - "caughtClassErrors": "51/51" - } - }, - "tests": { - "wgslReserved": "pass", - "hybridContract": "pass", - "hybridRiskModel": "pass", - "crossoverContract": "pass", - "sourceContract": "pass", - "treeContract": "pass", - "referenceWorker": "pass", - "directModel": "pass", - "backendPolicy": "pass", - "cpuDeepModel": "pass", - "sparseCorrection": "pass", - "blaDisabled": "pass", - "geometry": "pass", - "coordinateFormat": "pass", - "pngStreaming": "pass", - "acceptanceContract": "pass" - }, - "realWebGPU": "not-run-in-container; run Hybrid実験 on target GPU", - "hashes": { - "source/index.html": "39427d275732e5bbce8d06253ce5f3e0a07e173081fc230cd34292050ec29827", - "source/gpu-kernels.js": "2773c39e416c52a7919aad0146cd95cf6fc108847ba3718db9253986a4f489e5", - "source/script.js": "d354ff6ce0937c616a02d34ed0accebe5c8c5640cc6ac98720d66f8d784359be", - "standalone/index.html": "39427d275732e5bbce8d06253ce5f3e0a07e173081fc230cd34292050ec29827", - "standalone/gpu-kernels.js": "2773c39e416c52a7919aad0146cd95cf6fc108847ba3718db9253986a4f489e5", - "standalone/script.js": "d354ff6ce0937c616a02d34ed0accebe5c8c5640cc6ac98720d66f8d784359be" - }, - "sourceStandaloneMatch": true -} \ No newline at end of file diff --git a/audit/v24-hybrid-risk-model.json b/audit/v24-hybrid-risk-model.json deleted file mode 100644 index 5d62939..0000000 --- a/audit/v24-hybrid-risk-model.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "format": "mandelbrot-hybrid-risk-model-v1", - "kind": "CPU Math.fround DS + BigInt P/P+64 oracle; not real GPU", - "viewport": [ - 1600, - 900 - ], - "sampleGrid": [ - 31, - 21 - ], - "riskLimit": 10000000000000, - "rows": [ - { - "z": 6, - "tested": 651, - "guardMismatch": 0, - "wrong": 4, - "uncertain": 18, - "caught": 4, - "uncertainRate": 0.027649769585253458, - "catchRate": 1 - }, - { - "z": 8, - "tested": 651, - "guardMismatch": 0, - "wrong": 4, - "uncertain": 72, - "caught": 4, - "uncertainRate": 0.11059907834101383, - "catchRate": 1 - }, - { - "z": 10, - "tested": 651, - "guardMismatch": 0, - "wrong": 10, - "uncertain": 225, - "caught": 10, - "uncertainRate": 0.3456221198156682, - "catchRate": 1 - }, - { - "z": 12, - "tested": 651, - "guardMismatch": 0, - "wrong": 51, - "uncertain": 594, - "caught": 51, - "uncertainRate": 0.9124423963133641, - "catchRate": 1 - } - ], - "interpretation": "At riskLimit=1e13 the derivative guard catches every observed DS classification error at z6/z8/z10 while sending about 3%/11%/35% of samples to Deep correction. At z12 it sends over 75%, which is a deliberate signal that Full Deep should replace the hybrid." -} \ No newline at end of file diff --git a/audit/v24-hybrid-ui-hotfix.json b/audit/v24-hybrid-ui-hotfix.json deleted file mode 100644 index a77e1c6..0000000 --- a/audit/v24-hybrid-ui-hotfix.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "format": "mandelbrot-v24.2.5-hybrid-ui-hotfix-v1", - "createdAt": "2026-08-23T11:54:13Z", - "rootCause": "Toolbar Hybrid experiment button opened the dialog only; actual measurement required a second click on the dialog start button.", - "fix": [ - "Toolbar click opens the dialog and auto-starts on the next animation frame.", - "Dialog primary button is a rerun action.", - "Concurrent Backend benchmark state is surfaced explicitly instead of silently doing nothing.", - "Hybrid running state is surfaced explicitly.", - "Static contract tests assert toolbar auto-start and rerun wiring." - ], - "tests": { - "syntax": "pass", - "hybridContract": "pass", - "sourceContract": "pass", - "wgslReserved": "pass", - "cpuNumeric": "pass", - "sparseCorrection": "pass", - "build": "pass" - }, - "realWebGPU": "not-run-in-container; UI execution contract fixed independently of GPU availability", - "sha256": { - "script.js": "b02a47d6b6b8ea7c95e85ee26e69f877ef5c8c9cffb34e68ca37121c2fa652ac", - "gpu-kernels.js": "6e00107df84dd6e5427774ffca2cfdf6c7bfae0989fd69054b11f500a97e048e", - "index.html": "d12a742455fa2febf3639beddad063a3217901923b65927aeb1c21b224c00f3a", - "package.json": "93662efdf99f04b4bb2497f6f9512ad3c617fef7209843469f8a50ed5d6a6786", - "tests/v24-hybrid-contract.mjs": "18cb1e4b5fd9656285b021de6bea02ca767c7531cb9ee57b3cde3c38f3e887c0" - } -} diff --git a/audit/v24-real-webgpu-status.json b/audit/v24-real-webgpu-status.json deleted file mode 100644 index d4b8891..0000000 --- a/audit/v24-real-webgpu-status.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "status": "v24.2.8-fixed96-sparse-queue-real-webgpu-pending", - "reason": "container browser does not expose a usable WebGPU adapter / GPU process initialization fails", - "requiredExternalRuns": [ - "tests/webgpu-acceptance.html", - "Diagnostics > Fixed96 Direct実験" - ], - "experimentDepths": [ - 6, - 8, - 10, - 12 - ], - "riskLimits": [ - 100000000000.0, - 1000000000000.0, - 10000000000000.0, - 100000000000000.0 - ], - "outputColumns": [ - "z", - "risk", - "fixed96 est", - "deep cold", - "speedup", - "correction", - "speed q", - "dispatch", - "remain", - "Δclass", - "queue", - "status" - ], - "queueAcceptance": [ - "selected == enqueued == dispatchCount == processed", - "overflow == 0", - "indirectWorkgroups == ceil(enqueued/64)", - "invalidIndex == 0", - "staleEntry == 0", - "corrected + remain == processed" - ], - "cpuQualityReport": "audit/v24-fixed96-model.json" -} diff --git a/audit/v24-release-manifest.json b/audit/v24-release-manifest.json deleted file mode 100644 index 1d78837..0000000 --- a/audit/v24-release-manifest.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "format": "mandelbrot-v24.2.2-crossover-release-manifest", - "generatedAt": "2026-08-23T10:22:38.264933+00:00", - "files": [ - { - "path": "AUDIT_V24.md", - "bytes": 3906, - "sha256": "0ac5444e8a798e183312cf4a9c4dbe08133bccf471feac13d5e6ed06c91e5133" - }, - { - "path": "CROSSOVER_BENCHMARK.md", - "bytes": 3766, - "sha256": "5f72a5a73cc73b2308e14b4a100bfd546f7067433e24db8b2b33b809f535c629" - }, - { - "path": "IMPLEMENTATION_REPORT.md", - "bytes": 2869, - "sha256": "983e5c8b217b957612f7bbabdda04c7df8da16ba28941c16a4a4b5f8e85e7cc5" - }, - { - "path": "MIGRATION_V24.md", - "bytes": 4710, - "sha256": "b4d55a30f38ba4c56e0aace0e69f00714878f5098f88d5c9a4f9e76546b24c16" - }, - { - "path": "README.md", - "bytes": 6040, - "sha256": "c651c78374c5288cd2d5739d9cd166d38840db6069d0b80b262888d333ce5c24" - }, - { - "path": "audit/legacy-v23/final-browser-benchmark.json", - "bytes": 2686, - "sha256": "bca2e141c101722f8a077d43346c2c28b5d8d7c99e70415730031065c8ab5a1c" - }, - { - "path": "audit/v24-backend-policy.json", - "bytes": 205, - "sha256": "d34d894ab9d062439e7cfe5074e47ab295defa74044ed3e2619ef0289759b2fe" - }, - { - "path": "audit/v24-bla-status.json", - "bytes": 177, - "sha256": "84587eb2183b1e6a2837f5018e74f32bc968c536d64cee7cf5fed55f7d3b21df" - }, - { - "path": "audit/v24-cpu-numeric-model.json", - "bytes": 6217, - "sha256": "8c8f7320ae29f367d4eea111eeb658a861dd0e001e5fc2653857c24b3baf71c1" - }, - { - "path": "audit/v24-crossover-gpu-status.json", - "bytes": 441, - "sha256": "dbd94c4b034c2ae521c5f591dfd745c40c9cf9d17bc08dbc00f4017216949f90" - }, - { - "path": "audit/v24-crossover-quality.json", - "bytes": 16247, - "sha256": "063deef5079d077d35d5cf349186405edcc0f17c5812f38051735a455de3ea20" - }, - { - "path": "audit/v24-real-webgpu-status.json", - "bytes": 601, - "sha256": "ec5171b4f059f4f810b64db1ba82d83979571001f17f564919322c69e59ba721" - }, - { - "path": "audit/v24-release-verification.json", - "bytes": 944, - "sha256": "b54797dfdc9e99c3e981ac39b45ef92281af5da5bb89d0ef21b837b41e4c4463" - }, - { - "path": "audit/v24-sparse-correction-model.json", - "bytes": 600, - "sha256": "0202491ac101543055d9a70c11ab3ed7bf7d253617875271c9d88c7ccd1f2acb" - }, - { - "path": "audit/v24-test-summary.json", - "bytes": 1665, - "sha256": "b7afe3205c814318d2d7646bbb235b2e4c8411ecedf746e72924cb20cfcc562b" - }, - { - "path": "dist/hosted/_headers", - "bytes": 68, - "sha256": "3d11be43c945209eb7e4503e384ac3007c42ee4022cfc23fc005f6217bda8d1b" - }, - { - "path": "dist/hosted/gpu-kernels.js", - "bytes": 22425, - "sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329" - }, - { - "path": "dist/hosted/index.html", - "bytes": 13092, - "sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6" - }, - { - "path": "dist/hosted/script.js", - "bytes": 85737, - "sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2" - }, - { - "path": "dist/standalone/gpu-kernels.js", - "bytes": 22425, - "sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329" - }, - { - "path": "dist/standalone/index.html", - "bytes": 13092, - "sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6" - }, - { - "path": "dist/standalone/script.js", - "bytes": 85737, - "sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2" - }, - { - "path": "gpu-kernels.js", - "bytes": 22425, - "sha256": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329" - }, - { - "path": "index.html", - "bytes": 13092, - "sha256": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6" - }, - { - "path": "package.json", - "bytes": 199, - "sha256": "7c8ccf08afdaef9dc91c9041a5f9b9e1d730a1a9ba237f11d46ec293dc9690ab" - }, - { - "path": "script.js", - "bytes": 85737, - "sha256": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2" - }, - { - "path": "scripts/build.mjs", - "bytes": 657, - "sha256": "84706582dd01d6de28cbc0fcdba9758c3f6f72a3d5693b74736109b35819997f" - }, - { - "path": "scripts/test-all.mjs", - "bytes": 934, - "sha256": "c3ec078b2591cba429eef90cc4f58a7c55e97756c083ed7b2d9437342ad450b4" - }, - { - "path": "tests/legacy-v23/numeric-policy-v23.json", - "bytes": 787, - "sha256": "7afe223d27715a79e34f8c9ea11993f1c37e75bc5d49a6b3284f412d8171a00d" - }, - { - "path": "tests/scenes.json", - "bytes": 3225, - "sha256": "a92c5535c54106ce18823443244f115bf5a2a3d42abfc4ee3573f38e8234e553" - }, - { - "path": "tests/v24-acceptance-contract.mjs", - "bytes": 1035, - "sha256": "114d90b92f89b8b27f6ba35b904464908ec0e1e6393adb02ade8a8dfe5a7bbf0" - }, - { - "path": "tests/v24-backend-policy.mjs", - "bytes": 1192, - "sha256": "81cffe6276ed660210646067502ab9edcce7d0fc608048a98c06633760714b5a" - }, - { - "path": "tests/v24-bla-model.mjs", - "bytes": 677, - "sha256": "04f22c659b61635792b9662b718ec9ae6140189a3daf553ac580b841ee48bd6b" - }, - { - "path": "tests/v24-coordinate-format.mjs", - "bytes": 1253, - "sha256": "a993f04f12fceb4d38c16206636f7951d8bc4afad63a57491e0164bb3575ca3a" - }, - { - "path": "tests/v24-cpu-numeric-model.mjs", - "bytes": 8452, - "sha256": "2ee7a2ef16357db9b537da4534611ea59c80d7279980cf8957adf069093f7fd6" - }, - { - "path": "tests/v24-crossover-contract.mjs", - "bytes": 1460, - "sha256": "1b11958712e27308074123dea0376e067e4cb5dd29cf28a68d87bcf42b7e06ff" - }, - { - "path": "tests/v24-crossover-quality.mjs", - "bytes": 13215, - "sha256": "8ab9ca4da65aee229d0b5d6bdf4a068923a337ed75263514678d4b71a8273e41" - }, - { - "path": "tests/v24-direct-model.mjs", - "bytes": 2452, - "sha256": "9c9ba9cfeaf5e352059a639ba1014ae7a3b0382309a634ee128bc99e62ee3dc5" - }, - { - "path": "tests/v24-geometry-contract.mjs", - "bytes": 1177, - "sha256": "2e5e6fb87f4b7130eff8fff72fd776ef8b0d0e70e91845136a3b18cbf6c86701" - }, - { - "path": "tests/v24-index-contract.mjs", - "bytes": 504, - "sha256": "90f52bb91f12db47da1ebf39a6770cb58d5c4e2809f4d6aa2fdb1263d77cee57" - }, - { - "path": "tests/v24-png-stream-model.mjs", - "bytes": 3411, - "sha256": "ca4dee6252bf6545310ba52c1433e82e83886d04cdb1feb20c7ab08045f431a7" - }, - { - "path": "tests/v24-reference-worker.mjs", - "bytes": 2140, - "sha256": "4d637c549cdda2c28666f5bfe2925c5742103c44b752c337cadbe9bc9df6431d" - }, - { - "path": "tests/v24-source-contract.mjs", - "bytes": 4239, - "sha256": "349c6d344f5f9325cb811cbb9f63017cb2d27139e7557be829f7c85f13bb39d4" - }, - { - "path": "tests/v24-sparse-correction-model.mjs", - "bytes": 9810, - "sha256": "eb72f0aa062bcf8bf915c048ba89e58a7ad5ab338ea559cfc467ba260255c241" - }, - { - "path": "tests/v24-tree-contract.mjs", - "bytes": 691, - "sha256": "933c8c5035806584ee3ffc7645b14aa60f262920528ebb8e5e47666023174770" - }, - { - "path": "tests/v24-wgsl-reserved.mjs", - "bytes": 2260, - "sha256": "98c153ac038852d3402f36ee18ce1f8263de1a016240b4363f01d84a31f0f0ab" - }, - { - "path": "tests/webgpu-acceptance.html", - "bytes": 405, - "sha256": "89b03a08f248b048eead508fc3599057a8cf5afc1d9f846de206a69272dd95bd" - }, - { - "path": "tests/webgpu-acceptance.js", - "bytes": 6884, - "sha256": "e1ef5b21906b4da0fb7154f1e3e555c36267d8a5a7a1bd7ed0474eee147bfd13" - } - ] -} \ No newline at end of file diff --git a/audit/v24-release-verification.json b/audit/v24-release-verification.json deleted file mode 100644 index b6afccc..0000000 --- a/audit/v24-release-verification.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "format": "mandelbrot-v24.2.2-crossover-release-verification", - "generatedAt": "2026-08-23T10:22:38.262457+00:00", - "version": "24.2.2", - "sourceHashes": { - "script.js": "d730983d488805268605176353049d34052026621a7473ada8603db2a0cc05b2", - "gpu-kernels.js": "51fec9480aef5c463c7b6053d156808f8033f22dd181e89329836580afc4d329", - "index.html": "8f954af7fe06b611e10fecdd67fc83db0fc6b4d291baa2d2e74693dbb7c85ca6" - }, - "standaloneMatchesSource": true, - "hostedMatchesSource": true, - "qualitySweep": "audit/v24-crossover-quality.json", - "gpuTiming": "pending external WebGPU adapter; use Diagnostics > Backend比較", - "gates": { - "wgslReserved": "pass", - "sourceContract": "pass", - "indexContract": "pass", - "crossoverContract": "pass", - "crossoverQuality": "pass", - "cpuNumericModel": "pass individually", - "sparseCorrectionModel": "pass individually", - "build": "pass", - "realWebGPU": "pending" - } -} \ No newline at end of file diff --git a/audit/v24-sparse-correction-model.json b/audit/v24-sparse-correction-model.json deleted file mode 100644 index e9bc342..0000000 --- a/audit/v24-sparse-correction-model.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "status": "pass", - "kind": "sparse-double-single-correction-cpu-model-not-real-gpu", - "scene": "swirly-seahorses-z12", - "grid": [ - 61, - 39 - ], - "limit": 2000, - "referenceLength": 2000, - "guardMismatch": 0, - "primary": { - "unknown": 478, - "falseEscaped": 0, - "falseBounded": 0 - }, - "correction": { - "corrected": 478, - "remaining": 0, - "falseEscaped": 0, - "falseBounded": 0, - "escapeIterationMismatch": 5, - "maxEscapeIterationDifference": 374 - }, - "note": "DS correction is a visual-quality recovery pass; escape iteration equality is not certified." -} diff --git a/audit/v24-test-summary.json b/audit/v24-test-summary.json deleted file mode 100644 index c8396c1..0000000 --- a/audit/v24-test-summary.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "status": "v24.2.2-static-cpu-build-pass-real-webgpu-timing-pending", - "rendererVersion": 24, - "shaderVersion": "24.2.2-benchmark", - "packageVersion": "24.2.2", - "generatedAt": "2026-08-23T10:22:11.466258+00:00", - "production": { - "legacyDeepAssets": 0, - "bigIntPixelRenderer": false, - "bla": false, - "multiReference": false, - "sparseDoubleSingleCorrection": true, - "viewSpecificBackendProbe": true, - "correctedDeepExport": true, - "experimentalWholeFrameDsDirect": "benchmark-only" - }, - "crossoverQuality": { - "firstMaterialF32DegradationDepth": 6, - "firstMaterialDsDegradationDepth": 10, - "deepIssueRows": [ - { - "z": 8, - "falseEscaped": 1, - "falseBounded": 0, - "unknown": 0, - "escapeIterationMismatch": 2 - } - ], - "interpretation": "On the Seahorse track at a 1600x900 viewport, f32 direct becomes materially unsuitable around z6; DS direct remains a plausible bridge through roughly z8 and degrades by z10. Corrected deep is the best of the modeled backends, but one z8 false escape means it is not a membership certificate." - }, - "sparseCorrection": { - "primaryUnknown": 478, - "corrected": 478, - "remaining": 0, - "falseEscaped": 0, - "falseBounded": 0 - }, - "build": { - "status": "pass", - "sourceDistHashMatch": true - }, - "realWebGPU": { - "status": "not-run-in-container", - "benchmark": "Diagnostics > Backend比較", - "hardwareTimingRequired": true - }, - "note": "Heavy CPU model gates were also run individually. The aggregate npm test wrapper can exceed the execution harness time budget even though its constituent gates pass." -} \ No newline at end of file diff --git a/build/wasm-v23/bla-scalar.wasm b/build/wasm-v23/bla-scalar.wasm deleted file mode 100644 index 5ac82b3292ab2de840001cdacc2d55aa7b069c1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4593 zcmbVPOKcp+5$&1To&C%%clcRpN$OKQ5`Rn4;?mSgvVo9eD6r)qMgqgR(iBO@98n)H zR|Enqv`id0I@o~@IT%8a3`YT?g9Gc3Kt9-z5g<;Ef&l|L>SiAlKsP(SD6hI__9NLk zWQk^XS66peSH1d-Slzy02qBE)sn>C!1^UBsGk?)zlxhb67xhtDn>wLQ? z3^g_`u7iz5X>6=DS9TUevGf>G4CHUfQdao8*LHV>qXyTL!EHUb{^$Q<;0Rw#o9}=2 z_g^OC-}j8ep7Ec0#;F`9U)0S{d5Mf0GVS00_q*>1Y5RuESj&XIZVIye z@ZOhii{OP_{rhI=q{Y-vfA{ULK^1y-pAQT`zIF{d2xcweWhonaxzmby_uVgkrIz1k z&2*RZSU!`0OlI+b9OWX)Q$7m+=;c1~oS%@>x%rX!X#>+o$xpo9v-Db{D6rq;{b|bG z483gU?hx|@ztW?bU0ftPu)C>-d? zRw}n%igj(s0u*OaD)5Sw0;6y2cmsj|k6S~p6iq7lX4A8MrTl;fSi82b^dC?WGaQ!U zlfZ~O?AUAtlW)q5K=AO1Q|CGfJ#37Nk3oFH> zF}!$eZ5$)B!@|=>Ou{2ApX4EOr&)wF5Fm zwk%Kv?97j_cm3{HzrnpFOgRe10n)iv9<NlZ5sR zM5Kv`CV@ni;Z+_x9(waMABA2mKG|JfhpZ)Eyhu~{_m*5Rm#OL(*a!>!*lC)MXohC6 z=h;Nu_BwUPYYpE3Y(RPMKYt6q{_OS-*02w_J;-_CVBL&yK*Lo1OUoOMkyA)HiB@sr ztTuZMFDbLklYHG;(c%Zb1ZBm2A%_vW(&C#4VT?-)w{BKClhH;Xn1r( zX(BJ~?k1g6bTk4%XIl=qu!lxaH;w)mzslE_l(BCaa_f_yB@j+6HfFW)q8J-2z!;ekK<;RnVeEOBr3SN1z$DP0ijFkKd&lagN;+Vf zy$jBZk-Bqb&(sy`)8L&F_O5U4n{#DXGvWl`zmWi>9-Q9?YK zGdVrZ$M3zDq(|T`NsrU~g_AIJ(&I$flOEwWof<5lGD3n4))pu=~`9F&Fh%9FRe%6Z@`kA0}kA}&v>&(VJI9+OLD0MAzTtdf0Rax9<|90w?K9_OHh zOpu_S)1!Y}`sCSK3HD;Om-w?iv*Xq65P6)WqDbY5!VJC2J^_)4BVZHl`oWY2{_ zE&@YyflQ-TfEG(ii{KoRh!(C>7l4`3Qo{<9umc@(FbpFZt^z~{2i75ha)==#K%5+f01lj-bh8giLO2II1{Tb#?wS2i zG7ni{*uU!T>ZCDKOO@Pa${lXmjH;Uq3BU$|8Ikts5Zm#$n~Uz@+YUJ@d8Vc`e5 zYGg{wFR#s)RKKYkE2?3&t*tMt&!1mim|t68zPu)4y5sWt`LH9NRT8D;^9yUsYa)?T z&6SJm%cYe|C6R1b?JE~c%WIcbHFdI;Wi@DaW}~uZ z$9VX|pMUj}#;_jbzpfn$++&4&NM5@JJNh+Y2{%jG+K%C7Ly^jxAOBd1yv-KYB00*j zNG=rF`Re00AYxO3c=G24vB2jBWNQEBmC%#Kz<;>M%|S35soc=1_BW%^y;If$J7u>G z1GVRPXjlKrQR%kV&QV+K?-<4&&tg-%cDD8%lzQM3qjs+LJZ3(x^h|pxAEr!sjxSS` z@wR+7lM^<@$of{!u*veQbqrWud5wfGGpL#C+5?!!mS>kXtbQ79;NO?C+85f`r-g3q zvFtbu>yQ(;`BTbqd#qX??!fSw$SBeQGV+wKQU`TZgI~J&58Te@sI$KFuJ~>V&Cnpb zyW;hxp%+Uzf%zuSk5m3e;5zk{7lAeBgRhzR0<2M&l#UIPg&uZT`Lq|)c7!?Ej(|4X z(e@0PR#dQ_Rvbv7qYGwBQOtL{0b#@Uj19NP_x^OV<6O?9F4)&idx{i3^{{Qzp5ld1 z-Doi_cqIrLzMBr|lr?#$4E1;hf8Xwx2e39=*9$#S97NINH|lO`Ss+!PHK!Ptp&0a} zgVgK0Aett^LF)6}{;&n7v^cBZ(_X9hYxYrpNW*%+MgsbErQRP}u|G6t*q&U zsCJKZ!6*;|W-9)1D_@m|(D9j4l{-4ns;lIhRe2adajMGY+cJ(u6arX!po*qenVW;a zMIZ@6gH;+pV_W7!ukYNE62=E!c`cKX*MQE_H(|Au_od=I7NSADzyQR^sBV*ULrR+t z)1g2*L=gRpib4<#Bx`h2#Vnqg4`g2XBHjwc5YJ!iiWeWmY3pEqznLBT4Aub-jkI6b zC?-jqST6RXX4XZx8JVWM*uoBzy`~TpGFnk2@*%}FfI^^z+z;WwRIhJNW<)KZffy1X z7h)2nG%Zx<>f_MW2g;coo_aZ+^&(1vjustY9v%P>_wt3waW-dwM4e|bs~eo{kugfk zF4D9WgmiTM&SyWxzU`RK*(pq1_CLBhtg^9X;sRrArpm~8>N7WE<(TSI+SN+tG8PMj99)5G5rmP)`{BbB3dRi$WM zRasnDRdUx=mGgB~T?Hs{8>da)mfZ?%S-oc-7Lz^H9eVcOY*ik>L3;5W6I96vSN6Iy9=ojovZH7x0baJBDwnC(DT+me;0i5(XDSRVjhrwm@~uSq8Z|VI;rrRO}8^dPBvyo zR>h9v+U-*~NjK{}%-b#IEI#1Vd;!p9HjNsSY33E`p_zUJ;U0v-{Mmkm@tj`e80A=f z2gyYyIit`FIc{PD(#bHRfy3t@k_=NC^V;#<=|(z256o$*b!gmYGed+5K4rP~C{4rQ zO~Gl3rmEq&2l?ECMN_eYPyODe?966t?6&F4BqbQz4hn&lkTLE5L(|8;R#Ugw(A7N= zY5SOlMD_+Rq zo&x&WG$w~>RCc3<8v)$bU(^p2I);`!MnySJCuD(!`Ay~3T_#KOJF}0 z9jTA!_7zQ)bRaU!!!{L&gBp8UQL+(;7z=y*V%*mfz?yE|Q5uqO%SL!wS*oI_c*6z(xT^m6#R#X%mIuO#$VI4N+x0HulVkz~@iKg5~#@db8iHACz9InSsi8T%P#&I}hb0Ky-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 diff --git a/build/wasm-v23/color-simd.wasm b/build/wasm-v23/color-simd.wasm deleted file mode 100644 index ddeeaaa775b966de550a12e7068996f65717d0b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/build/wasm-v23/deep-scalar.wasm b/build/wasm-v23/deep-scalar.wasm deleted file mode 100644 index e851c8990e8b989e77f3b3d2d9536b340cd93cbd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5246 zcmcgwJ#Q387@qy`V!S?->ae{BytBsm6ZU);1*P*!#DEipIu#h>M7hL3&IS?b!nui* zE)=P*I*uY;l#YLZQl!lfNF55KRH>5Zo!$AkI}?H|1?$+EcW2(2d7k%cR&4L@c|r*9 zoPSGr-CNa8x6|q3U-7?sqUm*mXad887fyKhJw=g60{(_XB)XtD^a&E(w{-p`f0dUa!f zk@|?-o3*Rma8_f@aZCy_C@m4xYJ}*^Ge!;f&e|oifq|ee&zVs;njV7`PaK7ht{ald$R%*T48m8Y{g`IaokMp>^#=OrwO(u;^O4mcS0lj>em_1Yu1$`KY9H8wVi zS7=+9*09uMl=GSnoHqQtO5Z}p2DsS zYVsl|Db-aSDO_d^ze`q>*=C)ciPmTW{N`O5P6wrK=Dql%Y91g~+BxgC3G=|~Z!lTe z34p(ruEHX!F!M~VtG6e+xnH*8v}C z1fXyX0mv)@#wLM4?F|SxV~=xgI~OaCAfPte{RqdzE@26ZGIfX?^nH=LOzLx}bY}Jj zs7B85=tGtFoDq5lCL;j7x0k8iPl7*XF-x*R%d$x;u)AvE16L~^SiRE!<&SUw{Pp+0 zb)!r5axu;kvZm$E08P9au&R6jkc=D?_t&{w#e31pmhm@tmZcqk)NBb}# zP=!!#fq!Ed41lpOo2OBTvERFkkUq?1P z8a=>98IaMZ6apz8HsBqoicpLHJvTToAP1W{@Nv^NS=~r>{r>0B_SUspG$oO698qVfb1<>qK z)9kOT=ujbzxIutb4S%}KM(N*@k(rD`xMqo-W0oAr^?nS?MxrW!sBkMhj&1U`I#cDQ zt<_?b5~gko31u9ufNNZ9a6q(2xlSCoP?ZgFt(_btM1r|&p|9uKDjMdxs04T;y!0{x z6O^G6Di(uWt2MO`=q(0mI3eKSWl3?wy$L1Vn7=pS(alkh1_s7)G|6R|XQ%?LP63VH z(9Oy^UO}?e2Z}jWAdY*eiV@6hD3=bB&GG2310CZ$9mX(@G8mXW-5A;vdd{B4gMkXH ziS>RbtvTC6`EGIz^o3lWX>bb0ZAFIrCbJhJKs98-mSnPcDSO?foq8M z=`_>FEXEQjM=^#Psw~FTPPr68 zwqjB&oBS-t(c^oCgI)6o>>&HE*!lk-b^)#w#m5lf!=+q9X4I5*{1PtdoOrJ>^jy@A z#*m}ScqOHEsX{3^Vt^)$2dGE@930faXt_DqB4-cecw`7Jfxsk+0nN)h8~pi%E)^e7 zeB5-(;Dn3K$9SYxN7@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{ diff --git a/build/wasm-v23/manifest.json b/build/wasm-v23/manifest.json deleted file mode 100644 index 6e98a4e..0000000 --- a/build/wasm-v23/manifest.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "format": "mandelbrot-source-wasm-manifest-v23", - "toolchain": { - "format": "mandelbrot-wasm-toolchain-v1", - "compiler": "clang", - "version": "17.0.6", - "target": "wasm32-unknown-unknown", - "commonFlags": [ - "-O3", - "-nostdlib", - "-ffreestanding", - "-fno-builtin", - "-Wl,--no-entry", - "-Wl,--export-memory", - "-Wl,--strip-all" - ], - "scalarFlags": [ - "-mno-simd128", - "-fno-vectorize", - "-fno-slp-vectorize" - ], - "simdFlags": [ - "-msimd128" - ] - }, - "abi": "src/abi.json", - "payloads": [ - { - "symbol": "WASM_SIMD_B64", - "file": "wasm-simd.wasm", - "bytes": 511, - "sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b", - "source": "src/shallow_kernel.c" - }, - { - "symbol": "WASM_SCALAR_B64", - "file": "wasm-scalar.wasm", - "bytes": 511, - "sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b", - "source": "src/shallow_kernel.c" - }, - { - "symbol": "DEEP_SIMD_B64", - "file": "deep-simd.wasm", - "bytes": 4866, - "sha256": "d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214", - "source": "src/deep_kernel.c" - }, - { - "symbol": "DEEP_SCALAR_B64", - "file": "deep-scalar.wasm", - "bytes": 5246, - "sha256": "d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2", - "source": "src/deep_kernel.c" - }, - { - "symbol": "BLA_SIMD_B64", - "file": "bla-simd.wasm", - "bytes": 4959, - "sha256": "f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e", - "source": "src/bla_kernel_v18.c" - }, - { - "symbol": "BLA_SCALAR_B64", - "file": "bla-scalar.wasm", - "bytes": 4593, - "sha256": "4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f", - "source": "src/bla_kernel_v18.c" - }, - { - "symbol": "COLOR_SIMD_B64", - "file": "color-simd.wasm", - "bytes": 1387, - "sha256": "8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03", - "source": "src/color_kernel.c" - }, - { - "symbol": "COLOR_SCALAR_B64", - "file": "color-scalar.wasm", - "bytes": 632, - "sha256": "aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90", - "source": "src/color_kernel.c" - } - ] -} diff --git a/build/wasm-v23/wasm-scalar.wasm b/build/wasm-v23/wasm-scalar.wasm deleted file mode 100644 index 14840c05c5cd20fbdf32f770381da1e3e8a4c6b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/build/wasm-v23/wasm-simd.wasm b/build/wasm-v23/wasm-simd.wasm deleted file mode 100644 index 14840c05c5cd20fbdf32f770381da1e3e8a4c6b4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 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 diff --git a/dist/_headers b/dist/_headers deleted file mode 100644 index 8571cd2..0000000 --- a/dist/_headers +++ /dev/null @@ -1,11 +0,0 @@ -/* - Cross-Origin-Opener-Policy: same-origin - Cross-Origin-Embedder-Policy: require-corp - X-Content-Type-Options: nosniff - -/wasm/* - Content-Type: application/wasm - Cache-Control: public, max-age=31536000, immutable - -/hosted/* - Cache-Control: no-cache diff --git a/dist/hosted/gpu-kernels-v24.2.6.js b/dist/hosted/gpu-kernels-v24.2.6.js deleted file mode 100644 index ddc62b8..0000000 --- a/dist/hosted/gpu-kernels-v24.2.6.js +++ /dev/null @@ -1,598 +0,0 @@ -(()=>{'use strict'; -const COMMON=String.raw` -const FIELD_UNKNOWN:u32=0u; -const FIELD_ESCAPED:u32=1u; -const FIELD_INTERIOR_LIKELY:u32=2u; -const FIELD_INTERIOR_PROVEN:u32=3u; -const ITER_MASK:u32=0x000fffffu; -const REASON_SHIFT:u32=20u; -const REASON_MASK:u32=0x0ff00000u; -const REASON_NONE:u32=0u; -const REASON_ERROR_BOUND:u32=1u; -const REASON_ESCAPE_UNCERTAIN:u32=2u; -const REASON_REFERENCE_END:u32=3u; -const REASON_REBASE_GAP:u32=4u; -const REASON_RANGE:u32=5u; -const REASON_OPERATION_LIMIT:u32=6u; - -fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); } -fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); } -fn cmul(a:vec2, 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=1.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; -} -`; - - -// Benchmark candidate: whole-frame double-single direct iteration. -// This intentionally has no CPU reference-build cost. It is not selected by -// the production router until the crossover benchmark justifies it. -const DIRECT_DS_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, _flags:u32, - centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32, - spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32, - invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32, - _p2:f32, _p3:f32, _p4:f32, _p5:f32, -}; -struct DS{ h:f32, l:f32 }; -struct CDS{ r:DS, i:DS }; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var fieldSmooth:array; -fn ds_quick(a:f32,b:f32)->DS{ - let q=a+b; let e=b-(q-a); return DS(q,e); -} -fn ds_sum(a:f32,b:f32)->DS{ - let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e); -} -fn ds_prod(a:f32,b:f32)->DS{ - let q=a*b; - let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah; - let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh; - var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl; - return DS(q,e); -} -fn ds_add(a:DS,b:DS)->DS{ - let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l)); -} -fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);} -fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));} -fn ds_mul(a:DS,b:DS)->DS{ - let q=ds_prod(a.h,b.h); - var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l; - return ds_quick(q.h,e); -} -fn ds_scale(a:DS,b:f32)->DS{ - let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b); -} -fn ds_cmp(a:DS,b:DS)->i32{ - if(a.hb.h){return 1;} - if(a.lb.l){return 1;} return 0; -} -fn ds_value(a:DS)->f32{return a.h+a.l;} -fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));} -fn cds_mul(a:CDS,b:CDS)->CDS{ - let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i)); - let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r)); - return CDS(rr,ii); -} -fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=gid.y*p.tileW+gid.x; - let invW=DS(p.invFullWHi,p.invFullWLo); - let span=DS(p.spanHi,p.spanLo); - let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW); - let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY); - let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy); - let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx)); - let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy)); - let c=CDS(cr,ci); - var z=CDS(DS(0.0,0.0),DS(0.0,0.0)); - var n=0u; - loop{ - if(n>=p.maxIter){break;} - z=cds_add(cds_mul(z,z),c); n+=1u; - let mag=cds_mag2(z); - if(ds_cmp(mag,DS(4.0,0.0))>0){ - fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); - fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag))); - return; - } - } - fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0; -} -`; - - - -// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels -// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and -// can be re-evaluated by the existing deep double-single correction pass. -// This shader is benchmark-only and is not selected by the production router. -const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, _flags:u32, - centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32, - spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32, - invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32, - _p2:f32, _p3:f32, _p4:f32, _p5:f32, -}; -struct DS{ h:f32, l:f32 }; -struct CDS{ r:DS, i:DS }; -struct HybridStats{ - remaining:atomic, _a:atomic, _b:atomic, _c:atomic, - _d:atomic, _e:atomic, _f:atomic, corrected:atomic, -}; -@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 hybridStats:HybridStats; -fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);} -fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);} -fn ds_prod(a:f32,b:f32)->DS{ - let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah; - let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh; - var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e); -} -fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));} -fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);} -fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));} -fn ds_mul(a:DS,b:DS)->DS{ - let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e); -} -fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);} -fn ds_cmp(a:DS,b:DS)->i32{if(a.hb.h){return 1;}if(a.lb.l){return 1;}return 0;} -fn ds_value(a:DS)->f32{return a.h+a.l;} -fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));} -fn cds_mul(a:CDS,b:CDS)->CDS{ - let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i)); - let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii); -} -fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));} -fn mark_sensitive(out:u32,n:u32){ - fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0;atomicAdd(&hybridStats.remaining,1u); -} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=gid.y*p.tileW+gid.x; - let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo); - let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW); - let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY); - let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy); - let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx)); - let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy)); - let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0)); - var deriv=vec2(0.0);var n=0u; - loop{ - if(n>=p.maxIter){break;} - let zv=vec2(ds_value(z.r),ds_value(z.i)); - deriv=2.0*cmul(zv,deriv)+vec2(1.0,0.0); - if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;} - z=cds_add(cds_mul(z,z),c);n+=1u; - let mag=cds_mag2(z); - if(ds_cmp(mag,DS(4.0,0.0))>0){ - fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); - fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return; - } - } - fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0; -} -`; - -// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation. -const DEEP_PERTURB_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct RefPoint{ hi:vec2, lo:vec2 }; -struct UnresolvedHead{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -@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,reason:u32){ - fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0; - atomicAdd(&unresolved.remaining,1u); - if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);} - else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);} - else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);} - else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);} - else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);} - else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);} -} -fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ - let dx=(gx-p.refPixelX)/f32(p.fullW); - let dy=(p.refPixelY-gy)/f32(p.fullW); - // dc = d * 2^scaleExp. Keep d and w in one shared scale. - var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); - var w=vec2(0.0); - var scaleExp=p.spanExp; - var n=0u; var m=0u; var operations=0u; - var errScaled=1.0*F32_U*maxabs(d); - loop{ - if(n>=p.maxIter){ - let rpEnd=refs[min(m,p.refLen)]; - let deltaEnd=scaled_to_f32(w,scaleExp); - let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd); - let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd); - let limit=select(1.0e-3,1.0e-4,strictMode); - if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);} - return; - } - if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;} - let rp=refs[m]; - let delta=scaled_to_f32(w,scaleExp); - let z=rp.hi+(rp.lo+delta); - let mag=dot(z,z); - if(mag>4.0){ - let errAbs=safe_abs_error(errScaled,scaleExp,z,delta); - if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} - mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return; - } - - // Rebase only when dc remains numerically representable in the new scale. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u; - errScaled+=1.0*F32_U*maxabs(d); - continue; - } - - if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;} - let r=refs[m]; - let refAbs=maxabs(r.hi)+maxabs(r.lo); - let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp)); - let gain=2.0*refAbs+2.0*wAbs*p2; - let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30); - errScaled=gain*errScaled+roundErr; - let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); - // delta^2 / 2^scaleExp = w^2 * 2^scaleExp - let sq=cmul(w,w)*pow2_safe(scaleExp); - w=linear+sq+d; m+=1u; n+=1u; operations+=1u; - if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;} - let mm=max(maxabs(w),maxabs(d)); - if(mm>65536.0){ - w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16; - }else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){ - w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16; - } - if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;} - if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;} - } -} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; - render_pixel(out,gx,gy,p.strict!=0u); -} -`; - - -// Sparse correction: only UNKNOWN pixels are re-evaluated with double-single -// perturbation. It is a visual-quality pass, not a membership certificate. -const DEEP_CORRECT_WGSL=COMMON+String.raw` -const CORRECTION_MARK:u32=128u; -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct RefPoint{ hi:vec2, lo:vec2 }; -struct CorrectionStats{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -struct DS{ h:f32, l:f32 }; -struct CDS{ r:DS, i:DS }; -@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 stats:CorrectionStats; - -fn count_remaining(out:u32){ - let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu; - atomicAdd(&stats.remaining,1u); - if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);} - else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);} - else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);} - else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);} - else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);} - else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);} -} -fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){ - fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u); -} - -fn ds_quick(a:f32,b:f32)->DS{ - let q=a+b; - let e=b-(q-a); - return DS(q,e); -} -fn ds_sum(a:f32,b:f32)->DS{ - let q=a+b; - let bb=q-a; - let e=(a-(q-bb))+(b-bb); - return DS(q,e); -} -fn ds_prod(a:f32,b:f32)->DS{ - let q=a*b; - let ca=4097.0*a; - let ah=ca-(ca-a); - let al=a-ah; - let cb=4097.0*b; - let bh=cb-(cb-b); - let bl=b-bh; - var e=ah*bh-q; - e=e+ah*bl; - e=e+al*bh; - e=e+al*bl; - return DS(q,e); -} -fn ds_add(a:DS,b:DS)->DS{ - let q=ds_sum(a.h,b.h); - return ds_quick(q.h,q.l+(a.l+b.l)); -} -fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);} -fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));} -fn ds_mul(a:DS,b:DS)->DS{ - let q=ds_prod(a.h,b.h); - var e=q.l+a.h*b.l; - e=e+a.l*b.h; - e=e+a.l*b.l; - return ds_quick(q.h,e); -} -fn ds_scale(a:DS,b:f32)->DS{ - let q=ds_prod(a.h,b); - return ds_quick(q.h,q.l+a.l*b); -} -fn ds_pow2(a:DS,e:i32)->DS{ - if(e < -126){return DS(0.0,0.0);} - if(e > 126){return DS(8.507059e37,0.0);} - return DS(ldexp(a.h,e),ldexp(a.l,e)); -} -fn ds_cmp(a:DS,b:DS)->i32{ - if(a.hb.h){return 1;} - if(a.lb.l){return 1;} return 0; -} -fn ds_value(a:DS)->f32{return a.h+a.l;} -fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));} -fn cds_mul(a:CDS,b:CDS)->CDS{ - let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i)); - let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r)); - return CDS(rr,ii); -} -fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));} -fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));} -fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));} -fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));} -fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;} - if(m>p.refLen){count_remaining(out);return;} - let rp=refs[m]; - let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y)); - let delta=cds_pow2(w,scaleExp); - let z=cds_add(r,delta); - let mag=cds_mag2(z); - if(ds_cmp(mag,DS(4.0,0.0))>0){ - accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag)))); - return; - } - let dmag=cds_mag2(delta); - if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){ - if(p.spanExp-scaleExp < -96){count_remaining(out);return;} - w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u; - continue; - } - if(m>=p.refLen){count_remaining(out);return;} - let linear=cds_scale(cds_mul(r,w),2.0); - let sq=cds_pow2(cds_mul(w,w),scaleExp); - w=cds_add(cds_add(linear,sq),d); - m+=1u; n+=1u; operations+=1u; - let mm=max(cds_maxabs(w),cds_maxabs(d)); - if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;} - if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;} - else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;} - if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;} - } -} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - correct_pixel(out,gx,gy); -} -`; - -const COLOR_WGSL=String.raw` -struct Params{ - width:u32,height:u32,palette:u32,edgeAA:u32, - cycle:f32,shift:f32,_p0:f32,_p1:f32, -}; -@group(0) @binding(0) var 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(43,20,58)/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&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38); - let floorc=select(vec3(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&0x000fffffu)-i32(m&0x000fffffu))>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.2.6-hybrid-ui-cacheproof',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL -}); -})(); diff --git a/dist/hosted/gpu-kernels.js b/dist/hosted/gpu-kernels.js index 9bcd272..4f73ff2 100644 --- a/dist/hosted/gpu-kernels.js +++ b/dist/hosted/gpu-kernels.js @@ -87,362 +87,77 @@ fn main(@builtin(global_invocation_id) gid:vec3){ `; -// Benchmark candidate: whole-frame double-single direct iteration. -// This intentionally has no CPU reference-build cost. It is not selected by -// the production router until the crossover benchmark justifies it. -const DIRECT_DS_WGSL=COMMON+String.raw` +// Fast-mode precision extension. This keeps the interactive path independent +// of the verified Deep backend: a reduced-precision reference orbit is built +// quickly on the CPU worker and this lightweight perturbation kernel preserves +// sub-f32 pixel offsets once absolute Direct coordinates begin to collapse. +const FAST_PERTURB_WGSL=COMMON+String.raw` struct Params{ tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, _flags:u32, - centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32, - spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32, - invFullWHi:f32, invFullWLo:f32, _p0:f32, _p1:f32, - _p2:f32, _p3:f32, _p4:f32, _p5:f32, + tileX:u32, tileY:u32, maxIter:u32, refLen:u32, + strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, + spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, + refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, + invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, }; -struct DS{ h:f32, l:f32 }; -struct CDS{ r:DS, i:DS }; +struct RefPoint{ hi:vec2, lo:vec2 }; @group(0) @binding(0) var p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var fieldSmooth:array; -fn ds_quick(a:f32,b:f32)->DS{ - let q=a+b; let e=b-(q-a); return DS(q,e); +@group(0) @binding(1) var refs:array; +@group(0) @binding(2) var fieldMeta:array; +@group(0) @binding(3) var fieldSmooth:array; +@group(0) @binding(4) var unresolved:array>; +fn mark_fast_unresolved(out:u32,n:u32,reason:u32){ + fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u); + if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);} } -fn ds_sum(a:f32,b:f32)->DS{ - let q=a+b; let bb=q-a; let e=(a-(q-bb))+(b-bb); return DS(q,e); -} -fn ds_prod(a:f32,b:f32)->DS{ - let q=a*b; - let ca=4097.0*a; let ah=ca-(ca-a); let al=a-ah; - let cb=4097.0*b; let bh=cb-(cb-b); let bl=b-bh; - var e=ah*bh-q; e=e+ah*bl; e=e+al*bh; e=e+al*bl; - return DS(q,e); -} -fn ds_add(a:DS,b:DS)->DS{ - let q=ds_sum(a.h,b.h); return ds_quick(q.h,q.l+(a.l+b.l)); -} -fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);} -fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));} -fn ds_mul(a:DS,b:DS)->DS{ - let q=ds_prod(a.h,b.h); - var e=q.l+a.h*b.l; e=e+a.l*b.h; e=e+a.l*b.l; - return ds_quick(q.h,e); -} -fn ds_scale(a:DS,b:f32)->DS{ - let q=ds_prod(a.h,b); return ds_quick(q.h,q.l+a.l*b); -} -fn ds_cmp(a:DS,b:DS)->i32{ - if(a.hb.h){return 1;} - if(a.lb.l){return 1;} return 0; -} -fn ds_value(a:DS)->f32{return a.h+a.l;} -fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));} -fn cds_mul(a:CDS,b:CDS)->CDS{ - let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i)); - let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r)); - return CDS(rr,ii); -} -fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=gid.y*p.tileW+gid.x; - let invW=DS(p.invFullWHi,p.invFullWLo); - let span=DS(p.spanHi,p.spanLo); - let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW); - let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY); - let dx=ds_scale(invW,ox); let dy=ds_scale(invW,oy); - let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx)); - let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy)); - let c=CDS(cr,ci); - var z=CDS(DS(0.0,0.0),DS(0.0,0.0)); - var n=0u; + +fn render_fast(out:u32,gx:f32,gy:f32){ + let dx=(gx-p.refPixelX)/f32(p.fullW); + let dy=(p.refPixelY-gy)/f32(p.fullW); + var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); + var w=vec2(0.0); + var scaleExp=p.spanExp; + var n=0u; var m=0u; loop{ - if(n>=p.maxIter){break;} - z=cds_add(cds_mul(z,z),c); n+=1u; - let mag=cds_mag2(z); - if(ds_cmp(mag,DS(4.0,0.0))>0){ - fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); - fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag))); - return; + if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;} + let rp=refs[min(m,p.refLen)]; + let delta=scaled_to_f32(w,scaleExp); + let z=rp.hi+(rp.lo+delta); + let mag=dot(z,z); + if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} + + // Rebase the perturbation when it grows beyond the reference value. This + // keeps the fast path usable across pans without any verified correction. + if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); + scaleExp=0; m=0u; + continue; } - } - fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0; -} -`; - - - -// Diagnostic hybrid candidate: DS direct with a sensitivity guard. Pixels -// whose coordinate derivative grows beyond riskLimit are marked UNKNOWN and -// can be re-evaluated by the existing deep double-single correction pass. -// This shader is benchmark-only and is not selected by the production router. -const DIRECT_DS_GUARDED_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, _flags:u32, - centerReHi:f32, centerReLo:f32, centerImHi:f32, centerImLo:f32, - spanHi:f32, spanLo:f32, sampleX:f32, sampleY:f32, - invFullWHi:f32, invFullWLo:f32, riskLimit:f32, _p1:f32, - _p2:f32, _p3:f32, _p4:f32, _p5:f32, -}; -struct DS{ h:f32, l:f32 }; -struct CDS{ r:DS, i:DS }; -struct HybridStats{ - remaining:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - _d:atomic, _e:atomic, _f:atomic, corrected:atomic, -}; -@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 hybridStats:HybridStats; -@group(0) @binding(4) var riskQueue:array; -fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);} -fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);} -fn ds_prod(a:f32,b:f32)->DS{ - let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah; - let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh; - var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e); -} -fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+(a.l+b.l));} -fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);} -fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));} -fn ds_mul(a:DS,b:DS)->DS{ - let q=ds_prod(a.h,b.h);var e=q.l+a.h*b.l;e=e+a.l*b.h;e=e+a.l*b.l;return ds_quick(q.h,e); -} -fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);} -fn ds_cmp(a:DS,b:DS)->i32{if(a.hb.h){return 1;}if(a.lb.l){return 1;}return 0;} -fn ds_value(a:DS)->f32{return a.h+a.l;} -fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));} -fn cds_mul(a:CDS,b:CDS)->CDS{ - let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i)); - let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r));return CDS(rr,ii); -} -fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));} -fn mark_sensitive(out:u32,n:u32){ - fieldMeta[out]=pack_unknown(n,7u);fieldSmooth[out]=0.0; - let slot=atomicAdd(&hybridStats.remaining,1u); - let capacity=p.tileW*p.tileH; - if(slot=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;} + let r=rp; + let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); + let sq=cmul(w,w)*pow2_safe(scaleExp); + w=linear+sq+d; m+=1u; n+=1u; + let mm=max(maxabs(w),maxabs(d)); + if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;} + if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;} + else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;} + if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;} } } @compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) gid:vec3){ if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=gid.y*p.tileW+gid.x; - let invW=DS(p.invFullWHi,p.invFullWLo);let span=DS(p.spanHi,p.spanLo); - let ox=f32(p.tileX+gid.x)+p.sampleX-0.5*f32(p.fullW); - let oy=0.5*f32(p.fullH)-(f32(p.tileY+gid.y)+p.sampleY); - let dx=ds_scale(invW,ox);let dy=ds_scale(invW,oy); - let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_mul(span,dx)); - let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_mul(span,dy)); - let c=CDS(cr,ci);var z=CDS(DS(0.0,0.0),DS(0.0,0.0)); - var deriv=vec2(0.0);var n=0u; - loop{ - if(n>=p.maxIter){break;} - let zv=vec2(ds_value(z.r),ds_value(z.i)); - deriv=2.0*cmul(zv,deriv)+vec2(1.0,0.0); - if(length(deriv)>p.riskLimit || any(deriv!=deriv)){mark_sensitive(out,n);return;} - z=cds_add(cds_mul(z,z),c);n+=1u; - let mag=cds_mag2(z); - if(ds_cmp(mag,DS(4.0,0.0))>0){ - fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); - fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return; - } - } - fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0; + let out=p.outputBase+gid.y*p.outputStride+gid.x; + let gx=f32(p.tileX+gid.x)+p.sampleX; + let gy=f32(p.tileY+gid.y)+p.sampleY; + render_fast(out,gx,gy); } `; -// Converts the GPU-written risk queue length into indirect dispatch arguments. -// The correction pass therefore launches one lane per enqueued risky pixel, -// instead of one lane per screen pixel. -const DIRECT_FIXED96_QUEUE_ARGS_WGSL=COMMON+String.raw` -struct HybridStats{ - remaining:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - _d:atomic, _e:atomic, _f:atomic, corrected:atomic, -}; -struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 }; -@group(0) @binding(0) var hybridStats:HybridStats; -@group(0) @binding(1) var indirectArgs:IndirectArgs; -@compute @workgroup_size(1) -fn main(){ - let n=atomicLoad(&hybridStats.enqueued); - atomicStore(&hybridStats.dispatchCount,n); - indirectArgs.x=(n+63u)/64u; - indirectArgs.y=1u; - indirectArgs.z=1u; - indirectArgs._pad=0u; -} -`; - - - -// Diagnostic sparse fixed-point direct correction. It does not consume a -// deep reference. The guarded DS pass marks uncertain pixels with reason 7; -// this shader re-evaluates only those pixels using signed Q8.88 arithmetic -// implemented entirely with u32 limbs. Integer arithmetic avoids the -// reassociation/fusion sensitivity of float-expansion correction kernels. -const DIRECT_FIXED96_CORRECT_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, maxIter:u32, _flags:u32, - baseReLo:u32, baseReMid:u32, baseReHi:u32, - baseImLo:u32, baseImMid:u32, baseImHi:u32, - stepLo:u32, stepMid:u32, stepHi:u32, - _pad0:u32, _pad1:u32, _pad2:u32, -}; -struct FX96{ lo:u32, mid:u32, hi:u32 }; -struct FixedStats{ - remaining:atomic, invalidIndex:atomic, processed:atomic, staleEntry:atomic, - _d:atomic, _e:atomic, _f:atomic, corrected:atomic, -}; -struct HybridStats{ - remaining:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - _d:atomic, _e:atomic, _f:atomic, corrected:atomic, -}; -@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 fixedStats:FixedStats; -@group(0) @binding(4) var hybridStats:HybridStats; -@group(0) @binding(5) var riskQueue:array; - -fn fx_add(a:FX96,b:FX96)->FX96{ - let lo=a.lo+b.lo; let c0=select(0u,1u,loFX96{ - let lo=(~a.lo)+1u; let c0=select(0u,1u,lo==0u); - let mid=(~a.mid)+c0; let c1=select(0u,1u,c0!=0u && mid==0u); - return FX96(lo,mid,(~a.hi)+c1); -} -fn fx_sub(a:FX96,b:FX96)->FX96{return fx_add(a,fx_neg(b));} -fn fx_negative(a:FX96)->bool{return (a.hi&0x80000000u)!=0u;} -fn fx_abs(a:FX96)->FX96{if(fx_negative(a)){return fx_neg(a);}return a;} -fn mul32(a:u32,b:u32)->vec2{ - let a0=a&0xffffu; let a1=a>>16u; let b0=b&0xffffu; let b1=b>>16u; - let p0=a0*b0; let p1=a0*b1; let p2=a1*b0; let p3=a1*b1; - let middle=(p0>>16u)+(p1&0xffffu)+(p2&0xffffu); - let lo=(p0&0xffffu)|(middle<<16u); - let hi=p3+(p1>>16u)+(p2>>16u)+(middle>>16u); - return vec2(lo,hi); -} -fn add_at(a:ptr>,idx0:u32,value0:u32){ - var idx=idx0; var value=value0; - loop{ - if(value==0u || idx>=6u){break;} - let old=(*a)[idx]; let sum=old+value; (*a)[idx]=sum; - value=select(0u,1u,sum>,idx:u32,x:u32,y:u32){ - let q=mul32(x,y); add_at(a,idx,q.x); add_at(a,idx+1u,q.y); -} -fn fx_mul(a0:FX96,b0:FX96)->FX96{ - let neg=fx_negative(a0)!=fx_negative(b0); let a=fx_abs(a0); let b=fx_abs(b0); - var w=array(0u,0u,0u,0u,0u,0u); - add_product(&w,0u,a.lo,b.lo); add_product(&w,1u,a.lo,b.mid); add_product(&w,2u,a.lo,b.hi); - add_product(&w,1u,a.mid,b.lo); add_product(&w,2u,a.mid,b.mid);add_product(&w,3u,a.mid,b.hi); - add_product(&w,2u,a.hi,b.lo); add_product(&w,3u,a.hi,b.mid); add_product(&w,4u,a.hi,b.hi); - // Q8.88 * Q8.88 -> Q16.176. Shift right by 88 with one-bit rounding. - var r=FX96((w[2]>>24u)|(w[3]<<8u),(w[3]>>24u)|(w[4]<<8u),(w[4]>>24u)|(w[5]<<8u)); - if(((w[2]>>23u)&1u)!=0u){r=fx_add(r,FX96(1u,0u,0u));} - if(neg){return fx_neg(r);}return r; -} -fn fx_mul_small(a0:FX96,k:u32)->FX96{ - let neg=fx_negative(a0); let a=fx_abs(a0); - let q0=mul32(a.lo,k); let q1=mul32(a.mid,k); let q2=mul32(a.hi,k); - let mid=q1.x+q0.y; let cm=select(0u,1u,midi32{ - if(a.hib.hi){return 1;} - if(a.midb.mid){return 1;} - if(a.lob.lo){return 1;} return 0; -} -fn fx_to_f32_positive(a:FX96)->f32{ - return f32(a.hi)*5.960464477539063e-8 + f32(a.mid)*1.3877787807814457e-17 + f32(a.lo)*3.2311742677852644e-27; -} -fn fx_to_f32(a:FX96)->f32{ - if(fx_negative(a)){return -fx_to_f32_positive(fx_neg(a));} - return fx_to_f32_positive(a); -} -fn write_escape(out:u32,n:u32,zr:FX96,zi:FX96){ - // Once either component exceeds 2 in magnitude, escape is mathematically - // certain. Use f32 only for the smooth-color estimate; membership remains - // decided by the integer fixed-point state. This avoids squaring a large - // escaped value and therefore avoids Q8.88 wraparound. - let zrf=fx_to_f32(zr); let zif=fx_to_f32(zi); - let mag=max(4.0000005,zrf*zrf+zif*zif); - fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); - fieldSmooth[out]=smooth_escape(n,mag); - atomicAdd(&fixedStats.corrected,1u); -} - -@compute @workgroup_size(64) -fn main(@builtin(global_invocation_id) gid:vec3){ - let qi=gid.x; - let queued=atomicLoad(&hybridStats.dispatchCount); - if(qi>=queued){return;} - atomicAdd(&fixedStats.processed,1u); - let out=riskQueue[qi]; - let capacity=p.tileW*p.tileH; - if(out>=capacity){ - atomicAdd(&fixedStats.invalidIndex,1u); - atomicAdd(&fixedStats.remaining,1u); - return; - } - let old=fieldMeta[out]; - let cls=(old>>28u)&3u; let reason=(old&REASON_MASK)>>REASON_SHIFT; - if(cls!=FIELD_UNKNOWN || reason!=7u){ - atomicAdd(&fixedStats.staleEntry,1u); - atomicAdd(&fixedStats.remaining,1u); - return; - } - let px=out%p.tileW; let py=out/p.tileW; - let baseRe=FX96(p.baseReLo,p.baseReMid,p.baseReHi); - let baseIm=FX96(p.baseImLo,p.baseImMid,p.baseImHi); - let step=FX96(p.stepLo,p.stepMid,p.stepHi); - let cr=fx_add(baseRe,fx_mul_small(step,px)); - let ci=fx_sub(baseIm,fx_mul_small(step,py)); - var zr=FX96(0u,0u,0u); var zi=FX96(0u,0u,0u); var n=0u; - let TWO=FX96(0u,0u,0x02000000u); - let FOUR=FX96(0u,0u,0x04000000u); - loop{ - if(n>=p.maxIter){break;} - // The previous state is inside the bailout disk, so these products are - // bounded and cannot overflow Q8.88. - let zr2=fx_mul(zr,zr); let zi2=fx_mul(zi,zi); let zri=fx_mul(zr,zi); - let nextR=fx_add(fx_sub(zr2,zi2),cr); - let nextI=fx_add(fx_add(zri,zri),ci); - zr=nextR; zi=nextI; n+=1u; - // Do not mistake the Q8.88 integer byte for sign-extension. Values such - // as +1.2 are valid. The old range guard rejected them and left virtually - // every risky escaping pixel UNKNOWN. - if(fx_cmp_unsigned(fx_abs(zr),TWO)>0 || fx_cmp_unsigned(fx_abs(zi),TWO)>0){ - write_escape(out,n,zr,zi); return; - } - let rr=fx_mul(zr,zr); let ii=fx_mul(zi,zi); let mag=fx_add(rr,ii); - if(fx_cmp_unsigned(mag,FOUR)>0){ - fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); - fieldSmooth[out]=smooth_escape(n,max(4.0000005,fx_to_f32_positive(mag))); - atomicAdd(&fixedStats.corrected,1u); return; - } - } - fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0; - atomicAdd(&fixedStats.corrected,1u); -} -`; - - -// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation. const DEEP_PERTURB_WGSL=COMMON+String.raw` struct Params{ tileW:u32, tileH:u32, fullW:u32, fullH:u32, @@ -480,7 +195,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); var w=vec2(0.0); var scaleExp=p.spanExp; - var n=0u; var m=0u; var operations=0u; + var n=0u; var m=0u; var errScaled=1.0*F32_U*maxabs(d); loop{ if(n>=p.maxIter){ @@ -492,7 +207,6 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);} return; } - if(m>p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;} let rp=refs[m]; let delta=scaled_to_f32(w,scaleExp); let z=rp.hi+(rp.lo+delta); @@ -513,7 +227,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ } if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;} - let r=refs[m]; + let r=rp; let refAbs=maxabs(r.hi)+maxabs(r.lo); let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp)); let gain=2.0*refAbs+2.0*wAbs*p2; @@ -522,7 +236,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); // delta^2 / 2^scaleExp = w^2 * 2^scaleExp let sq=cmul(w,w)*pow2_safe(scaleExp); - w=linear+sq+d; m+=1u; n+=1u; operations+=1u; + w=linear+sq+d; m+=1u; n+=1u; if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;} let mm=max(maxabs(w),maxabs(d)); if(mm>65536.0){ @@ -531,7 +245,6 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16; } if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;} - if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n,REASON_OPERATION_LIMIT);return;} } } @compute @workgroup_size(8,8) @@ -545,6 +258,182 @@ fn main(@builtin(global_invocation_id) gid:vec3){ `; +// Idle refinement is compiled as a separate guarded-Deep module so the +// production Fast/Deep main shaders remain byte-for-byte identical to v24.2.18. +const DEEP_LIKELY_REFINE_WGSL=DEEP_PERTURB_WGSL.replace( + '@group(0) @binding(4) var unresolved:UnresolvedHead;', + '@group(0) @binding(4) var unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var refineQueue:array;\n@group(1) @binding(1) var refineBatch:RefineBatch;' +)+String.raw` +@compute @workgroup_size(64) +fn refine_likely(@builtin(global_invocation_id) gid:vec3){ + let slot=refineBatch.base+gid.x; + if(slot>=refineBatch.end){return;} + let out=refineQueue[slot]; + if(((fieldMeta[out]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;} + let x=out%p.fullW; let y=out/p.fullW; + render_pixel(out,f32(x)+p.sampleX,f32(y)+p.sampleY,p.strict!=0u); +} +`; + +// Production Deep sparse path: keep the long-running perturbation kernel +// untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets. +// Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS +// correction kernel sees less workgroup-level iteration divergence. +// +// Bucket state layout (24 u32 atomics): +// [0..7] histogram counts +// [8..15] scatter cursors (initialized from prefix offsets) +// [16..23] immutable prefix offsets for queue accounting +const DEEP_BUCKET_HIST_WGSL=COMMON+String.raw` +struct Params{ + tileW:u32, tileH:u32, fullW:u32, fullH:u32, + tileX:u32, tileY:u32, maxIter:u32, refLen:u32, + strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, + spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, + refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, + invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, +}; +@group(0) @binding(0) var p:Params; +@group(0) @binding(1) var fieldMeta:array; +@group(0) @binding(2) var bucketState:array>; +var localCounts:array,8>; +fn deep_bucket(packed:u32)->u32{ + let n=packed&ITER_MASK; + return min(7u,(n*8u)/max(1u,p.maxIter)); +} +@compute @workgroup_size(64) +fn main( + @builtin(local_invocation_id) lid3:vec3, + @builtin(workgroup_id) wid:vec3 +){ + let lane=lid3.x; + if(lane<8u){atomicStore(&localCounts[lane],0u);} + workgroupBarrier(); + let lx=wid.x*64u+lane; + let ly=wid.y; + if(lx>28u)&3u)==FIELD_UNKNOWN){ + atomicAdd(&localCounts[deep_bucket(packed)],1u); + } + } + workgroupBarrier(); + if(lane<8u){ + let c=atomicLoad(&localCounts[lane]); + if(c>0u){atomicAdd(&bucketState[lane],c);} + } +} +`; + +// Convert the 8-bin histogram into contiguous queue ranges, initialize each +// scatter cursor, and produce the correction indirect-dispatch arguments. +const DEEP_BUCKET_PREFIX_WGSL=String.raw` +struct SparseQueueStats{ + selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, + invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, +}; +struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 }; +@group(0) @binding(0) var bucketState:array>; +@group(0) @binding(1) var sparseQueueStats:SparseQueueStats; +@group(0) @binding(2) var indirectArgs:IndirectArgs; +@compute @workgroup_size(1) +fn main(){ + var total=0u; + var b=0u; + loop{ + if(b>=8u){break;} + let c=atomicLoad(&bucketState[b]); + atomicStore(&bucketState[8u+b],total); + atomicStore(&bucketState[16u+b],total); + total+=c; + b+=1u; + } + atomicStore(&sparseQueueStats.selected,total); + atomicStore(&sparseQueueStats.overflow,0u); + atomicStore(&sparseQueueStats.enqueued,total); + atomicStore(&sparseQueueStats.dispatchCount,total); + indirectArgs.x=(total+63u)/64u; + indirectArgs.y=1u; + indirectArgs.z=1u; + indirectArgs._pad=0u; +} +`; + +// Scatter UNKNOWN indices into the precomputed bucket ranges. Within each +// 64-lane workgroup, workgroup-memory atomics allocate local ranks; each +// non-empty bucket reserves one global subrange, so global atomics scale with +// non-empty (workgroup,bucket) pairs rather than with UNKNOWN pixels. +const DEEP_BUCKET_SCATTER_WGSL=COMMON+String.raw` +struct Params{ + tileW:u32, tileH:u32, fullW:u32, fullH:u32, + tileX:u32, tileY:u32, maxIter:u32, refLen:u32, + strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, + spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, + refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, + invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, +}; +struct SparseQueueStats{ + selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, + invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, +}; +@group(0) @binding(0) var p:Params; +@group(0) @binding(1) var fieldMeta:array; +@group(0) @binding(2) var bucketState:array>; +@group(0) @binding(3) var sparseQueueStats:SparseQueueStats; +@group(0) @binding(4) var unknownQueue:array; +var localCounts:array,8>; +var localRanks:array,8>; +var groupBase:array; +fn deep_bucket(packed:u32)->u32{ + let n=packed&ITER_MASK; + return min(7u,(n*8u)/max(1u,p.maxIter)); +} +@compute @workgroup_size(64) +fn main( + @builtin(local_invocation_id) lid3:vec3, + @builtin(workgroup_id) wid:vec3 +){ + let lane=lid3.x; + if(lane<8u){ + atomicStore(&localCounts[lane],0u); + atomicStore(&localRanks[lane],0u); + groupBase[lane]=0u; + } + workgroupBarrier(); + + let lx=wid.x*64u+lane; + let ly=wid.y; + var out=0u; + var hit=0u; + var bucket=0u; + if(lx>28u)&3u)==FIELD_UNKNOWN){ + hit=1u; + bucket=deep_bucket(packed); + atomicAdd(&localCounts[bucket],1u); + } + } + workgroupBarrier(); + + if(lane<8u){ + let c=atomicLoad(&localCounts[lane]); + if(c>0u){groupBase[lane]=atomicAdd(&bucketState[8u+lane],c);} + } + workgroupBarrier(); + + if(hit!=0u){ + let rank=atomicAdd(&localRanks[bucket],1u); + let qi=groupBase[bucket]+rank; + let capacity=p.tileW*p.tileH; + if(qi=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;} - if(m>p.refLen){count_remaining(out);return;} let rp=refs[m]; let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y)); let delta=cds_pow2(w,scaleExp); @@ -682,12 +570,12 @@ fn correct_pixel(out:u32,gx:f32,gy:f32){ let linear=cds_scale(cds_mul(r,w),2.0); let sq=cds_pow2(cds_mul(w,w),scaleExp); w=cds_add(cds_add(linear,sq),d); - m+=1u; n+=1u; operations+=1u; + m+=1u; n+=1u; let mm=max(cds_maxabs(w),cds_maxabs(d)); if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;} if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;} else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;} - if(scaleExp>126 || operations>p.maxIter*2u+2048u){count_remaining(out);return;} + if(scaleExp>126){count_remaining(out);return;} } } @compute @workgroup_size(8,8) @@ -701,7 +589,76 @@ fn main(@builtin(global_invocation_id) gid:vec3){ } `; + +// Production sparse correction variant. It shares the complete DS arithmetic +// with DEEP_CORRECT_WGSL, but consumes only indices emitted by the queued deep +// perturbation pass and launches via dispatchWorkgroupsIndirect. +const DEEP_CORRECT_QUEUE_WGSL=DEEP_CORRECT_WGSL + .replace( +`struct CorrectionStats{ + remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, + rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, +};`, +`struct CorrectionStats{ + remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, + rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, +}; +struct SparseQueueStats{ + selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, + invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, +};`) + .replace( +`@group(0) @binding(4) var stats:CorrectionStats;`, +`@group(0) @binding(4) var stats:CorrectionStats; +@group(0) @binding(5) var sparseQueueStats:SparseQueueStats; +@group(0) @binding(6) var unknownQueue:array;`) + .replace( +`@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=p.outputBase+gid.y*p.outputStride+gid.x; + if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} + let gx=f32(p.tileX+gid.x)+p.sampleX; + let gy=f32(p.tileY+gid.y)+p.sampleY; + correct_pixel(out,gx,gy); +}`, +`@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid:vec3){ + let qi=gid.x; + let queued=atomicLoad(&sparseQueueStats.dispatchCount); + if(qi>=queued){return;} + let out=unknownQueue[qi]; + atomicAdd(&sparseQueueStats.processed,1u); + if(out=p.tileW||ly>=p.tileH){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;} + if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;} + let gx=f32(p.tileX+lx)+p.sampleX; + let gy=f32(p.tileY+ly)+p.sampleY; + correct_pixel(out,gx,gy); +}`); + +if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)')){ + throw new Error('failed to derive queued deep correction shader'); +} + +const LIKELY_QUEUE_WGSL=COMMON+String.raw` +struct QueueParams{width:u32,height:u32,mode:u32,sparseStride:u32}; +struct QueueStats{count:atomic,overflow:atomic,_p0:atomic,_p1:atomic}; +@group(0) @binding(0) var p:QueueParams; +@group(0) @binding(1) var fieldMeta:array; +@group(0) @binding(2) var queue:array; +@group(0) @binding(3) var stats:QueueStats; +fn is_frontier(x:u32,y:u32,r:i32)->bool{let xi=i32(x);let yi=i32(y);var oy=-1;loop{if(oy>1){break;}var ox=-1;loop{if(ox>1){break;}if(ox!=0||oy!=0){let xx=xi+ox*r;let yy=yi+oy*r;if(xx>=0&&yy>=0&&xx>28u)&3u;if(cls==FIELD_ESCAPED||cls==FIELD_UNKNOWN){return true;}}}ox+=1;}oy+=1;}return false;} +@compute @workgroup_size(256) +fn main(@builtin(global_invocation_id) gid:vec3){let i=gid.x;let total=p.width*p.height;if(i>=total){return;}if(((fieldMeta[i]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}let x=i%p.width;let y=i/p.width;var selected=false;if(p.mode==0u){selected=true;}else{let radius:i32=select(2,select(3,4,p.mode>=3u),p.mode>=2u);selected=is_frontier(x,y,radius);if(!selected&&p.sparseStride>0u){selected=(x%p.sparseStride==0u)&&(y%p.sparseStride==0u);}}if(!selected){return;}let slot=atomicAdd(&stats.count,1u);if(slotvec3{ 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 palette_color(phase:f32)->vec3{ + if(p.palette==1u){return hsv(phase,0.92,1.0);}if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3(g);}return current_palette(phase); +} +fn escaped_color(m:u32,sm:f32)->vec3{ + let phase=fract(p.shift+sm*p.cycle);let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.34+0.66*pow(edge,0.38);let floorc=select(vec3(2,5,15)/255.0,vec3(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv); +} +fn provisional_unknown(m:u32)->vec3{ + let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3(6,10,22)/255.0,vec3(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52); +} fn base_color(i:u32)->vec3{ - let m=fieldMeta[i]; let cls=(m>>28u)&3u; - if(cls==0u){return vec3(43,20,58)/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&0x000fffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38); - let floorc=select(vec3(2,5,15)/255.0,vec3(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv); + let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3(0.0);}return escaped_color(m,fieldSmooth[i]); } 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(gid.x>=p.width||gid.y>=p.height){return;}let i=gid.y*p.width+gid.x;let m=fieldMeta[i];let cls=(m>>28u)&3u;var c=base_color(i);let x=i32(gid.x);let y=i32(gid.y); + if(cls==FIELD_UNKNOWN){ + var fill=vec3(0.0);var fillCount=0.0; + for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];if(((mj>>28u)&3u)!=FIELD_UNKNOWN){fill+=linearize(base_color(j));fillCount+=1.0;}}} + if(fillCount>0.0){c=delinearize(fill/fillCount);} + } if(p.edgeAA!=0u){ - let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0; - let x=i32(gid.x); let y=i32(gid.y); - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}} + var boundary=false;var sum=linearize(c);var cnt=1.0; + for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}} if(boundary){c=delinearize(sum/cnt);} } textureStore(outTex,vec2(gid.xy),vec4(c,1.0)); } -`; +` const AA_RESOLVE_WGSL=String.raw` @@ -786,7 +751,28 @@ struct VSOut{@builtin(position) pos:vec4,@location(0) uv:vec2}; } `; + +// v24.2.14 production screen path: defer UNKNOWN statistics out of the long +// perturbation kernel. The existing bucket histogram pass already scans the +// same field before sparse correction, so it can aggregate remaining/reason +// counters per workgroup without per-UNKNOWN global atomics in the primary. +// The counted primary is retained for export and verification paths. +const DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u);\n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}'); + +const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL + .replace('@group(0) @binding(2) var bucketState:array>;', '@group(0) @binding(2) var bucketState:array>;\n@group(0) @binding(3) var unresolvedStats:array>;') + .replace('var localCounts:array,8>;', 'var localCounts:array,8>;\nvar localReasons:array,8>;') + .replace('if(lane<8u){atomicStore(&localCounts[lane],0u);}', 'if(lane<8u){atomicStore(&localCounts[lane],0u);atomicStore(&localReasons[lane],0u);}') + .replace('atomicAdd(&localCounts[deep_bucket(packed)],1u);', `atomicAdd(&localCounts[deep_bucket(packed)],1u); + atomicAdd(&localReasons[0],1u); + let reason=(packed&REASON_MASK)>>REASON_SHIFT; + if(reason>=1u && reason<=6u){atomicAdd(&localReasons[reason],1u);}`) + .replace('if(c>0u){atomicAdd(&bucketState[lane],c);}', `if(c>0u){atomicAdd(&bucketState[lane],c);} + let r=atomicLoad(&localReasons[lane]); + if(r>0u){atomicAdd(&unresolvedStats[lane],r);}`); +if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('v24.2.14 post-stats shader derivation failed'); + globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({ - version:'24.2.8-fixed96-sparse-queue',DIRECT_F32_WGSL,DIRECT_DS_WGSL,DIRECT_DS_GUARDED_WGSL,DIRECT_FIXED96_QUEUE_ARGS_WGSL,DIRECT_FIXED96_CORRECT_WGSL,DEEP_PERTURB_WGSL,DEEP_CORRECT_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL + version:'24.2.26-baseline-idle-refinement',DIRECT_F32_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_LIKELY_REFINE_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,LIKELY_QUEUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL }); })(); diff --git a/dist/hosted/hosted-loader.js b/dist/hosted/hosted-loader.js deleted file mode 100644 index 9a21d2b..0000000 --- a/dist/hosted/hosted-loader.js +++ /dev/null @@ -1,36 +0,0 @@ -const wasmRoot = new URL('../wasm/', import.meta.url); - -async function compileAsset(name) { - const url = new URL(name, wasmRoot); - const response = await fetch(url, { cache: 'force-cache' }); - if (!response.ok) throw new Error(`WASM fetch failed: ${name} (${response.status})`); - if (WebAssembly.compileStreaming) { - try { return await WebAssembly.compileStreaming(Promise.resolve(response.clone())); } - catch { /* A proxy may have supplied the wrong MIME type; use bytes below. */ } - } - return WebAssembly.compile(await response.arrayBuffer()); -} - -let shallowModule, shallowSimd = true; -try { shallowModule = await compileAsset('wasm-simd.d19b26c04e1b2f59.wasm'); } -catch { shallowModule = await compileAsset('wasm-scalar.d19b26c04e1b2f59.wasm'); shallowSimd = false; } -const asset = name => new URL(name, wasmRoot).href; - -globalThis.MANDEL_KERNELS = Object.freeze({ - // The compiled module is structured-cloned to the shallow Worker. No Base64 - // conversion or second compile is needed in the hosted build. - WASM_SIMD_B64: shallowModule, - WASM_SCALAR_B64: shallowModule, - DEEP_SIMD_B64: asset('deep-simd.d49385f06e4a8f55.wasm'), - DEEP_SCALAR_B64: asset('deep-scalar.d133bbecc8f6dbdf.wasm'), - BLA_SIMD_B64: asset('bla-simd.f80f136e9fb676ce.wasm'), - BLA_SCALAR_B64: asset('bla-scalar.4f06913487044823.wasm'), - COLOR_SIMD_B64: asset('color-simd.8cf83374ed0c680b.wasm'), - COLOR_SCALAR_B64: asset('color-scalar.aa2914ec1acacaa2.wasm') -}); -globalThis.MANDEL_KERNEL_META = Object.freeze({"WASM_SIMD_B64":"d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b","WASM_SCALAR_B64":"d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b","DEEP_SIMD_B64":"d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214","DEEP_SCALAR_B64":"d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2","BLA_SIMD_B64":"f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e","BLA_SCALAR_B64":"4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f","COLOR_SIMD_B64":"8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03","COLOR_SCALAR_B64":"aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90"}); -globalThis.MANDEL_HOSTED_SHALLOW_SIMD = shallowSimd; - -const app = document.createElement('script'); -app.src = './script.js'; -document.body.appendChild(app); diff --git a/dist/hosted/index.html b/dist/hosted/index.html index 78d2818..1b9bf1e 100644 --- a/dist/hosted/index.html +++ b/dist/hosted/index.html @@ -4,7 +4,7 @@ -Mandelbrot Deep Zoom v24.2.8 Fixed96 Sparse Queue Experiment +Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement -

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 deleted file mode 100644 index 02d0c10..0000000 --- a/tests/document-contract.ps1 +++ /dev/null @@ -1,133 +0,0 @@ -$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 deleted file mode 100644 index 006bf46..0000000 --- a/tests/js-syntax.mjs +++ /dev/null @@ -1,157 +0,0 @@ -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'); -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+2,singleFile:true},null,2)); +if(!/select option\{background:#000;color:#fff\}/.test(h))throw new Error('dropdown theme not embedded'); +if(!/overflow-y:auto/.test(h))throw new Error('panel scroll fix not embedded'); +console.log(JSON.stringify({status:'pass',checks:req.length+9,singleFile:true},null,2)); diff --git a/tests/v24-pan-reuse-model.mjs b/tests/v24-pan-reuse-model.mjs new file mode 100644 index 0000000..2cb4ec3 --- /dev/null +++ b/tests/v24-pan-reuse-model.mjs @@ -0,0 +1,16 @@ +import fs from 'node:fs/promises'; +const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); +const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; +must(/source:\{bits:snap\.bits,re:snap\.re,im:snap\.im,span:snap\.span,iter\}/.test(script),'reference source metadata is not retained'); +must(/newSpan!==oldSpan/.test(script),'span equality reuse guard missing'); +must(/src\.iter!==iter/.test(script),'iteration reuse guard missing'); +must(/pixel\.x<0\|\|pixel\.x>w\|\|pixel\.y<0\|\|pixel\.y>h/.test(script),'reference viewport reuse guard missing'); +must(/referencePixelForSource/.test(script),'selected reference pixel mapping missing'); +const W=800,H=500; +for(const [dx,dy] of [[0,0],[24,0],[-100,30],[399,-249]]){ + // Pure pan: new center = old center - dx*span/W (real), +dy*span/W (imag). + // Mapping old center into new view must recover center+pointer displacement. + const refX=W/2+dx,refY=H/2+dy; + if(refX<0||refX>W||refY<0||refY>H)throw new Error('test vector outside viewport'); +} +console.log(JSON.stringify({status:'pass',check:'pan-reference-reuse-model',vectors:4,policy:'same span + same iter + old reference inside new viewport'},null,2)); diff --git a/tests/v24-primary-post-stats-model.mjs b/tests/v24-primary-post-stats-model.mjs new file mode 100644 index 0000000..589c40a --- /dev/null +++ b/tests/v24-primary-post-stats-model.mjs @@ -0,0 +1,30 @@ +import fs from 'node:fs/promises'; +await import('../gpu-kernels.js'); +const G=globalThis.MANDEL_WEBGPU_KERNELS; +const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); +const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; +const ITER_MASK=0x000fffff, REASON_SHIFT=20, FIELD_ESCAPED=1; +const packUnknown=(n,r)=>((n&ITER_MASK)|((r&0xff)<>>0; +const packKnown=n=>((FIELD_ESCAPED<<28)|(n&ITER_MASK))>>>0; +const bucket=(m,maxIter)=>Math.min(7,Math.floor(((m&ITER_MASK)*8)/Math.max(1,maxIter))); +let seed=0x31415926; const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/0x100000000}; +const cases=[]; +for(const [w,h,maxIter,rate] of [[1,1,350,1],[65,7,611,.14],[320,180,667,.18],[257,131,721,.03],[511,23,901,.65]]){ + const meta=new Uint32Array(w*h),direct=new Uint32Array(8),wg=new Uint32Array(8),directBuckets=new Uint32Array(8),wgBuckets=new Uint32Array(8); + for(let i=0;i=w)continue;const m=meta[y*w+x];if((m>>>28)===0){localReasons[0]++;const r=(m>>>REASON_SHIFT)&0xff;if(r>=1&&r<=6)localReasons[r]++;localBuckets[bucket(m,maxIter)]++;}} + for(let i=0;i<8;i++){wg[i]+=localReasons[i];wgBuckets[i]+=localBuckets[i];} + } + must([...direct].every((v,i)=>v===wg[i]),`reason stats mismatch ${w}x${h}`); + must([...directBuckets].every((v,i)=>v===wgBuckets[i]),`bucket stats mismatch ${w}x${h}`); + cases.push({w,h,maxIter,unknown:direct[0],reasons:Array.from(direct.slice(1,7)),buckets:Array.from(directBuckets)}); +} +must(G.DEEP_PERTURB_POSTSTATS_WGSL.includes('fieldMeta[out]=pack_unknown')&&!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('atomicAdd(&unresolved.remaining'),'post-stats primary contract broken'); +must(G.DEEP_BUCKET_HIST_STATS_WGSL.includes('localReasons')&&G.DEEP_BUCKET_HIST_STATS_WGSL.includes('unresolvedStats'),'histogram stats aggregation contract broken'); +must(/ensureDeepBucketState/.test(script)&&/encodeDeepBucketHistogramStats/.test(script),'production integration contract broken'); +console.log(JSON.stringify({status:'pass',check:'primary-post-stats-workgroup-model',workgroupSize:64,cases},null,2)); diff --git a/tests/v24-reference-recovery-model.mjs b/tests/v24-reference-recovery-model.mjs new file mode 100644 index 0000000..04c2a07 --- /dev/null +++ b/tests/v24-reference-recovery-model.mjs @@ -0,0 +1,15 @@ +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'); +const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; +function extract(prefix,suffix){const start=source.indexOf(prefix)+prefix.length,end=source.indexOf(suffix,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});self.onmessage({data});if(!message)throw new Error('worker produced no message');if(message.type==='error')throw new Error(message.error);return message} +const bits=256,ONE=1n<<256n,re=ONE/2n,im=0n,span=34n*ONE/10n,iter=700,width=800,height=600; +const deep=runWorker(extract("function referenceWorkerSource(){return String.raw`","`}\nclass ReferenceService"),{type:'build',id:1,key:'deep-select',bits,re:re.toString(),im:im.toString(),span:span.toString(),width,height,iter}); +must(BigInt(deep.referenceRe)!==re,'Deep candidate selection stayed on short-lived center'); +must(deep.selectionEscape===0,'Deep candidate selection did not find a full-length reference'); +must(deep.refLen===iter,'Deep selected reference is not full-length'); +const fast=runWorker(extract("function fastReferenceWorkerSource(){return String.raw`","`}\nclass FastReferenceService"),{type:'build',id:2,key:'fast-select',sourceBits:bits,targetBits:128,re:re.toString(),im:im.toString(),span:span.toString(),width,height,iter}); +must(BigInt(fast.referenceRe)!==re,'Fast candidate selection stayed on short-lived center'); +must(fast.selectionEscape===0,'Fast candidate selection did not find a full-length reference'); +must(fast.refLen===iter,'Fast selected reference is not full-length'); +console.log(JSON.stringify({status:'pass',check:'reference-recovery-selection',deepReference:deep.referenceRe,fastReference:fast.referenceRe,iter},null,2)); diff --git a/tests/v24-source-contract.mjs b/tests/v24-source-contract.mjs index 5cdd217..fd00d85 100644 --- a/tests/v24-source-contract.mjs +++ b/tests/v24-source-contract.mjs @@ -1,33 +1,26 @@ import fs from 'node:fs/promises'; -const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8'); +const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),dev=await fs.readFile(new URL('index.external.html',root),'utf8'); const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; -must(/const VERSION=24/.test(script),'renderer version is not 24'); -must(/navigator\.gpu\.requestAdapter/.test(script),'WebGPU adapter path missing'); -must(/guarded perturbation/.test(script)&&/DEEP_CORRECT_WGSL/.test(kernels)&&/correctUnknownFrame/.test(script),'guarded deep + sparse DS correction path missing'); +must(/renderMode:'fast'/.test(script)&&/manual-accurate/.test(script)&&/manual-fast/.test(script),'manual fast/accurate backend policy missing'); +must(/\$\('#renderMode'\)\.onchange/.test(script)&&/mandelbrot\.renderMode/.test(script),'manual render mode control/persistence missing'); +must(!/ensureBackendProbe|scheduleBackendProbeAfterPaint|backendProfiles|backendViewProbes|runCrossoverBenchmark|runFixed96Experiment|__MANDEL_DIAG__/.test(script),'obsolete diagnostic/router system remains'); +must(!/DIRECT_DS_WGSL|DIRECT_DS_GUARDED_WGSL|DIRECT_FIXED96/.test(kernels),'diagnostic Direct DS/Fixed96 kernels remain'); +must(/正確 perturbation|DEEP_PERTURB_WGSL/.test(script+kernels)&&/DEEP_CORRECT_WGSL/.test(kernels)&&/correctUnknownFrame/.test(script),'accurate perturbation + sparse DS correction path missing'); +must(/DEEP_BUCKET_HIST_WGSL/.test(kernels)&&/DEEP_BUCKET_PREFIX_WGSL/.test(kernels)&&/DEEP_BUCKET_SCATTER_WGSL/.test(kernels)&&/DEEP_CORRECT_QUEUE_WGSL/.test(kernels),'production bucketed Deep queue path missing'); +must(/DEEP_PERTURB_POSTSTATS_WGSL/.test(kernels)&&/DEEP_BUCKET_HIST_STATS_WGSL/.test(kernels)&&/encodeDeepPostStatsNumeric/.test(script),'post-stats Deep primary missing'); +must(/presentTransform/.test(script)&&/前フレーム再利用/.test(script),'pan frame reprojection reuse missing'); +must(/FAST_PERTURB_WGSL/.test(kernels)&&/fastNeedsExtended/.test(script)&&/FastReferenceService/.test(script)&&/manual-fast-extended/.test(script),'fast deep-coordinate precision extension missing'); +must(/reusableDeepReference/.test(script)&&/referencePixel/.test(script)&&/reference再利用/.test(script),'Deep pan reference reuse missing'); +must(/colorAutoStep/.test(script)&&/colorCycleDir/.test(script)&&/colorShiftDir/.test(script)&&/state\.cycle>=cmax/.test(script)&&/state\.shift>=smax/.test(script),'bouncing color auto-slide missing'); +must(/effectiveColorCycle/.test(script)&&/state\.cycle\*base\/effective/.test(script)&&/sliderToCycle/.test(script)&&/cycleToSlider/.test(script),'zoom-relative/logarithmic color-period control missing'); +must(/mark_fast_unresolved/.test(kernels)&&/REASON_REFERENCE_END/.test(kernels)&&/REASON_REBASE_GAP/.test(kernels)&&/@group\(0\) @binding\(4\) var unresolved/.test(kernels),'Fast unresolved accounting missing'); +must(/provisional_unknown/.test(kernels)&&/fillCount>0\.0/.test(kernels),'UNKNOWN visual neighbor fill missing'); +must(/if\(!deep\)\{const cbg/.test(script)&&/if\(!deep\)\[f\.front,f\.back\]/.test(script)&&/const painted=await r\.recolor\(token,iter\)/.test(script),'deferred Accurate color/commit missing'); +must(/chooseReference/.test(script)&&/referenceCandidates/.test(script)&&/selectionEscape/.test(script),'long-lived reference selection missing'); +must(/max-height:calc\(100dvh - 20px\)/.test(dev)&&/overflow-y:auto/.test(dev),'scrollable bottom panel missing'); +must(/select option\{background:#000;color:#fff\}/.test(dev),'black/white dropdown styling missing'); +must(!/詳細設定・正確な座標|診断情報|backendBench|sparseBench/.test(dev),'removed settings/diagnostics UI remains'); 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 removed'); -must(/F32_U/.test(kernels)&&/safe_abs_error/.test(kernels)&&/errAbs/.test(kernels),'deep error-bound guard missing'); -must(/struct UnresolvedHead/.test(kernels)&&/atomicAdd\(&unresolved\.remaining/.test(kernels)&&/REASON_ERROR_BOUND/.test(kernels),'unresolved reason counters missing'); -must(/CORRECTION_MARK/.test(kernels)&&/ds_prod/.test(kernels)&&/correct_pixel/.test(kernels),'double-single correction kernel missing'); -must(!/strict_queue|QUEUE_FINALIZE_WGSL/.test(kernels)&&!/qitems|strict-indirect/.test(script),'obsolete strict retry queue remains'); -must(/texture_storage_2d/.test(kernels),'GPU color field path missing'); must(/AA_RESOLVE_WGSL/.test(kernels)&&/renderTileRGBA2x/.test(script),'GPU 2x2 export resolve missing'); -must(/correctUnknown=true/.test(script)&&/encodeCorrectionNumeric/.test(script),'export/probe DS correction integration missing'); -must(/class StreamingPng/.test(script)&&/CompressionStream\('deflate'\)/.test(script)&&/async abort\(reason\)/.test(script),'streaming PNG export/abort missing'); -must(/PRESENT_WGSL/.test(kernels),'GPU reprojection/presentation missing'); -must(/recolorPending/.test(script)&&!/const token=\+\+state\.token,stateSnap/.test(script),'recolor race fix missing'); -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(/mandelbrot-bundle/.test(html)&&/data-bundle="gpu-kernels"/.test(html)&&/data-bundle="app"/.test(html)&&!/{if(!ok)throw new Error(msg)}; must(!/高速<\//.test(h)&&/>正確<\//.test(h)&&!/高速(標準)|正確(深部)/.test(h)&&/id="colorAuto"/.test(h),'manual render labels/color auto UI contract failed'); +must(!/診断情報|benchmarkDialog|sparseDialog|coordReInput/.test(h),'removed diagnostic/exact-coordinate UI still embedded'); must(/ - - - diff --git a/dist/hosted/script.js b/dist/hosted/script.js deleted file mode 100644 index 865cb8f..0000000 --- a/dist/hosted/script.js +++ /dev/null @@ -1,371 +0,0 @@ -(()=>{'use strict'; -const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません'); -const $=s=>document.querySelector(s),canvas=$('#view'); -const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n; -const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000; -const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20; -const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity']; -const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96,REFINE_QUEUE_PARAM_BYTES=16,REFINE_STATS_BYTES=16,REFINE_BATCH_BYTES=16; -const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorCycleDir:1,colorShiftDir:1,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,refinementRunning:false,refinementStage:0,refinementQueue:0,refinedIter:0}; -let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,idleRefineTimer=0,lastWrittenHash='',navigationHash=''; -const viewHistory=[];let viewHistoryIndex=-1; -const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,idleRefineBatches:0,idleRefineQueued:0}; - -// ── exact fixed-point view state ───────────────────────────────────────── -function one(bits=state.bits){return 1n<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 fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v} -function f32ToFixed(x,bits){ - x=Math.fround(x);if(!Number.isFinite(x))throw new Error('non-finite f32 split component');if(x===0)return 0n; - const ab=new ArrayBuffer(4),dv=new DataView(ab);dv.setFloat32(0,x,false);const u=dv.getUint32(0,false),neg=(u>>>31)!==0,eb=(u>>>23)&255,frac=u&0x7fffff,m=BigInt(eb===0?frac:(0x800000|frac)),e=(eb===0?-149:eb-127-23),sh=bits+e; - let q=sh>=0?m<>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} -function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} -function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}} -function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))} -function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e} -function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}} -function f32Ulp(x){x=Math.abs(Math.fround(x));if(!Number.isFinite(x))return Infinity;if(x===0)return Math.pow(2,-149);if(x=4)} -function chooseBackend(snap=snapshot(),w=canvas.width){const deep=state.renderMode==='accurate',fastExtended=!deep&&fastNeedsExtended(snap,w);return{backend:deep?'deep':fastExtended?'fast-extended':'direct',deep,fastExtended,probe:false,reason:deep?'manual-accurate':fastExtended?'manual-fast-extended':'manual-fast'}} -function deepNeeded(){return state.renderMode==='accurate'} -function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,renderMode:state.renderMode}} -function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')} -function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()} -function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,renderMode:v.renderMode||state.renderMode});ensurePrecision();syncControls();saveHash(false);markDirty()} - -// ── iteration / quality policy ─────────────────────────────────────────── -function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))} -function effectiveColorCycle(iter=maxIter()){const base=Math.max(1,state.baseIter),effective=Math.max(base,Number(iter)||base);return state.cycle*base/effective} -function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))} -function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)} -function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)} -function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}} - -// ── 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 orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)return n+1}return 0} -function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]} -function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;kbest.score){best={...c,escape,score}}if(escape===0)break}return best} -function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<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 sourceBits=d.bits,sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),limit=Math.min(MAX_REF-1,d.iter),bits=sourceBits+64,centerRef=orbit(bits,sourceRe<<64n,sourceIm<<64n,limit),chosen=chooseReference(sourceBits,sourceRe,sourceIm,sourceSpan,w,h,limit,centerRef.escape),re=chosen.re<<64n,im=chosen.im<<64n,ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,limit),v=verify(sourceBits,chosen.re,chosen.im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} -`} -class ReferenceService{ - constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16} - ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1,fresh=false){const key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,'guarded-perturb-v24.2.26'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} - cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} - destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} -} -const refs=new ReferenceService(); - -function fastReferenceBits(snap){const need=Math.ceil(Math.max(0,-log2FixedAt(snap.span,snap.bits)))+48;return Math.max(96,Math.min(snap.bits,need))} -function fastReferenceWorkerSource(){return String.raw` -'use strict'; -function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length} -function roundShift(v,b){if(b<=0)return v;const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q} -function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n} -function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}} -function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)return n+1}return 0} -function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]} -function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;kbest.score){best={...c,escape,score}}if(escape===0)break}return best} -function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf} -self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),bits=d.targetBits,centerRe=requant(sourceRe,d.sourceBits,bits),centerIm=requant(sourceIm,d.sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,d.iter),chosen=chooseReference(d.sourceBits,sourceRe,sourceIm,sourceSpan,w,h,d.iter,centerRef.escape),re=requant(chosen.re,d.sourceBits,bits),im=requant(chosen.im,d.sourceBits,bits),ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,d.iter),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} -`} -class FastReferenceService{ - constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=12} - ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([fastReferenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'fast reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.2.26'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} - cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} - destroy(){this.cancelPending('destroyed');this.cache.clear()} -} -const fastRefs=new FastReferenceService(); - -// ── WebGPU renderer ────────────────────────────────────────────────────── -function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})} -function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}} -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.fastCtx=null;this.refineDeepCtx=null;this.exportWs=null;this.refinePipelinePromise=null;this.likelyQueue=null;this.deepLikelyRefine=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'}); - device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)}); - this.ready=this.initPipelines(); - device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()}); - } - 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,fm,xm,xhm,xpm,xsm,zm,zqm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]); - this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}}); - this.fast=this.device.createComputePipeline({layout:'auto',compute:{module:fm,entryPoint:'main'}}); - this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}}); - const [xpsm,xhsm]=await Promise.all([this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL),this.module('deep-bucket-histogram-stats-production',G.DEEP_BUCKET_HIST_STATS_WGSL)]); - this.deepPostStats=this.device.createComputePipeline({layout:'auto',compute:{module:xpsm,entryPoint:'main'}}); - this.deepBucketHist=this.device.createComputePipeline({layout:'auto',compute:{module:xhm,entryPoint:'main'}}); - this.deepBucketHistStats=this.device.createComputePipeline({layout:'auto',compute:{module:xhsm,entryPoint:'main'}}); - this.deepBucketPrefix=this.device.createComputePipeline({layout:'auto',compute:{module:xpm,entryPoint:'main'}}); - this.deepBucketScatter=this.device.createComputePipeline({layout:'auto',compute:{module:xsm,entryPoint:'main'}}); - this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}}); - this.correctQueued=this.device.createComputePipeline({layout:'auto',compute:{module:zqm,entryPoint:'main'}}); - this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}}); - this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}}); - this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}}); - 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}} - } - async ensureRefinePipelines(){if(this.likelyQueue&&this.deepLikelyRefine)return;if(this.refinePipelinePromise)return this.refinePipelinePromise;this.refinePipelinePromise=(async()=>{const [lqm,drm]=await Promise.all([this.module('likely-refine-queue',G.LIKELY_QUEUE_WGSL),this.module('deep-likely-refine',G.DEEP_LIKELY_REFINE_WGSL)]);this.likelyQueue=this.device.createComputePipeline({layout:'auto',compute:{module:lqm,entryPoint:'main'}});this.deepLikelyRefine=this.device.createComputePipeline({layout:'auto',compute:{module:drm,entryPoint:'refine_likely'}})})().finally(()=>{this.refinePipelinePromise=null});return this.refinePipelinePromise} - frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','refineQueue','refineStats','refineQueueParams','refineBatchParams','refineRead','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null} - ensureFrame(w,h){ - const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage; - this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,refineQueue:null,refineStats:null,refineQueueParams:null,refineBatchParams:null,refineRead:null,numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame; - } - ensureDeepBucketState(f=this.frame){if(!f)return null;if(!f.deepBucketState){const d=this.device,B=GPUBufferUsage;f.deepBucketState=buf(d,DEEP_BUCKET_STATE_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-bucket-state')}return f} - ensureDeepQueueWorkspace(f=this.frame){if(!f)return null;this.ensureDeepBucketState(f);if(f.deepQueue&&f.deepQueueStats&&f.deepIndirect)return f;const d=this.device,B=GPUBufferUsage;f.deepQueueStats=buf(d,SPARSE_QUEUE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue-stats');f.deepQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue');f.deepIndirect=buf(d,SPARSE_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'deep-correction-indirect');return f} - ensureRefineWorkspace(f=this.frame){if(!f)return null;if(f.refineQueue&&f.refineStats&&f.refineQueueParams&&f.refineBatchParams&&f.refineRead)return f;const d=this.device,B=GPUBufferUsage;f.refineQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-queue');f.refineStats=buf(d,REFINE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-stats');f.refineQueueParams=buf(d,REFINE_QUEUE_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-queue-params');f.refineBatchParams=buf(d,REFINE_BATCH_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-batch-params');f.refineRead=buf(d,REFINE_STATS_BYTES,B.COPY_DST|B.MAP_READ,'likely-refine-readback');return f} - ensureExportWorkspace(){ - if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S; - this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs; - } - exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null} - setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}} - destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null} - setRefineDeepContext(ctx){if(this.refineDeepCtx&&this.refineDeepCtx.key===ctx.key)return;this.destroyRefineDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'idle-refinement-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.refineDeepCtx={...ctx,refsB}} - destroyRefineDeepContext(){if(this.refineDeepCtx)destroy(this.refineDeepCtx.refsB);this.refineDeepCtx=null} - setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}} - destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null} - directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})} - deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,0,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - colorParamsData(w,h,iter=state.fieldView?.iter??maxIter()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,effectiveColorCycle(iter),true);d.setFloat32(20,state.shift,true)})} - encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,pipeline=this.deepPostStats,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.fast.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fast);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHist.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHist);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHistStats.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeDeepBucketPrefix(encoder,{bucketState,queueStats,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketPrefix.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:bucketState}},{binding:1,resource:{buffer:queueStats}},{binding:2,resource:{buffer:indirect}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketPrefix);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end()} - encodeDeepBucketScatter(encoder,{pbuf,meta,bucketState,queueStats,queue,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeQueuedDeepCorrection(encoder,{pbuf,meta,smooth,unresolved,queueStats,queue,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.correctQueued.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{binding:5,resource:{buffer:queueStats}},{binding:6,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.correctQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()} - encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})} - async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null){ - await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep){this.setDeepContext(deepContext);this.ensureDeepBucketState(f)}else if(fastExtended){this.setFastContext(fastContext)}d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep)d.queue.writeBuffer(f.deepBucketState,0,new Uint32Array(DEEP_BUCKET_STATE_BYTES/4)); - const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;const params=deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,refX,refY):fastExtended?this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap);d.queue.writeBuffer(f.numericParams,0,params); - d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'}); - if(deep){this.encodeDeepPostStatsNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeDeepBucketHistogramStats(encoder,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,unresolved:f.unresolved,w:f.w,h:f.h});} - else if(fastExtended){this.encodeFastNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});} - else{this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});} - if(!deep){const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();} - d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;if(!deep)[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true; - } - async buildLikelyRefineQueue(token,mode=1,sparseStride=4){await this.ready;await this.ensureRefinePipelines();if(token!==state.token)return{count:0,overflow:0,cancelled:true};const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;d.queue.writeBuffer(f.refineStats,0,new Uint32Array(REFINE_STATS_BYTES/4));d.queue.writeBuffer(f.refineQueueParams,0,new Uint32Array([f.w,f.h,mode>>>0,sparseStride>>>0]));const e=d.createCommandEncoder({label:'likely-refine-queue-build'}),bg=d.createBindGroup({layout:this.likelyQueue.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.refineQueueParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.refineQueue}},{binding:3,resource:{buffer:f.refineStats}}]}),pass=e.beginComputePass();pass.setPipeline(this.likelyQueue);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.n/256));pass.end();e.copyBufferToBuffer(f.refineStats,0,f.refineRead,0,REFINE_STATS_BYTES);d.queue.submit([e.finish()]);await f.refineRead.mapAsync(GPUMapMode.READ);const a=new Uint32Array(f.refineRead.getMappedRange().slice(0));f.refineRead.unmap();if(token!==state.token)return{count:0,overflow:0,cancelled:true};return{count:Math.min(f.n,a[0]||0),overflow:a[1]||0,cancelled:false}} - async refineLikelyBatch({snap,iter,deepContext,token,base,end,forceStrict=false}){await this.ready;await this.ensureRefinePipelines();if(token!==state.token||base>=end)return false;const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setRefineDeepContext(deepContext);const ref=referencePixelForSource(deepContext?.source,snap,f.w,f.h);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,ref.x,ref.y,0,f.w,0));d.queue.writeBuffer(f.refineBatchParams,0,new Uint32Array([base>>>0,end>>>0,0,0]));const pipeline=this.deepLikelyRefine,bg0=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.refineDeepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),bg1=d.createBindGroup({layout:pipeline.getBindGroupLayout(1),entries:[{binding:0,resource:{buffer:f.refineQueue}},{binding:1,resource:{buffer:f.refineBatchParams}}]}),e=d.createCommandEncoder({label:'likely-idle-refine'}),pass=e.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg0);pass.setBindGroup(1,bg1);pass.dispatchWorkgroups(Math.ceil((end-base)/64));pass.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.idleRefineBatches++;return true} - async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true} - presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}} - presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])} - async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4)); - if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})} - const ca=this.colorParamsData(w,h,iter),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end(); - const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h,iter);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca); - for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()} - const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end(); - const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y=Math.max(32,Math.floor(n*.0005));return true} -function referencePixelForSource(src,snap,w,h){if(!src)return{x:w*.5,y:h*.5};const b=Math.max(snap.bits,src.bits),span=align(snap.span,snap.bits,b),dr=align(src.re,src.bits,b)-align(snap.re,snap.bits,b),di=align(src.im,src.bits,b)-align(snap.im,snap.bits,b);return{x:w*.5+fixedRatio(dr,span)*w,y:h*.5-fixedRatio(di,span)*w}} -function reusableDeepReference(snap,iter,w,h){const ctx=renderer&&renderer.deepCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} -function reusableFastReference(snap,iter,w,h){const ctx=renderer&&renderer.fastCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} - -// ── GPU startup / rendering orchestration ──────────────────────────────── -async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise} -function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx} -function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span} -function cancelIdleRefinement(){if(idleRefineTimer){clearTimeout(idleRefineTimer);idleRefineTimer=0}state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0} -function idleRefinementTargets(baseIter){if(baseIter>=8000)return[];const z=zoomExp(),mode=state.processMode,mul=mode==='power'?.75:mode==='fine'?1.25:mode==='validate'?1.4:1,cap=Math.min(8000,Math.max(baseIter*2,Math.round((1200+300*z)*mul))),raw=mode==='power'?[Math.min(cap,baseIter*2)]:[Math.min(cap,baseIter*2),Math.min(cap,baseIter*4),cap],out=[];for(const v of raw){const n=Math.max(baseIter+1,Math.floor(v));if(n>baseIter&&(!out.length||n!==out[out.length-1]))out.push(n)}return out} -function idleRefinementBatchBudget(){const m=state.processMode;if(m==='power')return 900000;if(m==='fine')return 2800000;if(m==='validate')return 1800000;return 1800000} -function idleRefinementQueuePolicy(stage){if(stage<=0)return{mode:1,stride:4};if(stage===1)return{mode:2,stride:8};return{mode:3,stride:16}} -function idleRefinementSliceMs(){return state.processMode==='power'?100:state.processMode==='fine'?220:160} -function idleRefinementPauseMs(){return state.processMode==='power'?160:80} -function canIdleRefine(token,snap,baseIter){return token===state.token&&renderer&&!state.rendering&&!state.pointerActive&&!state.wheelActive&&sameSnapshot(snap,snapshot())&&state.fieldView&&state.fieldView.iter===baseIter&&state.fieldView.w===canvas.width&&state.fieldView.h===canvas.height} -const sleep=ms=>new Promise(r=>setTimeout(r,ms)); -async function paintIdleRefinement(token,snap,baseIter){while(state.recoloring&&canIdleRefine(token,snap,baseIter))await sleep(12);if(!canIdleRefine(token,snap,baseIter))return false;await recolor();while((state.recoloring||state.recolorPending)&&canIdleRefine(token,snap,baseIter))await sleep(8);return canIdleRefine(token,snap,baseIter)} -function scheduleIdleRefinement(snap,baseIter,decision,token){cancelIdleRefinement();if(!renderer||(!decision.deep&&!decision.fastExtended))return;const targets=idleRefinementTargets(baseIter);if(!targets.length)return;idleRefineTimer=setTimeout(()=>{idleRefineTimer=0;runIdleRefinement(snap,baseIter,decision,token,targets)},320)} -async function runIdleRefinement(snap,baseIter,decision,token,targets){if(!canIdleRefine(token,snap,baseIter))return;state.refinementRunning=true;state.refinedIter=baseIter;updateStats();try{for(let stage=0;stage=idleRefinementSliceMs()){const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;updateStats();lastPaint=performance.now();await sleep(idleRefinementPauseMs())}}const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;state.fieldView.refinedIter=targetIter;updateStats();await sleep(idleRefinementPauseMs())}}catch(e){if(token===state.token){console.warn('idle refinement stopped:',e);state.gpuError=String(e&&e.message||e)}}finally{if(token===state.token){state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0;updateStats()}}} - -function cancelRender(){cancelIdleRefinement();state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled')} -function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()} -function schedule(){if(!raf)raf=requestAnimationFrame(loop)} -async function renderFrame(){ - const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height); - state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats(); - try{ - const r=renderer||await initRenderer();if(token!==state.token)return; - if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return} - const deep=decision.deep,fastExtended=decision.fastExtended;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false; - if(deep){const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height);if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 正確 reference再利用'}else{state.lastEngine='WebGPU · 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · 正確 perturbation'}} - else if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 高速拡張 reference再利用'}else{state.lastEngine='WebGPU · 高速拡張準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height);state.lastEngine='WebGPU · 高速拡張'}} - else state.lastEngine='WebGPU · 高速'; - const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate',referencePixel,fastExtended,fastCtx);if(!ok)return; - if(!deep){ - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false; - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0}); - if(fastExtended){const stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons} - const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token);return; - } - // Accurate mode keeps the previous committed frame visible while the new - // primary field is checked/corrected. The incomplete primary texture lives - // only in frame.back and is never presented to the viewer. - let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats(); - const n=canvas.width*canvas.height; - if(shouldApplySparseCorrection(state.unresolved,n)){ - state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats(); - const corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return;state.correctionPasses=1; - stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected; - } - const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return; - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · '+(referenceReused?'正確 reference再利用':'正確 reference自動選択')+(state.correctionPasses?' + DS correction':''); - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token); - }catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)} -} -function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · 正確モード非対応';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))))*effectiveColorCycle(iter)+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y{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');p.set('rm',state.renderMode);const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}} -function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';if(p.has('rm'))state.renderMode=p.get('rm')==='accurate'?'accurate':'fast';ensurePrecision();return true}catch{return false}} -function syncHistoryButtons(){} -function syncControls(){$('#renderMode').value=state.renderMode;$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()} -function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)} -function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')} -$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()}; -$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()}; -$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}}; -let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0; -function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色アニメ停止':'色を自動変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')} -function stopColorAuto(){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton()} -function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;const cmin=.001,cmax=.05,smin=0,smax=1;state.cycle+=state.colorCycleDir*.0006*dt;state.shift+=state.colorShiftDir*.012*dt;if(state.cycle>=cmax){state.cycle=cmax;state.colorCycleDir=-1}else if(state.cycle<=cmin){state.cycle=cmin;state.colorCycleDir=1}if(state.shift>=smax){state.shift=smax;state.colorShiftDir=-1}else if(state.shift<=smin){state.shift=smin;state.colorShiftDir=1}$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;recolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)} -$('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf)colorAutoRaf=requestAnimationFrame(colorAutoStep);else if(!state.colorAuto)stopColorAuto()}; -$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()}; -$('#renderMode').onchange=e=>{state.renderMode=e.target.value==='accurate'?'accurate':'fast';try{localStorage.setItem('mandelbrot.renderMode',state.renderMode)}catch{}markDirty()}; -$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}}; -addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}}); -addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}}); - -// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ────── -const exportJob={active:false,cancelled:false}; -function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)} -const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})(); -function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0} -function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out} -class StreamingPng{ - constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()} - async rows(filteredRows){await this.writer.write(filteredRows)} - async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})} - async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}} -} -function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}} -async function runExport(){ - if(exportJob.active)return; - const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return} - const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),fastExtended=!deep&&fastNeedsExtended(snap,w),strict=$('#exportPrecision').value==='strict'; - let ctx=null,fastCtx=null; - if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}else if(fastExtended){$('#exportStatus').textContent='高速拡張参照を準備中…';fastCtx=await fastRefs.request(snap,iter,w,h)} - const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1; - exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true; - let done=0,unresolvedSamples=0; - 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'); - -function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.deep?'WebGPU 正確':decision.fastExtended?'WebGPU 高速':'WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'正確描画の補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.refinementRunning)status+=' · 補足精密化 '+state.refinementStage+' ('+state.refinedIter+' iter, 残 '+state.refinementQueue+')';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status} - -globalThis.__MANDEL_TEST__={ - async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard',renderMode='fast'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.renderMode=renderMode==='accurate'?'accurate':'fast';state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,state:this.state()}}, - async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)}, - state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter(),renderMode:state.renderMode,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused}), - async panPixels(dx,dy){pan(dx,dy);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test pan timeout');return this.state()}, - async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded():!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))}, - async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}} -}; - -// ── boot / teardown ────────────────────────────────────────────────────── -addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();refs.destroy();fastRefs.destroy();if(renderer)renderer.destroy()},{once:true}); -try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}state.renderMode=localStorage.getItem('mandelbrot.renderMode')==='accurate'?'accurate':'fast'}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule(); -})(); diff --git a/dist/standalone/index.html b/dist/standalone/index.html deleted file mode 100644 index ff9a2e2..0000000 --- a/dist/standalone/index.html +++ /dev/null @@ -1,1216 +0,0 @@ - - - - - - - -Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement - - - - -
MANDELBROT DEEP ZOOM
中心 …
倍率 … 表示幅 …
計算 起動中… 描画 …
-
起動中
-
-
-
-
-
-
-
0.0080
-
.18
-
-
-
起動中
-
ドラッグ中は直前フレームを再投影して再利用。ホイール / ピンチでズーム、HキーでUI表示を切り替え。
-
- -
- -

高解像度 PNG 出力

-
- - - - -
- -
-
-
- - - - diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md deleted file mode 100644 index f333761..0000000 --- a/docs/CHANGELOG.md +++ /dev/null @@ -1,15 +0,0 @@ -# CHANGELOG - -## v24.2.26 - -- v24.2.23〜v24.2.24 のLOD / frontier全画面再計算系を撤回。 -- v24.2.18 の Direct / Fast / Deep / Color WGSL を完全復元。 -- v24.2.18 の `maxIter` と fallback を復元。 -- 一次描画後だけ起動する GPU-only `INTERIOR_LIKELY` queue を追加。 -- 補足精密化は高速モードでも guarded Deep で実行。 -- 精密化シェーダーは一次表示後に遅延コンパイル。 -- queue候補は境界近傍 + 疎なscreen-space seedに限定。 -- CPUへ戻す精密化情報は16 byte統計のみ。全meta readbackなし。 -- 標準精密化batchを最大1.8M sample-iterationsに制限。 -- 自動色変化速度を v24.2.18 の1/10へ変更。 -- baseline一致・batch予算・品質単調性の専用テストを追加。 diff --git a/docs/README.md b/docs/README.md index eada060..9fe55a6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,117 +1,31 @@ -# Mandelbrot Deep Zoom v24.2.26 +# Mandelbrot WebGPU v24.2.55 ## 目的 +v24.2.54で確認された、FAST深部描画の矩形状/穴状の黒欠損と、数値補修を最終表示前に待つことによるレイテンシ増大を修正する。 -v24.2.23〜v24.2.24 系で発生した「品質低下・速度低下」を撤回し、**v24.2.18 の一次描画を基準線として復元**した版です。 +## 黒穴の原因 +COLOR passでは数値UNKNOWNを透明としていたが、PRESENT passが履歴補完を `scale <= 1.035 / offset <= 0.022` に制限していた。そのため通常のズームでも履歴補完が無効になり、透明UNKNOWNがcanvasの黒背景へ落ちた。 -この版の合格条件は次の4点です。 +さらに、その不完全front textureを完成historyとしてcommitする経路があった。局所reference補修が予算内で全UNKNOWNを回収できない場合、タイル境界に沿った黒欠損が完成画像へ固定され得た。 -1. 一次表示は v24.2.18 より遅くしない。 -2. 補足精密化を停止しても v24.2.18 より品質を下げない。 -3. 通常描画ループで fieldMeta 全体を GPU→CPU readback しない。 -4. 黒い `INTERIOR_LIKELY` の追加判定は一次表示後・アイドル時だけ行う。 +## v24.2.55の契約 +1. 数値UNKNOWNを含むfrontはstable historyへ昇格しない。 +2. 通常のパン/ズーム範囲では、数値UNKNOWNを直前のstable frameから再投影して埋める。 +3. FASTの数値補修は一次画像のpublishをブロックせず、同一viewのbackground taskとして継続する。 +4. background補修が完了して数値UNKNOWNが0になった時点で初めてhistoryを更新する。 +5. tiled FAST開始時にmeta/smoothをzero sentinelへ初期化し、最終color前にreason=0の未書込pixelだけbaseline FASTで再計算する。 -## 一次描画 +`REASON_OPERATION_LIMIT` は数値故障ではなく、maxIterまで未脱出だったpixelなので従来どおり黒表示する。今回の矩形状欠損対策は主に数値UNKNOWN/未書込pixelを対象とする。 -一次描画の以下のWGSLは v24.2.18 と **SHA-256まで一致**しています。 +## FASTレイテンシ +production BLAのCPU側64点shadow計算を廃止した。BLA workerはtable構築だけを行い、採用判定は既存の64点GPU probeへ一本化した。 -- `DIRECT_F32_WGSL` -- `FAST_PERTURB_WGSL` -- `DEEP_PERTURB_WGSL` -- `COLOR_WGSL` +classic Seahorse Valley相当 (654x690, span 3.4e-13, 2900 iter) のNode/V8 workerモデル: +- 旧 CPU validate 64点: 約446.9 ms +- 新 table build only: 約60.0 ms +- 同期prep削減: 約386.9 ms -`maxIter()` も v24.2.18 と同じです。 +BLAが不利なviewでは本体FAST計算時間そのものは残る。今回の変更では、数値補修Deep/multi-referenceを一次表示のクリティカルパスから外すことで、黒穴を出さずに操作から画像表示までの待ち時間を短縮する。 -```text -baseIter + floor(70*sqrt(zoomExp) + 15*zoomExp) -``` - -1920×1080 / 標準負荷の代表条件では内部描画は約 1672×941 = 1,573,352 px です。 - -| zoom | 一次 iter | 一次描画の最悪 sample-iterations | -|---:|---:|---:| -| 10^6 | 611 | 0.961 B | -| 10^8 | 667 | 1.049 B | -| 10^12 | 772 | 1.215 B | -| 10^20 | 963 | 1.515 B | - -これは v24.2.18 と同じ計算量です。補足精密化用WGSLも起動時にはコンパイルせず、一次描画完了から320ms後に初めて遅延コンパイルします。 - -## アイドル補足精密化 - -一次描画後にだけ `FIELD_INTERIOR_LIKELY` を追加判定します。 - -### GPU-only queue - -全 `fieldMeta` をCPUへ戻しません。GPU上で候補インデックスqueueを作り、CPUへ戻すのは **16 byte のqueue統計だけ**です。 - -候補は次のように絞ります。 - -- stage 1: 境界近傍 + 4px間隔 seed -- stage 2: 境界近傍 + 8px間隔 seed -- stage 3: 境界近傍 + 16px間隔 seed - -境界判定は各候補につき最大8点だけ参照します。広い真内部を毎回全面再計算する方式にはしていません。1920×1080/標準の代表内部解像度では、純粋な疎seedだけなら stage 1/2/3 はそれぞれ約98,648 / 24,662 / 6,195画素です。10^8 の場合、疎seed部分の合計は約0.220B sample-iterationsで、一次描画最悪値約1.049Bの約21%です。境界画素はこれに追加されますが、batch上限と休止でGPU占有を制限します。 - -### 数値エンジン - -高速モードの一次表示は従来のFast経路ですが、**アイドル精密化は高速モードでも guarded Deep を使用**します。精密化用reference bufferは通常描画の `deepCtx / fastCtx` と分離しているため、精密化後もv24.2.18のpan reference再利用を壊しません。 - -これにより、一次描画で `INTERIOR_LIKELY` だった点だけを保守的に再評価します。精密化シェーダーは実行直前にも対象がまだ `INTERIOR_LIKELY` か確認するため、既存の `ESCAPED` / `INTERIOR_PROVEN` を上書きしません。 - -### 反復ターゲット - -標準モードでは概ね次の3段階です。 - -| zoom | 一次 | stage 1 | stage 2 | stage 3 | -|---:|---:|---:|---:|---:| -| 10^6 | 611 | 1222 | 2444 | 3000 | -| 10^8 | 667 | 1334 | 2668 | 3600 | -| 10^12 | 772 | 1544 | 3088 | 4800 | -| 10^20 | 963 | 1926 | 3852 | 7200 | - -### 1 GPU batch の上限 - -標準モードの補足精密化は **1.8 million sample-iterations / batch 以下**に丸めます。 - -例: `10^8` - -- 1334 iter: 1344 pixels / batch → 1,792,896 sample-iterations -- 2668 iter: 640 pixels / batch → 1,707,520 -- 3600 iter: 448 pixels / batch → 1,612,800 - -ユーザー操作が入ると `state.token` が変わり、次batchへ進む前に中断します。また一定時間ごとにGPUを休ませます。 - -## 品質の単調性 - -補足精密化前の画像は v24.2.18 と同じです。 - -精密化は `FIELD_INTERIOR_LIKELY` のみを対象とし、既に確定した画素には触れません。さらに再着色時も一次描画の `baseIter` を色周期正規化に使うため、精密化によって既存の外部色が一斉に変化することを避けています。 - -## 色操作 - -v24.2.18 のズーム相対色周期と対数スライダーを維持しています。`色を自動変化` の速度のみ、要求どおり v24.2.18 の **1/10** にしています。 - -## fallback - -JavaScript fallback は v24.2.18 と SHA-256一致です。疑似等高線・縞模様は追加していません。 - -## 検査 - -```bash -npm test -npm run build -``` - -`tests/v24-baseline-idle-refinement-model.mjs` が以下を検査します。 - -- 一次WGSL 4本の v24.2.18 SHA-256一致 -- fallback の v24.2.18 SHA-256一致 -- `maxIter()` の基準線維持 -- 精密化WGSLの遅延コンパイル -- 全meta readback不使用 -- 16 byte queue統計readback -- 既存確定画素を上書きしない契約 -- 各倍率で1batchが予算を超えないこと - -実GPUのfpsやdevice-lost有無は、このコンテナでWebGPUが初期化できない場合は保証できません。静的・CPUモデル結果と実機測定は区別します。 +## 数値コア +v24.2.54から変更したproduction WGSLはFAST_PERTURB_WGSLのみ。通常 `unknownOnly=0` の計算式・perturbation recurrenceは変更していない。追加分はcoverage repair用のearly guardである。 diff --git a/docs/VALIDATION_REPORT.md b/docs/VALIDATION_REPORT.md index a45f6c0..253c22d 100644 --- a/docs/VALIDATION_REPORT.md +++ b/docs/VALIDATION_REPORT.md @@ -1,101 +1,75 @@ -# v24.2.26 検査報告 +# Validation Report — v24.2.55 -## 結論 +## 1. JavaScript / bundle syntax +- kernel bundle: PASS +- application bundle: PASS -v24.2.26 は v24.2.18 を一次描画の基準線として復元し、黒い `INTERIOR_LIKELY` の追加判定だけを一次表示後のアイドルGPU処理として分離した。 +## 2. production kernel差分 +v24.2.54とのSHA-256比較。 -一次描画の品質・数値計算量を変えないことを、ソースハッシュ・式・静的契約で確認した。実GPU上のfps/device-lostは、この実行環境でWebGPU/EGLを初期化できなかったため未測定である。 +変更: +- `version` +- `FAST_PERTURB_WGSL` -## 一次描画の同一性 +不変: +- Direct +- Accurate seed / DS direct +- Deep perturbation +- Deep correction +- COLOR +- PRESENT shader本体 +- BLA candidate / production frame kernels -v24.2.18 と SHA-256 が一致する本番WGSL: +FAST_PERTURBの通常recurrenceは不変。`unknownOnly=2` 時だけzero-sentinel pixelを再計算するearly guardを追加。 -| Shader | SHA-256 | -|---|---| -| DIRECT_F32_WGSL | `59301bf0c5ba3dda4a4055335b32d4a3f84a21991be30f8efc74deb5a5f11ccf` | -| FAST_PERTURB_WGSL | `03b45713b1bbb7b56e18bd8fdf0f2d8161d7ed25adf02482b41c1038dc16e3b8` | -| DEEP_PERTURB_WGSL | `2865a015d5c1a9b459085b7044e23563fcc5f0eec5362634249b6604a47e660a` | -| COLOR_WGSL | `cccac7bfb69c64093c46c3468ff9d685d680ba3a88de0d1ea57168d91a067acf` | +## 3. coverage contract +PASS: +- tiled numeric開始時にmeta/smoothをzero clear +- color前にbaseline FAST coverage repair +- coverage repairはreason=0 UNKNOWNだけをiteration +- 既計算pixelはearly return -JavaScript fallback も v24.2.18 と一致: +これにより、strip/tile未書込が発生してもrectangular zero-metadata holeをそのままpublishしない。 -`689a6f56aedd416b5e512b17ab6a3ddebcf7c73494e0a8a600ce74a06d8073a2` +## 4. temporal fill / history +PASS: +- history利用範囲をstable reprojectionと同じ `2.25 / 0.45` へ統一 +- numeric recovery pending/running時はstable historyへcommitしない +- recolor経路も `numericFrameComplete()` を満たさない限りhistoryへcommitしない -`maxIter()` は v24.2.18 と同じ: +## 5. background numeric recovery +PASS (static/control-flow): +- FAST primary終了後に数値failureがあれば60ms後にbackground recoveryを予約 +- local multi-referenceを先行 +- 残存時だけfull Deep + DS + local recovery +- view tokenが変われば結果をpublishしない +- recovery中のrecolorはqueueへ退避 -`baseIter + floor(70*sqrt(zoomExp) + 15*zoomExp)` +## 6. BLA production prep +classic Seahorse Valley相当のworker-model実測: +- 旧64-point CPU validation: 446.9 ms +- 新build-only: 60.0 ms +- 同期prep削減: 386.9 ms -補足精密化用WGSLは本番pipeline初期化ではコンパイルしない。一次表示完了後、320msのアイドル遅延後に必要な場合だけ遅延コンパイルする。 +BLA table buildそのものはほぼ同時間 (58.4 vs 59.4 ms)。削減分は描画前CPU shadow sample計算。 -## 代表計算量 +## 7. BLA safety regression +既存289-point Seahorse test、safety=1/64: +- classification mismatch: 0 +- first escape mismatch: 0 -1920×1080 CSS / 標準負荷では内部描画を約1672×941 = 1,573,352画素とする。 +productionではCPU per-view validationを外したが、64-point GPU probeとBLA pixel fallback契約は維持。 -| zoom | 一次iter | 一次最悪 sample-iterations | -|---:|---:|---:| -| 10^6 | 611 | 961,318,072 | -| 10^8 | 667 | 1,049,425,784 | -| 10^12 | 772 | 1,214,627,744 | -| 10^20 | 963 | 1,515,137,976 | +## 8. browser smoke +Chromiumでlocalhostからロード: +- page JavaScript error: 0 +- kernelVersion: `24.2.55-no-black-hole-async-recovery` -これはv24.2.18と同一。 +このコンテナでは当該起動条件でWebGPU adapterを取得できなかったため、canvasを含む実端末フレーム時間は未測定。 -標準モードのアイドル補足精密化は1バッチ1,800,000 sample-iterations以下に制限する。 +## 9. release layout +- executable HTML: `index.html` の1本のみ +- `index.baseline-*.html`: なし -| zoom | refine iter | pixels/batch | 最悪 sample-iterations | -|---:|---:|---:|---:| -| 10^6 | 1222 | 1472 | 1,798,784 | -| 10^6 | 2444 | 704 | 1,720,576 | -| 10^6 | 3000 | 576 | 1,728,000 | -| 10^8 | 1334 | 1344 | 1,792,896 | -| 10^8 | 2668 | 640 | 1,707,520 | -| 10^8 | 3600 | 448 | 1,612,800 | -| 10^12 | 1544 | 1152 | 1,778,688 | -| 10^12 | 3088 | 576 | 1,778,688 | -| 10^12 | 4800 | 320 | 1,536,000 | -| 10^20 | 1926 | 896 | 1,725,696 | -| 10^20 | 3852 | 448 | 1,725,696 | -| 10^20 | 7200 | 192 | 1,382,400 | - -10^8 の最初のrefine batchは一次描画最悪値の約0.171%。1バッチを巨大dispatchにしないことを優先している。 - -## 候補量モデル - -代表解像度で境界判定以外の疎seedだけを数えると: - -- stride 4: 約98,648画素 -- stride 8: 約24,662画素 -- stride 16: 約6,195画素 - -10^8 でこの疎seed全部を3段階処理した場合は約0.220B sample-iterationsで、一次描画最悪値1.049Bの約20.9%。実際には境界候補も加わるが、処理は小batchに分割し、一定時間ごとに休止し、ユーザー操作で中止する。 - -## 品質契約 - -- 一次画像はv24.2.18と同じ本番シェーダーで生成する。 -- refinement対象は `FIELD_INTERIOR_LIKELY` のみ。 -- refinement shaderは実行時にも対象classを再確認する。 -- `FIELD_ESCAPED` / `FIELD_INTERIOR_PROVEN` は上書きしない。 -- 高速モードのidle refinementもguarded Deepを使う。 -- refinement用reference bufferを通常描画の `deepCtx / fastCtx` と分離し、pan reference reuseを壊さない。 -- 再着色は一次描画の色周期スケールを維持し、既存の外部色を一斉に動かさない。 -- JavaScript fallbackに疑似等高線を追加しない。 - -## 自動テスト - -最終ソースに対して以下を実行し、すべてPASS: - -```text -npm test -> RC=0 -npm run build -> RC=0 -npm test -> RC=0 (build後再検査) -node --check script.js -> PASS -node --check gpu-kernels.js -> PASS -``` - -専用モデル `tests/v24-baseline-idle-refinement-model.mjs` もPASSし、上記ハッシュ、batch上限、readback契約、品質単調性を検査した。 - -## 実WebGPU検査の制約 - -このコンテナのChromiumではGPUプロセスがEGL/ANGLE初期化に失敗したため、実WebGPUのfps・device lost試験は完了できなかった。観測した主な失敗は `EGL_NOT_INITIALIZED` / `SwANGLE failed` 系である。 - -したがって「実機でv24.2.18より何%速い」という主張はしない。保証しているのは一次描画経路の構造的な基準線復元と、idle refinementのbatch仕事量上限である。 +## 残る性能課題 +BLA probeがOFFになるSeahorse/Misiurewicz型viewではbaseline FASTのpixel-iteration量が依然支配的。v24.2.55はこのケースのCPU prepと同期補修待ちを削るが、primary perturbation本体を別アルゴリズムで高速化する変更ではない。 diff --git a/gpu-kernels.js b/gpu-kernels.js deleted file mode 100644 index 4f73ff2..0000000 --- a/gpu-kernels.js +++ /dev/null @@ -1,778 +0,0 @@ -(()=>{'use strict'; -const COMMON=String.raw` -const FIELD_UNKNOWN:u32=0u; -const FIELD_ESCAPED:u32=1u; -const FIELD_INTERIOR_LIKELY:u32=2u; -const FIELD_INTERIOR_PROVEN:u32=3u; -const ITER_MASK:u32=0x000fffffu; -const REASON_SHIFT:u32=20u; -const REASON_MASK:u32=0x0ff00000u; -const REASON_NONE:u32=0u; -const REASON_ERROR_BOUND:u32=1u; -const REASON_ESCAPE_UNCERTAIN:u32=2u; -const REASON_REFERENCE_END:u32=3u; -const REASON_REBASE_GAP:u32=4u; -const REASON_RANGE:u32=5u; -const REASON_OPERATION_LIMIT:u32=6u; - -fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); } -fn pack_unknown(n:u32, reason:u32)->u32 { return (n & ITER_MASK) | ((reason & 0xffu) << REASON_SHIFT); } -fn cmul(a:vec2, 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=1.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; -} -`; - - -// Fast-mode precision extension. This keeps the interactive path independent -// of the verified Deep backend: a reduced-precision reference orbit is built -// quickly on the CPU worker and this lightweight perturbation kernel preserves -// sub-f32 pixel offsets once absolute Direct coordinates begin to collapse. -const FAST_PERTURB_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct RefPoint{ hi:vec2, lo:vec2 }; -@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:array>; -fn mark_fast_unresolved(out:u32,n:u32,reason:u32){ - fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicAdd(&unresolved[0],1u); - if(reason>=1u && reason<=6u){atomicAdd(&unresolved[reason],1u);} -} - -fn render_fast(out:u32,gx:f32,gy:f32){ - let dx=(gx-p.refPixelX)/f32(p.fullW); - let dy=(p.refPixelY-gy)/f32(p.fullW); - var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); - var w=vec2(0.0); - var scaleExp=p.spanExp; - var n=0u; var m=0u; - loop{ - if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;} - let rp=refs[min(m,p.refLen)]; - let delta=scaled_to_f32(w,scaleExp); - let z=rp.hi+(rp.lo+delta); - let mag=dot(z,z); - if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} - - // Rebase the perturbation when it grows beyond the reference value. This - // keeps the fast path usable across pans without any verified correction. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); - scaleExp=0; m=0u; - continue; - } - if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;} - let r=rp; - let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); - let sq=cmul(w,w)*pow2_safe(scaleExp); - w=linear+sq+d; m+=1u; n+=1u; - let mm=max(maxabs(w),maxabs(d)); - if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;} - if(mm>65536.0){w*=0.0000152587890625;d*=0.0000152587890625;scaleExp+=16;} - else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w*=65536.0;d*=65536.0;scaleExp-=16;} - if(scaleExp>126){mark_fast_unresolved(out,n,REASON_RANGE);return;} - } -} -@compute @workgroup_size(8,8) -fn main(@builtin(global_invocation_id) gid:vec3){ - if(gid.x>=p.tileW||gid.y>=p.tileH){return;} - let out=p.outputBase+gid.y*p.outputStride+gid.x; - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - render_fast(out,gx,gy); -} -`; - - -const DEEP_PERTURB_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct RefPoint{ hi:vec2, lo:vec2 }; -struct UnresolvedHead{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -@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,reason:u32){ - fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0; - atomicAdd(&unresolved.remaining,1u); - if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);} - else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);} - else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);} - else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);} - else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);} - else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);} -} -fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ - let dx=(gx-p.refPixelX)/f32(p.fullW); - let dy=(p.refPixelY-gy)/f32(p.fullW); - // dc = d * 2^scaleExp. Keep d and w in one shared scale. - var d=vec2(p.spanMantHi*dx,p.spanMantHi*dy); - var w=vec2(0.0); - var scaleExp=p.spanExp; - var n=0u; var m=0u; - var errScaled=1.0*F32_U*maxabs(d); - loop{ - if(n>=p.maxIter){ - let rpEnd=refs[min(m,p.refLen)]; - let deltaEnd=scaled_to_f32(w,scaleExp); - let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd); - let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd); - let limit=select(1.0e-3,1.0e-4,strictMode); - if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);} - return; - } - let rp=refs[m]; - let delta=scaled_to_f32(w,scaleExp); - let z=rp.hi+(rp.lo+delta); - let mag=dot(z,z); - if(mag>4.0){ - let errAbs=safe_abs_error(errScaled,scaleExp,z,delta); - if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} - mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return; - } - - // Rebase only when dc remains numerically representable in the new scale. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u; - errScaled+=1.0*F32_U*maxabs(d); - continue; - } - - if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;} - let r=rp; - let refAbs=maxabs(r.hi)+maxabs(r.lo); - let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp)); - let gain=2.0*refAbs+2.0*wAbs*p2; - let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30); - errScaled=gain*errScaled+roundErr; - let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); - // delta^2 / 2^scaleExp = w^2 * 2^scaleExp - let sq=cmul(w,w)*pow2_safe(scaleExp); - w=linear+sq+d; m+=1u; n+=1u; - if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;} - let mm=max(maxabs(w),maxabs(d)); - if(mm>65536.0){ - w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16; - }else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){ - w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16; - } - if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n,REASON_RANGE);return;} - } -} -@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=p.outputBase+gid.y*p.outputStride+gid.x; - if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; - render_pixel(out,gx,gy,p.strict!=0u); -} -`; - - -// Idle refinement is compiled as a separate guarded-Deep module so the -// production Fast/Deep main shaders remain byte-for-byte identical to v24.2.18. -const DEEP_LIKELY_REFINE_WGSL=DEEP_PERTURB_WGSL.replace( - '@group(0) @binding(4) var unresolved:UnresolvedHead;', - '@group(0) @binding(4) var unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var refineQueue:array;\n@group(1) @binding(1) var refineBatch:RefineBatch;' -)+String.raw` -@compute @workgroup_size(64) -fn refine_likely(@builtin(global_invocation_id) gid:vec3){ - let slot=refineBatch.base+gid.x; - if(slot>=refineBatch.end){return;} - let out=refineQueue[slot]; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;} - let x=out%p.fullW; let y=out/p.fullW; - render_pixel(out,f32(x)+p.sampleX,f32(y)+p.sampleY,p.strict!=0u); -} -`; - -// Production Deep sparse path: keep the long-running perturbation kernel -// untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets. -// Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS -// correction kernel sees less workgroup-level iteration divergence. -// -// Bucket state layout (24 u32 atomics): -// [0..7] histogram counts -// [8..15] scatter cursors (initialized from prefix offsets) -// [16..23] immutable prefix offsets for queue accounting -const DEEP_BUCKET_HIST_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var bucketState:array>; -var localCounts:array,8>; -fn deep_bucket(packed:u32)->u32{ - let n=packed&ITER_MASK; - return min(7u,(n*8u)/max(1u,p.maxIter)); -} -@compute @workgroup_size(64) -fn main( - @builtin(local_invocation_id) lid3:vec3, - @builtin(workgroup_id) wid:vec3 -){ - let lane=lid3.x; - if(lane<8u){atomicStore(&localCounts[lane],0u);} - workgroupBarrier(); - let lx=wid.x*64u+lane; - let ly=wid.y; - if(lx>28u)&3u)==FIELD_UNKNOWN){ - atomicAdd(&localCounts[deep_bucket(packed)],1u); - } - } - workgroupBarrier(); - if(lane<8u){ - let c=atomicLoad(&localCounts[lane]); - if(c>0u){atomicAdd(&bucketState[lane],c);} - } -} -`; - -// Convert the 8-bin histogram into contiguous queue ranges, initialize each -// scatter cursor, and produce the correction indirect-dispatch arguments. -const DEEP_BUCKET_PREFIX_WGSL=String.raw` -struct SparseQueueStats{ - selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, -}; -struct IndirectArgs{ x:u32, y:u32, z:u32, _pad:u32 }; -@group(0) @binding(0) var bucketState:array>; -@group(0) @binding(1) var sparseQueueStats:SparseQueueStats; -@group(0) @binding(2) var indirectArgs:IndirectArgs; -@compute @workgroup_size(1) -fn main(){ - var total=0u; - var b=0u; - loop{ - if(b>=8u){break;} - let c=atomicLoad(&bucketState[b]); - atomicStore(&bucketState[8u+b],total); - atomicStore(&bucketState[16u+b],total); - total+=c; - b+=1u; - } - atomicStore(&sparseQueueStats.selected,total); - atomicStore(&sparseQueueStats.overflow,0u); - atomicStore(&sparseQueueStats.enqueued,total); - atomicStore(&sparseQueueStats.dispatchCount,total); - indirectArgs.x=(total+63u)/64u; - indirectArgs.y=1u; - indirectArgs.z=1u; - indirectArgs._pad=0u; -} -`; - -// Scatter UNKNOWN indices into the precomputed bucket ranges. Within each -// 64-lane workgroup, workgroup-memory atomics allocate local ranks; each -// non-empty bucket reserves one global subrange, so global atomics scale with -// non-empty (workgroup,bucket) pairs rather than with UNKNOWN pixels. -const DEEP_BUCKET_SCATTER_WGSL=COMMON+String.raw` -struct Params{ - tileW:u32, tileH:u32, fullW:u32, fullH:u32, - tileX:u32, tileY:u32, maxIter:u32, refLen:u32, - strict:u32, unknownOnly:u32, outputStride:u32, outputBase:u32, - spanMantHi:f32, spanExp:i32, sampleX:f32, sampleY:f32, - refPixelX:f32, refPixelY:f32, spanMantLo:f32, invFullWHi:f32, - invFullWLo:f32, _numeric0:f32, _numeric1:f32, _numeric2:f32, -}; -struct SparseQueueStats{ - selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, -}; -@group(0) @binding(0) var p:Params; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var bucketState:array>; -@group(0) @binding(3) var sparseQueueStats:SparseQueueStats; -@group(0) @binding(4) var unknownQueue:array; -var localCounts:array,8>; -var localRanks:array,8>; -var groupBase:array; -fn deep_bucket(packed:u32)->u32{ - let n=packed&ITER_MASK; - return min(7u,(n*8u)/max(1u,p.maxIter)); -} -@compute @workgroup_size(64) -fn main( - @builtin(local_invocation_id) lid3:vec3, - @builtin(workgroup_id) wid:vec3 -){ - let lane=lid3.x; - if(lane<8u){ - atomicStore(&localCounts[lane],0u); - atomicStore(&localRanks[lane],0u); - groupBase[lane]=0u; - } - workgroupBarrier(); - - let lx=wid.x*64u+lane; - let ly=wid.y; - var out=0u; - var hit=0u; - var bucket=0u; - if(lx>28u)&3u)==FIELD_UNKNOWN){ - hit=1u; - bucket=deep_bucket(packed); - atomicAdd(&localCounts[bucket],1u); - } - } - workgroupBarrier(); - - if(lane<8u){ - let c=atomicLoad(&localCounts[lane]); - if(c>0u){groupBase[lane]=atomicAdd(&bucketState[8u+lane],c);} - } - workgroupBarrier(); - - if(hit!=0u){ - let rank=atomicAdd(&localRanks[bucket],1u); - let qi=groupBase[bucket]+rank; - let capacity=p.tileW*p.tileH; - if(qi, lo:vec2 }; -struct CorrectionStats{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -struct DS{ h:f32, l:f32 }; -struct CDS{ r:DS, i:DS }; -@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 stats:CorrectionStats; - -fn count_remaining(out:u32){ - let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu; - atomicAdd(&stats.remaining,1u); - if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);} - else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);} - else if(reason==REASON_REFERENCE_END){atomicAdd(&stats.referenceEnd,1u);} - else if(reason==REASON_REBASE_GAP){atomicAdd(&stats.rebaseGap,1u);} - else if(reason==REASON_RANGE){atomicAdd(&stats.rangeFailure,1u);} - else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&stats.operationLimit,1u);} -} -fn accept_corrected(out:u32,n:u32,cls:u32,sm:f32){ - fieldMeta[out]=corrected(n,cls); fieldSmooth[out]=sm; atomicAdd(&stats.corrected,1u); -} - -fn ds_quick(a:f32,b:f32)->DS{ - let q=a+b; - let e=b-(q-a); - return DS(q,e); -} -fn ds_sum(a:f32,b:f32)->DS{ - let q=a+b; - let bb=q-a; - let e=(a-(q-bb))+(b-bb); - return DS(q,e); -} -fn ds_prod(a:f32,b:f32)->DS{ - let q=a*b; - let ca=4097.0*a; - let ah=ca-(ca-a); - let al=a-ah; - let cb=4097.0*b; - let bh=cb-(cb-b); - let bl=b-bh; - var e=ah*bh-q; - e=e+ah*bl; - e=e+al*bh; - e=e+al*bl; - return DS(q,e); -} -fn ds_add(a:DS,b:DS)->DS{ - let q=ds_sum(a.h,b.h); - return ds_quick(q.h,q.l+(a.l+b.l)); -} -fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);} -fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));} -fn ds_mul(a:DS,b:DS)->DS{ - let q=ds_prod(a.h,b.h); - var e=q.l+a.h*b.l; - e=e+a.l*b.h; - e=e+a.l*b.l; - return ds_quick(q.h,e); -} -fn ds_scale(a:DS,b:f32)->DS{ - let q=ds_prod(a.h,b); - return ds_quick(q.h,q.l+a.l*b); -} -fn ds_pow2(a:DS,e:i32)->DS{ - if(e < -126){return DS(0.0,0.0);} - if(e > 126){return DS(8.507059e37,0.0);} - return DS(ldexp(a.h,e),ldexp(a.l,e)); -} -fn ds_cmp(a:DS,b:DS)->i32{ - if(a.hb.h){return 1;} - if(a.lb.l){return 1;} return 0; -} -fn ds_value(a:DS)->f32{return a.h+a.l;} -fn cds_add(a:CDS,b:CDS)->CDS{return CDS(ds_add(a.r,b.r),ds_add(a.i,b.i));} -fn cds_mul(a:CDS,b:CDS)->CDS{ - let rr=ds_sub(ds_mul(a.r,b.r),ds_mul(a.i,b.i)); - let ii=ds_add(ds_mul(a.r,b.i),ds_mul(a.i,b.r)); - return CDS(rr,ii); -} -fn cds_scale(a:CDS,b:f32)->CDS{return CDS(ds_scale(a.r,b),ds_scale(a.i,b));} -fn cds_pow2(a:CDS,e:i32)->CDS{return CDS(ds_pow2(a.r,e),ds_pow2(a.i,e));} -fn cds_mag2(a:CDS)->DS{return ds_add(ds_mul(a.r,a.r),ds_mul(a.i,a.i));} -fn cds_maxabs(a:CDS)->f32{return max(abs(ds_value(a.r)),abs(ds_value(a.i)));} -fn corrected(n:u32,cls:u32)->u32{return pack_meta(n,cls)|(CORRECTION_MARK<=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;} - let rp=refs[m]; - let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y)); - let delta=cds_pow2(w,scaleExp); - let z=cds_add(r,delta); - let mag=cds_mag2(z); - if(ds_cmp(mag,DS(4.0,0.0))>0){ - accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag)))); - return; - } - let dmag=cds_mag2(delta); - if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){ - if(p.spanExp-scaleExp < -96){count_remaining(out);return;} - w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u; - continue; - } - if(m>=p.refLen){count_remaining(out);return;} - let linear=cds_scale(cds_mul(r,w),2.0); - let sq=cds_pow2(cds_mul(w,w),scaleExp); - w=cds_add(cds_add(linear,sq),d); - m+=1u; n+=1u; - let mm=max(cds_maxabs(w),cds_maxabs(d)); - if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;} - if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;} - else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;} - if(scaleExp>126){count_remaining(out);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=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - correct_pixel(out,gx,gy); -} -`; - - -// Production sparse correction variant. It shares the complete DS arithmetic -// with DEEP_CORRECT_WGSL, but consumes only indices emitted by the queued deep -// perturbation pass and launches via dispatchWorkgroupsIndirect. -const DEEP_CORRECT_QUEUE_WGSL=DEEP_CORRECT_WGSL - .replace( -`struct CorrectionStats{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -};`, -`struct CorrectionStats{ - remaining:atomic, errorBound:atomic, escapeUncertain:atomic, referenceEnd:atomic, - rebaseGap:atomic, rangeFailure:atomic, operationLimit:atomic, corrected:atomic, -}; -struct SparseQueueStats{ - selected:atomic, overflow:atomic, enqueued:atomic, dispatchCount:atomic, - invalidIndex:atomic, staleEntry:atomic, processed:atomic, _reserved:atomic, -};`) - .replace( -`@group(0) @binding(4) var stats:CorrectionStats;`, -`@group(0) @binding(4) var stats:CorrectionStats; -@group(0) @binding(5) var sparseQueueStats:SparseQueueStats; -@group(0) @binding(6) var unknownQueue:array;`) - .replace( -`@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=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} - let gx=f32(p.tileX+gid.x)+p.sampleX; - let gy=f32(p.tileY+gid.y)+p.sampleY; - correct_pixel(out,gx,gy); -}`, -`@compute @workgroup_size(64) -fn main(@builtin(global_invocation_id) gid:vec3){ - let qi=gid.x; - let queued=atomicLoad(&sparseQueueStats.dispatchCount); - if(qi>=queued){return;} - let out=unknownQueue[qi]; - atomicAdd(&sparseQueueStats.processed,1u); - if(out=p.tileW||ly>=p.tileH){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;} - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;} - let gx=f32(p.tileX+lx)+p.sampleX; - let gy=f32(p.tileY+ly)+p.sampleY; - correct_pixel(out,gx,gy); -}`); - -if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)')){ - throw new Error('failed to derive queued deep correction shader'); -} - -const LIKELY_QUEUE_WGSL=COMMON+String.raw` -struct QueueParams{width:u32,height:u32,mode:u32,sparseStride:u32}; -struct QueueStats{count:atomic,overflow:atomic,_p0:atomic,_p1:atomic}; -@group(0) @binding(0) var p:QueueParams; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var queue:array; -@group(0) @binding(3) var stats:QueueStats; -fn is_frontier(x:u32,y:u32,r:i32)->bool{let xi=i32(x);let yi=i32(y);var oy=-1;loop{if(oy>1){break;}var ox=-1;loop{if(ox>1){break;}if(ox!=0||oy!=0){let xx=xi+ox*r;let yy=yi+oy*r;if(xx>=0&&yy>=0&&xx>28u)&3u;if(cls==FIELD_ESCAPED||cls==FIELD_UNKNOWN){return true;}}}ox+=1;}oy+=1;}return false;} -@compute @workgroup_size(256) -fn main(@builtin(global_invocation_id) gid:vec3){let i=gid.x;let total=p.width*p.height;if(i>=total){return;}if(((fieldMeta[i]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}let x=i%p.width;let y=i/p.width;var selected=false;if(p.mode==0u){selected=true;}else{let radius:i32=select(2,select(3,4,p.mode>=3u),p.mode>=2u);selected=is_frontier(x,y,radius);if(!selected&&p.sparseStride>0u){selected=(x%p.sparseStride==0u)&&(y%p.sparseStride==0u);}}if(!selected){return;}let slot=atomicAdd(&stats.count,1u);if(slot 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 palette_color(phase:f32)->vec3{ - if(p.palette==1u){return hsv(phase,0.92,1.0);}if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3(g);}return current_palette(phase); -} -fn escaped_color(m:u32,sm:f32)->vec3{ - let phase=fract(p.shift+sm*p.cycle);let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.34+0.66*pow(edge,0.38);let floorc=select(vec3(2,5,15)/255.0,vec3(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv); -} -fn provisional_unknown(m:u32)->vec3{ - let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3(6,10,22)/255.0,vec3(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52); -} -fn base_color(i:u32)->vec3{ - let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3(0.0);}return escaped_color(m,fieldSmooth[i]); -} -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;let m=fieldMeta[i];let cls=(m>>28u)&3u;var c=base_color(i);let x=i32(gid.x);let y=i32(gid.y); - if(cls==FIELD_UNKNOWN){ - var fill=vec3(0.0);var fillCount=0.0; - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];if(((mj>>28u)&3u)!=FIELD_UNKNOWN){fill+=linearize(base_color(j));fillCount+=1.0;}}} - if(fillCount>0.0){c=delinearize(fill/fillCount);} - } - if(p.edgeAA!=0u){ - var boundary=false;var sum=linearize(c);var cnt=1.0; - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}} - if(boundary){c=delinearize(sum/cnt);} - } - textureStore(outTex,vec2(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); -} -`; - - -// v24.2.14 production screen path: defer UNKNOWN statistics out of the long -// perturbation kernel. The existing bucket histogram pass already scans the -// same field before sparse correction, so it can aggregate remaining/reason -// counters per workgroup without per-UNKNOWN global atomics in the primary. -// The counted primary is retained for export and verification paths. -const DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u);\n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}'); - -const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL - .replace('@group(0) @binding(2) var bucketState:array>;', '@group(0) @binding(2) var bucketState:array>;\n@group(0) @binding(3) var unresolvedStats:array>;') - .replace('var localCounts:array,8>;', 'var localCounts:array,8>;\nvar localReasons:array,8>;') - .replace('if(lane<8u){atomicStore(&localCounts[lane],0u);}', 'if(lane<8u){atomicStore(&localCounts[lane],0u);atomicStore(&localReasons[lane],0u);}') - .replace('atomicAdd(&localCounts[deep_bucket(packed)],1u);', `atomicAdd(&localCounts[deep_bucket(packed)],1u); - atomicAdd(&localReasons[0],1u); - let reason=(packed&REASON_MASK)>>REASON_SHIFT; - if(reason>=1u && reason<=6u){atomicAdd(&localReasons[reason],1u);}`) - .replace('if(c>0u){atomicAdd(&bucketState[lane],c);}', `if(c>0u){atomicAdd(&bucketState[lane],c);} - let r=atomicLoad(&localReasons[lane]); - if(r>0u){atomicAdd(&unresolvedStats[lane],r);}`); -if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('v24.2.14 post-stats shader derivation failed'); - -globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({ - version:'24.2.26-baseline-idle-refinement',DIRECT_F32_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_LIKELY_REFINE_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,LIKELY_QUEUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL -}); -})(); diff --git a/index.external.html b/index.external.html deleted file mode 100644 index 1b9bf1e..0000000 --- a/index.external.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - -Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement - - - - -
MANDELBROT DEEP ZOOM
中心 …
倍率 … 表示幅 …
計算 起動中… 描画 …
-
起動中
-
-
-
-
-
-
-
0.0080
-
.18
-
-
-
起動中
-
ドラッグ中は直前フレームを再投影して再利用。ホイール / ピンチでズーム、HキーでUI表示を切り替え。
-
- -
- -

高解像度 PNG 出力

-
- - - - -
- -
-
-
- - - - diff --git a/index.html b/index.html index ff9a2e2..93170eb 100644 --- a/index.html +++ b/index.html @@ -1,47 +1,45 @@ - + - -Mandelbrot Deep Zoom v24.2.26 Baseline + Idle Refinement + +Mandelbrot Deep Zoom v24.2.46 Stable Frame + Color UI -
MANDELBROT DEEP ZOOM
中心 …
倍率 … 表示幅 …
計算 起動中… 描画 …
+
MANDELBROT DEEP ZOOM333の部屋に戻る
中心 …
倍率 … 表示幅 …
計算 起動中… 描画 …
起動中
-
+
0.0080
.18
-
+
起動中
-
ドラッグ中は直前フレームを再投影して再利用。ホイール / ピンチでズーム、HキーでUI表示を切り替え。
@@ -82,21 +80,20 @@ fn cmul(a:vec2, b:vec2)->vec2{ } 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=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30); - return propagated+reconstruction; +fn scale_scalar(v:f32,e:i32)->f32{ + // ldexp rounds the combined v*2^e result. Do not reject solely because e + // is below the normal f32 exponent: a large mantissa can still produce a + // perfectly normal representable result (critical around 10^38.461 zoom). + return ldexp(v,e); } 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 safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2, delta:vec2)->f32{ + let propagated=abs(scale_scalar(errScaled,scaleExp)); + let reconstruction=1.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30); + return propagated+reconstruction; +} 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)); @@ -142,7 +139,7 @@ fn main(@builtin(global_invocation_id) gid:vec3){ 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; + fieldMeta[out]=pack_unknown(p.maxIter,REASON_OPERATION_LIMIT); fieldSmooth[out]=0.0; } `; @@ -151,6 +148,108 @@ fn main(@builtin(global_invocation_id) gid:vec3){ // of the verified Deep backend: a reduced-precision reference orbit is built // quickly on the CPU worker and this lightweight perturbation kernel preserves // sub-f32 pixel offsets once absolute Direct coordinates begin to collapse. + + +// Low/mid-depth accurate screen path. A cheap f32 seed accepts only pixels +// that escape very early; everything delicate is recomputed from iteration 0 +// with direct double-single arithmetic. This avoids paying full-screen Deep +// perturbation where absolute pixel coordinates are still widely separated. +const DIRECT_ACCURATE_SEED_WGSL=COMMON+String.raw` +struct Params{ + tileW:u32,tileH:u32,fullW:u32,fullH:u32, + tileX:u32,tileY:u32,maxIter:u32,lateThreshold:u32, + outputStride:u32,outputBase:u32,_u0:u32,_u1:u32, + centerReHi:f32,centerReLo:f32,centerImHi:f32,centerImLo:f32, + spanHi:f32,spanLo:f32,invWHi:f32,invWLo:f32, + sampleX:f32,sampleY:f32,_f0:f32,_f1: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=p.outputBase+gid.y*p.outputStride+gid.x; + let gx=f32(p.tileX+gid.x)+p.sampleX; + let gy=f32(p.tileY+gid.y)+p.sampleY; + let centerRe=p.centerReHi+p.centerReLo; + let centerIm=p.centerImHi+p.centerImLo; + let span=p.spanHi+p.spanLo; + let invW=p.invWHi+p.invWLo; + let cr=centerRe+(gx-0.5*f32(p.fullW))*span*invW; + let ci=centerIm+(0.5*f32(p.fullH)-gy)*span*invW; + 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){fieldMeta[out]=pack_unknown(p.maxIter,REASON_OPERATION_LIMIT);fieldSmooth[out]=0.0;return;} + 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){ + if(n p:Params; +@group(0) @binding(1) var fieldMeta:array; +@group(0) @binding(2) var fieldSmooth:array; +fn ds_quick(a:f32,b:f32)->DS{let q=a+b;let e=b-(q-a);return DS(q,e);} +fn ds_sum(a:f32,b:f32)->DS{let q=a+b;let bb=q-a;let e=(a-(q-bb))+(b-bb);return DS(q,e);} +fn ds_prod(a:f32,b:f32)->DS{ + let q=a*b;let ca=4097.0*a;let ah=ca-(ca-a);let al=a-ah;let cb=4097.0*b;let bh=cb-(cb-b);let bl=b-bh; + var e=ah*bh-q;e=e+ah*bl;e=e+al*bh;e=e+al*bl;return DS(q,e); +} +fn ds_add(a:DS,b:DS)->DS{let q=ds_sum(a.h,b.h);return ds_quick(q.h,q.l+a.l+b.l);} +fn ds_neg(a:DS)->DS{return DS(-a.h,-a.l);} +fn ds_sub(a:DS,b:DS)->DS{return ds_add(a,ds_neg(b));} +fn ds_mul(a:DS,b:DS)->DS{let q=ds_prod(a.h,b.h);let e=q.l+a.h*b.l+a.l*b.h+a.l*b.l;return ds_quick(q.h,e);} +fn ds_scale(a:DS,b:f32)->DS{let q=ds_prod(a.h,b);return ds_quick(q.h,q.l+a.l*b);} +fn ds_cmp(a:DS,b:DS)->i32{if(a.hb.h){return 1;}if(a.lb.l){return 1;}return 0;} +fn ds_value(a:DS)->f32{return a.h+a.l;} +@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=p.outputBase+gid.y*p.outputStride+gid.x; + if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} + let gx=f32(p.tileX+gid.x)+p.sampleX;let gy=f32(p.tileY+gid.y)+p.sampleY; + let invW=DS(p.invWHi,p.invWLo);let span=DS(p.spanHi,p.spanLo);let scale=ds_mul(span,invW); + let cr=ds_add(DS(p.centerReHi,p.centerReLo),ds_scale(scale,gx-0.5*f32(p.fullW))); + let ci=ds_add(DS(p.centerImHi,p.centerImLo),ds_scale(scale,0.5*f32(p.fullH)-gy)); + var zr=DS(0.0,0.0);var zi=DS(0.0,0.0);var n=0u; + loop{ + if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;} + let zr2=ds_mul(zr,zr);let zi2=ds_mul(zi,zi);let zri=ds_mul(zr,zi); + zr=ds_add(ds_sub(zr2,zi2),cr);zi=ds_add(ds_scale(zri,2.0),ci);n+=1u; + let mag=ds_add(ds_mul(zr,zr),ds_mul(zi,zi)); + if(ds_cmp(mag,DS(4.0,0.0))>0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,max(4.0000005,ds_value(mag)));return;} + let av=max(abs(ds_value(zr)),abs(ds_value(zi))); + if(av!=av||av>1.0e20){fieldMeta[out]=pack_unknown(n,REASON_RANGE);fieldSmooth[out]=0.0;return;} + } +} +`; + const FAST_PERTURB_WGSL=COMMON+String.raw` struct Params{ tileW:u32, tileH:u32, fullW:u32, fullH:u32, @@ -179,26 +278,18 @@ fn render_fast(out:u32,gx:f32,gy:f32){ var scaleExp=p.spanExp; var n=0u; var m=0u; loop{ - if(n>=p.maxIter){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;return;} + if(n>=p.maxIter){mark_fast_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);return;} let rp=refs[min(m,p.refLen)]; let delta=scaled_to_f32(w,scaleExp); let z=rp.hi+(rp.lo+delta); let mag=dot(z,z); if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} - - // Rebase the perturbation when it grows beyond the reference value. This - // keeps the fast path usable across pans without any verified correction. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); - scaleExp=0; m=0u; - continue; - } + // Reference-near-zero episodes are not an automatic failure. Continue the + // valid perturbation recurrence; explicit range/reference guards remain. if(m>=p.refLen){mark_fast_unresolved(out,n,REASON_REFERENCE_END);return;} let r=rp; let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w)); - let sq=cmul(w,w)*pow2_safe(scaleExp); + let sq=scaled_to_f32(cmul(w,w),scaleExp); w=linear+sq+d; m+=1u; n+=1u; let mm=max(maxabs(w),maxabs(d)); if(mm>=1.0e30 || mm!=mm){mark_fast_unresolved(out,n,REASON_RANGE);return;} @@ -211,6 +302,13 @@ fn render_fast(out:u32,gx:f32,gy:f32){ fn main(@builtin(global_invocation_id) gid:vec3){ if(gid.x>=p.tileW||gid.y>=p.tileH){return;} let out=p.outputBase+gid.y*p.outputStride+gid.x; + if(p.unknownOnly!=0u){ + let prior=fieldMeta[out];let cls=(prior>>28u)&3u; + if(cls!=FIELD_UNKNOWN){return;} + let reason=(prior>>20u)&0xffu; + // mode 2 repairs only untouched coverage holes (zero-filled metadata). + if(p.unknownOnly==2u && reason!=0u){return;} + } let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; render_fast(out,gx,gy); @@ -264,7 +362,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd); let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd); let limit=select(1.0e-3,1.0e-4,strictMode); - if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n,REASON_ERROR_BOUND);} + if(errAbs<=limit){mark_unresolved(out,p.maxIter,REASON_OPERATION_LIMIT);}else{mark_unresolved(out,n,REASON_ERROR_BOUND);} return; } let rp=refs[m]; @@ -276,26 +374,23 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){ if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;} mark_unresolved(out,n,REASON_ESCAPE_UNCERTAIN);return; } - - // Rebase only when dc remains numerically representable in the new scale. - if(m>0u && dot(delta,delta)>0.0 && mag(p.spanMantHi*dx,p.spanMantHi*dy),p.spanExp); scaleExp=0; m=0u; - errScaled+=1.0*F32_U*maxabs(d); - continue; - } + // Keep the perturbation recurrence through reference-near-zero episodes. + // Escape acceptance is still guarded by the propagated absolute error. if(m>=p.refLen){mark_unresolved(out,n,REASON_REFERENCE_END);return;} let r=rp; let refAbs=maxabs(r.hi)+maxabs(r.lo); - let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp)); - let gain=2.0*refAbs+2.0*wAbs*p2; - let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30); + let wAbs=maxabs(w); let dAbs=maxabs(d); + let actualDelta=scaled_to_f32(w,scaleExp); + let deltaAbs=maxabs(actualDelta); + let sq=scaled_to_f32(cmul(w,w),scaleExp); + let sqAbs=maxabs(sq); + let gain=2.0*refAbs+2.0*deltaAbs; + let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+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); + // delta^2 / 2^scaleExp = w^2 * 2^scaleExp. Scale the product + // directly so e<-126 does not discard a representable combined value. w=linear+sq+d; m+=1u; n+=1u; if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n,REASON_RANGE);return;} let mm=max(maxabs(w),maxabs(d)); @@ -318,23 +413,6 @@ fn main(@builtin(global_invocation_id) gid:vec3){ `; -// Idle refinement is compiled as a separate guarded-Deep module so the -// production Fast/Deep main shaders remain byte-for-byte identical to v24.2.18. -const DEEP_LIKELY_REFINE_WGSL=DEEP_PERTURB_WGSL.replace( - '@group(0) @binding(4) var unresolved:UnresolvedHead;', - '@group(0) @binding(4) var unresolved:UnresolvedHead;\nstruct RefineBatch{base:u32,end:u32,_p0:u32,_p1:u32};\n@group(1) @binding(0) var refineQueue:array;\n@group(1) @binding(1) var refineBatch:RefineBatch;' -)+String.raw` -@compute @workgroup_size(64) -fn refine_likely(@builtin(global_invocation_id) gid:vec3){ - let slot=refineBatch.base+gid.x; - if(slot>=refineBatch.end){return;} - let out=refineQueue[slot]; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;} - let x=out%p.fullW; let y=out/p.fullW; - render_pixel(out,f32(x)+p.sampleX,f32(y)+p.sampleY,p.strict!=0u); -} -`; - // Production Deep sparse path: keep the long-running perturbation kernel // untouched, then reorder FIELD_UNKNOWN pixels into 8 coarse iteration buckets. // Similar UNKNOWN-onset iteration counts are kept adjacent so the queued DS @@ -361,6 +439,11 @@ fn deep_bucket(packed:u32)->u32{ let n=packed&ITER_MASK; return min(7u,(n*8u)/max(1u,p.maxIter)); } +fn selected_unknown(packed:u32)->bool{ + if(((packed>>28u)&3u)!=FIELD_UNKNOWN){return false;} + if(p.unknownOnly==2u){let reason=(packed&REASON_MASK)>>REASON_SHIFT;return reason>=1u && reason<=5u;} + return true; +} @compute @workgroup_size(64) fn main( @builtin(local_invocation_id) lid3:vec3, @@ -374,7 +457,7 @@ fn main( if(lx>28u)&3u)==FIELD_UNKNOWN){ + if(selected_unknown(packed)){ atomicAdd(&localCounts[deep_bucket(packed)],1u); } } @@ -449,6 +532,11 @@ fn deep_bucket(packed:u32)->u32{ let n=packed&ITER_MASK; return min(7u,(n*8u)/max(1u,p.maxIter)); } +fn selected_unknown(packed:u32)->bool{ + if(((packed>>28u)&3u)!=FIELD_UNKNOWN){return false;} + if(p.unknownOnly==2u){let reason=(packed&REASON_MASK)>>REASON_SHIFT;return reason>=1u && reason<=5u;} + return true; +} @compute @workgroup_size(64) fn main( @builtin(local_invocation_id) lid3:vec3, @@ -470,7 +558,7 @@ fn main( if(lx>28u)&3u)==FIELD_UNKNOWN){ + if(selected_unknown(packed)){ hit=1u; bucket=deep_bucket(packed); atomicAdd(&localCounts[bucket],1u); @@ -519,8 +607,8 @@ struct CDS{ r:DS, i:DS }; @group(0) @binding(3) var fieldSmooth:array; @group(0) @binding(4) var stats:CorrectionStats; -fn count_remaining(out:u32){ - let reason=(fieldMeta[out]>>REASON_SHIFT)&0xffu; +fn count_remaining(out:u32,n:u32,reason:u32){ + fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0; atomicAdd(&stats.remaining,1u); if(reason==REASON_ERROR_BOUND){atomicAdd(&stats.errorBound,1u);} else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&stats.escapeUncertain,1u);} @@ -576,8 +664,6 @@ fn ds_scale(a:DS,b:f32)->DS{ return ds_quick(q.h,q.l+a.l*b); } fn ds_pow2(a:DS,e:i32)->DS{ - if(e < -126){return DS(0.0,0.0);} - if(e > 126){return DS(8.507059e37,0.0);} return DS(ldexp(a.h,e),ldexp(a.l,e)); } fn ds_cmp(a:DS,b:DS)->i32{ @@ -610,7 +696,7 @@ fn correct_pixel(out:u32,gx:f32,gy:f32){ var scaleExp=p.spanExp; var n=0u; var m=0u; loop{ - if(n>=p.maxIter){accept_corrected(out,p.maxIter,FIELD_INTERIOR_LIKELY,0.0);return;} + if(n>=p.maxIter){count_remaining(out,p.maxIter,REASON_OPERATION_LIMIT);return;} let rp=refs[m]; let r=CDS(DS(rp.hi.x,rp.lo.x),DS(rp.hi.y,rp.lo.y)); let delta=cds_pow2(w,scaleExp); @@ -620,29 +706,29 @@ fn correct_pixel(out:u32,gx:f32,gy:f32){ accept_corrected(out,n,FIELD_ESCAPED,smooth_escape(n,max(4.0000005,ds_value(mag)))); return; } - let dmag=cds_mag2(delta); - if(m>0u && ds_cmp(dmag,DS(0.0,0.0))>0 && ds_cmp(mag,dmag)<0){ - if(p.spanExp-scaleExp < -96){count_remaining(out);return;} - w=z; d=cds_pow2(d0,p.spanExp); scaleExp=0; m=0u; - continue; - } - if(m>=p.refLen){count_remaining(out);return;} + // DS correction follows the same continuous perturbation recurrence. + if(m>=p.refLen){count_remaining(out,n,REASON_REFERENCE_END);return;} let linear=cds_scale(cds_mul(r,w),2.0); let sq=cds_pow2(cds_mul(w,w),scaleExp); w=cds_add(cds_add(linear,sq),d); m+=1u; n+=1u; let mm=max(cds_maxabs(w),cds_maxabs(d)); - if(mm>=1.0e30 || mm!=mm){count_remaining(out);return;} + if(mm>=1.0e30 || mm!=mm){count_remaining(out,n,REASON_RANGE);return;} if(mm>65536.0){w=cds_scale(w,0.0000152587890625);d=cds_scale(d,0.0000152587890625);scaleExp+=16;} else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){w=cds_scale(w,65536.0);d=cds_scale(d,65536.0);scaleExp-=16;} - if(scaleExp>126){count_remaining(out);return;} + if(scaleExp>126){count_remaining(out,n,REASON_RANGE);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=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} + let prior=fieldMeta[out]; + if(((prior>>28u)&3u)!=FIELD_UNKNOWN){return;} + if(p.unknownOnly==2u){ + let reason=(prior&REASON_MASK)>>REASON_SHIFT; + if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;} + } let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; correct_pixel(out,gx,gy); @@ -677,7 +763,12 @@ struct SparseQueueStats{ fn main(@builtin(global_invocation_id) gid:vec3){ if(gid.x>=p.tileW||gid.y>=p.tileH){return;} let out=p.outputBase+gid.y*p.outputStride+gid.x; - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;} + let prior=fieldMeta[out]; + if(((prior>>28u)&3u)!=FIELD_UNKNOWN){return;} + if(p.unknownOnly==2u){ + let reason=(prior&REASON_MASK)>>REASON_SHIFT; + if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;} + } let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY; correct_pixel(out,gx,gy); @@ -694,7 +785,12 @@ fn main(@builtin(global_invocation_id) gid:vec3){ let ly=local/p.outputStride; let lx=local-ly*p.outputStride; if(lx>=p.tileW||ly>=p.tileH){atomicAdd(&sparseQueueStats.invalidIndex,1u);return;} - if(((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;} + let prior=fieldMeta[out]; + if(((prior>>28u)&3u)!=FIELD_UNKNOWN){atomicAdd(&sparseQueueStats.staleEntry,1u);return;} + if(p.unknownOnly==2u){ + let reason=(prior&REASON_MASK)>>REASON_SHIFT; + if(reason==REASON_OPERATION_LIMIT || reason==REASON_NONE){return;} + } let gx=f32(p.tileX+lx)+p.sampleX; let gy=f32(p.tileY+ly)+p.sampleY; correct_pixel(out,gx,gy); @@ -704,18 +800,6 @@ if(DEEP_CORRECT_QUEUE_WGSL===DEEP_CORRECT_WGSL || !DEEP_CORRECT_QUEUE_WGSL.inclu throw new Error('failed to derive queued deep correction shader'); } -const LIKELY_QUEUE_WGSL=COMMON+String.raw` -struct QueueParams{width:u32,height:u32,mode:u32,sparseStride:u32}; -struct QueueStats{count:atomic,overflow:atomic,_p0:atomic,_p1:atomic}; -@group(0) @binding(0) var p:QueueParams; -@group(0) @binding(1) var fieldMeta:array; -@group(0) @binding(2) var queue:array; -@group(0) @binding(3) var stats:QueueStats; -fn is_frontier(x:u32,y:u32,r:i32)->bool{let xi=i32(x);let yi=i32(y);var oy=-1;loop{if(oy>1){break;}var ox=-1;loop{if(ox>1){break;}if(ox!=0||oy!=0){let xx=xi+ox*r;let yy=yi+oy*r;if(xx>=0&&yy>=0&&xx>28u)&3u;if(cls==FIELD_ESCAPED||cls==FIELD_UNKNOWN){return true;}}}ox+=1;}oy+=1;}return false;} -@compute @workgroup_size(256) -fn main(@builtin(global_invocation_id) gid:vec3){let i=gid.x;let total=p.width*p.height;if(i>=total){return;}if(((fieldMeta[i]>>28u)&3u)!=FIELD_INTERIOR_LIKELY){return;}let x=i%p.width;let y=i/p.width;var selected=false;if(p.mode==0u){selected=true;}else{let radius:i32=select(2,select(3,4,p.mode>=3u),p.mode>=2u);selected=is_frontier(x,y,radius);if(!selected&&p.sparseStride>0u){selected=(x%p.sparseStride==0u)&&(y%p.sparseStride==0u);}}if(!selected){return;}let slot=atomicAdd(&stats.count,1u);if(slotvec3{ 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 rgb8(r:f32,g:f32,b:f32)->vec3{return vec3(r,g,b)/255.0;} +fn tri_phase(t0:f32)->f32{return 1.0-abs(2.0*fract(t0)-1.0);} +fn ramp5(t:f32,c0:vec3,c1:vec3,c2:vec3,c3:vec3,c4:vec3)->vec3{ + if(t<0.25){return mix(c0,c1,smoothstep(0.0,0.25,t));} + if(t<0.50){return mix(c1,c2,smoothstep(0.25,0.50,t));} + if(t<0.75){return mix(c2,c3,smoothstep(0.50,0.75,t));} + return mix(c3,c4,smoothstep(0.75,1.0,t)); +} 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; + if(t<0.11){return mix(rgb8(4.0,10.0,27.0),rgb8(12.0,53.0,79.0),smoothstep(0.0,0.11,t));} + if(t<0.25){return mix(rgb8(12.0,53.0,79.0),rgb8(31.0,156.0,184.0),smoothstep(0.11,0.25,t));} + if(t<0.38){return mix(rgb8(31.0,156.0,184.0),rgb8(91.0,226.0,234.0),smoothstep(0.25,0.38,t));} + if(t<0.50){return mix(rgb8(91.0,226.0,234.0),rgb8(66.0,53.0,151.0),smoothstep(0.38,0.50,t));} + if(t<0.62){return mix(rgb8(66.0,53.0,151.0),rgb8(139.0,49.0,170.0),smoothstep(0.50,0.62,t));} + if(t<0.73){return mix(rgb8(139.0,49.0,170.0),rgb8(232.0,72.0,145.0),smoothstep(0.62,0.73,t));} + if(t<0.84){return mix(rgb8(232.0,72.0,145.0),rgb8(255.0,137.0,64.0),smoothstep(0.73,0.84,t));} + if(t<0.93){return mix(rgb8(255.0,137.0,64.0),rgb8(255.0,211.0,99.0),smoothstep(0.84,0.93,t));} + return mix(rgb8(255.0,211.0,99.0),rgb8(255.0,250.0,223.0),smoothstep(0.93,1.0,t)); } fn palette_color(phase:f32)->vec3{ - if(p.palette==1u){return hsv(phase,0.92,1.0);}if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3(g);}return current_palette(phase); + if(p.palette==0u){return current_palette(phase);} + if(p.palette==1u){return hsv(phase,0.92,1.0);} + if(p.palette==2u){let g=(18.0+237.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;return vec3(g);} + let t=tri_phase(phase); + if(p.palette==3u){return ramp5(t,rgb8(0.0,0.0,2.0),rgb8(40.0,8.0,84.0),rgb8(141.0,36.0,101.0),rgb8(228.0,88.0,48.0),rgb8(252.0,255.0,166.0));} + if(p.palette==5u){return ramp5(t,rgb8(1.0,4.0,14.0),rgb8(7.0,36.0,92.0),rgb8(0.0,132.0,178.0),rgb8(116.0,232.0,240.0),rgb8(242.0,254.0,255.0));} + if(p.palette==7u){return ramp5(t,rgb8(3.0,6.0,20.0),rgb8(63.0,26.0,132.0),rgb8(10.0,146.0,174.0),rgb8(48.0,224.0,140.0),rgb8(229.0,255.0,184.0));} + if(p.palette==9u){return ramp5(t,rgb8(0.0,5.0,20.0),rgb8(4.0,42.0,82.0),rgb8(0.0,113.0,136.0),rgb8(34.0,176.0,162.0),rgb8(221.0,246.0,202.0));} + if(p.palette==10u){return ramp5(t,rgb8(5.0,4.0,2.0),rgb8(44.0,24.0,4.0),rgb8(132.0,83.0,10.0),rgb8(228.0,171.0,46.0),rgb8(255.0,245.0,198.0));} + if(p.palette==11u){return ramp5(t,rgb8(3.0,0.0,16.0),rgb8(250.0,0.0,132.0),rgb8(0.0,228.0,255.0),rgb8(126.0,255.0,0.0),rgb8(255.0,247.0,255.0));} + if(p.palette==13u){return ramp5(t,rgb8(0.0,0.0,14.0),rgb8(21.0,16.0,152.0),rgb8(0.0,188.0,255.0),rgb8(220.0,0.0,255.0),rgb8(255.0,255.0,255.0));} + if(p.palette==15u){return ramp5(t,rgb8(2.0,8.0,26.0),rgb8(18.0,85.0,210.0),rgb8(192.0,244.0,255.0),rgb8(255.0,134.0,36.0),rgb8(255.0,242.0,196.0));} + if(p.palette==17u){return ramp5(t,rgb8(20.0,1.0,14.0),rgb8(204.0,26.0,72.0),rgb8(255.0,226.0,182.0),rgb8(0.0,202.0,226.0),rgb8(226.0,249.0,255.0));} + if(p.palette==18u){return ramp5(t,rgb8(22.0,8.0,24.0),rgb8(171.0,36.0,122.0),rgb8(255.0,211.0,149.0),rgb8(45.0,204.0,176.0),rgb8(232.0,255.0,243.0));} + return current_palette(phase); } fn escaped_color(m:u32,sm:f32)->vec3{ - let phase=fract(p.shift+sm*p.cycle);let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.34+0.66*pow(edge,0.38);let floorc=select(vec3(2,5,15)/255.0,vec3(8.0/255.0),p.palette==2u);return mix(floorc,c,mixv); -} -fn provisional_unknown(m:u32)->vec3{ - let n=f32(m&0x000fffffu);let c=palette_color(fract(p.shift+(n+0.5)*p.cycle));let floorc=select(vec3(6,10,22)/255.0,vec3(14.0/255.0),p.palette==2u);return mix(floorc,c,0.52); + // Direct phase mapping: no iteration-band/anchor compensation. + let phase=fract(p.shift+sm*p.cycle); + let c=palette_color(phase);let n=f32(m&0x000fffffu);let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0);let mixv=0.30+0.70*pow(edge,0.40);let floorc=select(vec3(0.0,0.0,0.0),vec3(2.0/255.0),p.palette==2u);return mix(floorc,c,mixv); } fn base_color(i:u32)->vec3{ - let m=fieldMeta[i];let cls=(m>>28u)&3u;if(cls==FIELD_UNKNOWN){return provisional_unknown(m);}if(cls!=FIELD_ESCAPED){return vec3(0.0);}return escaped_color(m,fieldSmooth[i]); + let m=fieldMeta[i];return escaped_color(m,fieldSmooth[i]); } 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;let m=fieldMeta[i];let cls=(m>>28u)&3u;var c=base_color(i);let x=i32(gid.x);let y=i32(gid.y); + if(gid.x>=p.width||gid.y>=p.height){return;} + let i=gid.y*p.width+gid.x;let m=fieldMeta[i];let cls=(m>>28u)&3u; if(cls==FIELD_UNKNOWN){ - var fill=vec3(0.0);var fillCount=0.0; - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];if(((mj>>28u)&3u)!=FIELD_UNKNOWN){fill+=linearize(base_color(j));fillCount+=1.0;}}} - if(fillCount>0.0){c=delinearize(fill/fillCount);} + let reason=(m>>20u)&0xffu; + // Iteration-limit pixels are not a numeric hole: display them as opaque + // black so temporal history cannot paint stale exterior colour through. + if(reason==6u){textureStore(outTex,vec2(gid.xy),vec4(0.0,0.0,0.0,1.0));} + else{textureStore(outTex,vec2(gid.xy),vec4(0.0));} + return; } + if(cls!=FIELD_ESCAPED){ + // Certified / likely interior is real image content, not a temporal hole. + // Keep it opaque so a reprojected history frame can never leak exterior + // colour into an already-known Mandelbrot interior pixel. + textureStore(outTex,vec2(gid.xy),vec4(0.0,0.0,0.0,1.0)); + return; + } + var c=escaped_color(m,fieldSmooth[i]); if(p.edgeAA!=0u){ - var boundary=false;var sum=linearize(c);var cnt=1.0; - for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;}let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x000fffffu)-i32(m&0x000fffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}} - if(boundary){c=delinearize(sum/cnt);} + let x=i32(gid.x);let y=i32(gid.y);var sum=linearize(c);var cnt=1.0; + for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){ + if(ox==0&&oy==0){continue;} + let xx=x+ox;let yy=y+oy; + if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;} + let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j]; + if(((mj>>28u)&3u)==FIELD_ESCAPED){sum+=linearize(escaped_color(mj,fieldSmooth[j]));cnt+=1.0;} + }} + c=delinearize(sum/cnt); } textureStore(outTex,vec2(gid.xy),vec4(c,1.0)); } @@ -788,35 +911,443 @@ fn srgb3(v:vec3)->vec3{return vec3(to_srgb(v.x),to_srgb(v.y),to_s @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)); + let q=vec2(gid.xy);let va=textureLoad(a,q,0);let vb=textureLoad(b,q,0);let vc=textureLoad(c,q,0);let vd=textureLoad(d,q,0); + let alpha=(va.a+vb.a+vc.a+vd.a)*0.25; + let premulLin=(lin3(va.rgb)*va.a+lin3(vb.rgb)*vb.a+lin3(vc.rgb)*vc.a+lin3(vd.rgb)*vd.a)*0.25; + if(alpha<=0.00001){textureStore(outTex,q,vec4(0.0));return;} + let straightLin=premulLin/alpha; + textureStore(outTex,q,vec4(srgb3(straightLin)*alpha,alpha)); } `; const PRESENT_WGSL=String.raw` -struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32}; +struct Params{ + frontScaleX:f32,frontScaleY:f32,frontOffsetX:f32,frontOffsetY:f32, + histScaleX:f32,histScaleY:f32,histOffsetX:f32,histOffsetY:f32, + historyEnabled:f32,_p0:f32,_p1:f32,_p2:f32, +}; @group(0) @binding(0) var samp:sampler; -@group(0) @binding(1) var tex:texture_2d; -@group(0) @binding(2) var p:Params; +@group(0) @binding(1) var frontTex:texture_2d; +@group(0) @binding(2) var historyTex:texture_2d; +@group(0) @binding(3) 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; } +fn mapped_uv(uv:vec2,scale:vec2,offset:vec2)->vec2{ + return vec2(0.5)+(uv-vec2(0.5))*scale+offset; +} +fn inside(uv:vec2)->bool{return all(uv>=vec2(0.0))&&all(uv<=vec2(1.0));} @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); + let fuv=mapped_uv(in.uv,vec2(p.frontScaleX,p.frontScaleY),vec2(p.frontOffsetX,p.frontOffsetY)); + var front=vec4(0.0); + if(inside(fuv)){front=textureSampleLevel(frontTex,samp,fuv,0.0);} + if(p.historyEnabled<0.5||front.a>=0.999){return front;} + let huv=mapped_uv(in.uv,vec2(p.histScaleX,p.histScaleY),vec2(p.histOffsetX,p.histOffsetY)); + var hist=vec4(0.0); + if(inside(huv)){hist=textureSampleLevel(historyTex,samp,huv,0.0);} + return front+hist*(1.0-front.a); } `; -// v24.2.14 production screen path: defer UNKNOWN statistics out of the long -// perturbation kernel. The existing bucket histogram pass already scans the -// same field before sparse correction, so it can aggregate remaining/reason -// counters per workgroup without per-UNKNOWN global atomics in the primary. -// The counted primary is retained for export and verification paths. +const BLA_GPU_SHADOW_WGSL=COMMON+String.raw` +// Comparison-only BLA shadow kernel. It never writes visible Mandelbrot +// field/color/history resources. Each invocation computes one deterministic +// screen sample twice: (1) the existing FAST scaled perturbation recurrence, +// (2) the guarded BLA accelerator with checkpoint/replay. Any unsafe BLA +// candidate is rejected and replayed through the baseline recurrence. +struct ShadowParams{ + maxIter:u32, refLen:u32, sampleCount:u32, levelCount:u32, + fullW:f32, refPixelX:f32, refPixelY:f32, spanMantHi:f32, + spanExp:i32, safety:f32, _p0:f32, _p1:f32, +}; +struct RefPoint{ hi:vec2, lo:vec2 }; +struct BlaEntry{ + aHi:vec2, aLo:vec2, bHi:vec2, bLo:vec2, + q:f32, aAbs:f32, bAbs:f32, aRes:f32, + bRes:f32, skip:u32, start:u32, ok:u32, +}; +struct LevelMeta{ offset:u32, count:u32 }; +struct ShadowResult{ + baseCode:u32, baseIter:u32, baseSteps:u32, blaCode:u32, + blaIter:u32, blaSteps:u32, blaApps:u32, blaSkipped:u32, + blaRejected:u32, blaReplays:u32, replaySteps:u32, maxSkip:u32, + internalFailure:u32, _r0:u32, _r1:u32, _r2:u32, +}; +struct PState{ d:vec2, w:vec2, scaleExp:i32, n:u32 }; +struct AdvanceResult{ s:PState, code:u32, iter:u32, steps:u32 }; +struct BlaRunResult{ + code:u32, iter:u32, steps:u32, apps:u32, + skipped:u32, rejected:u32, replays:u32, replaySteps:u32, + maxSkip:u32, internalFailure:u32, +}; +@group(0) @binding(0) var p:ShadowParams; +@group(0) @binding(1) var refs:array; +@group(0) @binding(2) var blas:array; +@group(0) @binding(3) var levels:array; +@group(0) @binding(4) var samples:array>; +@group(0) @binding(5) var results:array; + +const CODE_MAX_ITER:u32=0u; +const CODE_ESCAPED:u32=1u; +const CODE_REFERENCE_END:u32=2u; +const CODE_RANGE:u32=3u; +const CODE_CONTINUE:u32=4u; +const F32_MAX:f32=bitcast(0x7f7fffffu); +const F32_MIN_NORMAL:f32=1.1754943508222875e-38; +const F32_MIN_SUB_BITS:u32=1u; +const F32_INF_BITS:u32=0x7f800000u; +const BAIL_GUARD:f32=1.75; + +fn finite_f(x:f32)->bool{return x==x && abs(x)<=F32_MAX;} +fn finite_v(v:vec2)->bool{return finite_f(v.x)&&finite_f(v.y);} +fn next_up_pos(x0:f32)->f32{ + let x=max(0.0,x0); + if(x!=x){return F32_MAX;} + if(x==0.0){return bitcast(F32_MIN_SUB_BITS);} + let u=bitcast(x); + if(u>=0x7f7fffffu){return F32_MAX;} + return bitcast(u+1u); +} +fn next_down_pos(x0:f32)->f32{ + let x=max(0.0,x0); + if(x!=x||x==0.0){return 0.0;} + let u=bitcast(x); + if(u<=F32_MIN_SUB_BITS){return 0.0;} + return bitcast(u-1u); +} +fn up_add(a:f32,b:f32)->f32{ + let s=a+b; + if(!finite_f(s)){return F32_MAX;} + return next_up_pos(s); +} +fn up_mul_scale(a:f32,b:f32,e:i32)->f32{ + if(!(a>0.0)&&!(b>0.0)){return 0.0;} + if(a<=0.0||b<=0.0){return 0.0;} + if(!finite_f(a)||!finite_f(b)){return F32_MAX;} + // WGSL frexp has indeterminate results for subnormal f32 inputs. + // Normalize any positive subnormal exactly by 2^24 before calling frexp. + var aa=a;var bb=b;var ee=e; + if(aa(F32_MIN_SUB_BITS);} + if(!finite_f(y)){return F32_MAX;} + return next_up_pos(y); +} +fn up_mul(a:f32,b:f32)->f32{return up_mul_scale(a,b,0);} +fn up_scale(a:f32,e:i32)->f32{ + if(a<=0.0){return 0.0;} + let y=ldexp(a,e); + if(y==0.0){return bitcast(F32_MIN_SUB_BITS);} + if(!finite_f(y)){return F32_MAX;} + return next_up_pos(y); +} +fn coef_mul(h:vec2,l:vec2,x:vec2)->vec2{ + return cmul(h,x)+cmul(l,x); +} +fn state_z(s:PState)->vec2{ + let rp=refs[min(s.n,p.refLen)]; + let delta=scaled_to_f32(s.w,s.scaleExp); + return rp.hi+(rp.lo+delta); +} +fn normalize_state(s0:PState)->PState{ + var s=s0; + var mm=max(maxabs(s.w),maxabs(s.d)); + var k=0u; + loop{ + if(!(mm>65536.0)||k>=24u){break;} + s.w*=0.0000152587890625; s.d*=0.0000152587890625; s.scaleExp+=16; k+=1u; + mm=max(maxabs(s.w),maxabs(s.d)); + } + k=0u; + loop{ + if(!(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp)||k>=24u){break;} + s.w*=65536.0; s.d*=65536.0; s.scaleExp-=16; k+=1u; + mm=max(maxabs(s.w),maxabs(s.d)); + } + return s; +} +fn advance_baseline(s0:PState,targetN:u32)->AdvanceResult{ + var s=s0; var steps=0u; + loop{ + if(s.n>=targetN||s.n>=p.maxIter){return AdvanceResult(s,CODE_CONTINUE,s.n,steps);} + let z=state_z(s); let mag=dot(z,z); + if(mag>4.0){return AdvanceResult(s,CODE_ESCAPED,s.n,steps);} + if(s.n>=p.refLen){return AdvanceResult(s,CODE_REFERENCE_END,s.n,steps);} + let r=refs[s.n]; + let linear=2.0*(cmul(r.hi,s.w)+cmul(r.lo,s.w)); + let sq=scaled_to_f32(cmul(s.w,s.w),s.scaleExp); + s.w=linear+sq+s.d; s.n+=1u; steps+=1u; + let mm=max(maxabs(s.w),maxabs(s.d)); + if(mm>=1.0e30||mm!=mm){return AdvanceResult(s,CODE_RANGE,s.n,steps);} + if(mm>65536.0){s.w*=0.0000152587890625;s.d*=0.0000152587890625;s.scaleExp+=16;} + else if(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp){s.w*=65536.0;s.d*=65536.0;s.scaleExp-=16;} + if(s.scaleExp>126){return AdvanceResult(s,CODE_RANGE,s.n,steps);} + } +} +fn run_baseline(g:vec2)->AdvanceResult{ + let dx=(g.x-p.refPixelX)/p.fullW; + let dy=(p.refPixelY-g.y)/p.fullW; + var s=PState(vec2(p.spanMantHi*dx,p.spanMantHi*dy),vec2(0.0),p.spanExp,0u); + let r=advance_baseline(s,p.maxIter); + if(r.code!=CODE_CONTINUE){return r;} + return AdvanceResult(r.s,CODE_MAX_ITER,p.maxIter,r.steps); +} +fn get_bla(n:u32,lev:u32)->BlaEntry{ + let lm=levels[lev]; + let j=(n-1u)>>lev; + return blas[lm.offset+j]; +} +fn has_bla(n:u32,lev:u32)->bool{ + if(n<1u||lev>=p.levelCount){return false;} + let skip=1u<>lev; + return jf32{ + // This is a LOWER bound on the admissible error budget. The 128-ulp + // shrink covers the few f32 operations before the final nextDown. + let denom=max(1.0,p.fullW)*max(1.0,f32(p.maxIter)); + let base=(abs(p.spanMantHi)*p.safety/denom)*(1.0-128.0*F32_U); + if(!(base>0.0)){return 0.0;} + let y=ldexp(base,p.spanExp-scaleExp); + if(!(y>0.0)||!finite_f(y)){return 0.0;} + return next_down_pos(y); +} +fn physical_error(errScaled:f32,s:PState,z:vec2)->f32{ + let propagated=up_scale(errScaled,s.scaleExp); + let delta=scaled_to_f32(s.w,s.scaleExp); + let recon=up_mul(8.0*F32_U,up_add(up_add(maxabs(z),maxabs(delta)),1.0e-30)); + return up_add(propagated,recon); +} +fn run_bla(g:vec2)->BlaRunResult{ + let dx=(g.x-p.refPixelX)/p.fullW; + let dy=(p.refPixelY-g.y)/p.fullW; + var s=PState(vec2(p.spanMantHi*dx,p.spanMantHi*dy),vec2(0.0),p.spanExp,0u); + var anchor=s; + var errScaled=0.0; + var steps=0u; var apps=0u; var skipped=0u; var rejected=0u; + var replays=0u; var replaySteps=0u; var maxSkip=0u; var internalFailure=0u; + loop{ + if(s.n>=p.maxIter){ + if(errScaled>0.0){ + let z=state_z(s); let e=physical_error(errScaled,s,z); + if(up_add(next_up_pos(length(z)),e)>=2.0){ + let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps; + if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + s=rr.s;errScaled=0.0;anchor=s; + } + } + return BlaRunResult(CODE_MAX_ITER,p.maxIter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure); + } + let z=state_z(s); let zUpper=next_up_pos(length(z)); + if(errScaled>0.0){ + let e=physical_error(errScaled,s,z); + if(up_add(zUpper,e)>=BAIL_GUARD){ + let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps; + if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + s=rr.s;errScaled=0.0;anchor=s;continue; + } + } + if(dot(z,z)>4.0){ + if(errScaled>0.0){ + let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps; + if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + s=rr.s;errScaled=0.0;anchor=s;continue; + } + return BlaRunResult(CODE_ESCAPED,s.n,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure); + } + if(s.n>=p.refLen){return BlaRunResult(CODE_REFERENCE_END,s.n,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + + var found=false; var chosen:BlaEntry; var chosenW=vec2(0.0); var chosenErr=0.0; + if(s.n>=1u&&p.levelCount>0u){ + let za=length(s.w); let ca=length(s.d); let budget=target_scaled(s.scaleExp); + var li=i32(p.levelCount)-1; + loop{ + if(li<0){break;} + let lev=u32(li); + if(has_bla(s.n,lev)){ + let b=get_bla(s.n,lev); + if(b.ok!=0u&&b.start==s.n&&b.skip>0u&&s.n+b.skip<=p.maxIter&&s.n+b.skip<=p.refLen&&budget>0.0){ + let quad=up_add(up_add(up_mul(za,za),up_mul(za,ca)),up_mul(ca,ca)); + let local=up_mul_scale(b.q,quad,s.scaleExp); + let carry=up_mul(b.aAbs,errScaled); + let coeffA=up_mul(up_mul(up_mul(b.aRes,za),4.0),F32_U); + let coeffB=up_mul(up_mul(up_mul(b.bRes,ca),4.0),F32_U); + let opMag=up_add(up_mul(b.aAbs,za),up_mul(b.bAbs,ca)); + let roundErr=up_mul(up_mul(64.0,F32_U),opMag); + let eOut=up_add(up_add(up_add(carry,local),up_add(coeffA,coeffB)),roundErr); + if(finite_f(eOut)&&eOut<=budget){ + let nw=coef_mul(b.aHi,b.aLo,s.w)+coef_mul(b.bHi,b.bLo,s.d); + if(finite_v(nw)){ + var trial=PState(s.d,nw,s.scaleExp,s.n+b.skip); + var trialErr=eOut; + var mm=max(maxabs(trial.w),maxabs(trial.d)); var rk=0u; + loop{ + if(!(mm>65536.0)||rk>=24u){break;} + trial.w*=0.0000152587890625;trial.d*=0.0000152587890625;trialErr*=0.0000152587890625;trial.scaleExp+=16;rk+=1u; + mm=max(maxabs(trial.w),maxabs(trial.d)); + } + rk=0u; + loop{ + if(!(mm>0.0&&mm<0.0000152587890625&&trial.scaleExp>p.spanExp)||rk>=24u){break;} + trial.w*=65536.0;trial.d*=65536.0;trialErr*=65536.0;trial.scaleExp-=16;rk+=1u; + mm=max(maxabs(trial.w),maxabs(trial.d)); + } + if(trial.scaleExp<=126&&finite_v(trial.w)&&finite_v(trial.d)&&finite_f(trialErr)){ + let ez=state_z(trial); let ee=physical_error(trialErr,trial,ez); + if(up_add(next_up_pos(length(ez)),ee)0.0){ + let rr=advance_baseline(anchor,s.n);replays+=1u;replaySteps+=rr.steps;steps+=rr.steps; + if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + s=rr.s;errScaled=0.0;anchor=s;continue; + } + let rr=advance_baseline(s,s.n+1u);steps+=rr.steps; + if(rr.code!=CODE_CONTINUE){return BlaRunResult(rr.code,rr.iter,steps,apps,skipped,rejected,replays,replaySteps,maxSkip,internalFailure);} + s=rr.s;anchor=s; + } +} +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid:vec3){ + let i=gid.x;if(i>=p.sampleCount){return;} + let g=samples[i]; + let b=run_baseline(g); let s=run_bla(g); + results[i]=ShadowResult(b.code,b.iter,b.steps,s.code,s.iter,s.steps,s.apps,s.skipped,s.rejected,s.replays,s.replaySteps,s.maxSkip,s.internalFailure,0u,0u,0u); +} +`; + + +// Canary stage: derived from the exact GPU-shadow BLA core. It does NOT +// replace the production FAST kernel. It recomputes a deterministic 1/64 +// pixel lattice, compares candidate metadata with the already completed +// baseline fieldMeta, and writes only to a private candidate buffer. +// A separate apply pass is allowed only after CPU readback proves zero +// mismatches / zero numeric UNKNOWN / zero internal failures. +const BLA_GPU_CANARY_WGSL=BLA_GPU_SHADOW_WGSL + .replace('@group(0) @binding(5) var results:array;', `@group(0) @binding(5) var candidateMeta:array; +@group(0) @binding(6) var sampleIndices:array; +@group(0) @binding(7) var baselineMeta:array; +@group(0) @binding(8) var canaryStats:array>;`) + .replace(`@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid:vec3){ + let i=gid.x;if(i>=p.sampleCount){return;} + let g=samples[i]; + let b=run_baseline(g); let s=run_bla(g); + results[i]=ShadowResult(b.code,b.iter,b.steps,s.code,s.iter,s.steps,s.apps,s.skipped,s.rejected,s.replays,s.replaySteps,s.maxSkip,s.internalFailure,0u,0u,0u); +}`, `fn canary_meta(code:u32,iter:u32)->u32{ + if(code==CODE_ESCAPED){return pack_meta(iter,FIELD_ESCAPED);} + if(code==CODE_MAX_ITER){return pack_unknown(p.maxIter,REASON_OPERATION_LIMIT);} + if(code==CODE_REFERENCE_END){return pack_unknown(iter,REASON_REFERENCE_END);} + if(code==CODE_RANGE){return pack_unknown(iter,REASON_RANGE);} + return pack_unknown(iter,REASON_ERROR_BOUND); +} +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid:vec3){ + let i=gid.x;if(i>=p.sampleCount){return;} + let s=run_bla(samples[i]); + let cm=canary_meta(s.code,s.iter); + let bm=baselineMeta[sampleIndices[i]]; + candidateMeta[i]=cm; + atomicAdd(&canaryStats[0],1u); + if(cm!=bm){atomicAdd(&canaryStats[1],1u);} + if(s.code==CODE_REFERENCE_END||s.code==CODE_RANGE){atomicAdd(&canaryStats[2],1u);} + if(s.internalFailure!=0u){atomicAdd(&canaryStats[3],s.internalFailure);} + atomicAdd(&canaryStats[4],s.apps); + atomicAdd(&canaryStats[5],s.skipped); + atomicAdd(&canaryStats[6],s.rejected); + atomicAdd(&canaryStats[7],s.replays); + atomicAdd(&canaryStats[8],s.replaySteps); + atomicMax(&canaryStats[9],s.maxSkip); +}`); +if(BLA_GPU_CANARY_WGSL===BLA_GPU_SHADOW_WGSL||!BLA_GPU_CANARY_WGSL.includes('candidateMeta'))throw new Error('BLA canary shader derivation failed'); + +const BLA_GPU_CANARY_APPLY_WGSL=String.raw` +struct Params{ count:u32,_p0:u32,_p1:u32,_p2:u32 }; +@group(0) @binding(0) var p:Params; +@group(0) @binding(1) var sampleIndices:array; +@group(0) @binding(2) var candidateMeta:array; +@group(0) @binding(3) var fieldMeta:array; +@compute @workgroup_size(64) +fn main(@builtin(global_invocation_id) gid:vec3){ + let i=gid.x;if(i>=p.count){return;} + fieldMeta[sampleIndices[i]]=candidateMeta[i]; +} +`; + + +// Simplified production-candidate BLA kernel. Not connected to visible rendering in v24.2.52. +// Unsafe approximate paths restart the existing FAST recurrence instead of emitting UNKNOWN. +const BLA_GPU_FAST_CANDIDATE_WGSL=COMMON+String.raw` +struct Params{maxIter:u32,refLen:u32,sampleCount:u32,levelCount:u32,fullW:f32,refPixelX:f32,refPixelY:f32,spanMant:f32,spanExp:i32,safety:f32,_p0:f32,_p1:f32}; +struct RefPoint{hi:vec2,lo:vec2}; +struct Bla{aHi:vec2,aLo:vec2,bHi:vec2,bLo:vec2,q:f32,aAbs:f32,bAbs:f32,aRes:f32,bRes:f32,skip:u32,start:u32,ok:u32}; +struct Lev{offset:u32,count:u32};struct Result{packed:u32,smoothBits:u32,steps:u32,aux:u32};struct S{d:vec2,w:vec2,e:i32,n:u32}; +@group(0) @binding(0) var p:Params;@group(0) @binding(1) var refs:array;@group(0) @binding(2) var bt:array;@group(0) @binding(3) var lm:array;@group(0) @binding(4) var pts:array>;@group(0) @binding(5) var out:array; +const GUARD:f32=1.75;fn fin(x:f32)->bool{return x==x&&abs(x)<=bitcast(0x7f7fffffu);}fn fin2(v:vec2)->bool{return fin(v.x)&&fin(v.y);}fn cof(h:vec2,l:vec2,x:vec2)->vec2{return cmul(h,x)+cmul(l,x);}fn zof(s:S)->vec2{let r=refs[min(s.n,p.refLen)];return r.hi+(r.lo+scaled_to_f32(s.w,s.e));} +fn baseline(g:vec2,pre:u32)->Result{let dx=(g.x-p.refPixelX)/p.fullW;let dy=(p.refPixelY-g.y)/p.fullW;var s=S(vec2(p.spanMant*dx,p.spanMant*dy),vec2(0),p.spanExp,0u);var st=pre;loop{if(s.n>=p.maxIter){return Result(pack_unknown(p.maxIter,REASON_OPERATION_LIMIT),0u,st,1u);}let z=zof(s);let mag=dot(z,z);if(mag>4.0){return Result(pack_meta(s.n,FIELD_ESCAPED),bitcast(smooth_escape(s.n,mag)),st,1u);}if(s.n>=p.refLen){return Result(pack_unknown(s.n,REASON_REFERENCE_END),0u,st,1u);}let r=refs[s.n];let lin=2.0*(cmul(r.hi,s.w)+cmul(r.lo,s.w));let sq=scaled_to_f32(cmul(s.w,s.w),s.e);s.w=lin+sq+s.d;s.n++;st++;let mm=max(maxabs(s.w),maxabs(s.d));if(mm>=1e30||mm!=mm){return Result(pack_unknown(s.n,REASON_RANGE),0u,st,1u);}if(mm>65536.0){s.w/=65536.0;s.d/=65536.0;s.e+=16;}else if(mm>0.0&&mm<1.0/65536.0&&s.e>p.spanExp){s.w*=65536.0;s.d*=65536.0;s.e-=16;}if(s.e>126){return Result(pack_unknown(s.n,REASON_RANGE),0u,st,1u);}}} +fn has(n:u32,l:u32)->bool{if(n<1u||l>=p.levelCount){return false;}let sk=1u<>l)Bla{return bt[lm[l].offset+((n-1u)>>l)];} +fn budget(e:i32)->f32{let b=abs(p.spanMant)*p.safety/(max(1.0,p.fullW)*max(1.0,f32(p.maxIter)));if(!(b>0)){return 0;}let y=ldexp(b,p.spanExp-e);return select(0.0,y,y>0&&fin(y));}fn perr(er:f32,s:S,z:vec2)->f32{return abs(ldexp(er,s.e))+8.0*F32_U*(maxabs(z)+maxabs(scaled_to_f32(s.w,s.e))+1e-30);} +fn run(g:vec2)->Result{let dx=(g.x-p.refPixelX)/p.fullW;let dy=(p.refPixelY-g.y)/p.fullW;var s=S(vec2(p.spanMant*dx,p.spanMant*dy),vec2(0),p.spanExp,0u);var er=0.0;var ap=false;var st=0u;loop{if(s.n>=p.maxIter){if(ap&&length(zof(s))+perr(er,s,zof(s))>=2.0){return baseline(g,st);}return Result(pack_unknown(p.maxIter,REASON_OPERATION_LIMIT),0u,st,0u);}let z=zof(s);if(ap&&length(z)+perr(er,s,z)>=GUARD){return baseline(g,st);}let mag=dot(z,z);if(mag>4.0){if(ap){return baseline(g,st);}return Result(pack_meta(s.n,FIELD_ESCAPED),bitcast(smooth_escape(s.n,mag)),st,0u);}if(s.n>=p.refLen){return baseline(g,st);}var found=false;var ne=0.0;if(s.n>=1u){let za=length(s.w);let ca=length(s.d);let bd=budget(s.e);var li=i32(p.levelCount)-1;loop{if(li<0){break;}let l=u32(li);if(has(s.n,l)){let b=get_bla(s.n,l);if(b.ok!=0u&&b.start==s.n&&b.skip>0u&&s.n+b.skip<=p.maxIter&&s.n+b.skip<=p.refLen&&bd>0){let qd=za*za+za*ca+ca*ca;let local=abs(ldexp(b.q*qd,s.e));let eo=b.aAbs*er+local+b.aRes*za*4.0*F32_U+b.bRes*ca*4.0*F32_U;if(fin(eo)&&eo<=bd){let nw=cof(b.aHi,b.aLo,s.w)+cof(b.bHi,b.bLo,s.d);if(fin2(nw)){var t=S(s.d,nw,s.e,s.n+b.skip);var te=eo;var mm=max(maxabs(t.w),maxabs(t.d));var k=0u;loop{if(!(mm>65536.0)||k>=24u){break;}t.w/=65536.0;t.d/=65536.0;te/=65536.0;t.e+=16;k++;mm=max(maxabs(t.w),maxabs(t.d));}loop{if(!(mm>0.0&&mm<1.0/65536.0&&t.e>p.spanExp)||k>=48u){break;}t.w*=65536.0;t.d*=65536.0;te*=65536.0;t.e-=16;k++;mm=max(maxabs(t.w),maxabs(t.d));}if(t.e<=126&&fin2(t.w)&&fin2(t.d)&&fin(te)&&length(zof(t))+perr(te,t,zof(t))=1e30||mm!=mm){return baseline(g,st);}if(mm>65536.0){s.w/=65536.0;s.d/=65536.0;s.e+=16;}else if(mm>0.0&&mm<1.0/65536.0&&s.e>p.spanExp){s.w*=65536.0;s.d*=65536.0;s.e-=16;}if(s.e>126){return baseline(g,st);}}} +@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id:vec3){if(id.x p:Params;@group(0) @binding(1) var refs:array;@group(0) @binding(2) var bt:array;@group(0) @binding(3) var lm:array;@group(0) @binding(4) var pts:array>;@group(0) @binding(5) var out:array;', + '@group(0) @binding(0) var p:Params;@group(0) @binding(1) var refs:array;@group(0) @binding(2) var bt:array;@group(0) @binding(3) var lm:array;@group(0) @binding(4) var fieldMeta:array;@group(0) @binding(5) var fieldSmooth:array;@group(0) @binding(6) var unresolved:array>;' + ) + .replace( + '@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id:vec3){if(id.x(r.smoothBits); + let cls=(r.packed>>28u)&3u; + if(cls==FIELD_UNKNOWN){let reason=(r.packed>>20u)&255u;atomicAdd(&unresolved[0],1u);if(reason>=1u&&reason<=6u){atomicAdd(&unresolved[reason],1u);}} + } + @compute @workgroup_size(8,8) fn main(@builtin(global_invocation_id) gid:vec3){ + if(gid.x>=p.width||gid.y>=p.height){return;} + let g=vec2(f32(p.tileX+gid.x)+0.5,f32(p.tileY+gid.y)+0.5); + let i=p.outputBase+gid.y*p.outputStride+gid.x; + store_result(i,run(g)); + }` + ); +if(BLA_GPU_FAST_FRAME_WGSL===BLA_GPU_FAST_CANDIDATE_WGSL||!BLA_GPU_FAST_FRAME_WGSL.includes('store_result'))throw new Error('adaptive FAST BLA shader derivation failed'); + +// Screen path: collect UNKNOWN statistics in the bucket histogram pass rather +// than using per-UNKNOWN global atomics in the primary perturbation kernel. const DEEP_PERTURB_POSTSTATS_WGSL=DEEP_PERTURB_WGSL.replace('fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n atomicAdd(&unresolved.remaining,1u);\n if(reason==REASON_ERROR_BOUND){atomicAdd(&unresolved.errorBound,1u);}\n else if(reason==REASON_ESCAPE_UNCERTAIN){atomicAdd(&unresolved.escapeUncertain,1u);}\n else if(reason==REASON_REFERENCE_END){atomicAdd(&unresolved.referenceEnd,1u);}\n else if(reason==REASON_REBASE_GAP){atomicAdd(&unresolved.rebaseGap,1u);}\n else if(reason==REASON_RANGE){atomicAdd(&unresolved.rangeFailure,1u);}\n else if(reason==REASON_OPERATION_LIMIT){atomicAdd(&unresolved.operationLimit,1u);}\n}','fn mark_unresolved(out:u32,n:u32,reason:u32){\n fieldMeta[out]=pack_unknown(n,reason); fieldSmooth[out]=0.0;\n}'); const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL @@ -830,10 +1361,10 @@ const DEEP_BUCKET_HIST_STATS_WGSL=DEEP_BUCKET_HIST_WGSL .replace('if(c>0u){atomicAdd(&bucketState[lane],c);}', `if(c>0u){atomicAdd(&bucketState[lane],c);} let r=atomicLoad(&localReasons[lane]); if(r>0u){atomicAdd(&unresolvedStats[lane],r);}`); -if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('v24.2.14 post-stats shader derivation failed'); +if(DEEP_PERTURB_POSTSTATS_WGSL===DEEP_PERTURB_WGSL || DEEP_BUCKET_HIST_STATS_WGSL===DEEP_BUCKET_HIST_WGSL)throw new Error('post-stats shader derivation failed'); globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({ - version:'24.2.26-baseline-idle-refinement',DIRECT_F32_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_LIKELY_REFINE_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,LIKELY_QUEUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL + version:'24.2.55-no-black-hole-async-recovery',DIRECT_F32_WGSL,DIRECT_ACCURATE_SEED_WGSL,DIRECT_DS_CORRECT_WGSL,FAST_PERTURB_WGSL,DEEP_PERTURB_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_HIST_STATS_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL,BLA_GPU_SHADOW_WGSL,BLA_GPU_CANARY_WGSL,BLA_GPU_CANARY_APPLY_WGSL,BLA_GPU_FAST_CANDIDATE_WGSL,BLA_GPU_FAST_FRAME_WGSL }); })(); @@ -843,14 +1374,14 @@ globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({ const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません'); const $=s=>document.querySelector(s),canvas=$('#view'); const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n; -const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000; +const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000,PALETTE_MAX=18,VALID_PALETTE_IDS=new Set([0,1,2,3,5,7,9,10,11,13,15,17,18]); const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20; const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity']; -const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96,REFINE_QUEUE_PARAM_BYTES=16,REFINE_STATS_BYTES=16,REFINE_BATCH_BYTES=16; -const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorCycleDir:1,colorShiftDir:1,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,refinementRunning:false,refinementStage:0,refinementQueue:0,refinedIter:0}; -let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,idleRefineTimer=0,lastWrittenHash='',navigationHash=''; +const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96; +const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,multiRefPasses:0,multiRefRecovered:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,qualityStage:'idle',qualityIter:0,qualityFinalIter:0,previewMs:0,refineMs:0,refinePending:false,temporalFill:false,numericRecoveryPending:false,numericRecoveryRunning:false,gpuStage:'',gpuTileRows:0}; +let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,refineTimer=0,lastWrittenHash='',navigationHash=''; const viewHistory=[];let viewHistoryIndex=-1; -const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,idleRefineBatches:0,idleRefineQueued:0}; +const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,multiRefBuilds:0,numericSubmits:0,correctionSubmits:0,gpuSyncWaits:0,blaShadowBuilds:0,blaShadowFailures:0,blaShadowLatest:null,blaGpuShadowRuns:0,blaGpuShadowFailures:0,blaGpuShadowLatest:null,blaGpuCanaryRuns:0,blaGpuCanaryFailures:0,blaGpuCanaryApplied:0,blaGpuCanaryLatest:null,blaFastProbeRuns:0,blaFastEnabledFrames:0,blaFastBaselineFrames:0,blaFastProbeLatest:null,blaFastFailures:0}; // ── exact fixed-point view state ───────────────────────────────────────── function one(bits=state.bits){return 1n<=4)} -function chooseBackend(snap=snapshot(),w=canvas.width){const deep=state.renderMode==='accurate',fastExtended=!deep&&fastNeedsExtended(snap,w);return{backend:deep?'deep':fastExtended?'fast-extended':'direct',deep,fastExtended,probe:false,reason:deep?'manual-accurate':fastExtended?'manual-fast-extended':'manual-fast'}} -function deepNeeded(){return state.renderMode==='accurate'} +function accurateHybridSafe(snap=snapshot(),w=canvas.width){return directPixelRatio(snap,w)>=32} +function chooseBackend(snap=snapshot(),w=canvas.width){const accurate=state.renderMode==='accurate',accurateHybrid=accurate&&accurateHybridSafe(snap,w),deep=accurate&&!accurateHybrid,fastExtended=!accurate&&fastNeedsExtended(snap,w);return{backend:accurateHybrid?'accurate-direct-ds':deep?'deep':fastExtended?'fast-extended':'direct',deep,accurateHybrid,fastExtended,probe:false,reason:accurateHybrid?'accurate-direct-ds-safe-spacing':deep?'manual-accurate-deep':fastExtended?'manual-fast-extended':'manual-fast'}} +function deepNeeded(snap=snapshot(),w=canvas.width){return state.renderMode==='accurate'} function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,renderMode:state.renderMode}} function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')} function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()} function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,renderMode:v.renderMode||state.renderMode});ensurePrecision();syncControls();saveHash(false);markDirty()} // ── iteration / quality policy ─────────────────────────────────────────── -function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))} -function effectiveColorCycle(iter=maxIter()){const base=Math.max(1,state.baseIter),effective=Math.max(base,Number(iter)||base);return state.cycle*base/effective} +function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),w=Math.max(1,canvas.width||innerWidth||1),screenBits=Math.max(0,Math.log2(w)+z*Math.LOG2E*Math.LN10),q=state.processMode==='power'?.72:state.processMode==='fine'?1.25:state.processMode==='validate'?1.5:1,bonus=Math.floor(48*q*screenBits);return Math.min(12000,Math.max(state.baseIter,state.baseIter+bonus))} +function previewIter(finalIter=maxIter()){ + const z=Math.max(0,zoomExp()),m=state.processMode==='power'?.72:state.processMode==='fine'?1.28:state.processMode==='validate'?1.55:1; + // The old fixed 1100-iteration ceiling starved deep previews: around 10^12, + // finalIter is ~2700 while many visible boundary pixels need >1100 steps. + // Make the first pass track final quality as zoom deepens, with a mode-aware + // ceiling so responsiveness is still bounded on extreme views. + const legacy=Math.round(m*(420+24*z)); + const fractionBase=Math.min(.84,.48+.03*z); + const fraction=state.processMode==='power'?Math.min(.68,fractionBase*.82):state.processMode==='fine'?Math.min(.90,fractionBase+.04):state.processMode==='validate'?Math.min(.94,fractionBase+.08):fractionBase; + const deepFloor=Math.round(finalIter*fraction); + const cap=state.processMode==='power'?1800:state.processMode==='fine'?4800:state.processMode==='validate'?6000:3600; + return Math.min(finalIter,Math.max(state.baseIter,Math.min(cap,Math.max(legacy,deepFloor)))); +} +function canUseStableReprojection(){ + if(!renderer?.historyReady||!state.frameView)return false; + // Covers normal wheel/pinch, one 2x toolbar zoom, and ordinary pans while + // avoiding grotesque reuse after a reset or a distant shared-URL jump. + return reprojectionSafe(renderer.presentTransform(),2.25,.45); +} +function qualityStages(finalIter=maxIter()){ + // Once a completed nearby frame exists, temporal reprojection is the + // preview. Do not replace it with a lower-iteration numerical frame: that + // was the source of the stressful black patches during small zoom/pan. + // Accurate mode also skips its expensive low-iteration Deep pass entirely; + // the final Deep result is computed once and swapped in atomically. + if(state.renderMode==='accurate'||canUseStableReprojection())return[finalIter]; + const p=previewIter(finalIter); + return finalIter>p?[p,finalIter]:[p]; +} +function refinementDelay(stageIndex){ + // Full-quality latency matters more than leaving a long idle gap after the + // first preview. Keep a short interaction settling window only. + if(stageIndex<=1)return state.processMode==='validate'?300:state.processMode==='fine'?240:state.processMode==='power'?420:160; + return state.processMode==='validate'?240:state.processMode==='fine'?200:360; +} +function sameViewSnapshot(a,b){return !!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span} +function reprojectionSafe(t,maxZoom=1.30,maxShift=.18){const s=Math.abs(Number(t?.scaleX));if(!Number.isFinite(s)||s<=0)return false;const z=Math.max(s,1/s),shift=Math.hypot(Number(t?.offsetX)||0,Number(t?.offsetY)||0);return z<=maxZoom&&shift<=maxShift} + function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))} function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)} +function fract01(x){return x-Math.floor(x)} +function colorStyleSnapshot(){return{palette:state.palette,cycle:state.cycle,shift:state.shift,hq:state.hq}} +function colorStyleKey(style=colorStyleSnapshot()){return[style.palette,style.cycle,style.shift,style.hq?1:0].join(':')} 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)}} @@ -915,7 +1487,7 @@ self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=perfor class ReferenceService{ constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16} ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1,fresh=false){const key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,'guarded-perturb-v24.2.26'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} + request(snap,iter,width=1,height=1,fresh=false){const key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,'guarded-perturb-v24.2.35'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} } @@ -938,12 +1510,115 @@ self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=perfor class FastReferenceService{ constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=12} ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([fastReferenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'fast reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.2.26'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} + request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.2.35'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} destroy(){this.cancelPending('destroyed');this.cache.clear()} } const fastRefs=new FastReferenceService(); +// ── BLA shadow laboratory ──────────────────────────────────────────────── +// Safety rule for v24.2.49: BLA never participates in visible rendering. +// A separate worker builds a real hierarchical BLA table from the exact same +// packed reference orbit used by the existing perturbation kernel and compares +// its result against baseline perturbation on deterministic screen samples. +// Any mismatch therefore blocks promotion instead of creating a black/UNKNOWN +// pixel or changing the image. +function blaShadowWorkerSource(){return String.raw` +'use strict'; +function cabs(ar,ai){return Math.hypot(ar,ai)} +function cmul(ar,ai,br,bi){return[ar*br-ai*bi,ar*bi+ai*br]} +function cadd(ar,ai,br,bi){return[ar+br,ai+bi]} +function finite2(a,b){return Number.isFinite(a)&&Number.isFinite(b)} +function ldexp(v,e){if(!Number.isFinite(v)||v===0)return v;return v*Math.pow(2,e)} +function unpackRefs(buf,refLen){const dv=new DataView(buf),rr=new Float64Array(refLen+1),ri=new Float64Array(refLen+1);for(let i=0;i<=refLen;i++){const o=i*16;rr[i]=dv.getFloat32(o,true)+dv.getFloat32(o+8,true);ri[i]=dv.getFloat32(o+4,true)+dv.getFloat32(o+12,true)}return{rr,ri}} +function item(start,skip,ar,ai,br,bi,dr,di,er,ei,fr,fi){return{start,skip,ar,ai,br,bi,dr,di,er,ei,fr,fi,q:Math.max(cabs(dr,di),.5*cabs(er,ei),cabs(fr,fi))}} +function buildTable(rr,ri,refLen){const levels=[];let l0=[];for(let m=1;m0)u[0]++;else u[0]--;return a[0]} +function upF32(x){if(!(x>=0)||!Number.isFinite(x))return Math.fround(x);let y=Math.fround(x);if(y=b.q&&aAbs>=cabs(b.ar,b.ai)&&bAbs>=cabs(b.br,b.bi);const o=n*stride;dv.setFloat32(o,arh,true);dv.setFloat32(o+4,aih,true);dv.setFloat32(o+8,arl,true);dv.setFloat32(o+12,ail,true);dv.setFloat32(o+16,brh,true);dv.setFloat32(o+20,bih,true);dv.setFloat32(o+24,brl,true);dv.setFloat32(o+28,bil,true);dv.setFloat32(o+32,q,true);dv.setFloat32(o+36,aAbs,true);dv.setFloat32(o+40,bAbs,true);dv.setFloat32(o+44,aRes,true);dv.setFloat32(o+48,bRes,true);dv.setUint32(o+52,b.skip,true);dv.setUint32(o+56,b.start,true);dv.setUint32(o+60,ok?1:0,true);n++}}return{buffer:buf,offsets,counts,total,stride}} +function at(levels,n,d){const skip=1<>d,lev=levels[d];return lev&&j4)return{cls:1,iter:n,wr,wi,physical};if(n>=refLen)return{cls:0,iter:n,wr,wi,physical};const lr=2*(rr[n]*wr-ri[n]*wi),li=2*(rr[n]*wi+ri[n]*wr),sq=[wr*wr-wi*wi,2*wr*wi];wr=lr+sq[0]+dcr;wi=li+sq[1]+dci;n++;physical++}return{cls:0,iter:maxIter,wr,wi,physical}} +function shadow(rr,ri,levels,refLen,dcr,dci,maxIter,pixelSpacing,safety,allowBla=true){ + let wr=0,wi=0,n=0,physical=0,apps=0,skipped=0,rejected=0,maxSkip=0,replaySteps=0,replays=0,err=0; + let anchorWr=0,anchorWi=0,anchorN=0; + const ca=cabs(dcr,dci),L=Math.max(1,maxIter),BAIL_GUARD=1.75; + function replayTo(target){ + wr=anchorWr;wi=anchorWi;n=anchorN;err=0;replays++; + while(n4)return{escaped:true,iter:n}; + const lr=2*(rr[n]*wr-ri[n]*wi),li=2*(rr[n]*wi+ri[n]*wr),sqR=wr*wr-wi*wi,sqI=2*wr*wi; + wr=lr+sqR+dcr;wi=li+sqI+dci;n++;physical++;replaySteps++; + } + anchorWr=wr;anchorWi=wi;anchorN=n; + return{escaped:false}; + } + while(n0&&zmag+err>=BAIL_GUARD){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};continue;} + if(zr*zr+zi*zi>4){if(err>0){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};continue;}return{cls:1,iter:n,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};} + if(n>=refLen)return{cls:0,iter:n,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays}; + let chosen=null; + if(n>=1&&allowBla){ + const za=cabs(wr,wi); + for(let d=levels.length-1;d>=0;d--){ + const b=at(levels,n,d);if(!b||n+b.skip>Math.min(maxIter,refLen)||!Number.isFinite(b.q))continue; + const local=b.q*(za*za+za*ca+ca*ca),budget=cabs(b.br,b.bi)*pixelSpacing/L*safety,eOut=cabs(b.ar,b.ai)*err+local; + if(!(Number.isFinite(eOut)&&eOut<=budget)){rejected++;continue;} + const az=cmul(b.ar,b.ai,wr,wi),bc=cmul(b.br,b.bi,dcr,dci),nw=cadd(az[0],az[1],bc[0],bc[1]),end=n+b.skip,ezr=rr[end]+nw[0],ezi=ri[end]+nw[1]; + if(cabs(ezr,ezi)+eOut>=BAIL_GUARD){rejected++;continue;} + chosen={b,nw,eOut};break; + } + } + if(chosen){wr=chosen.nw[0];wi=chosen.nw[1];err=chosen.eOut;n+=chosen.b.skip;physical++;apps++;skipped+=chosen.b.skip-1;if(chosen.b.skip>maxSkip)maxSkip=chosen.b.skip;continue;} + // A normal perturbation step is an exact checkpoint only if no earlier BLA + // uncertainty remains. Otherwise replay first, then continue baseline. + if(err>0){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};continue;} + const lr=2*(rr[n]*wr-ri[n]*wi),li=2*(rr[n]*wi+ri[n]*wr),sqR=wr*wr-wi*wi,sqI=2*wr*wi; + wr=lr+sqR+dcr;wi=li+sqI+dci;n++;physical++;anchorWr=wr;anchorWi=wi;anchorN=n; + } + if(err>0&&cabs(rr[Math.min(n,refLen)]+wr,ri[Math.min(n,refLen)]+wi)+err>=2){const r=replayTo(n);if(r.escaped)return{cls:1,iter:r.iter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays};} + return{cls:0,iter:maxIter,wr,wi,physical,apps,skipped,rejected,maxSkip,replaySteps,replays}; +} +function samplePoints(w,h,count){const side=Math.max(3,Math.ceil(Math.sqrt(Math.max(1,count)))),out=[];for(let j=0;j=count)break;const x=(i+.5)*w/side,y=(j+.5)*h/side;out.push([x,y])}return out} +self.onmessage=e=>{const d=e.data;if(!d||(d.type!=='validate'&&d.type!=='build'))return;const t0=performance.now();try{const {rr,ri}=unpackRefs(d.refs,d.refLen),tb0=performance.now(),levels=buildTable(rr,ri,d.refLen),packed=flattenTable(levels),tableBuildMs=performance.now()-tb0,span=ldexp(d.spanMant,d.spanExp),pixelSpacing=Math.abs(span)/Math.max(1,d.width),buildOnly=d.type==='build',pts=buildOnly?[]:samplePoints(d.width,d.height,d.samples||49),referenceFull=d.refLen>=d.maxIter;let classMismatch=0,escapeIterationMismatch=0,logical=0,physical=0,apps=0,skipped=0,rejected=0,maxSkip=0,maxEndpointDelta=0,replaySteps=0,replays=0;for(const [x,y] of pts){const dcr=(x-d.refPixelX)*span/d.width,dci=(d.refPixelY-y)*span/d.width,b=baseline(rr,ri,d.refLen,dcr,dci,d.maxIter),sh=shadow(rr,ri,levels,d.refLen,dcr,dci,d.maxIter,pixelSpacing,d.safety||0.015625,referenceFull);logical+=b.physical;physical+=sh.physical;apps+=sh.apps;skipped+=sh.skipped;rejected+=sh.rejected;replaySteps+=sh.replaySteps||0;replays+=sh.replays||0;maxSkip=Math.max(maxSkip,sh.maxSkip);if(b.cls!==sh.cls)classMismatch++;if(b.cls===1&&sh.cls===1&&b.iter!==sh.iter)escapeIterationMismatch++;if(b.cls===sh.cls&&b.iter===sh.iter){const de=Math.hypot(b.wr-sh.wr,b.wi-sh.wi);if(Number.isFinite(de))maxEndpointDelta=Math.max(maxEndpointDelta,de)}}const eligibleForBla=referenceFull&&span!==0,gatePassed=eligibleForBla&&(buildOnly||(classMismatch===0&&escapeIterationMismatch===0&&!Number.isNaN(maxEndpointDelta)));const stats={samples:pts.length,classMismatch,escapeIterationMismatch,logicalIterations:logical,physicalSteps:physical,blaApplications:apps,blaSkippedIterations:skipped,blaRejected:rejected,blaReplaySteps:replaySteps,blaReplays:replays,maxSkip,skipRate:logical?Math.max(0,(logical-physical)/logical):0,candidateSkipRate:logical?skipped/logical:0,maxEndpointDelta,tableLevels:levels.length,tableEntries:packed.total,tableStride:packed.stride,gpuPacking:'A/B hi+lo; bounds upward-rounded',unsafeRawF32Rejected:true,gpuShadowEligible:gatePassed,tableBuildMs,totalMs:performance.now()-t0,spanUnderflow:span===0,referenceFull,eligibleForBla,buildOnly,productionEligible:false,gatePassed};postMessage({type:'validated',id:d.id,key:d.key,stats,table:packed.buffer,levelOffsets:packed.offsets,levelCounts:packed.counts},[packed.buffer])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} +`} +class BlaShadowService{ + constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.latest=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([blaShadowWorkerSource()],{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'){runtime.blaShadowFailures++;p.reject(new Error(d.error));return}const result={...d,source:p.source,createdAt:performance.now()};this.latest=result;runtime.blaShadowLatest=result.stats;runtime.blaShadowBuilds++;p.resolve(result);updateStats()};this.worker.onerror=e=>{this.failed=true;runtime.blaShadowFailures++;for(const p of this.pending.values())p.reject(new Error(e.message||'BLA shadow worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} + request({ctx,snap,iter,width,height,refPixel,samples=49,safety=1/64}){if(!ctx?.refs||!ctx.refLen)return Promise.reject(new Error('BLA shadow requires perturbation reference'));if(!this.ensure())return Promise.reject(new Error('BLA shadow Workerを作成できません'));if(this.pending.size)this.cancelPending('superseded BLA shadow request');const id=++this.serial,se=spanMantExp(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,ctx.refLen,samples,safety].join(':');const copy=ctx.refs.slice(0),source={bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y};return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source});this.worker.postMessage({type:'validate',id,key,refs:copy,refLen:ctx.refLen,maxIter:iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y,spanMant:se.mant,spanExp:se.exp,samples,safety},[copy])})} + requestBuild({ctx,snap,iter,width,height,refPixel,safety=1/64}){if(!ctx?.refs||!ctx.refLen)return Promise.reject(new Error('BLA build requires perturbation reference'));if(!this.ensure())return Promise.reject(new Error('BLA Workerを作成できません'));if(this.pending.size)this.cancelPending('superseded BLA build request');const id=++this.serial,se=spanMantExp(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,ctx.refLen,'build',safety].join(':'),copy=ctx.refs.slice(0),source={bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y};return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source});this.worker.postMessage({type:'build',id,key,refs:copy,refLen:ctx.refLen,maxIter:iter,width,height,refPixelX:refPixel.x,refPixelY:refPixel.y,spanMant:se.mant,spanExp:se.exp,safety},[copy])})} + cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear()} + 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=''}this.latest=null} +} +const blaShadow=new BlaShadowService();let blaShadowTimer=0,numericRecoveryTimer=0; +function blaSamplePoints(w,h,count){const side=Math.max(3,Math.ceil(Math.sqrt(Math.max(1,count)))),out=[];for(let j=0;j=count)break;out.push([(i+.5)*w/side,(j+.5)*h/side])}return out} +function blaCanaryGrid(w,h){const points=[],indices=[];for(let y=5;y{blaShadowTimer=0;if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot()))return; + const gpuGateRun=kind==='fast'&&state.processMode==='validate',samples=gpuGateRun?289:49; + blaShadow.request({ctx,snap,iter,width:canvas.width,height:canvas.height,refPixel,samples,safety:1/64}).then(async result=>{ + if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot()))return; + if(gpuGateRun&&result.stats.gpuShadowEligible&&renderer){ + const gpu=await renderer.runBlaGpuShadow({shadow:result,ctx,snap,iter,refPixel,safety:1/64}); + if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot()))return; + if(gpu?.gatePassed&&renderer.blaGpuGate?.armed){await renderer.runBlaGpuCanary({shadow:result,ctx,snap,iter,refPixel,safety:1/64,token});} + } + }).catch(e=>{if(!/cancelled|superseded/i.test(String(e&&e.message||e))){runtime.blaShadowFailures++;console.warn('BLA shadow validation skipped:',e)}}) + },700) +} + // ── 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{}} @@ -955,18 +1630,191 @@ class WebGpuRenderer{ this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''}; this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)}; this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat(); - this.frame=null;this.deepCtx=null;this.fastCtx=null;this.refineDeepCtx=null;this.exportWs=null;this.refinePipelinePromise=null;this.likelyQueue=null;this.deepLikelyRefine=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'}); + this.frame=null;this.deepCtx=null;this.fastCtx=null;this.blaFastCtx=null;this.exportWs=null;this.blaFastCandidatePipeline=null;this.blaFastFramePipeline=null;this.blaFastCompilePromise=null;this.blaFastWarmScheduled=false;this.blaFastDisabled=false;this.blaGpuShadowPipeline=null;this.blaGpuShadowCompilePromise=null;this.blaGpuShadowDisabled=false;this.blaGpuCanaryPipeline=null;this.blaGpuCanaryApplyPipeline=null;this.blaGpuCanaryCompilePromise=null;this.blaGpuCanaryDisabled=false;this.blaGpuGate={consecutive:0,totalSamples:0,distinctViews:0,viewKeys:[],armed:false,lastReason:'not-tested'};this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.activeGpuStage='';this.lastCompletedGpuStage='';this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false;this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});this.reprojectSampler=device.createSampler({magFilter:'nearest',minFilter:'nearest'}); 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()}); + device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;const where=this.activeGpuStage||this.lastCompletedGpuStage||'unknown-stage';state.gpuStage=where;state.gpuError='device lost @ '+where+': '+this.lossReason;if(renderer===this){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} + configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'premultiplied'})} + 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=>'line '+x.lineNum+':'+x.linePos+' '+x.message).join('\n'))}return m} + resetBlaGpuGate(reason='reset'){ + this.blaGpuGate={consecutive:0,totalSamples:0,distinctViews:0,viewKeys:[],armed:false,lastReason:reason}; + runtime.blaGpuCanaryLatest={gateArmed:false,reason}; + } + updateBlaGpuGate(stats,viewKey=''){ + const pass=!!(stats&&stats.gatePassed&&stats.exactMismatch===0&&stats.blaDerivedUnknown===0&&stats.internalFailure===0&&stats.samples>=289); + if(!pass){this.resetBlaGpuGate(stats?.error||'gpu-shadow-mismatch');return false} + this.blaGpuGate.consecutive++;this.blaGpuGate.totalSamples+=stats.samples;this.blaGpuGate.lastReason='gpu-shadow-pass'; + if(viewKey&&!this.blaGpuGate.viewKeys.includes(viewKey)){if(this.blaGpuGate.viewKeys.length<8)this.blaGpuGate.viewKeys.push(viewKey);this.blaGpuGate.distinctViews=this.blaGpuGate.viewKeys.length} + this.blaGpuGate.armed=this.blaGpuGate.consecutive>=3&&this.blaGpuGate.totalSamples>=867&&this.blaGpuGate.distinctViews>=3; + return this.blaGpuGate.armed; + } + warmBlaFastPipelines(){ + if(this.blaFastCandidatePipeline&&this.blaFastFramePipeline)return Promise.resolve({probe:this.blaFastCandidatePipeline,frame:this.blaFastFramePipeline}); + if(this.blaFastDisabled)return Promise.resolve(null); + if(this.blaFastCompilePromise)return this.blaFastCompilePromise; + this.blaFastCompilePromise=(async()=>{ + let scope=false; + try{ + if(this.device.pushErrorScope){this.device.pushErrorScope('validation');scope=true} + const [pm,fm]=await Promise.all([this.module('bla-fast-probe',G.BLA_GPU_FAST_CANDIDATE_WGSL),this.module('bla-fast-frame',G.BLA_GPU_FAST_FRAME_WGSL)]); + const make=async module=>this.device.createComputePipelineAsync?this.device.createComputePipelineAsync({layout:'auto',compute:{module,entryPoint:'main'}}):this.device.createComputePipeline({layout:'auto',compute:{module,entryPoint:'main'}}); + const [probe,frame]=await Promise.all([make(pm),make(fm)]); + if(scope&&this.device.popErrorScope){const e=await this.device.popErrorScope();scope=false;if(e)throw e} + this.blaFastCandidatePipeline=probe;this.blaFastFramePipeline=frame;return{probe,frame}; + }catch(e){ + try{if(scope&&this.device.popErrorScope)await this.device.popErrorScope()}catch{} + this.blaFastDisabled=true;runtime.blaFastFailures++;runtime.blaFastProbeLatest={enabled:false,reason:'compile-failed',error:String(e&&e.message||e)};console.warn('Adaptive FAST BLA disabled:',e);updateStats();return null; + }finally{this.blaFastCompilePromise=null} + })(); + return this.blaFastCompilePromise; + } + scheduleBlaFastWarmup(){if(this.blaFastWarmScheduled||this.blaFastCandidatePipeline||this.blaFastCompilePromise||this.blaFastDisabled)return;this.blaFastWarmScheduled=true;const run=()=>{this.blaFastWarmScheduled=false;if(renderer===this&&!state.rendering&&!this.blaFastCandidatePipeline&&!this.blaFastDisabled)this.warmBlaFastPipelines()};if(typeof requestIdleCallback==='function')requestIdleCallback(run,{timeout:2500});else setTimeout(run,1200)} + destroyBlaFastContext(){if(!this.blaFastCtx)return;destroy(this.blaFastCtx.tableB);destroy(this.blaFastCtx.levelsB);this.blaFastCtx=null} + setBlaFastContext(shadow){ + const key=shadow?.key||'';if(this.blaFastCtx&&this.blaFastCtx.key===key)return this.blaFastCtx;this.destroyBlaFastContext(); + const levelCount=Math.min(20,shadow?.levelOffsets?.length||0);if(!levelCount||!shadow?.table?.byteLength)return null; + const levelData=new Uint32Array(levelCount*2);for(let i=0;i>>0;levelData[2*i+1]=shadow.levelCounts[i]>>>0} + const d=this.device,B=GPUBufferUsage,tableB=buf(d,shadow.table.byteLength,B.STORAGE|B.COPY_DST,'bla-fast-table'),levelsB=buf(d,levelData.byteLength,B.STORAGE|B.COPY_DST,'bla-fast-levels');d.queue.writeBuffer(tableB,0,shadow.table);d.queue.writeBuffer(levelsB,0,levelData); + this.blaFastCtx={key,tableB,levelsB,levelCount,tableBytes:shadow.table.byteLength};return this.blaFastCtx; + } + blaFastProbeParams(sampleCount,iter,snap,refPixel,safety=1/64){const se=spanMantExp(snap);return writeU32F32(48,d=>{d.setUint32(0,iter,true);d.setUint32(4,this.fastCtx.refLen,true);d.setUint32(8,sampleCount,true);d.setUint32(12,this.blaFastCtx.levelCount,true);d.setFloat32(16,canvas.width,true);d.setFloat32(20,Math.fround(refPixel.x),true);d.setFloat32(24,Math.fround(refPixel.y),true);d.setFloat32(28,Math.fround(se.mant),true);d.setInt32(32,se.exp,true);d.setFloat32(36,Math.fround(safety),true)})} + blaFastFrameParams(w,h,tileX,tileY,iter,snap,refPixel,outputStride,outputBase,safety=1/64){const se=spanMantExp(snap);return writeU32F32(80,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,tileX,true);d.setUint32(12,tileY,true);d.setUint32(16,iter,true);d.setUint32(20,this.fastCtx.refLen,true);d.setUint32(24,this.blaFastCtx.levelCount,true);d.setUint32(28,outputStride,true);d.setUint32(32,outputBase,true);d.setFloat32(48,canvas.width,true);d.setFloat32(52,Math.fround(refPixel.x),true);d.setFloat32(56,Math.fround(refPixel.y),true);d.setFloat32(60,Math.fround(se.mant),true);d.setInt32(64,se.exp,true);d.setFloat32(68,Math.fround(safety),true)})} + async runBlaFastProbe({shadow,snap,iter,refPixel,safety=1/64,samples=64}){ + if(this.blaFastDisabled||!this.blaFastCandidatePipeline)return{enabled:false,reason:'pipeline-not-ready'}; + if(!this.fastCtx||this.fastCtx.refLen>>28)&3,it=packed&ITER_MASK;if(aux)fallback++;if(cls===FIELD_UNKNOWN)unknown++;logical+=Math.max(1,it);physical+=steps}const fallbackRate=fallback/n,operationReduction=logical?1-physical/logical:0,enabled=fallbackRate<=0.125&&operationReduction>=0.5;const out={enabled,reason:enabled?'probe-pass':'probe-cost',samples:n,fallback,fallbackRate,logicalSteps:logical,physicalSteps:physical,operationReduction,gpuMs,unknown,tableBytes:bc.tableBytes};runtime.blaFastProbeRuns++;runtime.blaFastProbeLatest=out;updateStats();return out; + }finally{if(mapped)try{readB.unmap()}catch{};[pbuf,ptsB,outB,readB].forEach(destroy)} + } + encodeBlaFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.blaFastFramePipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:this.blaFastCtx.tableB}},{binding:3,resource:{buffer:this.blaFastCtx.levelsB}},{binding:4,resource:{buffer:meta}},{binding:5,resource:{buffer:smooth}},{binding:6,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass({label:'adaptive-bla-fast'});pass.setPipeline(this.blaFastFramePipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} + async ensureBlaGpuCanaryPipelines(){ + if(this.blaGpuCanaryPipeline&&this.blaGpuCanaryApplyPipeline)return{compare:this.blaGpuCanaryPipeline,apply:this.blaGpuCanaryApplyPipeline}; + if(this.blaGpuCanaryDisabled||!this.blaGpuGate.armed)return null; + if(this.blaGpuCanaryCompilePromise)return this.blaGpuCanaryCompilePromise; + this.blaGpuCanaryCompilePromise=(async()=>{ + let scope=false; + try{ + if(this.device.pushErrorScope){this.device.pushErrorScope('validation');scope=true} + const [cm,am]=await Promise.all([this.module('bla-gpu-canary-compare',G.BLA_GPU_CANARY_WGSL),this.module('bla-gpu-canary-apply',G.BLA_GPU_CANARY_APPLY_WGSL)]); + const compare=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}}),apply=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}}); + if(scope&&this.device.popErrorScope){const e=await this.device.popErrorScope();scope=false;if(e)throw e} + this.blaGpuCanaryPipeline=compare;this.blaGpuCanaryApplyPipeline=apply;return{compare,apply}; + }catch(e){ + try{if(scope&&this.device.popErrorScope)await this.device.popErrorScope()}catch{} + this.blaGpuCanaryDisabled=true;this.resetBlaGpuGate('canary-compile-failed');runtime.blaGpuCanaryFailures++;runtime.blaGpuCanaryLatest={gateArmed:false,compileFailed:true,error:String(e&&e.message||e)};console.warn('BLA GPU canary disabled:',e);updateStats();return null; + }finally{this.blaGpuCanaryCompilePromise=null} + })(); + return this.blaGpuCanaryCompilePromise; + } + async ensureBlaGpuShadowPipeline(){ + if(this.blaGpuShadowPipeline)return this.blaGpuShadowPipeline; + if(this.blaGpuShadowDisabled)return null; + if(this.blaGpuShadowCompilePromise)return this.blaGpuShadowCompilePromise; + this.blaGpuShadowCompilePromise=(async()=>{ + try{ + this.device.pushErrorScope?.('validation'); + const m=await this.module('bla-gpu-shadow',G.BLA_GPU_SHADOW_WGSL); + const pipe=this.device.createComputePipeline({layout:'auto',compute:{module:m,entryPoint:'main'}}); + if(this.device.popErrorScope){const e=await this.device.popErrorScope();if(e)throw e} + this.blaGpuShadowPipeline=pipe;return pipe; + }catch(e){ + try{if(this.device.popErrorScope)await this.device.popErrorScope()}catch{} + this.blaGpuShadowDisabled=true;runtime.blaGpuShadowFailures++; + runtime.blaGpuShadowLatest={gatePassed:false,compileFailed:true,error:String(e&&e.message||e),productionEligible:false}; + console.warn('BLA GPU shadow disabled:',e);updateStats();return null; + }finally{this.blaGpuShadowCompilePromise=null} + })(); + return this.blaGpuShadowCompilePromise; + } + async runBlaGpuShadow({shadow,ctx,snap,iter,refPixel,safety=1/64}){ + await this.ready; + if(!shadow?.stats?.gatePassed||!shadow?.stats?.gpuShadowEligible)return{skipped:true,reason:'cpu-gate'}; + if(!ctx?.refs||ctx.refLen>>0;levelData[2*i+1]=shadow.levelCounts[i]>>>0;} + const B=GPUBufferUsage,d=this.device,resultStride=64,resultBytes=resultStride*sampleCount; + const pbuf=buf(d,48,B.UNIFORM|B.COPY_DST,'bla-shadow-params'),refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-reference'),tableB=buf(d,shadow.table.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-table'),levelsB=buf(d,levelData.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-levels'),samplesB=buf(d,sampleData.byteLength,B.STORAGE|B.COPY_DST,'bla-shadow-samples'),resultsB=buf(d,resultBytes,B.STORAGE|B.COPY_SRC,'bla-shadow-results'),readB=buf(d,resultBytes,B.COPY_DST|B.MAP_READ,'bla-shadow-readback'); + const se=spanMantExp(snap),mantHi=Math.fround(se.mant),params=writeU32F32(48,dv=>{dv.setUint32(0,iter,true);dv.setUint32(4,ctx.refLen,true);dv.setUint32(8,sampleCount,true);dv.setUint32(12,levelCount,true);dv.setFloat32(16,canvas.width,true);dv.setFloat32(20,Math.fround(refPixel.x),true);dv.setFloat32(24,Math.fround(refPixel.y),true);dv.setFloat32(28,mantHi,true);dv.setInt32(32,se.exp,true);dv.setFloat32(36,Math.fround(safety),true)}); + let mapped=false,scopePushed=false; + try{ + d.queue.writeBuffer(pbuf,0,params);d.queue.writeBuffer(refsB,0,ctx.refs);d.queue.writeBuffer(tableB,0,shadow.table);d.queue.writeBuffer(levelsB,0,levelData);d.queue.writeBuffer(samplesB,0,sampleData); + if(d.pushErrorScope){d.pushErrorScope('validation');scopePushed=true;}this.activeGpuStage='bla-shadow-comparison'; + const bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:refsB}},{binding:2,resource:{buffer:tableB}},{binding:3,resource:{buffer:levelsB}},{binding:4,resource:{buffer:samplesB}},{binding:5,resource:{buffer:resultsB}}]}),enc=d.createCommandEncoder({label:'bla-shadow-comparison'}),pass=enc.beginComputePass({label:'bla-shadow-comparison'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(sampleCount/64));pass.end();enc.copyBufferToBuffer(resultsB,0,readB,0,resultBytes);d.queue.submit([enc.finish()]);await d.queue.onSubmittedWorkDone();this.lastCompletedGpuStage='bla-shadow-comparison';this.activeGpuStage=''; + if(scopePushed&&d.popErrorScope){const e=await d.popErrorScope();scopePushed=false;if(e)throw e} + await readB.mapAsync(GPUMapMode.READ);mapped=true;const raw=new Uint32Array(readB.getMappedRange().slice(0)); + let classMismatch=0,escapeIterationMismatch=0,exactMismatch=0,blaDerivedUnknown=0,internalFailure=0,baseSteps=0,blaSteps=0,apps=0,skipped=0,rejected=0,replays=0,replaySteps=0,maxSkip=0; + const rows=[]; + for(let i=0;i>>0} + const levelCount=Math.min(20,shadow.levelOffsets?.length||0),levelData=new Uint32Array(Math.max(1,levelCount)*2); + for(let i=0;i>>0;levelData[2*i+1]=shadow.levelCounts[i]>>>0} + const d=this.device,B=GPUBufferUsage,statsBytes=64; + const pbuf=buf(d,48,B.UNIFORM|B.COPY_DST,'bla-canary-params'),refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-reference'),tableB=buf(d,shadow.table.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-table'),levelsB=buf(d,levelData.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-levels'),samplesB=buf(d,sampleData.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-samples'),indicesB=buf(d,indexData.byteLength,B.STORAGE|B.COPY_DST,'bla-canary-indices'),candidateB=buf(d,sampleCount*4,B.STORAGE|B.COPY_SRC,'bla-canary-candidate-meta'),statsB=buf(d,statsBytes,B.STORAGE|B.COPY_SRC|B.COPY_DST,'bla-canary-stats'),readB=buf(d,statsBytes,B.COPY_DST|B.MAP_READ,'bla-canary-readback'),applyP=buf(d,16,B.UNIFORM|B.COPY_DST,'bla-canary-apply-params'); + const se=spanMantExp(snap),mantHi=Math.fround(se.mant),params=writeU32F32(48,dv=>{dv.setUint32(0,iter,true);dv.setUint32(4,ctx.refLen,true);dv.setUint32(8,sampleCount,true);dv.setUint32(12,levelCount,true);dv.setFloat32(16,f.w,true);dv.setFloat32(20,Math.fround(refPixel.x),true);dv.setFloat32(24,Math.fround(refPixel.y),true);dv.setFloat32(28,mantHi,true);dv.setInt32(32,se.exp,true);dv.setFloat32(36,Math.fround(safety),true)}); + let mapped=false,scope=false; + try{ + d.queue.writeBuffer(pbuf,0,params);d.queue.writeBuffer(refsB,0,ctx.refs);d.queue.writeBuffer(tableB,0,shadow.table);d.queue.writeBuffer(levelsB,0,levelData);d.queue.writeBuffer(samplesB,0,sampleData);d.queue.writeBuffer(indicesB,0,indexData);d.queue.writeBuffer(statsB,0,new Uint32Array(statsBytes/4)); + if(d.pushErrorScope){d.pushErrorScope('validation');scope=true}this.activeGpuStage='bla-canary-compare'; + const bg=d.createBindGroup({layout:pipes.compare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:refsB}},{binding:2,resource:{buffer:tableB}},{binding:3,resource:{buffer:levelsB}},{binding:4,resource:{buffer:samplesB}},{binding:5,resource:{buffer:candidateB}},{binding:6,resource:{buffer:indicesB}},{binding:7,resource:{buffer:f.meta}},{binding:8,resource:{buffer:statsB}}]}),enc=d.createCommandEncoder({label:'bla-canary-compare'}),pass=enc.beginComputePass({label:'bla-canary-compare'});pass.setPipeline(pipes.compare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(sampleCount/64));pass.end();enc.copyBufferToBuffer(statsB,0,readB,0,statsBytes);d.queue.submit([enc.finish()]);await d.queue.onSubmittedWorkDone();this.lastCompletedGpuStage='bla-canary-compare';this.activeGpuStage=''; + if(scope&&d.popErrorScope){const e=await d.popErrorScope();scope=false;if(e)throw e} + await readB.mapAsync(GPUMapMode.READ);mapped=true;const raw=new Uint32Array(readB.getMappedRange().slice(0));readB.unmap();mapped=false; + const stats={samples:sampleCount,checked:raw[0]||0,exactMismatch:raw[1]||0,blaDerivedUnknown:raw[2]||0,internalFailure:raw[3]||0,blaApplications:raw[4]||0,blaSkippedIterations:raw[5]||0,blaRejected:raw[6]||0,blaReplays:raw[7]||0,replaySteps:raw[8]||0,maxSkip:raw[9]||0,gateArmed:true,applied:false,metaOnly:true,productionEligible:false}; + stats.gatePassed=stats.checked===sampleCount&&stats.exactMismatch===0&&stats.blaDerivedUnknown===0&&stats.internalFailure===0; + runtime.blaGpuCanaryRuns++;runtime.blaGpuCanaryLatest=stats; + if(!stats.gatePassed){this.resetBlaGpuGate('canary-mismatch');stats.gateArmed=false;runtime.blaGpuCanaryLatest=stats;runtime.blaGpuCanaryFailures++;updateStats();return stats} + if(token!==state.token||state.rendering||state.dirty||!sameViewSnapshot(snap,snapshot())||this.frame!==f){stats.skippedApply='stale-view';updateStats();return stats} + // Only now may the candidate metadata touch the production field. Every + // value was compared bit-for-bit against that same fieldMeta first, so + // this write is semantically idempotent. smooth/color remain baseline. + d.queue.writeBuffer(applyP,0,new Uint32Array([sampleCount,0,0,0])); + if(d.pushErrorScope){d.pushErrorScope('validation');scope=true}this.activeGpuStage='bla-canary-apply'; + const abg=d.createBindGroup({layout:pipes.apply.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:applyP}},{binding:1,resource:{buffer:indicesB}},{binding:2,resource:{buffer:candidateB}},{binding:3,resource:{buffer:f.meta}}]}),ae=d.createCommandEncoder({label:'bla-canary-apply'}),ap=ae.beginComputePass({label:'bla-canary-apply'});ap.setPipeline(pipes.apply);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(sampleCount/64));ap.end();d.queue.submit([ae.finish()]);await d.queue.onSubmittedWorkDone();this.lastCompletedGpuStage='bla-canary-apply';this.activeGpuStage=''; + if(scope&&d.popErrorScope){const e=await d.popErrorScope();scope=false;if(e)throw e} + stats.applied=true;runtime.blaGpuCanaryApplied+=sampleCount;runtime.blaGpuCanaryLatest=stats;updateStats();return stats; + }catch(e){ + this.activeGpuStage='';try{if(scope&&d.popErrorScope)await d.popErrorScope()}catch{};this.resetBlaGpuGate('canary-runtime-error');runtime.blaGpuCanaryFailures++;const stats={gatePassed:false,gateArmed:false,error:String(e&&e.message||e),productionEligible:false};runtime.blaGpuCanaryLatest=stats;console.warn('BLA GPU canary failed:',e);updateStats();return stats; + }finally{if(mapped)try{readB.unmap()}catch{};for(const x of [pbuf,refsB,tableB,levelsB,samplesB,indicesB,candidateB,statsB,readB,applyP])destroy(x)} + } + async initPipelines(){ this.device.pushErrorScope?.('validation'); try{ - const [dm,fm,xm,xhm,xpm,xsm,zm,zqm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]); + const [dm,dasm,dadm,fm,xm,xhm,xpm,xsm,zm,zqm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('accurate-direct-seed',G.DIRECT_ACCURATE_SEED_WGSL),this.module('accurate-direct-ds',G.DIRECT_DS_CORRECT_WGSL),this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]); this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}}); + this.accurateDirectSeed=this.device.createComputePipeline({layout:'auto',compute:{module:dasm,entryPoint:'main'}}); + this.accurateDirectDs=this.device.createComputePipeline({layout:'auto',compute:{module:dadm,entryPoint:'main'}}); this.fast=this.device.createComputePipeline({layout:'auto',compute:{module:fm,entryPoint:'main'}}); this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}}); const [xpsm,xhsm]=await Promise.all([this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL),this.module('deep-bucket-histogram-stats-production',G.DEEP_BUCKET_HIST_STATS_WGSL)]); @@ -982,18 +1830,16 @@ class WebGpuRenderer{ 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(); + this.configure(); }finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}} } - async ensureRefinePipelines(){if(this.likelyQueue&&this.deepLikelyRefine)return;if(this.refinePipelinePromise)return this.refinePipelinePromise;this.refinePipelinePromise=(async()=>{const [lqm,drm]=await Promise.all([this.module('likely-refine-queue',G.LIKELY_QUEUE_WGSL),this.module('deep-likely-refine',G.DEEP_LIKELY_REFINE_WGSL)]);this.likelyQueue=this.device.createComputePipeline({layout:'auto',compute:{module:lqm,entryPoint:'main'}});this.deepLikelyRefine=this.device.createComputePipeline({layout:'auto',compute:{module:drm,entryPoint:'refine_likely'}})})().finally(()=>{this.refinePipelinePromise=null});return this.refinePipelinePromise} - frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','refineQueue','refineStats','refineQueueParams','refineBatchParams','refineRead','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null} + frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','numericParams','colorParams','presentParams','front','back','history'])destroy(this.frame[k]);this.frame=null;this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false} ensureFrame(w,h){ const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage; - this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,refineQueue:null,refineStats:null,refineQueueParams:null,refineBatchParams:null,refineRead:null,numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame; + this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,48,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'}),history:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.TEXTURE_BINDING|T.COPY_DST,label:'stable-history'})};return this.frame; } ensureDeepBucketState(f=this.frame){if(!f)return null;if(!f.deepBucketState){const d=this.device,B=GPUBufferUsage;f.deepBucketState=buf(d,DEEP_BUCKET_STATE_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-bucket-state')}return f} ensureDeepQueueWorkspace(f=this.frame){if(!f)return null;this.ensureDeepBucketState(f);if(f.deepQueue&&f.deepQueueStats&&f.deepIndirect)return f;const d=this.device,B=GPUBufferUsage;f.deepQueueStats=buf(d,SPARSE_QUEUE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue-stats');f.deepQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue');f.deepIndirect=buf(d,SPARSE_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'deep-correction-indirect');return f} - ensureRefineWorkspace(f=this.frame){if(!f)return null;if(f.refineQueue&&f.refineStats&&f.refineQueueParams&&f.refineBatchParams&&f.refineRead)return f;const d=this.device,B=GPUBufferUsage;f.refineQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-queue');f.refineStats=buf(d,REFINE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-stats');f.refineQueueParams=buf(d,REFINE_QUEUE_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-queue-params');f.refineBatchParams=buf(d,REFINE_BATCH_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-batch-params');f.refineRead=buf(d,REFINE_STATS_BYTES,B.COPY_DST|B.MAP_READ,'likely-refine-readback');return f} ensureExportWorkspace(){ if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S; this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs; @@ -1001,17 +1847,18 @@ class WebGpuRenderer{ exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null} setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}} destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null} - setRefineDeepContext(ctx){if(this.refineDeepCtx&&this.refineDeepCtx.key===ctx.key)return;this.destroyRefineDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'idle-refinement-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.refineDeepCtx={...ctx,refsB}} - destroyRefineDeepContext(){if(this.refineDeepCtx)destroy(this.refineDeepCtx.refsB);this.refineDeepCtx=null} setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}} - destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null} + destroyFastContext(){this.destroyBlaFastContext();if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null} directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})} + accurateDirectParams(w,h,fullW,fullH,tileX,tileY,iter,snap,outputStride=fullW,outputBase=0,lateThreshold=24,sx=.5,sy=.5){const re=fixedNum(snap.re,snap.bits),im=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),inv=1/Math.max(1,fullW),reHi=Math.fround(re),imHi=Math.fround(im),spHi=Math.fround(sp),invHi=Math.fround(inv);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,lateThreshold,outputStride,outputBase,0,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,reHi,true);d.setFloat32(52,Math.fround(re-reHi),true);d.setFloat32(56,imHi,true);d.setFloat32(60,Math.fround(im-imHi),true);d.setFloat32(64,spHi,true);d.setFloat32(68,Math.fround(sp-spHi),true);d.setFloat32(72,invHi,true);d.setFloat32(76,Math.fround(inv-invHi),true);d.setFloat32(80,sx,true);d.setFloat32(84,sy,true)})} deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,0,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - colorParamsData(w,h,iter=state.fieldView?.iter??maxIter()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,effectiveColorCycle(iter),true);d.setFloat32(20,state.shift,true)})} + fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0,unknownOnly=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} + colorParamsData(w,h,_iter=state.fieldView?.iter??maxIter(),style=colorStyleSnapshot()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,style.palette,true);d.setUint32(12,style.hq?1:0,true);d.setFloat32(16,style.cycle,true);d.setFloat32(20,style.shift,true)})} encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,pipeline=this.deepPostStats,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} + encodeAccurateDirectSeed(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.accurateDirectSeed.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass({label:'accurate-direct-seed'});pass.setPipeline(this.accurateDirectSeed);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} + encodeAccurateDirectDs(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.accurateDirectDs.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass({label:'accurate-direct-ds'});pass.setPipeline(this.accurateDirectDs);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.fast.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fast);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHist.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHist);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHistStats.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} @@ -1019,25 +1866,139 @@ class WebGpuRenderer{ encodeDeepBucketScatter(encoder,{pbuf,meta,bucketState,queueStats,queue,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} encodeQueuedDeepCorrection(encoder,{pbuf,meta,smooth,unresolved,queueStats,queue,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.correctQueued.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{binding:5,resource:{buffer:queueStats}},{binding:6,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.correctQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()} encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})} - async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null){ - await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep){this.setDeepContext(deepContext);this.ensureDeepBucketState(f)}else if(fastExtended){this.setFastContext(fastContext)}d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep)d.queue.writeBuffer(f.deepBucketState,0,new Uint32Array(DEEP_BUCKET_STATE_BYTES/4)); - const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;const params=deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,refX,refY):fastExtended?this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap);d.queue.writeBuffer(f.numericParams,0,params); - d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'}); - if(deep){this.encodeDeepPostStatsNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeDeepBucketHistogramStats(encoder,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,unresolved:f.unresolved,w:f.w,h:f.h});} - else if(fastExtended){this.encodeFastNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});} - else{this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});} - if(!deep){const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();} - d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;if(!deep)[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true; + initialNumericRows(w,iter,deep){ + // Structural workload cap, not a zoom threshold. The first submission is + // intentionally conservative; subsequent strips self-tune from measured + // queue completion time. This keeps one GPU submission bounded even when + // maxIter reaches 12000. + const workBudget=deep?24000000:48000000; + return Math.max(1,Math.min(128,Math.floor(workBudget/Math.max(1,w*iter)))); } - async buildLikelyRefineQueue(token,mode=1,sparseStride=4){await this.ready;await this.ensureRefinePipelines();if(token!==state.token)return{count:0,overflow:0,cancelled:true};const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;d.queue.writeBuffer(f.refineStats,0,new Uint32Array(REFINE_STATS_BYTES/4));d.queue.writeBuffer(f.refineQueueParams,0,new Uint32Array([f.w,f.h,mode>>>0,sparseStride>>>0]));const e=d.createCommandEncoder({label:'likely-refine-queue-build'}),bg=d.createBindGroup({layout:this.likelyQueue.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.refineQueueParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.refineQueue}},{binding:3,resource:{buffer:f.refineStats}}]}),pass=e.beginComputePass();pass.setPipeline(this.likelyQueue);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.n/256));pass.end();e.copyBufferToBuffer(f.refineStats,0,f.refineRead,0,REFINE_STATS_BYTES);d.queue.submit([e.finish()]);await f.refineRead.mapAsync(GPUMapMode.READ);const a=new Uint32Array(f.refineRead.getMappedRange().slice(0));f.refineRead.unmap();if(token!==state.token)return{count:0,overflow:0,cancelled:true};return{count:Math.min(f.n,a[0]||0),overflow:a[1]||0,cancelled:false}} - async refineLikelyBatch({snap,iter,deepContext,token,base,end,forceStrict=false}){await this.ready;await this.ensureRefinePipelines();if(token!==state.token||base>=end)return false;const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setRefineDeepContext(deepContext);const ref=referencePixelForSource(deepContext?.source,snap,f.w,f.h);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,ref.x,ref.y,0,f.w,0));d.queue.writeBuffer(f.refineBatchParams,0,new Uint32Array([base>>>0,end>>>0,0,0]));const pipeline=this.deepLikelyRefine,bg0=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.refineDeepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),bg1=d.createBindGroup({layout:pipeline.getBindGroupLayout(1),entries:[{binding:0,resource:{buffer:f.refineQueue}},{binding:1,resource:{buffer:f.refineBatchParams}}]}),e=d.createCommandEncoder({label:'likely-idle-refine'}),pass=e.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg0);pass.setBindGroup(1,bg1);pass.dispatchWorkgroups(Math.ceil((end-base)/64));pass.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.idleRefineBatches++;return true} - async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true} + adaptNumericRows(rows,elapsedMs,remaining,deep){ + if(remaining<=0)return rows; + // Never grow beyond the initial work-bound. Timing feedback may only + // reduce a strip if this adapter is slower than expected, so the + // worst-case pixel-iteration cap remains invariant across the frame. + const targetMs=deep?18:22,ms=Math.max(.25,Number(elapsedMs)||targetMs); + if(ms<=targetMs*1.25)return Math.max(1,Math.min(rows,remaining)); + const factor=Math.max(.5,Math.min(1,Math.sqrt(targetMs/ms))); + return Math.max(1,Math.min(rows,remaining,Math.round(rows*factor))); + } + async submitStage(encoder,stage,token,counter='numeric'){ + this.activeGpuStage=stage;state.gpuStage=stage; + const t0=performance.now();this.device.queue.submit([encoder.finish()]); + await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++; + const ms=performance.now()-t0;this.lastCompletedGpuStage=stage;this.activeGpuStage=''; + if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++; + return token===undefined||token===state.token?ms:null; + } + submitStageDeferred(encoder,stage,counter='numeric'){this.activeGpuStage=stage;state.gpuStage=stage;this.device.queue.submit([encoder.finish()]);if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++;return stage} + async waitStageBurst(stage,token,t0,count=1){await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++;const ms=(performance.now()-t0)/Math.max(1,count);this.lastCompletedGpuStage=stage;this.activeGpuStage='';return token===undefined||token===state.token?ms:null} + async computePerturbFrameTiled(snap,iter,deep,token,forceStrict,referencePixel,fastExtended,colorStyle,colorKey,blaPlan=null){ + const f=this.ensureFrame(canvas.width,canvas.height),d=this.device; + d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4)); + const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,useBla=!deep&&!!blaPlan?.enabled&&!!this.blaFastFramePipeline&&!!this.blaFastCtx; + let rows=this.initialNumericRows(f.w,iter,deep),lastUi=0,burstCount=0,burstStart=performance.now(),lastStage='',burstLimit=deep?2:(useBla?8:4); + // When the distributed probe reports a very clean BLA view, allow only a + // modest 2x strip growth. Even a pathological local fallback therefore + // stays far below the old unbounded full-frame dispatch. + if(useBla&&blaPlan.operationReduction>=.75&&blaPlan.fallbackRate<=.0625)rows=Math.min(256,rows*2); + for(let y=0;y=burstLimit||y>=f.h){ + const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false; + rows=this.adaptNumericRows(rows,ms,f.h-y,deep);burstCount=0;burstStart=performance.now(); + const now=performance.now();if(now-lastUi>100){lastUi=now;state.lastEngine='WebGPU · '+(deep?'正確':useBla?'高速BLA':'高速拡張')+' 計算 '+Math.round(100*y/f.h)+'%';updateStats()} + } + } + if(!deep){ + // Coverage contract: zero metadata means a pixel was never written by + // the tiled primary path. Re-run only those pixels through the original + // FAST recurrence before colouring. This pass is O(pixels) for the + // trivial guard but performs iterations only for actual coverage holes. + d.queue.writeBuffer(f.numericParams,0,this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY,f.w,0,2)); + d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle)); + const e=d.createCommandEncoder({label:'fast-coverage-repair-and-color'}); + this.encodeFastNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h}); + 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=e.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end(); + const ms=await this.submitStage(e,'fast coverage + color',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey; + } + if(!deep){if(useBla)runtime.blaFastEnabledFrames++;else runtime.blaFastBaselineFrames++;} + runtime.gpuFrames++;return true; + } + async computeAccurateDirectHybrid(snap,iter,token,colorStyle,colorKey){ + const f=this.ensureFrame(canvas.width,canvas.height),d=this.device; + // Pass 1: cheap f32 seed. Analytically proven main-cardioid/period-2 pixels + // are final, and only very early escapes (<24) are trusted. Everything + // else remains UNKNOWN for the DS pass below. + d.queue.writeBuffer(f.numericParams,0,this.accurateDirectParams(f.w,f.h,f.w,f.h,0,0,iter,snap,f.w,0,24)); + let e=d.createCommandEncoder({label:'accurate-direct-seed-frame'});this.encodeAccurateDirectSeed(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h}); + this.submitStageDeferred(e,'accurate direct seed','numeric'); + // Pass 2: recompute only UNKNOWN pixels from iteration zero in double-single. + // Each command buffer remains work-bounded, but several are queued before + // one CPU/GPU synchronization to avoid the latency regression of v24.2.53. + let rows=this.initialNumericRows(f.w,iter,true),y=0,burstCount=0,burstStart=performance.now(),lastStage='accurate direct seed',lastUi=0; + while(y=2||y>=f.h){const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;rows=this.adaptNumericRows(rows,ms,f.h-y,true);burstCount=0;burstStart=performance.now();const now=performance.now();if(now-lastUi>100){lastUi=now;state.lastEngine='WebGPU · 正確DS '+Math.round(100*y/f.h)+'%';updateStats()}} + } + d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));e=d.createCommandEncoder({label:'accurate-direct-color'});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=e.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();const cms=await this.submitStage(e,'accurate direct color',token,'numeric');if(cms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuFrames++;return true; + } + async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null,blaPlan=null,accurateHybrid=false){ + await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle); + if(accurateHybrid)return this.computeAccurateDirectHybrid(snap,iter,token,colorStyle,colorKey); + if(deep){this.setDeepContext(deepContext);return this.computePerturbFrameTiled(snap,iter,true,token,forceStrict,referencePixel,false,colorStyle,colorKey)} + if(fastExtended){this.setFastContext(fastContext);return this.computePerturbFrameTiled(snap,iter,false,token,false,referencePixel,true,colorStyle,colorKey,blaPlan)} + // Direct f32 remains the exact v24.2.42 single-pass path. It is only used + // while absolute f32 pixel coordinates retain sufficient separation. + d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4)); + d.queue.writeBuffer(f.numericParams,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,iter,colorStyle));const encoder=d.createCommandEncoder({label:'mandelbrot-direct-frame'}); + this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h}); + 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(); + const ms=await this.submitStage(encoder,'direct primary',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuFrames++;return true; + } + async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));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];this.frontColorKey=colorKey;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()])} + commitHistory(view=state.frameView){if(!this.frame||!view)return false;const f=this.frame,d=this.device,e=d.createCommandEncoder({label:'commit-stable-history'});e.copyTextureToTexture({texture:f.front},{texture:f.history},[f.w,f.h]);d.queue.submit([e.finish()]);this.historyView=view;this.historyColorKey=this.frontColorKey;this.historyReady=true;return true} + invalidateHistory(){this.historyView=null;this.historyColorKey='';this.historyReady=false} + presentFrame(transform=this.presentTransform()){if(!this.frame)return;const f=this.frame,d=this.device,pb=f.presentParams,interactive=state.pointerActive||state.wheelActive,finite=Number.isFinite(transform?.scaleX)&&Number.isFinite(transform?.scaleY)&&Number.isFinite(transform?.offsetX)&&Number.isFinite(transform?.offsetY)&&transform.scaleX>0&&transform.scaleY>0,ft=finite?transform:{scaleX:1,scaleY:1,offsetX:0,offsetY:0},historyCandidate=!!(state.temporalFill&&this.historyReady&&this.historyView&&this.historyColorKey===colorStyleKey()),rawHistory=historyCandidate?this.presentTransform(this.historyView):{scaleX:1,scaleY:1,offsetX:0,offsetY:0},useHistory=!!(historyCandidate&&reprojectionSafe(rawHistory,2.25,.45)),ht=useHistory?rawHistory:{scaleX:1,scaleY:1,offsetX:0,offsetY:0};d.queue.writeBuffer(pb,0,new Float32Array([ft.scaleX,ft.scaleY,ft.offsetX,ft.offsetY,ht.scaleX,ht.scaleY,ht.offsetX,ht.offsetY,useHistory?1:0,0,0,0]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:interactive?this.reprojectSampler:this.sampler},{binding:1,resource:f.front.createView()},{binding:2,resource:f.history.createView()},{binding:3,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:0,g:0,b:0,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])} async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i=Math.max(32,Math.floor(n*.0005));return true} +function numericalFailureCount(stats){const r=stats&&stats.reasons||{};return (r.errorBound||0)+(r.escapeUncertain||0)+(r.referenceEnd||0)+(r.rebaseGap||0)+(r.range||0)} +function mergeCorrectionStats(before,after){const br=before?.reasons||{},ar=after?.reasons||{},numericBefore=numericalFailureCount(before),nonNumeric=Math.max(0,(before?.total||0)-numericBefore),reasons={...ar,operationLimit:(br.operationLimit||0)+(ar.operationLimit||0)};return{total:nonNumeric+(after?.total||0),corrected:after?.corrected||0,reasons}} +// A broad failure is cheaper to normalize once with the full-frame Deep pass; +// sparse/moderate failures go directly to local references. +function shouldPromoteFastToDeep(stats,n){const failed=numericalFailureCount(stats);return failed>=Math.max(60000,Math.floor(n*.08))} +function tileRecoveryConfig(){const z=zoomExp(),deep=z>=32;if(state.processMode==='power')return{size:deep?224:320,maxRefs:4,minFailed:deep?4:48};if(state.processMode==='fine')return{size:deep?128:192,maxRefs:20,minFailed:deep?1:8};if(state.processMode==='validate')return{size:deep?112:160,maxRefs:32,minFailed:1};return{size:deep?160:224,maxRefs:12,minFailed:deep?1:12}} +const REASON_KEYS=['','errorBound','escapeUncertain','referenceEnd','rebaseGap','range','operationLimit']; +function summarizeUnknownMeta(meta){const reasons={errorBound:0,escapeUncertain:0,referenceEnd:0,rebaseGap:0,range:0,operationLimit:0};let total=0;for(const m of meta){if(((m>>>28)&3)!==FIELD_UNKNOWN)continue;total++;const reason=(m>>>REASON_SHIFT)&0xff,k=REASON_KEYS[reason];if(k)reasons[k]++}return{total,corrected:0,reasons}} +function buildNumericalFailureTiles(meta,w,h,size=tileRecoveryConfig().size){const cols=Math.ceil(w/size),rows=Math.ceil(h/size),tiles=Array.from({length:cols*rows},(_,i)=>{const tx=i%cols,ty=(i/cols)|0,x=tx*size,y=ty*size;return{x,y,w:Math.min(size,w-x),h:Math.min(size,h-y),numeric:0,sumX:0,sumY:0,minX:w,minY:h,maxX:-1,maxY:-1,reasons:{errorBound:0,escapeUncertain:0,referenceEnd:0,rebaseGap:0,range:0}}});for(let y=0;y>>28)&3;if(cls!==FIELD_UNKNOWN)continue;const reason=(m>>>REASON_SHIFT)&0xff;if(reason<1||reason>5)continue;const t=tiles[ty*cols+((x/size)|0)],k=REASON_KEYS[reason];t.numeric++;t.sumX+=x+.5;t.sumY+=y+.5;t.minX=Math.min(t.minX,x);t.maxX=Math.max(t.maxX,x);t.minY=Math.min(t.minY,y);t.maxY=Math.max(t.maxY,y);t.reasons[k]++}}return tiles.filter(t=>t.numeric>0).sort((a,b)=>b.numeric-a.numeric)} +function tileReferenceSnapshot(snap,tile,fullW,fullH){const cx=tile.numeric?tile.sumX/tile.numeric:tile.x+tile.w*.5,cy=tile.numeric?tile.sumY/tile.numeric:tile.y+tile.h*.5,bw=Math.max(1,tile.maxX-tile.minX+1),bh=Math.max(1,tile.maxY-tile.minY+1),refW=Math.max(8,Math.min(tile.w,Math.ceil(bw*1.75))),refH=Math.max(8,Math.min(tile.h,Math.ceil(bh*1.75))),den=2n*BigInt(fullW),nx=BigInt(Math.round(2*cx-fullW)),ny=BigInt(Math.round(fullH-2*cy)),re=snap.re+roundDivSigned(snap.span*nx,den),im=snap.im+roundDivSigned(snap.span*ny,den),localSpan=(()=>{const q=roundDivSigned(snap.span*BigInt(refW),BigInt(fullW));return q>0n?q:1n})();return{snap:{bits:snap.bits,re,im,span:localSpan},width:refW,height:refH}} +async function recoverNumericalUnknownTiles(r,snap,iter,token,initialStats){const initialFailed=numericalFailureCount(initialStats),cfg=tileRecoveryConfig();if(initialFailed0;round++){const meta=await r.readMetaAll();if(token!==state.token)return null;stats=summarizeUnknownMeta(meta);const failed=numericalFailureCount(stats);if(failed=roundCap||budget<=0)break;const local=tileReferenceSnapshot(snap,tile,canvas.width,canvas.height);state.lastEngine='WebGPU · 局所reference '+(passes+1)+'/'+cfg.maxRefs+' · '+tile.numeric+' px';updateStats();const ctx=await refs.request(local.snap,iter,local.width,local.height);if(token!==state.token)return null;if(ctx.checkpointMismatch)throw new Error('tile reference guard checkpoint mismatch');const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height),next=await r.recoverUnknownTile(snap,iter,token,ctx,tile,refPixel);if(!next||token!==state.token)return null;attempted+=tile.numeric;passes++;used++;budget--;state.multiRefPasses=passes;state.correctedPixels+=next.corrected||0}if(!used)break}if(passes){const meta=await r.readMetaAll();if(token!==state.token)return null;stats=summarizeUnknownMeta(meta)}const recovered=Math.max(0,initialFailed-numericalFailureCount(stats));state.multiRefRecovered=recovered;state.unresolved=stats.total;state.unknownReasons=stats.reasons;return{stats,passes,recovered,attempted}} + +function scheduleFastNumericRecovery(r,snap,iter,token,initialStats,initialRefPixel){ + if(numericalFailureCount(initialStats)<=0)return false; + if(numericRecoveryTimer){clearTimeout(numericRecoveryTimer);numericRecoveryTimer=0;} + state.numericRecoveryPending=true; + // The primary FAST image is already visible. Give input/recolour handlers a + // short turn, then repair only if the view is still current. + numericRecoveryTimer=setTimeout(()=>{numericRecoveryTimer=0;runFastNumericRecovery(r,snap,iter,token,initialStats,initialRefPixel);},60); + return true; +} +async function runFastNumericRecovery(r,snap,iter,token,initialStats,initialRefPixel){ + if(token!==state.token||renderer!==r||state.dirty||!sameViewSnapshot(snap,snapshot())){state.numericRecoveryPending=false;return;} + state.numericRecoveryPending=false;state.numericRecoveryRunning=true;state.drawState='REFINING'; + let stats=initialStats,referencePixel=initialRefPixel,ctx=null; + try{ + // First try the inexpensive local-reference recovery on the existing FAST + // field. It often removes isolated perturbation failures without touching + // the rest of the frame. + if(numericalFailureCount(stats)>0){ + state.lastEngine='WebGPU · 背景で数値穴を局所補修';updateStats(); + const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return;stats=mr.stats; + } + // If local references were insufficient, perform the old full Deep path + // in the background. The visible FAST/front texture is not replaced until + // Deep + correction + recolour has completed, so this can no longer create + // a black rectangle or extend the interaction-to-first-image latency. + if(numericalFailureCount(stats)>0){ + state.lastEngine='WebGPU · 背景で正確系補修';updateStats(); + const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height); + if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;} + else{ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('background reference guard checkpoint mismatch');} + const ok=await r.computeFrame(snap,iter,true,ctx,token,state.processMode==='validate',referencePixel,false,null);if(!ok||token!==state.token)return; + stats=await r.readUnresolvedStats();if(token!==state.token)return; + const n=canvas.width*canvas.height; + if(shouldApplySparseCorrection(numericalFailureCount(stats),n)){ + const before=stats,corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return;state.correctionPasses++; + stats=mergeCorrectionStats(before,await r.readUnresolvedStats());if(token!==state.token)return; + } + if(numericalFailureCount(stats)>0){const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return;stats=mr.stats;} + } + state.unresolved=stats.total;state.unknownReasons=stats.reasons; + const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return; + state.frameView=snap;state.fieldView={...state.fieldView,...snap,iter,w:canvas.width,h:canvas.height,backend:ctx?'deep-background-recovery':state.fieldView?.backend||'fast-extended',progressive:true}; + // Keep temporal fill active until every numeric UNKNOWN is gone. Max-iter + // non-escape pixels are not numeric failures and may still be black. + if(numericalFailureCount(stats)===0){r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});r.commitHistory(snap);state.temporalFill=false;state.drawState='COVERED';} + else{state.temporalFill=!!r.historyReady;r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});state.drawState='REFINED';} + }catch(e){if(token===state.token){state.gpuError='背景数値補修: '+String(e&&e.message||e);console.warn('background numeric recovery failed:',e);}} + finally{ + if(token===state.token){state.numericRecoveryRunning=false;state.numericRecoveryPending=false;updateStats();if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor);} + } } -function shouldApplySparseCorrection(total,n){if(total<=0)return false;if(state.processMode==='power')return total>=Math.max(32,Math.floor(n*.0005));return true} function referencePixelForSource(src,snap,w,h){if(!src)return{x:w*.5,y:h*.5};const b=Math.max(snap.bits,src.bits),span=align(snap.span,snap.bits,b),dr=align(src.re,src.bits,b)-align(snap.re,snap.bits,b),di=align(src.im,src.bits,b)-align(snap.im,snap.bits,b);return{x:w*.5+fixedRatio(dr,span)*w,y:h*.5-fixedRatio(di,span)*w}} function reusableDeepReference(snap,iter,w,h){const ctx=renderer&&renderer.deepCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} function reusableFastReference(snap,iter,w,h){const ctx=renderer&&renderer.fastCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} // ── GPU startup / rendering orchestration ──────────────────────────────── -async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise} -function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx} -function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span} -function cancelIdleRefinement(){if(idleRefineTimer){clearTimeout(idleRefineTimer);idleRefineTimer=0}state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0} -function idleRefinementTargets(baseIter){if(baseIter>=8000)return[];const z=zoomExp(),mode=state.processMode,mul=mode==='power'?.75:mode==='fine'?1.25:mode==='validate'?1.4:1,cap=Math.min(8000,Math.max(baseIter*2,Math.round((1200+300*z)*mul))),raw=mode==='power'?[Math.min(cap,baseIter*2)]:[Math.min(cap,baseIter*2),Math.min(cap,baseIter*4),cap],out=[];for(const v of raw){const n=Math.max(baseIter+1,Math.floor(v));if(n>baseIter&&(!out.length||n!==out[out.length-1]))out.push(n)}return out} -function idleRefinementBatchBudget(){const m=state.processMode;if(m==='power')return 900000;if(m==='fine')return 2800000;if(m==='validate')return 1800000;return 1800000} -function idleRefinementQueuePolicy(stage){if(stage<=0)return{mode:1,stride:4};if(stage===1)return{mode:2,stride:8};return{mode:3,stride:16}} -function idleRefinementSliceMs(){return state.processMode==='power'?100:state.processMode==='fine'?220:160} -function idleRefinementPauseMs(){return state.processMode==='power'?160:80} -function canIdleRefine(token,snap,baseIter){return token===state.token&&renderer&&!state.rendering&&!state.pointerActive&&!state.wheelActive&&sameSnapshot(snap,snapshot())&&state.fieldView&&state.fieldView.iter===baseIter&&state.fieldView.w===canvas.width&&state.fieldView.h===canvas.height} -const sleep=ms=>new Promise(r=>setTimeout(r,ms)); -async function paintIdleRefinement(token,snap,baseIter){while(state.recoloring&&canIdleRefine(token,snap,baseIter))await sleep(12);if(!canIdleRefine(token,snap,baseIter))return false;await recolor();while((state.recoloring||state.recolorPending)&&canIdleRefine(token,snap,baseIter))await sleep(8);return canIdleRefine(token,snap,baseIter)} -function scheduleIdleRefinement(snap,baseIter,decision,token){cancelIdleRefinement();if(!renderer||(!decision.deep&&!decision.fastExtended))return;const targets=idleRefinementTargets(baseIter);if(!targets.length)return;idleRefineTimer=setTimeout(()=>{idleRefineTimer=0;runIdleRefinement(snap,baseIter,decision,token,targets)},320)} -async function runIdleRefinement(snap,baseIter,decision,token,targets){if(!canIdleRefine(token,snap,baseIter))return;state.refinementRunning=true;state.refinedIter=baseIter;updateStats();try{for(let stage=0;stage=idleRefinementSliceMs()){const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;updateStats();lastPaint=performance.now();await sleep(idleRefinementPauseMs())}}const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;state.fieldView.refinedIter=targetIter;updateStats();await sleep(idleRefinementPauseMs())}}catch(e){if(token===state.token){console.warn('idle refinement stopped:',e);state.gpuError=String(e&&e.message||e)}}finally{if(token===state.token){state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0;updateStats()}}} - -function cancelRender(){cancelIdleRefinement();state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled')} +async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=true;state.gpuUnavailable=true;state.gpuError='WebGPU非対応 · 重いfallbackは無効';updateStats();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=true;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません · fallback無効';updateStats();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.gpuUnavailable=false;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e)+' · fallback無効';updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise} +function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;state.refinePending=false;state.numericRecoveryPending=false;state.numericRecoveryRunning=false;runtime.blaShadowLatest=null;runtime.blaGpuShadowLatest=null;if(refineTimer){clearTimeout(refineTimer);refineTimer=0}if(blaShadowTimer){clearTimeout(blaShadowTimer);blaShadowTimer=0}if(numericRecoveryTimer){clearTimeout(numericRecoveryTimer);numericRecoveryTimer=0}blaShadow.cancelPending('render cancelled');refs.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled')} function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()} function schedule(){if(!raf)raf=requestAnimationFrame(loop)} +async function renderQualityStage(r,snap,iter,token,isFinal=false){ + let decision=chooseBackend(snap,canvas.width,isFinal),deep=decision.deep,accurateHybrid=!!decision.accurateHybrid,fastExtended=decision.fastExtended; + state.backendDecision=decision;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false,blaPlan=null; + if(deep){ + const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height); + if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true} + else{state.lastEngine='WebGPU · '+(isFinal?'最終':'preview')+' 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch')} + }else if(fastExtended){ + const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height); + if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true} + else{state.lastEngine='WebGPU · '+(isFinal?'最終':'preview')+' 高速拡張 reference準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height)} + } + if(fastExtended&&isFinal&&!r.blaFastDisabled){ + if(r.blaFastCandidatePipeline&&r.blaFastFramePipeline&&fastCtx?.refLen>=iter){ + try{ + const shadow=await blaShadow.requestBuild({ctx:fastCtx,snap,iter,width:canvas.width,height:canvas.height,refPixel:referencePixel,safety:1/64});if(token!==state.token)return null; + if(shadow.stats.gatePassed&&shadow.stats.gpuShadowEligible){blaPlan=await r.runBlaFastProbe({shadow,snap,iter,refPixel:referencePixel,safety:1/64,samples:64});if(blaPlan?.enabled){blaPlan.safety=1/64;decision={...decision,backend:'fast-bla',reason:'adaptive-bla-probe-pass',probe:true};state.backendDecision=decision;}}else{runtime.blaFastProbeLatest={enabled:false,reason:'cpu-gate'};} + }catch(e){runtime.blaFastFailures++;runtime.blaFastProbeLatest={enabled:false,reason:'probe-error',error:String(e&&e.message||e)};blaPlan=null;console.warn('Adaptive FAST BLA probe skipped:',e)} + }else if(fastCtx?.refLen{if(renderer===r&&!r.blaFastDisabled)r.warmBlaFastPipelines()},250)} + } + state.lastEngine='WebGPU · '+(isFinal?'最終品質':'即時preview')+' '+(accurateHybrid?'正確DS':deep?'正確':blaPlan?.enabled?'高速BLA':fastExtended?'高速拡張':'高速');updateStats(); + let ok=await r.computeFrame(snap,iter,deep,ctx,token,isFinal&&state.processMode==='validate',referencePixel,fastExtended,fastCtx,blaPlan,accurateHybrid);if(!ok||token!==state.token)return null; + if(deep){ + if(isFinal){ + let stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0; + const n=canvas.width*canvas.height; + if(shouldApplySparseCorrection(numericalFailureCount(stats),n)){ + const beforeCorrection=stats,corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return null;state.correctionPasses=1; + stats=mergeCorrectionStats(beforeCorrection,await r.readUnresolvedStats());if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected; + } + if(numericalFailureCount(stats)>0){const mr=await recoverNumericalUnknownTiles(r,snap,iter,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;state.unresolved=stats.total;state.unknownReasons=stats.reasons;} + } + const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null; + }else if(accurateHybrid){ + // DS-direct hybrid resolves its own delicate pixels and colours the frame. + state.unresolved=0;state.unknownReasons=null; + }else if(isFinal&&fastExtended){ + // Do not block the visible frame on expensive Deep/multi-reference repair. + // Numeric UNKNOWN stays transparent and is filled from the previous stable + // frame while a same-view background repair runs. Incomplete numeric + // frames are never committed as stable history. + const stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons; + if(numericalFailureCount(stats)>0){scheduleFastNumericRecovery(r,snap,iter,token,stats,referencePixel);} + } + state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,accurateHybrid,fastExtended,backend:decision.backend,referenceReused,progressive:true}; + r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0}); + return{decision,referenceReused,shadowContext:deep?ctx:(fastExtended?fastCtx:null),referencePixel}; +} +function numericFrameComplete(){return numericalFailureCount({reasons:state.unknownReasons||{}})===0&&!state.numericRecoveryPending&&!state.numericRecoveryRunning} +function scheduleQualityRefinement(snap,token,stages,index){ + if(index>=stages.length||token!==state.token)return; + state.refinePending=true;if(refineTimer)clearTimeout(refineTimer); + refineTimer=setTimeout(()=>{refineTimer=0;runQualityRefinement(snap,token,stages,index)},refinementDelay(index)); +} +async function runQualityRefinement(snap,token,stages,index){ + if(token!==state.token||state.pointerActive||state.wheelActive||state.dirty||!sameViewSnapshot(snap,snapshot())){state.refinePending=false;return} + const r=renderer;if(!r){state.refinePending=false;return} + const iter=stages[index],isFinal=index===stages.length-1,t0=performance.now();state.rendering=true;state.refinePending=false;state.drawState='REFINING';state.qualityStage=isFinal?'final':'refining';state.qualityIter=iter;updateStats(); + try{ + const result=await renderQualityStage(r,snap,iter,token,isFinal);if(!result||token!==state.token)return; + state.refineMs=performance.now()-t0;state.rendering=false;state.drawState=isFinal?'COVERED':'REFINED';state.lastRender=state.refineMs;state.lastEngine='WebGPU · '+(isFinal?'最終品質':'中間品質')+' '+(result.decision.accurateHybrid?'正確DS':result.decision.deep?'正確':result.decision.backend==='fast-bla'?'高速BLA':result.decision.fastExtended?'高速拡張':'高速'); + if(isFinal){if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false;}else{state.temporalFill=!!r.historyReady;}r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});r.scheduleBlaFastWarmup();scheduleBlaShadowValidation(snap,iter,result.shadowContext,result.referencePixel,token,result.decision.fastExtended?'fast':result.decision.deep?'deep':'cpu');} + updateStats(); + if(!isFinal)scheduleQualityRefinement(snap,token,stages,index+1); + }catch(e){if(token!==state.token)return;state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU refinement error';updateStats();console.error(e)} +} async function renderFrame(){ - const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height); - state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats(); + const token=++state.token,snap=snapshot(),finalIter=maxIter(),holdStable=canUseStableReprojection(),stages=qualityStages(finalIter),iter=stages[0],isSingleFinal=stages.length===1,t0=performance.now(); + state.rendering=true;state.dirty=false;state.drawState=isSingleFinal&&holdStable?'REPROJECTED':'COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.multiRefPasses=0;state.multiRefRecovered=0;state.qualityStage=isSingleFinal?'final':'preview';state.qualityIter=iter;state.qualityFinalIter=finalIter;state.previewMs=0;state.refineMs=0;state.refinePending=false;runtime.renderStarts++;runtime.blaFastProbeLatest=null;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} - const deep=decision.deep,fastExtended=decision.fastExtended;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false; - if(deep){const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height);if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 正確 reference再利用'}else{state.lastEngine='WebGPU · 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · 正確 perturbation'}} - else if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 高速拡張 reference再利用'}else{state.lastEngine='WebGPU · 高速拡張準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height);state.lastEngine='WebGPU · 高速拡張'}} - else state.lastEngine='WebGPU · 高速'; - const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate',referencePixel,fastExtended,fastCtx);if(!ok)return; - if(!deep){ - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false; - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0}); - if(fastExtended){const stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons} - const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token);return; - } - // Accurate mode keeps the previous committed frame visible while the new - // primary field is checked/corrected. The incomplete primary texture lives - // only in frame.back and is never presented to the viewer. - let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats(); - const n=canvas.width*canvas.height; - if(shouldApplySparseCorrection(state.unresolved,n)){ - state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats(); - const corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return;state.correctionPasses=1; - stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected; - } - const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return; - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · '+(referenceReused?'正確 reference再利用':'正確 reference自動選択')+(state.correctionPasses?' + DS correction':''); - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token); - }catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)} + if(!r){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU必須 · fallback無効';updateStats();return} + state.temporalFill=!!r.historyReady; + // Paint the old completed frame at the new view immediately. The numeric + // buffers may now be rewritten strip by strip, but the user keeps seeing + // a coherent image until the complete new colour frame is ready. + if(holdStable){r.presentFrame(r.presentTransform());state.drawState='REPROJECTED';updateStats()} + const result=await renderQualityStage(r,snap,iter,token,isSingleFinal);if(!result||token!==state.token)return; + const elapsed=performance.now()-t0;state.lastRender=elapsed;state.rendering=false;state.drawState=isSingleFinal?'COVERED':'REFINED'; + if(isSingleFinal){state.refineMs=elapsed;state.previewMs=0;state.lastEngine='WebGPU · 最終品質 '+(result.decision.accurateHybrid?'正確DS':result.decision.deep?'正確':result.decision.backend==='fast-bla'?'高速BLA':result.decision.fastExtended?'高速拡張':'高速');if(numericFrameComplete()){r.commitHistory(snap);state.temporalFill=false;}else{state.temporalFill=!!r.historyReady;}r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});state.qualityStage='final';r.scheduleBlaFastWarmup();scheduleBlaShadowValidation(snap,iter,result.shadowContext,result.referencePixel,token,result.decision.fastExtended?'fast':result.decision.deep?'deep':'cpu');} + else{state.previewMs=elapsed;state.lastEngine='WebGPU · 即時preview '+(result.decision.accurateHybrid?'正確DS':result.decision.deep?'正確':result.decision.backend==='fast-bla'?'高速BLA':result.decision.fastExtended?'高速拡張':'高速');} + updateStats(); + if(state.recolorPending&&!state.recoloring)queueMicrotask(recolor); + if(!isSingleFinal)scheduleQualityRefinement(snap,token,stages,1); + }catch(e){if(token!==state.token)return;state.rendering=false;state.refinePending=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)} +} +function requestRecolor(){ + state.colorRevision++;state.recolorPending=true; + // Keep the old completed frame valid until the replacement colour pass has + // actually finished. Invalidating history at slider-down caused apparent + // non-response/flicker when GPU work was already in flight. + if(!state.rendering&&!state.recoloring&&!state.numericRecoveryRunning)queueMicrotask(recolor); +} +async function recolor(){ + if(!renderer||!state.fieldView){state.recolorPending=true;return false} + if(state.rendering||state.recoloring||state.numericRecoveryRunning){state.recolorPending=true;return false} + state.recoloring=true;let painted=false; + try{ + while(state.recolorPending&&!state.rendering&&!state.numericRecoveryRunning&&renderer&&state.fieldView){ + state.recolorPending=false;const revision=state.colorRevision,token=state.token,ok=await renderer.recolor(token); + if(!ok||token!==state.token){state.recolorPending=true;continue} + renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true; + // Only promote a colour frame to stable history if it still represents + // the newest slider value. Rapid dragging therefore collapses to the + // latest value instead of replaying stale colour states. + if(revision===state.colorRevision&&state.qualityStage==='final'&&!state.refinePending&&numericFrameComplete()){renderer.commitHistory(state.frameView);state.temporalFill=false} + updateStats(); + } + return painted + }catch(e){console.error(e);return false} + finally{state.recoloring=false;if(state.recolorPending&&!state.rendering&&!state.numericRecoveryRunning)queueMicrotask(recolor)} } -function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · 正確モード非対応';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*effectiveColorCycle(iter)+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(ye.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を更新しました')}}; +async function copyShareUrl(){ + saveHash(false);const url=location.href; + try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(url);toast('共有URLをコピーしました');return}} + catch{} + try{const ta=document.createElement('textarea');ta.value=url;ta.setAttribute('readonly','');ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();const ok=document.execCommand&&document.execCommand('copy');ta.remove();if(ok){toast('共有URLをコピーしました');return}}catch{} + try{if(navigator.share){await navigator.share({title:'MANDELBROT DEEP ZOOM',url});toast('共有しました');return}}catch(e){if(e?.name==='AbortError')return} + try{prompt('共有URLをコピーしてください',url)}catch{}toast('共有URLを表示しました') +} +$('#share').onclick=copyShareUrl; let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0; -function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色アニメ停止':'色を自動変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')} +function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色相変化停止':'色相変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')} function stopColorAuto(){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton()} -function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;const cmin=.001,cmax=.05,smin=0,smax=1;state.cycle+=state.colorCycleDir*.0006*dt;state.shift+=state.colorShiftDir*.012*dt;if(state.cycle>=cmax){state.cycle=cmax;state.colorCycleDir=-1}else if(state.cycle<=cmin){state.cycle=cmin;state.colorCycleDir=1}if(state.shift>=smax){state.shift=smax;state.colorShiftDir=-1}else if(state.shift<=smin){state.shift=smin;state.colorShiftDir=1}$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;recolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)} +function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;/* Hue animation advances shift directly. No cycle or phase compensation is applied. */state.shift=fract01(state.shift+.09*dt);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;requestRecolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)} $('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf)colorAutoRaf=requestAnimationFrame(colorAutoStep);else if(!state.colorAuto)stopColorAuto()}; -$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()}; +$('#palette').addEventListener('change',e=>{const pal=Math.max(0,Math.min(PALETTE_MAX,Number(e.target.value)|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;requestRecolor();saveHash(false)}); +function applyCycleControl(e){state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);requestRecolor()} +function applyShiftControl(e){if(state.colorAuto)stopColorAuto();state.shift=Math.max(0,Math.min(1,Number(e.target.value)||0));$('#shiftO').textContent=state.shift.toFixed(2);requestRecolor()} +$('#cycle').addEventListener('input',applyCycleControl);$('#cycle').addEventListener('change',()=>saveHash(false)); +$('#shift').addEventListener('input',applyShiftControl);$('#shift').addEventListener('change',()=>saveHash(false)); $('#renderMode').onchange=e=>{state.renderMode=e.target.value==='accurate'?'accurate':'fast';try{localStorage.setItem('mandelbrot.renderMode',state.renderMode)}catch{}markDirty()}; $('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}}; addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}}); @@ -1195,19 +2297,25 @@ async function runExport(){ } $('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png'); -function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.deep?'WebGPU 正確':decision.fastExtended?'WebGPU 高速':'WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'正確描画の補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.refinementRunning)status+=' · 補足精密化 '+state.refinementStage+' ('+state.refinedIter+' iter, 残 '+state.refinementQueue+')';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status} +function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.accurateHybrid?'WebGPU 正確DS':decision.deep?'WebGPU 正確':decision.fastExtended?'WebGPU 高速':'WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'WebGPU 復旧中':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'正確描画の補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved){const nf=numericalFailureCount({reasons:state.unknownReasons||{}}),op=state.unknownReasons?.operationLimit||0;status+=' · 未確定 '+state.unresolved+(nf||op?' (数値 '+nf+' / 上限 '+op+')':'');}if(state.correctionPasses)status+=' · DS補正';if(state.multiRefPasses)status+=' · 局所ref '+state.multiRefPasses+' / 回収 '+state.multiRefRecovered;if(state.qualityStage==='preview')status+=' · preview '+state.qualityIter+'/'+state.qualityFinalIter;if(state.refinePending)status+=' · 最終品質待機';if(state.numericRecoveryPending)status+=' · 数値穴補修待機';if(state.numericRecoveryRunning)status+=' · 数値穴補修中';if(state.qualityStage==='refining')status+=' · 精細化 '+state.qualityIter+'/'+state.qualityFinalIter;if(state.qualityStage==='final')status+=' · 最終 '+state.qualityIter;if(state.temporalFill)status+=' · 前フレーム保持';if(runtime.blaShadowLatest&&!runtime.blaShadowLatest.buildOnly){const bs=runtime.blaShadowLatest;status+=' · BLA影 '+bs.samples+'点 '+(!bs.eligibleForBla?(bs.referenceFull?'非対象(span)':'非対象(ref短)'):bs.gatePassed?'一致':'差異 '+(bs.classMismatch+bs.escapeIterationMismatch))+' / skip '+(100*bs.skipRate).toFixed(1)+'%';}if(runtime.blaGpuShadowLatest){const gs=runtime.blaGpuShadowLatest;status+=' · GPU影 '+(gs.gatePassed?'一致':gs.compileFailed?'コンパイル不可':gs.error?'実行不可':'差異 '+(gs.exactMismatch??'?'));if(renderer?.blaGpuGate)status+=' gate '+renderer.blaGpuGate.distinctViews+'/3';}if(runtime.blaGpuCanaryLatest){const cs=runtime.blaGpuCanaryLatest;status+=' · BLA canary '+(cs.applied?(cs.checked+' px一致'):cs.gateArmed?'待機':cs.error?'停止':'未解禁');}if(runtime.blaFastProbeLatest){const bp=runtime.blaFastProbeLatest;status+=' · FAST BLA '+(bp.enabled?'ON':'OFF')+(Number.isFinite(bp.operationReduction)?' / 削減 '+(100*bp.operationReduction).toFixed(0)+'%':'')+(Number.isFinite(bp.fallbackRate)?' / fallback '+(100*bp.fallbackRate).toFixed(0)+'%':'');}if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status} globalThis.__MANDEL_TEST__={ async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard',renderMode='fast'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.renderMode=renderMode==='accurate'?'accurate':'fast';state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,state:this.state()}}, async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)}, - state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter(),renderMode:state.renderMode,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused}), + async runBlaShadow({samples=289,safety=1/64}={}){const snap=snapshot(),iter=maxIter(),w=canvas.width,h=canvas.height;let ctx=null,refPixel=null;if(renderer?.deepCtx&&renderer.deepCtx.source?.iter===iter){ctx=renderer.deepCtx;refPixel=referencePixelForSource(ctx.source,snap,w,h)}else if(renderer?.fastCtx&&renderer.fastCtx.source?.iter===iter){ctx=renderer.fastCtx;refPixel=referencePixelForSource(ctx.source,snap,w,h)}else{ctx=await refs.request(snap,iter,w,h);refPixel=referencePixelForSource(ctx.source,snap,w,h)}const result=await blaShadow.request({ctx,snap,iter,width:w,height:h,refPixel,samples,safety});return{...result.stats,tableBytes:result.table.byteLength,levelOffsets:result.levelOffsets,levelCounts:result.levelCounts}}, + async runBlaGpuShadow({samples=49,safety=1/64}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),w=canvas.width,h=canvas.height,ctx=await fastRefs.request(snap,iter,w,h),refPixel=referencePixelForSource(ctx.source,snap,w,h),cpu=await blaShadow.request({ctx,snap,iter,width:w,height:h,refPixel,samples,safety});if(!cpu.stats.gpuShadowEligible)return{cpu:cpu.stats,gpu:{skipped:true,reason:'cpu-gate'}};const gpu=await renderer.runBlaGpuShadow({shadow:cpu,ctx,snap,iter,refPixel,safety});return{cpu:cpu.stats,gpu}}, + blaGpuGate:()=>renderer?{...renderer.blaGpuGate}:null, + async runBlaGpuCanary({safety=1/64}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');if(!renderer.blaGpuGate?.armed)return{skipped:true,reason:'gate-not-armed',gate:renderer.blaGpuGate?{...renderer.blaGpuGate}:null};const snap=snapshot(),iter=maxIter(),w=canvas.width,h=canvas.height,ctx=await fastRefs.request(snap,iter,w,h),refPixel=referencePixelForSource(ctx.source,snap,w,h),cpu=await blaShadow.request({ctx,snap,iter,width:w,height:h,refPixel,samples:289,safety});if(!cpu.stats.gpuShadowEligible)return{cpu:cpu.stats,canary:{skipped:true,reason:'cpu-gate'}};const canary=await renderer.runBlaGpuCanary({shadow:cpu,ctx,snap,iter,refPixel,safety,token:state.token});return{cpu:cpu.stats,canary}}, + state:()=>{const snap=snapshot(),se=spanMantExp(snap),reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),spanExp:se.exp,directPixelRatio:directPixelRatio(snap,canvas.width),width:canvas.width,height:canvas.height,iter:maxIter(),renderMode:state.renderMode,palette:state.palette,cycle:state.cycle,shift:state.shift,colorRevision:state.colorRevision,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,qualityStage:state.qualityStage,qualityIter:state.qualityIter,qualityFinalIter:state.qualityFinalIter,previewMs:state.previewMs,refineMs:state.refineMs,refinePending:state.refinePending,temporalFill:state.temporalFill,numericRecoveryPending:state.numericRecoveryPending,numericRecoveryRunning:state.numericRecoveryRunning,historyReady:!!renderer?.historyReady,multiRefPasses:state.multiRefPasses,multiRefRecovered:state.multiRefRecovered,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version,gpuStage:state.gpuStage,gpuTileRows:state.gpuTileRows,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,blaShadowBuilds:runtime.blaShadowBuilds,blaShadowFailures:runtime.blaShadowFailures,blaShadow:runtime.blaShadowLatest,blaGpuShadowRuns:runtime.blaGpuShadowRuns,blaGpuShadowFailures:runtime.blaGpuShadowFailures,blaGpuShadow:runtime.blaGpuShadowLatest,blaFastProbeRuns:runtime.blaFastProbeRuns,blaFastEnabledFrames:runtime.blaFastEnabledFrames,blaFastBaselineFrames:runtime.blaFastBaselineFrames,blaFastProbe:runtime.blaFastProbeLatest,blaFastDisabled:!!renderer?.blaFastDisabled}}, async panPixels(dx,dy){pan(dx,dy);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test pan timeout');return this.state()}, - async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded():!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))}, - async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}} + setBlaFastEnabled(enabled=true){if(!renderer)return false;renderer.blaFastDisabled=!enabled;if(enabled&&renderer.blaFastCandidatePipeline&&renderer.blaFastFramePipeline)renderer.blaFastDisabled=false;runtime.blaFastProbeLatest=null;return !renderer.blaFastDisabled}, + async compareCurrentMetaToBaseline(){if(!renderer||!renderer.frame)throw new Error('WebGPU frame unavailable');const snap=snapshot(),iter=state.fieldView?.iter??maxIter(),w=canvas.width,h=canvas.height,fastExtended=fastNeedsExtended(snap,w);let fastCtx=null;if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const baseline=await renderer.renderTileMeta({snap,iter,deep:false,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:false}),current=await renderer.readMetaAll();let mismatch=0,first=-1,ha=2166136261>>>0,hb=2166136261>>>0;for(let i=0;i>>0,b=baseline[i]>>>0;if(a!==b){mismatch++;if(first<0)first=i}ha=Math.imul((ha^a)>>>0,16777619)>>>0;hb=Math.imul((hb^b)>>>0,16777619)>>>0}return{pixels:current.length,mismatch,first,currentChecksum:ha>>>0,baselineChecksum:hb>>>0,backend:state.fieldView?.backend||null,unresolved:baseline.unresolved||0}}, + async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w):!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))}, + async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}} }; // ── boot / teardown ────────────────────────────────────────────────────── -addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();refs.destroy();fastRefs.destroy();if(renderer)renderer.destroy()},{once:true}); +addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();blaShadow.destroy();refs.destroy();fastRefs.destroy();if(renderer)renderer.destroy()},{once:true}); try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}state.renderMode=localStorage.getItem('mandelbrot.renderMode')==='accurate'?'accurate':'fast'}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule(); })(); diff --git a/package.json b/package.json deleted file mode 100644 index 26cffe4..0000000 --- a/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "mandelbrot-webgpu-v24", - "version": "24.2.26", - "private": true, - "type": "module", - "scripts": { - "test": "node scripts/test-all.mjs", - "build": "node scripts/build.mjs" - } -} diff --git a/script.js b/script.js deleted file mode 100644 index 865cb8f..0000000 --- a/script.js +++ /dev/null @@ -1,371 +0,0 @@ -(()=>{'use strict'; -const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません'); -const $=s=>document.querySelector(s),canvas=$('#view'); -const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n; -const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000; -const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20; -const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity']; -const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96,REFINE_QUEUE_PARAM_BYTES=16,REFINE_STATS_BYTES=16,REFINE_BATCH_BYTES=16; -const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorCycleDir:1,colorShiftDir:1,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,refinementRunning:false,refinementStage:0,refinementQueue:0,refinedIter:0}; -let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,idleRefineTimer=0,lastWrittenHash='',navigationHash=''; -const viewHistory=[];let viewHistoryIndex=-1; -const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,idleRefineBatches:0,idleRefineQueued:0}; - -// ── exact fixed-point view state ───────────────────────────────────────── -function one(bits=state.bits){return 1n<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 fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v} -function f32ToFixed(x,bits){ - x=Math.fround(x);if(!Number.isFinite(x))throw new Error('non-finite f32 split component');if(x===0)return 0n; - const ab=new ArrayBuffer(4),dv=new DataView(ab);dv.setFloat32(0,x,false);const u=dv.getUint32(0,false),neg=(u>>>31)!==0,eb=(u>>>23)&255,frac=u&0x7fffff,m=BigInt(eb===0?frac:(0x800000|frac)),e=(eb===0?-149:eb-127-23),sh=bits+e; - let q=sh>=0?m<>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} -function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} -function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}} -function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))} -function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e} -function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}} -function f32Ulp(x){x=Math.abs(Math.fround(x));if(!Number.isFinite(x))return Infinity;if(x===0)return Math.pow(2,-149);if(x=4)} -function chooseBackend(snap=snapshot(),w=canvas.width){const deep=state.renderMode==='accurate',fastExtended=!deep&&fastNeedsExtended(snap,w);return{backend:deep?'deep':fastExtended?'fast-extended':'direct',deep,fastExtended,probe:false,reason:deep?'manual-accurate':fastExtended?'manual-fast-extended':'manual-fast'}} -function deepNeeded(){return state.renderMode==='accurate'} -function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,renderMode:state.renderMode}} -function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')} -function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()} -function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,renderMode:v.renderMode||state.renderMode});ensurePrecision();syncControls();saveHash(false);markDirty()} - -// ── iteration / quality policy ─────────────────────────────────────────── -function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))} -function effectiveColorCycle(iter=maxIter()){const base=Math.max(1,state.baseIter),effective=Math.max(base,Number(iter)||base);return state.cycle*base/effective} -function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))} -function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)} -function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)} -function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}} - -// ── 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 orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)return n+1}return 0} -function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]} -function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;kbest.score){best={...c,escape,score}}if(escape===0)break}return best} -function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<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 sourceBits=d.bits,sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),limit=Math.min(MAX_REF-1,d.iter),bits=sourceBits+64,centerRef=orbit(bits,sourceRe<<64n,sourceIm<<64n,limit),chosen=chooseReference(sourceBits,sourceRe,sourceIm,sourceSpan,w,h,limit,centerRef.escape),re=chosen.re<<64n,im=chosen.im<<64n,ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,limit),v=verify(sourceBits,chosen.re,chosen.im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} -`} -class ReferenceService{ - constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=16} - ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1,fresh=false){const key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,'guarded-perturb-v24.2.26'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} - cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} - destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} -} -const refs=new ReferenceService(); - -function fastReferenceBits(snap){const need=Math.ceil(Math.max(0,-log2FixedAt(snap.span,snap.bits)))+48;return Math.max(96,Math.min(snap.bits,need))} -function fastReferenceWorkerSource(){return String.raw` -'use strict'; -function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length} -function roundShift(v,b){if(b<=0)return v;const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q} -function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n} -function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}} -function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<BAIL)return n+1}return 0} -function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const ox=span/4n,oy=(span*BigInt(h))/(4n*BigInt(w));return[{re,im},{re:re-ox,im},{re:re+ox,im},{re,im:im+oy},{re,im:im-oy},{re:re-ox,im:im+oy},{re:re+ox,im:im+oy},{re:re-ox,im:im-oy},{re:re+ox,im:im-oy}]} -function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;kbest.score){best={...c,escape,score}}if(escape===0)break}return best} -function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf} -self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const sourceRe=BigInt(d.re),sourceIm=BigInt(d.im),sourceSpan=d.span?BigInt(d.span):0n,w=Math.max(1,d.width||1),h=Math.max(1,d.height||1),bits=d.targetBits,centerRe=requant(sourceRe,d.sourceBits,bits),centerIm=requant(sourceIm,d.sourceBits,bits),centerRef=orbit(bits,centerRe,centerIm,d.iter),chosen=chooseReference(d.sourceBits,sourceRe,sourceIm,sourceSpan,w,h,d.iter,centerRef.escape),re=requant(chosen.re,d.sourceBits,bits),im=requant(chosen.im,d.sourceBits,bits),ref=(chosen.re===sourceRe&&chosen.im===sourceIm)?centerRef:orbit(bits,re,im,d.iter),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,selectionEscape:chosen.escape,referenceRe:chosen.re.toString(),referenceIm:chosen.im.toString(),precisionBits:bits,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}} -`} -class FastReferenceService{ - constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=12} - ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([fastReferenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};this.cache.set(d.key,d);while(this.cache.size>this.maxCache)this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'fast reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}} - request(snap,iter,width=1,height=1){const targetBits=fastReferenceBits(snap),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'fast-perturb-v24.2.26'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);if(this.pending.size)this.cancelPending('superseded fast reference request');if(!this.ensure())return Promise.reject(new Error('高速参照Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject,source:{bits:snap.bits,re:snap.re,im:snap.im,span:snap.span,iter}});this.worker.postMessage({type:'build',id,key,sourceBits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter})})} - cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}} - destroy(){this.cancelPending('destroyed');this.cache.clear()} -} -const fastRefs=new FastReferenceService(); - -// ── WebGPU renderer ────────────────────────────────────────────────────── -function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})} -function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}} -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.fastCtx=null;this.refineDeepCtx=null;this.exportWs=null;this.refinePipelinePromise=null;this.likelyQueue=null;this.deepLikelyRefine=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'}); - device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)}); - this.ready=this.initPipelines(); - device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()}); - } - 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,fm,xm,xhm,xpm,xsm,zm,zqm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('fast-perturb',G.FAST_PERTURB_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]); - this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}}); - this.fast=this.device.createComputePipeline({layout:'auto',compute:{module:fm,entryPoint:'main'}}); - this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}}); - const [xpsm,xhsm]=await Promise.all([this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL),this.module('deep-bucket-histogram-stats-production',G.DEEP_BUCKET_HIST_STATS_WGSL)]); - this.deepPostStats=this.device.createComputePipeline({layout:'auto',compute:{module:xpsm,entryPoint:'main'}}); - this.deepBucketHist=this.device.createComputePipeline({layout:'auto',compute:{module:xhm,entryPoint:'main'}}); - this.deepBucketHistStats=this.device.createComputePipeline({layout:'auto',compute:{module:xhsm,entryPoint:'main'}}); - this.deepBucketPrefix=this.device.createComputePipeline({layout:'auto',compute:{module:xpm,entryPoint:'main'}}); - this.deepBucketScatter=this.device.createComputePipeline({layout:'auto',compute:{module:xsm,entryPoint:'main'}}); - this.correct=this.device.createComputePipeline({layout:'auto',compute:{module:zm,entryPoint:'main'}}); - this.correctQueued=this.device.createComputePipeline({layout:'auto',compute:{module:zqm,entryPoint:'main'}}); - this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}}); - this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}}); - this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}}); - 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}} - } - async ensureRefinePipelines(){if(this.likelyQueue&&this.deepLikelyRefine)return;if(this.refinePipelinePromise)return this.refinePipelinePromise;this.refinePipelinePromise=(async()=>{const [lqm,drm]=await Promise.all([this.module('likely-refine-queue',G.LIKELY_QUEUE_WGSL),this.module('deep-likely-refine',G.DEEP_LIKELY_REFINE_WGSL)]);this.likelyQueue=this.device.createComputePipeline({layout:'auto',compute:{module:lqm,entryPoint:'main'}});this.deepLikelyRefine=this.device.createComputePipeline({layout:'auto',compute:{module:drm,entryPoint:'refine_likely'}})})().finally(()=>{this.refinePipelinePromise=null});return this.refinePipelinePromise} - frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','refineQueue','refineStats','refineQueueParams','refineBatchParams','refineRead','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null} - ensureFrame(w,h){ - const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage; - this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,refineQueue:null,refineStats:null,refineQueueParams:null,refineBatchParams:null,refineRead:null,numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame; - } - ensureDeepBucketState(f=this.frame){if(!f)return null;if(!f.deepBucketState){const d=this.device,B=GPUBufferUsage;f.deepBucketState=buf(d,DEEP_BUCKET_STATE_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-bucket-state')}return f} - ensureDeepQueueWorkspace(f=this.frame){if(!f)return null;this.ensureDeepBucketState(f);if(f.deepQueue&&f.deepQueueStats&&f.deepIndirect)return f;const d=this.device,B=GPUBufferUsage;f.deepQueueStats=buf(d,SPARSE_QUEUE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue-stats');f.deepQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue');f.deepIndirect=buf(d,SPARSE_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'deep-correction-indirect');return f} - ensureRefineWorkspace(f=this.frame){if(!f)return null;if(f.refineQueue&&f.refineStats&&f.refineQueueParams&&f.refineBatchParams&&f.refineRead)return f;const d=this.device,B=GPUBufferUsage;f.refineQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-queue');f.refineStats=buf(d,REFINE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'likely-refine-stats');f.refineQueueParams=buf(d,REFINE_QUEUE_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-queue-params');f.refineBatchParams=buf(d,REFINE_BATCH_BYTES,B.UNIFORM|B.COPY_DST,'likely-refine-batch-params');f.refineRead=buf(d,REFINE_STATS_BYTES,B.COPY_DST|B.MAP_READ,'likely-refine-readback');return f} - ensureExportWorkspace(){ - if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S; - this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+4*UNRESOLVED_BYTES,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs; - } - exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.unresolveds)destroy(b);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null} - setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}} - destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null} - setRefineDeepContext(ctx){if(this.refineDeepCtx&&this.refineDeepCtx.key===ctx.key)return;this.destroyRefineDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'idle-refinement-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.refineDeepCtx={...ctx,refsB}} - destroyRefineDeepContext(){if(this.refineDeepCtx)destroy(this.refineDeepCtx.refsB);this.refineDeepCtx=null} - setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}} - destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null} - directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})} - deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,0,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})} - colorParamsData(w,h,iter=state.fieldView?.iter??maxIter()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,effectiveColorCycle(iter),true);d.setFloat32(20,state.shift,true)})} - encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const d=this.device,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,pipeline=this.deepPostStats,bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.fast.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.fastCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.fast);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()} - encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHist.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHist);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketHistStats.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeDeepBucketPrefix(encoder,{bucketState,queueStats,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketPrefix.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:bucketState}},{binding:1,resource:{buffer:queueStats}},{binding:2,resource:{buffer:indirect}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketPrefix);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end()} - encodeDeepBucketScatter(encoder,{pbuf,meta,bucketState,queueStats,queue,w,h}){const d=this.device,bg=d.createBindGroup({layout:this.deepBucketScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()} - encodeQueuedDeepCorrection(encoder,{pbuf,meta,smooth,unresolved,queueStats,queue,indirect}){const d=this.device,bg=d.createBindGroup({layout:this.correctQueued.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{binding:5,resource:{buffer:queueStats}},{binding:6,resource:{buffer:queue}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.correctQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()} - encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h}){this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline:this.correct})} - async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false,referencePixel=null,fastExtended=false,fastContext=null){ - await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep){this.setDeepContext(deepContext);this.ensureDeepBucketState(f)}else if(fastExtended){this.setFastContext(fastContext)}d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep)d.queue.writeBuffer(f.deepBucketState,0,new Uint32Array(DEEP_BUCKET_STATE_BYTES/4)); - const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;const params=deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,refX,refY):fastExtended?this.fastParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,refX,refY):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap);d.queue.writeBuffer(f.numericParams,0,params); - d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'}); - if(deep){this.encodeDeepPostStatsNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeDeepBucketHistogramStats(encoder,{pbuf:f.numericParams,meta:f.meta,bucketState:f.deepBucketState,unresolved:f.unresolved,w:f.w,h:f.h});} - else if(fastExtended){this.encodeFastNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});} - else{this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});} - if(!deep){const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();} - d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;if(!deep)[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true; - } - async buildLikelyRefineQueue(token,mode=1,sparseStride=4){await this.ready;await this.ensureRefinePipelines();if(token!==state.token)return{count:0,overflow:0,cancelled:true};const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;d.queue.writeBuffer(f.refineStats,0,new Uint32Array(REFINE_STATS_BYTES/4));d.queue.writeBuffer(f.refineQueueParams,0,new Uint32Array([f.w,f.h,mode>>>0,sparseStride>>>0]));const e=d.createCommandEncoder({label:'likely-refine-queue-build'}),bg=d.createBindGroup({layout:this.likelyQueue.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.refineQueueParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.refineQueue}},{binding:3,resource:{buffer:f.refineStats}}]}),pass=e.beginComputePass();pass.setPipeline(this.likelyQueue);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.n/256));pass.end();e.copyBufferToBuffer(f.refineStats,0,f.refineRead,0,REFINE_STATS_BYTES);d.queue.submit([e.finish()]);await f.refineRead.mapAsync(GPUMapMode.READ);const a=new Uint32Array(f.refineRead.getMappedRange().slice(0));f.refineRead.unmap();if(token!==state.token)return{count:0,overflow:0,cancelled:true};return{count:Math.min(f.n,a[0]||0),overflow:a[1]||0,cancelled:false}} - async refineLikelyBatch({snap,iter,deepContext,token,base,end,forceStrict=false}){await this.ready;await this.ensureRefinePipelines();if(token!==state.token||base>=end)return false;const f=this.ensureRefineWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setRefineDeepContext(deepContext);const ref=referencePixelForSource(deepContext?.source,snap,f.w,f.h);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0,ref.x,ref.y,0,f.w,0));d.queue.writeBuffer(f.refineBatchParams,0,new Uint32Array([base>>>0,end>>>0,0,0]));const pipeline=this.deepLikelyRefine,bg0=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.refineDeepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),bg1=d.createBindGroup({layout:pipeline.getBindGroupLayout(1),entries:[{binding:0,resource:{buffer:f.refineQueue}},{binding:1,resource:{buffer:f.refineBatchParams}}]}),e=d.createCommandEncoder({label:'likely-idle-refine'}),pass=e.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg0);pass.setBindGroup(1,bg1);pass.dispatchWorkgroups(Math.ceil((end-base)/64));pass.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.idleRefineBatches++;return true} - async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true} - presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}} - presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])} - async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolveds[0],pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4)); - if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})} - const ca=this.colorParamsData(w,h,iter),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end(); - const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+UNRESOLVED_BYTES);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+UNRESOLVED_BYTES)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y512||h>512)throw new Error('export tile exceeds reusable workspace');const fastExtended=!deep&&!!fastContext;if(deep)this.setDeepContext(deepContext);else if(fastExtended)this.setFastContext(fastContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h,iter);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca); - for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si],unresolved=ws.unresolveds[si];d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()} - const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end(); - const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y=Math.max(32,Math.floor(n*.0005));return true} -function referencePixelForSource(src,snap,w,h){if(!src)return{x:w*.5,y:h*.5};const b=Math.max(snap.bits,src.bits),span=align(snap.span,snap.bits,b),dr=align(src.re,src.bits,b)-align(snap.re,snap.bits,b),di=align(src.im,src.bits,b)-align(snap.im,snap.bits,b);return{x:w*.5+fixedRatio(dr,span)*w,y:h*.5-fixedRatio(di,span)*w}} -function reusableDeepReference(snap,iter,w,h){const ctx=renderer&&renderer.deepCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} -function reusableFastReference(snap,iter,w,h){const ctx=renderer&&renderer.fastCtx,src=ctx&&ctx.source;if(!ctx||!src||src.iter!==iter)return null;const b=Math.max(snap.bits,src.bits),newSpan=align(snap.span,snap.bits,b),oldSpan=align(src.span,src.bits,b);if(newSpan!==oldSpan)return null;const pixel=referencePixelForSource(src,snap,w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x<0||pixel.x>w||pixel.y<0||pixel.y>h)return null;return{ctx,pixel}} - -// ── GPU startup / rendering orchestration ──────────────────────────────── -async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise} -function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx} -function sameSnapshot(a,b){return!!a&&!!b&&a.bits===b.bits&&a.re===b.re&&a.im===b.im&&a.span===b.span} -function cancelIdleRefinement(){if(idleRefineTimer){clearTimeout(idleRefineTimer);idleRefineTimer=0}state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0} -function idleRefinementTargets(baseIter){if(baseIter>=8000)return[];const z=zoomExp(),mode=state.processMode,mul=mode==='power'?.75:mode==='fine'?1.25:mode==='validate'?1.4:1,cap=Math.min(8000,Math.max(baseIter*2,Math.round((1200+300*z)*mul))),raw=mode==='power'?[Math.min(cap,baseIter*2)]:[Math.min(cap,baseIter*2),Math.min(cap,baseIter*4),cap],out=[];for(const v of raw){const n=Math.max(baseIter+1,Math.floor(v));if(n>baseIter&&(!out.length||n!==out[out.length-1]))out.push(n)}return out} -function idleRefinementBatchBudget(){const m=state.processMode;if(m==='power')return 900000;if(m==='fine')return 2800000;if(m==='validate')return 1800000;return 1800000} -function idleRefinementQueuePolicy(stage){if(stage<=0)return{mode:1,stride:4};if(stage===1)return{mode:2,stride:8};return{mode:3,stride:16}} -function idleRefinementSliceMs(){return state.processMode==='power'?100:state.processMode==='fine'?220:160} -function idleRefinementPauseMs(){return state.processMode==='power'?160:80} -function canIdleRefine(token,snap,baseIter){return token===state.token&&renderer&&!state.rendering&&!state.pointerActive&&!state.wheelActive&&sameSnapshot(snap,snapshot())&&state.fieldView&&state.fieldView.iter===baseIter&&state.fieldView.w===canvas.width&&state.fieldView.h===canvas.height} -const sleep=ms=>new Promise(r=>setTimeout(r,ms)); -async function paintIdleRefinement(token,snap,baseIter){while(state.recoloring&&canIdleRefine(token,snap,baseIter))await sleep(12);if(!canIdleRefine(token,snap,baseIter))return false;await recolor();while((state.recoloring||state.recolorPending)&&canIdleRefine(token,snap,baseIter))await sleep(8);return canIdleRefine(token,snap,baseIter)} -function scheduleIdleRefinement(snap,baseIter,decision,token){cancelIdleRefinement();if(!renderer||(!decision.deep&&!decision.fastExtended))return;const targets=idleRefinementTargets(baseIter);if(!targets.length)return;idleRefineTimer=setTimeout(()=>{idleRefineTimer=0;runIdleRefinement(snap,baseIter,decision,token,targets)},320)} -async function runIdleRefinement(snap,baseIter,decision,token,targets){if(!canIdleRefine(token,snap,baseIter))return;state.refinementRunning=true;state.refinedIter=baseIter;updateStats();try{for(let stage=0;stage=idleRefinementSliceMs()){const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;updateStats();lastPaint=performance.now();await sleep(idleRefinementPauseMs())}}const painted=await paintIdleRefinement(token,snap,baseIter);if(!painted)return;state.fieldView.refinedIter=targetIter;updateStats();await sleep(idleRefinementPauseMs())}}catch(e){if(token===state.token){console.warn('idle refinement stopped:',e);state.gpuError=String(e&&e.message||e)}}finally{if(token===state.token){state.refinementRunning=false;state.refinementStage=0;state.refinementQueue=0;updateStats()}}} - -function cancelRender(){cancelIdleRefinement();state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled')} -function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()} -function schedule(){if(!raf)raf=requestAnimationFrame(loop)} -async function renderFrame(){ - const token=++state.token,snap=snapshot(),iter=maxIter(),t0=performance.now();let decision=chooseBackend(snap,canvas.width,iter,canvas.height); - state.rendering=true;state.dirty=false;state.drawState='COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.backendDecision=decision;runtime.renderStarts++;updateStats(); - try{ - const r=renderer||await initRenderer();if(token!==state.token)return; - if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return} - const deep=decision.deep,fastExtended=decision.fastExtended;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false; - if(deep){const reuse=reusableDeepReference(snap,iter,canvas.width,canvas.height);if(reuse){ctx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 正確 reference再利用'}else{state.lastEngine='WebGPU · 正確 reference準備';updateStats();ctx=await refs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · 正確 perturbation'}} - else if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true;state.lastEngine='WebGPU · 高速拡張 reference再利用'}else{state.lastEngine='WebGPU · 高速拡張準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height);state.lastEngine='WebGPU · 高速拡張'}} - else state.lastEngine='WebGPU · 高速'; - const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate',referencePixel,fastExtended,fastCtx);if(!ok)return; - if(!deep){ - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false; - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0}); - if(fastExtended){const stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons} - const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token);return; - } - // Accurate mode keeps the previous committed frame visible while the new - // primary field is checked/corrected. The incomplete primary texture lives - // only in frame.back and is never presented to the viewer. - let stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=0;updateStats(); - const n=canvas.width*canvas.height; - if(shouldApplySparseCorrection(state.unresolved,n)){ - state.drawState='REFINING';state.lastEngine='WebGPU · sparse DS correction';updateStats(); - const corrected=await r.correctUnknownFrame(snap,iter,token,referencePixel);if(!corrected||token!==state.token)return;state.correctionPasses=1; - stats=await r.readUnresolvedStats();if(token!==state.token)return;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.correctedPixels=stats.corrected; - } - const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return; - state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep,fastExtended,backend:decision.backend,referenceReused};state.rendering=false;state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.lastEngine='WebGPU · '+(referenceReused?'正確 reference再利用':'正確 reference自動選択')+(state.correctionPasses?' + DS correction':''); - r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}updateStats();scheduleIdleRefinement(snap,iter,decision,token); - }catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)} -} -function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w,h)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · 正確モード非対応';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))))*effectiveColorCycle(iter)+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y{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');p.set('rm',state.renderMode);const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}} -function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';if(p.has('rm'))state.renderMode=p.get('rm')==='accurate'?'accurate':'fast';ensurePrecision();return true}catch{return false}} -function syncHistoryButtons(){} -function syncControls(){$('#renderMode').value=state.renderMode;$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()} -function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)} -function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')} -$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()}; -$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()}; -$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}}; -let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0; -function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色アニメ停止':'色を自動変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')} -function stopColorAuto(){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton()} -function colorAutoStep(now){if(!state.colorAuto){colorAutoRaf=0;return}if(!colorAutoLast)colorAutoLast=now;const dt=Math.min(.1,Math.max(0,(now-colorAutoLast)/1000));colorAutoLast=now;const cmin=.001,cmax=.05,smin=0,smax=1;state.cycle+=state.colorCycleDir*.0006*dt;state.shift+=state.colorShiftDir*.012*dt;if(state.cycle>=cmax){state.cycle=cmax;state.colorCycleDir=-1}else if(state.cycle<=cmin){state.cycle=cmin;state.colorCycleDir=1}if(state.shift>=smax){state.shift=smax;state.colorShiftDir=-1}else if(state.shift<=smin){state.shift=smin;state.colorShiftDir=1}$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);if(now-colorAutoPaint>=50){colorAutoPaint=now;recolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)} -$('#colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf)colorAutoRaf=requestAnimationFrame(colorAutoStep);else if(!state.colorAuto)stopColorAuto()}; -$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()}; -$('#renderMode').onchange=e=>{state.renderMode=e.target.value==='accurate'?'accurate':'fast';try{localStorage.setItem('mandelbrot.renderMode',state.renderMode)}catch{}markDirty()}; -$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}}; -addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}}); -addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}}); - -// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ────── -const exportJob={active:false,cancelled:false}; -function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)} -const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})(); -function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0} -function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out} -class StreamingPng{ - constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()} - async rows(filteredRows){await this.writer.write(filteredRows)} - async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})} - async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}} -} -function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}} -async function runExport(){ - if(exportJob.active)return; - const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return} - const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w,h),fastExtended=!deep&&fastNeedsExtended(snap,w),strict=$('#exportPrecision').value==='strict'; - let ctx=null,fastCtx=null; - if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}else if(fastExtended){$('#exportStatus').textContent='高速拡張参照を準備中…';fastCtx=await fastRefs.request(snap,iter,w,h)} - const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1; - exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true; - let done=0,unresolvedSamples=0; - 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'); - -function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(decision.deep?'WebGPU 正確':decision.fastExtended?'WebGPU 高速':'WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'正確描画の補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.correctionPasses)status+=' · DS補正';if(state.refinementRunning)status+=' · 補足精密化 '+state.refinementStage+' ('+state.refinedIter+' iter, 残 '+state.refinementQueue+')';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status} - -globalThis.__MANDEL_TEST__={ - async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard',renderMode='fast'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.renderMode=renderMode==='accurate'?'accurate':'fast';state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,state:this.state()}}, - async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)}, - state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter(),renderMode:state.renderMode,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused}), - async panPixels(dx,dy){pan(dx,dy);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test pan timeout');return this.state()}, - async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded():!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))}, - async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}} -}; - -// ── boot / teardown ────────────────────────────────────────────────────── -addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto();cancelRender();refs.destroy();fastRefs.destroy();if(renderer)renderer.destroy()},{once:true}); -try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}state.renderMode=localStorage.getItem('mandelbrot.renderMode')==='accurate'?'accurate':'fast'}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule(); -})(); diff --git a/scripts/build.mjs b/scripts/build.mjs deleted file mode 100644 index ac587b8..0000000 --- a/scripts/build.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import fs from 'node:fs/promises'; -const root=new URL('../',import.meta.url); -const template=await fs.readFile(new URL('../index.external.html',import.meta.url),'utf8'); -const kernels=await fs.readFile(new URL('../gpu-kernels.js',import.meta.url),'utf8'); -const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); -const safe=s=>s.replace(/<\/script/gi,'<\\/script'); -const standalone=template - .replace('',``) - .replace('',``) - .replace('','\n'); -await fs.writeFile(new URL('../index.html',import.meta.url),standalone); -for(const variant of ['standalone','hosted']){ - const dir=new URL(`../dist/${variant}/`,import.meta.url);await fs.rm(dir,{recursive:true,force:true});await fs.mkdir(dir,{recursive:true}); - if(variant==='standalone'){ - await fs.writeFile(new URL('index.html',dir),standalone); - }else{ - await fs.writeFile(new URL('index.html',dir),template); - await fs.copyFile(new URL('../gpu-kernels.js',import.meta.url),new URL('gpu-kernels.js',dir)); - await fs.copyFile(new URL('../script.js',import.meta.url),new URL('script.js',dir)); - await fs.writeFile(new URL('_headers',dir),`/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: no-referrer\n`); - } -} -console.log(JSON.stringify({status:'pass',outputs:['index.html','dist/standalone/index.html','dist/hosted'],standalone:'single-file'},null,2)); diff --git a/scripts/test-all.mjs b/scripts/test-all.mjs deleted file mode 100644 index dbcf029..0000000 --- a/scripts/test-all.mjs +++ /dev/null @@ -1,7 +0,0 @@ -import {spawnSync} from 'node:child_process'; -const commands=[ - ['node',['--check','script.js']],['node',['--check','gpu-kernels.js']],['node',['--check','tests/webgpu-acceptance.js']], - ...['v24-wgsl-reserved.mjs','v24-source-contract.mjs','v24-index-contract.mjs','v24-standalone-ui-contract.mjs','v24-tree-contract.mjs','v24-deep-sparse-queue-model.mjs','v24-primary-post-stats-model.mjs','v24-reference-worker.mjs','v24-reference-recovery-model.mjs','v24-pan-reuse-model.mjs','v24-direct-model.mjs','v24-fast-precision-extension-model.mjs','v24-color-relative-model.mjs','v24-baseline-idle-refinement-model.mjs','v24-cpu-numeric-model.mjs','v24-sparse-correction-model.mjs','v24-bla-model.mjs','v24-geometry-contract.mjs','v24-coordinate-format.mjs','v24-png-stream-model.mjs','v24-acceptance-contract.mjs'].map(f=>['node',['tests/'+f]]) -]; -for(const [cmd,args] of commands){const r=spawnSync(cmd,args,{stdio:'inherit'});if(r.status!==0)process.exit(r.status??1)} -console.log(JSON.stringify({status:'pass',suite:'v24-static-and-cpu-model',realWebGPU:'run tests/webgpu-acceptance.html in a WebGPU-capable browser'},null,2)); diff --git a/tests/ACCURATE_HYBRID_WEBGPU_RESULTS.json b/tests/ACCURATE_HYBRID_WEBGPU_RESULTS.json new file mode 100644 index 0000000..f84d5e9 --- /dev/null +++ b/tests/ACCURATE_HYBRID_WEBGPU_RESULTS.json @@ -0,0 +1,13 @@ +{ + "environment": "Chromium 144 + SwiftShader/Vulkan, compute-only WebGPU", + "size": [320, 220], + "iterations": 750, + "lateEscapeThreshold": 24, + "cases": [ + {"name": "reset", "center": [-0.5, 0.0], "span": 3.4, "gpuMs": 203.3, "sampledPixels": 1200, "classificationMismatchVsCpuDouble": 0, "escapeIterationMismatchVsCpuDouble": 2}, + {"name": "seahorse-low", "center": [-0.743643887037151, 0.13182590420533], "span": 0.1, "gpuMs": 160.7, "sampledPixels": 1200, "classificationMismatchVsCpuDouble": 0, "escapeIterationMismatchVsCpuDouble": 0}, + {"name": "cusp-low", "center": [0.25, 0.0], "span": 0.85, "gpuMs": 87.8, "sampledPixels": 1200, "classificationMismatchVsCpuDouble": 0, "escapeIterationMismatchVsCpuDouble": 0} + ], + "stripOrderTest": {"pixels": 6144, "fullPassVs4RowQueuedStripsMismatch": 0}, + "note": "GPU time is software-GPU time and is only a relative engineering check." +} diff --git a/tests/ACCURATE_SEED_FUZZ.json b/tests/ACCURATE_SEED_FUZZ.json new file mode 100644 index 0000000..de52715 --- /dev/null +++ b/tests/ACCURATE_SEED_FUZZ.json @@ -0,0 +1,8 @@ +{ + "samples": 60000, + "mixture": "global Mandelbrot bounds + targeted known boundary neighborhoods", + "maxIter": 900, + "acceptedRule": "f32 escape iteration < 24", + "acceptedSamples": 33637, + "acceptedEscapeIterationMismatchVsCpuDouble": 0 +} diff --git a/tests/ACTUAL_WEBGPU_COMPILE.json b/tests/ACTUAL_WEBGPU_COMPILE.json new file mode 100644 index 0000000..967d415 --- /dev/null +++ b/tests/ACTUAL_WEBGPU_COMPILE.json @@ -0,0 +1,38 @@ +{ + "DIRECT_F32_WGSL": { + "errors": [], + "warnings": [] + }, + "DIRECT_ACCURATE_SEED_WGSL": { + "errors": [], + "warnings": [] + }, + "DIRECT_DS_CORRECT_WGSL": { + "errors": [], + "warnings": [] + }, + "FAST_PERTURB_WGSL": { + "errors": [], + "warnings": [] + }, + "DEEP_PERTURB_WGSL": { + "errors": [], + "warnings": [] + }, + "DEEP_CORRECT_WGSL": { + "errors": [], + "warnings": [] + }, + "COLOR_WGSL": { + "errors": [], + "warnings": [] + }, + "PRESENT_WGSL": { + "errors": [], + "warnings": [] + }, + "BLA_GPU_FAST_FRAME_WGSL": { + "errors": [], + "warnings": [] + } +} diff --git a/tests/BASELINE_KERNEL_HASHES.json b/tests/BASELINE_KERNEL_HASHES.json new file mode 100644 index 0000000..d2e91b7 --- /dev/null +++ b/tests/BASELINE_KERNEL_HASHES.json @@ -0,0 +1,10 @@ +{ + "DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922", + "FAST_PERTURB_WGSL": "b9db140f03fbd0aa36a5ca7dbbcbfec471e1587ebc632953f9eb73a35fb9a978", + "DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65", + "DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88", + "COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3", + "PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc", + "BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019", + "BLA_GPU_FAST_FRAME_WGSL": "2e4cdbd31d60c22d87fb04847af4d2c1d6de7d213da5cdd27234e5d8f9672553" +} diff --git a/tests/BLA_PREP_BENCHMARK.json b/tests/BLA_PREP_BENCHMARK.json new file mode 100644 index 0000000..cecc833 --- /dev/null +++ b/tests/BLA_PREP_BENCHMARK.json @@ -0,0 +1,9 @@ +{ + "scenario": "classic Seahorse Valley, 654x690, span 3.4e-13, 2900 iterations, 64-point production gate equivalent", + "oldValidate64Ms": 446.9, + "oldTableBuildMs": 58.4, + "newBuildOnlyMs": 60.0, + "newTableBuildMs": 59.4, + "estimatedSynchronousPrepSavedMs": 386.9, + "note": "Node/V8 worker-model timing, not GPU frame time. The production path now builds the table only; the 64-point decision remains on the GPU probe." +} diff --git a/tests/KERNEL_HASHES_V24_2_54.json b/tests/KERNEL_HASHES_V24_2_54.json new file mode 100644 index 0000000..7531d51 --- /dev/null +++ b/tests/KERNEL_HASHES_V24_2_54.json @@ -0,0 +1,23 @@ +{ + "version": "9af1429215fe356104bcbe991c98d101bbf241a68a38232533918454505014d3", + "DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922", + "DIRECT_ACCURATE_SEED_WGSL": "478146d2f8a0bb5608850dcbbdfab183bc6e9105eab2afb4122892f7353349c3", + "DIRECT_DS_CORRECT_WGSL": "6c61934c796c67f08ebdee0b7156c8c8d891104f3fd1d2408bad16d9a76f7c00", + "FAST_PERTURB_WGSL": "b9db140f03fbd0aa36a5ca7dbbcbfec471e1587ebc632953f9eb73a35fb9a978", + "DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65", + "DEEP_PERTURB_POSTSTATS_WGSL": "364d916da3d90b049ac24423a8112dd587ebd756fa97c99a6717f869c6b6cfa1", + "DEEP_BUCKET_HIST_WGSL": "3458f908fa239b16667e5c82dae25274d5493d647595230574a3d65622a54aaf", + "DEEP_BUCKET_HIST_STATS_WGSL": "b464d503703646e3c990e10a0cb40dacdc3677447b7a35e03c80a50bfca23824", + "DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da", + "DEEP_BUCKET_SCATTER_WGSL": "7575c4fc5f4d5674dfc3997189212ffc6fbe240a9d8b8b3f8ef883cd4eae47a1", + "DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88", + "DEEP_CORRECT_QUEUE_WGSL": "1337469b0673a4da20670daf220bec5883366e2454acc8c7923e472a5cec7a7f", + "COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3", + "AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342", + "PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc", + "BLA_GPU_SHADOW_WGSL": "e830339a150dd5722ca7c58674193174606adaf488bfd817408f2eeb4583514f", + "BLA_GPU_CANARY_WGSL": "3c219f657b2890b40f7cfa0297e13a670819d4fed333fcb30cb37196c5755f55", + "BLA_GPU_CANARY_APPLY_WGSL": "d02bb3ef007680a687960125ee9dded1fc4b8a4b091a6d924408a2e1f94a05e1", + "BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019", + "BLA_GPU_FAST_FRAME_WGSL": "2e4cdbd31d60c22d87fb04847af4d2c1d6de7d213da5cdd27234e5d8f9672553" +} \ No newline at end of file diff --git a/tests/KERNEL_HASHES_V24_2_55.json b/tests/KERNEL_HASHES_V24_2_55.json new file mode 100644 index 0000000..df75a7a --- /dev/null +++ b/tests/KERNEL_HASHES_V24_2_55.json @@ -0,0 +1,23 @@ +{ + "version": "62ebee65d5378e9379394b24229266de255fa2f241a2eecd31fbab0721d2ae00", + "DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922", + "DIRECT_ACCURATE_SEED_WGSL": "478146d2f8a0bb5608850dcbbdfab183bc6e9105eab2afb4122892f7353349c3", + "DIRECT_DS_CORRECT_WGSL": "6c61934c796c67f08ebdee0b7156c8c8d891104f3fd1d2408bad16d9a76f7c00", + "FAST_PERTURB_WGSL": "2cc5841740456f10ed3f64005e72ac6ff87d2c38926260a1fcb1521a389013a0", + "DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65", + "DEEP_PERTURB_POSTSTATS_WGSL": "364d916da3d90b049ac24423a8112dd587ebd756fa97c99a6717f869c6b6cfa1", + "DEEP_BUCKET_HIST_WGSL": "3458f908fa239b16667e5c82dae25274d5493d647595230574a3d65622a54aaf", + "DEEP_BUCKET_HIST_STATS_WGSL": "b464d503703646e3c990e10a0cb40dacdc3677447b7a35e03c80a50bfca23824", + "DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da", + "DEEP_BUCKET_SCATTER_WGSL": "7575c4fc5f4d5674dfc3997189212ffc6fbe240a9d8b8b3f8ef883cd4eae47a1", + "DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88", + "DEEP_CORRECT_QUEUE_WGSL": "1337469b0673a4da20670daf220bec5883366e2454acc8c7923e472a5cec7a7f", + "COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3", + "AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342", + "PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc", + "BLA_GPU_SHADOW_WGSL": "e830339a150dd5722ca7c58674193174606adaf488bfd817408f2eeb4583514f", + "BLA_GPU_CANARY_WGSL": "3c219f657b2890b40f7cfa0297e13a670819d4fed333fcb30cb37196c5755f55", + "BLA_GPU_CANARY_APPLY_WGSL": "d02bb3ef007680a687960125ee9dded1fc4b8a4b091a6d924408a2e1f94a05e1", + "BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019", + "BLA_GPU_FAST_FRAME_WGSL": "2e4cdbd31d60c22d87fb04847af4d2c1d6de7d213da5cdd27234e5d8f9672553" +} \ No newline at end of file diff --git a/tests/SCHEDULER_SYNC_MODEL.json b/tests/SCHEDULER_SYNC_MODEL.json new file mode 100644 index 0000000..8c02ccd --- /dev/null +++ b/tests/SCHEDULER_SYNC_MODEL.json @@ -0,0 +1,37 @@ +[ + { + "iter": 900, + "baselineRows": 31, + "v53Waits": 31, + "v54FastWaits": 8, + "v54BlaWaits": 2 + }, + { + "iter": 1500, + "baselineRows": 19, + "v53Waits": 50, + "v54FastWaits": 13, + "v54BlaWaits": 4 + }, + { + "iter": 3000, + "baselineRows": 9, + "v53Waits": 105, + "v54FastWaits": 27, + "v54BlaWaits": 7 + }, + { + "iter": 6000, + "baselineRows": 4, + "v53Waits": 235, + "v54FastWaits": 59, + "v54BlaWaits": 15 + }, + { + "iter": 12000, + "baselineRows": 2, + "v53Waits": 470, + "v54FastWaits": 118, + "v54BlaWaits": 30 + } +] diff --git a/tests/legacy-v23/numeric-policy-v23.json b/tests/legacy-v23/numeric-policy-v23.json deleted file mode 100644 index c854853..0000000 --- a/tests/legacy-v23/numeric-policy-v23.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "format": "mandelbrot-numeric-policy-v1", - "id": "numeric-policy-v23", - "rendererVersion": 23, - "pixelContract": { - "mapping": "centered-rational", - "fixedPointRounding": "nearest-ties-away-from-zero", - "fixedPointIntegerMismatchMax": 0, - "float64CoordinateUlpMax": 4, - "tileSeamPixelMismatchMax": 0 - }, - "validated": { - "falseEscapedMax": 0, - "falseInteriorProvenMax": 0, - "unresolvedMax": 0, - "escapeIterationAbsMax": 0, - "magnitudeRelativeErrorMax": 1e-12, - "referenceGuardBits": 64 - }, - "balanced": { - "falseEscapedMax": 0, - "falseInteriorProvenMax": 0, - "interiorLikelyMismatch": "record", - "unresolved": "record" - }, - "color": { - "smoothCorrectionRelativeErrorMax": 0.00003, - "simdScalarRelativeErrorMax": 1e-7 - } -} diff --git a/tests/scenes.json b/tests/scenes.json deleted file mode 100644 index 2d3001b..0000000 --- a/tests/scenes.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "format": "mandelbrot-scene-corpus-v2", - "rendererVersion": 23, - "pixelContract": "centered-rational-ties-away-from-zero", - "numericPolicy": "numeric-policy-v23", - "scenes": [ - { - "id": "z0", - "re": "-0.5", - "im": "0", - "span": "3.4", - "coordinate": {"kind":"exact-rational","reNumerator":"-1","reDenominator":"2","imNumerator":"0","imDenominator":"1"}, - "oracle": {"kind":"overview","membership":"mixed"}, - "tags": ["shallow","overview"] - }, - { - "id": "period2-cusp-z14", - "re": "-0.75", - "im": "0", - "span": "3.4e-14", - "coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"}, - "oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"}, - "tags": ["deep","boundary","parabolic"] - }, - { - "id": "swirly-seahorses-z12", - "re": "-0.7453983606667815", - "im": "0.1125046349959942", - "span": "3.52e-12", - "coordinate": {"kind":"exact-rational","reNumerator":"-7453983606667815","reDenominator":"10000000000000000","imNumerator":"1125046349959942","imDenominator":"10000000000000000"}, - "oracle": {"kind":"published-performance-scene","membership":"mixed","iterations":2000,"source":"https://github.com/ckormanyos/mandelbrot#swirly-seahorses-and-mini-mandelbrot"}, - "tags": ["deep","boundary","performance"] - }, - { - "id": "period2-cusp-z20", - "re": "-0.75", - "im": "0", - "span": "3.4e-20", - "coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"}, - "oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"}, - "tags": ["deep","boundary","warm-reference","parabolic"] - }, - { - "id": "period2-cusp-z100", - "re": "-0.75", - "im": "0", - "span": "3.4e-100", - "coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"}, - "oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"}, - "tags": ["deep","precision","parabolic"] - }, - { - "id": "period3-interior", - "re": "-0.122561166876653619975245551820735654052", - "im": "0.744861766619744236593170428604392367240", - "span": "1e-8", - "coordinate": {"kind":"decimal","significantDigits":39,"guardDigitsAtViewport":27}, - "oracle": {"kind":"polynomial-root","polynomial":"c^3+2c^2+c+1","membership":"interior","period":3}, - "tags": ["interior","periodic"] - }, - { - "id": "period2-cusp-e280", - "re": "-0.75", - "im": "0", - "span": "1e-280", - "coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"}, - "oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"}, - "tags": ["deep","deep-scaled-boundary","parabolic"] - } - ], - "viewports": [ - {"id":"mobile","cssWidth":390,"cssHeight":844,"dpr":3}, - {"id":"desktop","cssWidth":1440,"cssHeight":900,"dpr":2}, - {"id":"4k","cssWidth":3840,"cssHeight":2160,"dpr":1} - ] -} diff --git a/tests/static_regression.py b/tests/static_regression.py new file mode 100644 index 0000000..e48f030 --- /dev/null +++ b/tests/static_regression.py @@ -0,0 +1,38 @@ +from pathlib import Path +import re, subprocess, tempfile, json +ROOT=Path(__file__).resolve().parents[1] +CUR=ROOT/'index.html' +OLD=ROOT/'tests'/'KERNEL_HASHES_V24_2_54.json' +NEW=ROOT/'tests'/'KERNEL_HASHES_V24_2_55.json' + +def scripts(path): return re.findall(r']*>(.*?)',path.read_text(),re.S) + +def syntax(path): + for i,code in enumerate(scripts(path)): + q=Path(tempfile.gettempdir())/f'mandel_v55_check_{i}.js'; q.write_text(code) + r=subprocess.run(['node','--check',str(q)],capture_output=True,text=True) + assert r.returncode==0, r.stderr + +def kernels(path): + code=scripts(path)[0] + q=Path(tempfile.gettempdir())/'mandel_v55_kernel_dump.js' + q.write_text(code+"\nconst crypto=require('crypto');const G=globalThis.MANDEL_WEBGPU_KERNELS;const out={};for(const k of Object.keys(G)){if(typeof G[k]==='string')out[k]=crypto.createHash('sha256').update(G[k]).digest('hex')}console.log(JSON.stringify(out));") + r=subprocess.run(['node',str(q)],capture_output=True,text=True,check=True) + return json.loads(r.stdout.strip().splitlines()[-1]) + +syntax(CUR) +old=json.loads(OLD.read_text()); cur=kernels(CUR); expected=json.loads(NEW.read_text()) +assert cur==expected +changed=[k for k,v in old.items() if cur.get(k)!=v] +assert changed==['version','FAST_PERTURB_WGSL'], changed +html=CUR.read_text() +assert "version:'24.2.55-no-black-hole-async-recovery'" in html +assert 'p.unknownOnly==2u && reason!=0u' in html +assert "fast-coverage-repair-and-color" in html +assert 'reprojectionSafe(rawHistory,2.25,.45)' in html +assert 'scheduleFastNumericRecovery' in html and 'runFastNumericRecovery' in html +assert 'numericFrameComplete()' in html +assert "requestBuild({ctx,snap,iter,width,height,refPixel,safety=1/64})" in html +assert "type:'build'" in html and 'buildOnly=d.type===\'build\'' in html +assert not list(ROOT.glob('index.baseline*.html')) +print('PASS: syntax; only FAST repair gate changed in production kernels; coverage repair, broad temporal fill, async recovery, build-only BLA prep, single HTML') diff --git a/tests/v24-acceptance-contract.mjs b/tests/v24-acceptance-contract.mjs deleted file mode 100644 index 9582170..0000000 --- a/tests/v24-acceptance-contract.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import fs from 'node:fs/promises'; -const s=await fs.readFile(new URL('./webgpu-acceptance.js',import.meta.url),'utf8'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; -must(/renderMode:'fast'/.test(s)&&/fast mode did not select Direct/.test(s),'manual fast acceptance gate missing'); -must(/renderMode:'accurate'/.test(s)&&/accurate mode did not select Deep/.test(s),'manual accurate acceptance gate missing'); -must(/referenceReused/.test(s)&&/pan did not reuse Deep reference/.test(s),'Deep pan reference reuse acceptance gate missing'); -must(/corrected production frame produced UNKNOWN/.test(s),'corrected Deep UNKNOWN gate missing'); -must(/forceDeep:true,correctUnknown:true/.test(s),'forced corrected Deep probe missing'); -must(/exportSmoke\.unresolved!==0/.test(s)&&/exportSmokeAA\.unresolved!==0/.test(s),'corrected export unresolved gate missing'); -console.log(JSON.stringify({status:'pass',checks:6,manualModes:true,panReferenceReuse:true},null,2)); diff --git a/tests/v24-baseline-idle-refinement-model.mjs b/tests/v24-baseline-idle-refinement-model.mjs deleted file mode 100644 index 109f9a5..0000000 --- a/tests/v24-baseline-idle-refinement-model.mjs +++ /dev/null @@ -1,46 +0,0 @@ -import fs from 'node:fs/promises'; -import vm from 'node:vm'; -import crypto from 'node:crypto'; - -const root=new URL('../',import.meta.url); -const script=await fs.readFile(new URL('script.js',root),'utf8'); -const kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; -const sha=s=>crypto.createHash('sha256').update(s).digest('hex'); -const ctx={globalThis:{}};vm.runInNewContext(kernels,ctx);const G=ctx.globalThis.MANDEL_WEBGPU_KERNELS; -const expected={ - DIRECT_F32_WGSL:'59301bf0c5ba3dda4a4055335b32d4a3f84a21991be30f8efc74deb5a5f11ccf', - FAST_PERTURB_WGSL:'03b45713b1bbb7b56e18bd8fdf0f2d8161d7ed25adf02482b41c1038dc16e3b8', - DEEP_PERTURB_WGSL:'2865a015d5c1a9b459085b7044e23563fcc5f0eec5362634249b6604a47e660a', - COLOR_WGSL:'cccac7bfb69c64093c46c3468ff9d685d680ba3a88de0d1ea57168d91a067acf' -}; -for(const [k,h] of Object.entries(expected))must(sha(G[k])===h,`${k} changed from v24.2.18 baseline`); - -const fallback=script.match(/function renderFallback\(.*?\nasync function recolor/s)?.[0]?.replace(/\nasync function recolor$/,''); -must(fallback,'fallback function not found'); -must(sha(fallback)==='689a6f56aedd416b5e512b17ab6a3ddebcf7c73494e0a8a600ce74a06d8073a2','fallback changed from v24.2.18 baseline'); -must(/function maxIter\(\)\{if\(!state\.adaptive\)return state\.baseIter;const z=zoomExp\(\),bonus=Math\.max\(0,Math\.floor\(70\*Math\.sqrt\(z\)\+15\*z\)\)/.test(script),'v24.2.18 maxIter policy changed'); -must(/scheduleIdleRefinement\(snap,iter,decision,token\);return;/.test(script),'fast idle refinement is not scheduled after primary completion'); -must(/presentFrame\(\{scaleX:1,scaleY:1,offsetX:0,offsetY:0\}\).*scheduleIdleRefinement\(snap,iter,decision,token\)/s.test(script),'accurate idle refinement is not scheduled after primary presentation'); -must(/ensureRefinePipelines\(\)/.test(script)&&/idleRefineTimer=setTimeout/.test(script),'refinement is not lazy/idle'); -const initBlock=script.slice(script.indexOf('async initPipelines(){'),script.indexOf('async ensureRefinePipelines(){')); -must(!/LIKELY_QUEUE_WGSL|FAST_LIKELY_REFINE_WGSL|DEEP_LIKELY_REFINE_WGSL/.test(initBlock),'refinement shaders are compiled on primary startup path'); -must(/if\(\(\(fieldMeta\[out\]>>28u\)&3u\)!=FIELD_INTERIOR_LIKELY\)\{return;\}/.test(G.DEEP_LIKELY_REFINE_WGSL),'deep refinement can overwrite non-likely pixels'); -must(/setRefineDeepContext/.test(script)&&/this\.refineDeepCtx\.refsB/.test(script),'idle refinement replaces the primary Deep reference context'); -must(/async function paintIdleRefinement/.test(script)&&/async recolor\(token,iter=state\.fieldView\?\.iter\?\?maxIter\(\)\)/.test(script),'idle refinement does not preserve fieldView baseline color normalization'); -must(!/readMeta\(|readFrameMeta|copyBufferToBuffer\(f\.meta/.test(script.slice(script.indexOf('async function runIdleRefinement'),script.indexOf('function renderFallback'))),'idle refinement performs full meta readback'); -must(/REFINE_STATS_BYTES=16/.test(script),'queue readback is not constrained to 16-byte stats'); -must(/\.0006\*dt/.test(script)&&/\.012\*dt/.test(script),'automatic color speed is not 1/10 of v24.2.18'); - -const baseIter=z=>350+Math.floor(70*Math.sqrt(z)+15*z); -const targetList=(z,base)=>{if(base>=8000)return[];const cap=Math.min(8000,Math.max(base*2,Math.round(1200+300*z))),raw=[Math.min(cap,base*2),Math.min(cap,base*4),cap],out=[];for(const v of raw){const n=Math.max(base+1,Math.floor(v));if(n>base&&(!out.length||n!==out.at(-1)))out.push(n)}return out}; -const batchPixels=(iter,budget)=>Math.max(64,Math.floor(Math.max(64,budget/Math.max(1,iter))/64)*64); -const cssW=1920,cssH=1080,pixelBudget=1572864,dpr=Math.sqrt(pixelBudget/(cssW*cssH)),w=Math.round(cssW*dpr),h=Math.round(cssH*dpr),pixels=w*h; -must(targetList(500,baseIter(500)).length===0,'idle refinement should stop once baseline iteration already exceeds 8000'); -const cases=[]; -for(const z of [6,8,12,20]){ - const base=baseIter(z),targets=targetList(z,base),row={zoomExp:z,baseIter:base,primaryWorstSampleIterations:pixels*base,targets,batches:{guardedIdle:[]}}; - for(const [name,budget] of [['guardedIdle',1800000]])for(const iter of targets){const px=batchPixels(iter,budget),work=px*iter;must(work<=budget,`${name} refinement batch exceeds budget at z${z}: ${work}>${budget}`);row.batches[name].push({iter,pixels:px,worstSampleIterations:work,budget})} - cases.push(row); -} -console.log(JSON.stringify({status:'pass',check:'v24.2.18-baseline-plus-idle-refinement',baselineShaderHashes:expected,representativeDisplay:{css:[cssW,cssH],internal:[w,h],pixels},cases,qualityInvariant:'primary output is v24.2.18; idle refinement always uses guarded Deep, re-checks FIELD_INTERIOR_LIKELY, and never overwrites resolved pixels'},null,2)); diff --git a/tests/v24-bla-model.mjs b/tests/v24-bla-model.mjs deleted file mode 100644 index 19730df..0000000 --- a/tests/v24-bla-model.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import fs from 'node:fs/promises'; -const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'); -const failures=[];if(/buildBla\(|BlaNode|useBla|blaLevels/.test(script+kernels))failures.push('BLA code remains in production');if(!/blaEnabled:false/.test(script))failures.push('sidecar/diagnostics do not declare BLA disabled');if(failures.length)throw new Error(failures.join('; ')); -console.log(JSON.stringify({status:'pass',kind:'bla-disabled-contract',reason:'dense swirly regression demonstrated unsafe f32-quantized BLA classifications',productionBla:false},null,2)); diff --git a/tests/v24-color-relative-model.mjs b/tests/v24-color-relative-model.mjs deleted file mode 100644 index d24efc9..0000000 --- a/tests/v24-color-relative-model.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import fs from 'node:fs/promises'; -const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; -const base=350,deltaCycle=0.001,relativeIteration=0.6; -function effectiveCycle(cycle,iter){return cycle*base/Math.max(base,iter)} -const changes=[350,3500,35000].map(iter=>relativeIteration*iter*(effectiveCycle(0.008+deltaCycle,iter)-effectiveCycle(0.008,iter))); -const spread=Math.max(...changes)-Math.min(...changes); -must(spread<1e-12,'zoom-relative cycle response is not invariant'); -const min=.001,max=.05,N=1000; -const sliderToCycle=v=>min*Math.pow(max/min,v/N); -const ratios=[100,500,900].map(v=>sliderToCycle(v+50)/sliderToCycle(v)); -must(Math.max(...ratios)-Math.min(...ratios)<1e-12,'log slider does not preserve equal multiplicative steps'); -must(/effectiveColorCycle\(iter/.test(script)&&/sliderToCycle/.test(script),'production relative color functions missing'); -console.log(JSON.stringify({status:'pass',check:'zoom-relative-color-cycle',phaseDelta:changes[0],sliderStepRatio:ratios[0]},null,2)); diff --git a/tests/v24-coordinate-format.mjs b/tests/v24-coordinate-format.mjs deleted file mode 100644 index 6642042..0000000 --- a/tests/v24-coordinate-format.mjs +++ /dev/null @@ -1,7 +0,0 @@ -function rd(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q} -function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const[m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits);if(neg)n=-n;return[n,10n**BigInt(places)]} -function fixed(t,b){const[n,d]=decRatio(t);return rd(n*(1n<>BigInt(bits);let s=q.toString().padStart(bits+1,'0');s=s.slice(0,-bits)+'.'+s.slice(-bits);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s} -function parseFixed(s,bits){const[n,d]=decRatio(s);return n*(1n<[s.id,s]));map.set('period2-cusp-e400',{id:'period2-cusp-e400',re:'-0.75',im:'0',span:'1e-400'});const F=Math.fround; -function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}function align(v,a,b){const d=b-a;return d===0?v:d>0?v<>BigInt(-d)} -function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]} -function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}function log2FixedAt(v,b){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-b}function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:F(Math.pow(2,l-exp)),exp}} -function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]} -function direct(cr,ci,b,limit){const bail=4n<bail)return n+1}return limit} -function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[refLen*2]=hr1;hi[refLen*2+1]=hi1;lo[refLen*2]=F(rr1-hr1);lo[refLen*2+1]=F(ri1-hi1);break}}return{hi,lo,refLen}} -function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))} -function perturb(ref,s,w,h,x,y,limit,strict=false){const se=spanME(s),dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w),U=2**-24;let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=1*U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+1*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30),threshold=strict?1e-4:1e-3;return ea<=threshold?{kind:'bounded',n,err:ea}:{kind:'unknown',n,reason:'error-bound',err:ea}}if(m>ref.refLen)return{kind:'unknown',n,reason:'ref'};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+1*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n,err:ea};return{kind:'unknown',n,reason:'escape-uncertain',err:ea}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag=ref.refLen)return{kind:'unknown',n,reason:'ref-end'};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=1*U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n,reason:'range'};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n,reason:'guard'}}} -const cfg=[['period2-cusp-z14',1200],['swirly-seahorses-z12',2000],['period2-cusp-z20',1800],['period2-cusp-z100',2400],['period3-interior',1200],['period2-cusp-e280',2400],['period2-cusp-e400',1600]],w=17,h=11,pts=[]; -for(let y=0;yr.guardMismatch||r.falseEscaped||r.falseBounded))throw new Error('numeric classification failure '+JSON.stringify(report)); -const allUnknownStrict=report.filter(r=>r.mode==='strict'&&r.unknown===pts.length); -if(allUnknownStrict.length)throw new Error('strict mode became all UNKNOWN: '+JSON.stringify(allUnknownStrict)); -console.log(JSON.stringify({status:'pass',kind:'guarded-production-equation-cpu-f32-model-not-real-gpu',thresholds:{balanced:1e-3,strict:1e-4},report,denseRegression:dense},null,2)); diff --git a/tests/v24-deep-sparse-queue-model.mjs b/tests/v24-deep-sparse-queue-model.mjs deleted file mode 100644 index 9f3b0c0..0000000 --- a/tests/v24-deep-sparse-queue-model.mjs +++ /dev/null @@ -1,78 +0,0 @@ -import fs from 'node:fs/promises'; - -await import('../gpu-kernels.js'); -const G=globalThis.MANDEL_WEBGPU_KERNELS; -const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; - -must(!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('bucketState'),'production post-stats perturbation shader contains bucket state'); -must(!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('unknownQueue'),'production post-stats perturbation shader writes the UNKNOWN queue'); -must(!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('atomicAdd(&unresolved.remaining'),'production post-stats perturbation still performs per-UNKNOWN stats atomics'); -must(G.DEEP_BUCKET_HIST_STATS_WGSL.includes('localReasons'),'post-stats histogram lacks workgroup reason aggregation'); -must(G.DEEP_BUCKET_HIST_WGSL.includes('array,8>'),'bucket histogram lacks 8-bin workgroup aggregation'); -must(G.DEEP_BUCKET_HIST_WGSL.includes('(n*8u)/max(1u,p.maxIter)'),'bucket histogram does not use normalized UNKNOWN iteration'); -must(G.DEEP_BUCKET_PREFIX_WGSL.includes('atomicStore(&bucketState[8u+b],total)'),'bucket prefix does not initialize scatter cursors'); -must(G.DEEP_BUCKET_PREFIX_WGSL.includes('atomicStore(&bucketState[16u+b],total)'),'bucket prefix does not retain diagnostic offsets'); -must(G.DEEP_BUCKET_SCATTER_WGSL.includes('atomicAdd(&bucketState[8u+lane],c)'),'bucket scatter lacks workgroup range reservation'); -must(G.DEEP_BUCKET_SCATTER_WGSL.includes('atomicAdd(&localRanks[bucket],1u)'),'bucket scatter lacks workgroup-local rank allocation'); -must(G.DEEP_CORRECT_QUEUE_WGSL.includes('@compute @workgroup_size(64)'),'queued correction is not a 1D 64-lane kernel'); -must(G.DEEP_CORRECT_QUEUE_WGSL.includes('let out=unknownQueue[qi]'),'queued correction does not consume queue indices'); -must(/this\.deepPostStats/.test(script)&&/this\.deepBucketHistStats/.test(script)&&/this\.deepBucketPrefix/.test(script)&&/this\.deepBucketScatter/.test(script)&&/this\.correctQueued/.test(script),'production post-stats/bucket pipelines missing'); -must(/encodeDeepPostStatsNumeric\(encoder,[\s\S]*encodeDeepBucketHistogramStats\(encoder,/.test(script),'production frame order is not post-stats primary -> histogram/stats'); -must(/correctUnknownFrame[\s\S]*encodeDeepBucketPrefix\(e,[\s\S]*encodeDeepBucketScatter\(e,[\s\S]*encodeQueuedDeepCorrection\(e,/.test(script),'production correction order is not prefix -> scatter -> indirect correction'); -must(!/correctUnknownFrame[\s\S]{0,1600}encodeDeepBucketHistogram\(e/.test(script),'production correction redundantly rescans the histogram'); -must((script.match(/dispatchWorkgroups\(Math\.ceil\(w\/64\),h\)/g)||[]).length>=2,'bucket histogram/scatter are not dispatched as 64-pixel row groups'); -must(/dispatchWorkgroupsIndirect\(indirect,0\)/.test(script),'production indirect correction dispatch missing'); -must(/encodeCorrectionNumeric/.test(script),'dense correction path for probes/exports was removed'); - -const FIELD_UNKNOWN=0,ITER_MASK=0x000fffff; -const packUnknown=n=>(n&ITER_MASK)>>>0; -const packKnown=n=>(((1<<28)|(n&ITER_MASK))>>>0); -const bucketOf=(meta,maxIter)=>Math.min(7,Math.floor(((meta&ITER_MASK)*8)/Math.max(1,maxIter))); -let seed=0x9e3779b9; -const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/0x100000000}; -const shuffle=a=>{for(let i=a.length-1;i>0;i--){const j=Math.floor(rnd()*(i+1));[a[i],a[j]]=[a[j],a[i]]}return a}; -const cases=[]; -for(const [w,h,rate,maxIter] of [[1,1,1,350],[17,9,.01,413],[64,64,.05,521],[65,7,.2,613],[257,131,.2,721],[320,180,.8,721],[511,257,0,901]]){ - const n=w*h,meta=new Uint32Array(n); - for(let i=0;i[]); - for(let i=0;i>>28)===FIELD_UNKNOWN)expected[bucketOf(meta[i],maxIter)].push(i); - const counts=expected.map(x=>x.length),offsets=[];let total=0; - for(const c of counts){offsets.push(total);total+=c} - const cursors=[...offsets],queue=new Array(total),groups=[]; - for(let y=0;y[]); - for(let lane=0;lane<64;lane++){ - const x=gx*64+lane;if(x>=w)continue;const out=y*w+x; - if((meta[out]>>>28)===FIELD_UNKNOWN)local[bucketOf(meta[out],maxIter)].push(out); - } - for(let b=0;b<8;b++)if(local[b].length){ - globalReservations++; - const base=cursors[b];cursors[b]+=local[b].length; - const lanes=shuffle([...local[b]]); // workgroup atomic rank order is unspecified. - for(let j=0;ja+x.length,0),`total mismatch ${w}x${h}`); - must(queue.length===total,`queue length mismatch ${w}x${h}`); - must(new Set(queue).size===queue.length,`duplicate queue entry ${w}x${h}`); - for(let b=0;b<8;b++){ - must(cursors[b]===offsets[b]+counts[b],`cursor mismatch bucket ${b} ${w}x${h}`); - const got=queue.slice(offsets[b],offsets[b]+counts[b]).sort((a,c)=>a-c); - const exp=[...expected[b]].sort((a,c)=>a-c); - must(got.length===exp.length&&got.every((v,i)=>v===exp[i]),`bucket membership mismatch b${b} ${w}x${h}`); - must(got.every(i=>bucketOf(meta[i],maxIter)===b),`bucket range contamination b${b} ${w}x${h}`); - } - const workgroups=Math.ceil(total/64),launched=workgroups*64,visited=[]; - for(let qi=0;qis.id==='z0'); const F=Math.fround; -function rd(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q} -function rs(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q} -function dec(t,b){let s=String(t).toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const[m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let dg=(i+f).replace(/^0+(?=\d)/,'')||'0',p=f.length-e;if(p<0){dg+='0'.repeat(-p);p=0}let n=BigInt(dg);if(neg)n=-n;return rd(n*(1n<bail)return n+1}return limit} -function direct(cr,ci,limit){cr=F(cr);ci=F(ci);const U=2**-24,y2=F(ci*ci),x=F(cr-.25),q=F(F(x*x)+y2),lhs=F(q*F(q+x)),rhs=F(.25*y2),margin=F(16*U*(Math.abs(lhs)+Math.abs(rhs)+1));if(lhs4)return n+1}return limit} -const bits=256,re=dec(scene.re,bits),im=dec(scene.im,bits),span=dec(scene.span,bits),W=33,H=21,limit=800;let falseEscaped=0,falseBounded=0,guardMismatch=0,countMismatch=0,tested=0; -for(let y=0;y{if(!ok)throw new Error(msg)}; -function f32Ulp(x){x=Math.abs(Math.fround(x));if(x===0)return 2**-149;if(x<2**-126)return 2**-149;return 2**(Math.floor(Math.log2(x))-23)} -function ratio(center,span,w){return (span/w)/f32Ulp(Math.max(Math.abs(center),span*.75,2**-126))} -must(ratio(-0.7436,3.4e-3,1600)>4,'shallow Direct should remain active'); -must(ratio(-0.7436,3.4e-5,1600)<4,'collapsed f32 coordinate regime should use fast extension'); -must(/fastReferenceBits\(snap\)/.test(script)&&/Math\.max\(96,Math\.min\(snap\.bits,need\)\)/.test(script),'adaptive reduced-precision reference policy missing'); -must(/FAST_PERTURB_WGSL/.test(kernels)&&/render_fast/.test(kernels),'fast perturbation shader missing'); -must(/fastExtended=!deep&&fastNeedsExtended/.test(script),'fast extension routing missing'); -console.log(JSON.stringify({status:'pass',check:'fast-precision-extension',shallowRatio:ratio(-0.7436,3.4e-3,1600),deepRatio:ratio(-0.7436,3.4e-5,1600)},null,2)); diff --git a/tests/v24-geometry-contract.mjs b/tests/v24-geometry-contract.mjs deleted file mode 100644 index 3a0e167..0000000 --- a/tests/v24-geometry-contract.mjs +++ /dev/null @@ -1,6 +0,0 @@ -const must=(x,m)=>{if(!x)throw new Error(m)}; -for(const [W,H] of [[17,11],[1024,768],[16384,9216]])for(const tile of [64,512])for(const sx of [.25,.5,.75])for(const sy of [.25,.5,.75])for(const [tx,ty,lx,ly] of [[0,0,0,0],[tile,0,3,5],[tile*2,tile,7,11]]){if(tx+lx>=W||ty+ly>=H)continue;const gx=tx+lx+sx,gy=ty+ly+sy,fullX=(gx-.5*W)/W,fullY=(.5*H-gy)/W,tiledX=((tx+lx+sx)-.5*W)/W,tiledY=(.5*H-(ty+ly+sy))/W;must(Object.is(fullX,tiledX)&&Object.is(fullY,tiledY),'tile mapping seam')} -const maxFramePixels=8*1048576,fieldBytes=maxFramePixels*4;must(fieldBytes<=128*1024*1024,'default storage binding budget exceeded'); -const oldQueueBytes=maxFramePixels*4,newCounterBytes=32;must(newCounterBytes16384?Math.max(64,Math.round(h/aspect)):w0;must(w<=16384&&h<=16384,'export dimension clamp failed')} -console.log(JSON.stringify({status:'pass',pixelContract:'centered',exportTile:512,strictRetryPass:false,unresolvedCounterBytes:newCounterBytes,maxExportSide:16384},null,2)); diff --git a/tests/v24-index-contract.mjs b/tests/v24-index-contract.mjs deleted file mode 100644 index 8aa2661..0000000 --- a/tests/v24-index-contract.mjs +++ /dev/null @@ -1,9 +0,0 @@ -import fs from 'node:fs/promises'; -const h=await fs.readFile(new URL('../index.html',import.meta.url),'utf8'); -const req=['id="view"','id="renderMode"','>高速<','>正確<','id="processMode"','id="palette"','id="cycle"','id="shift"','id="colorAuto"','id="exportDialog"','mandelbrot-bundle','data-bundle="gpu-kernels"','data-bundle="app"']; -for(const x of req)if(!h.includes(x))throw new Error('missing '+x); -for(const banned of ['詳細設定・正確な座標','診断情報','id="backendBench"','id="sparseBench"','id="benchmarkDialog"','id="sparseDialog"','id="coordReInput"'])if(h.includes(banned))throw new Error('obsolete UI remains: '+banned); -if(/{if(!ok)throw new Error(msg)}; -must(/source:\{bits:snap\.bits,re:snap\.re,im:snap\.im,span:snap\.span,iter\}/.test(script),'reference source metadata is not retained'); -must(/newSpan!==oldSpan/.test(script),'span equality reuse guard missing'); -must(/src\.iter!==iter/.test(script),'iteration reuse guard missing'); -must(/pixel\.x<0\|\|pixel\.x>w\|\|pixel\.y<0\|\|pixel\.y>h/.test(script),'reference viewport reuse guard missing'); -must(/referencePixelForSource/.test(script),'selected reference pixel mapping missing'); -const W=800,H=500; -for(const [dx,dy] of [[0,0],[24,0],[-100,30],[399,-249]]){ - // Pure pan: new center = old center - dx*span/W (real), +dy*span/W (imag). - // Mapping old center into new view must recover center+pointer displacement. - const refX=W/2+dx,refY=H/2+dy; - if(refX<0||refX>W||refY<0||refY>H)throw new Error('test vector outside viewport'); -} -console.log(JSON.stringify({status:'pass',check:'pan-reference-reuse-model',vectors:4,policy:'same span + same iter + old reference inside new viewport'},null,2)); diff --git a/tests/v24-png-stream-model.mjs b/tests/v24-png-stream-model.mjs deleted file mode 100644 index f2d29b2..0000000 --- a/tests/v24-png-stream-model.mjs +++ /dev/null @@ -1,40 +0,0 @@ -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-primary-post-stats-model.mjs b/tests/v24-primary-post-stats-model.mjs deleted file mode 100644 index 589c40a..0000000 --- a/tests/v24-primary-post-stats-model.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import fs from 'node:fs/promises'; -await import('../gpu-kernels.js'); -const G=globalThis.MANDEL_WEBGPU_KERNELS; -const script=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; -const ITER_MASK=0x000fffff, REASON_SHIFT=20, FIELD_ESCAPED=1; -const packUnknown=(n,r)=>((n&ITER_MASK)|((r&0xff)<>>0; -const packKnown=n=>((FIELD_ESCAPED<<28)|(n&ITER_MASK))>>>0; -const bucket=(m,maxIter)=>Math.min(7,Math.floor(((m&ITER_MASK)*8)/Math.max(1,maxIter))); -let seed=0x31415926; const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/0x100000000}; -const cases=[]; -for(const [w,h,maxIter,rate] of [[1,1,350,1],[65,7,611,.14],[320,180,667,.18],[257,131,721,.03],[511,23,901,.65]]){ - const meta=new Uint32Array(w*h),direct=new Uint32Array(8),wg=new Uint32Array(8),directBuckets=new Uint32Array(8),wgBuckets=new Uint32Array(8); - for(let i=0;i=w)continue;const m=meta[y*w+x];if((m>>>28)===0){localReasons[0]++;const r=(m>>>REASON_SHIFT)&0xff;if(r>=1&&r<=6)localReasons[r]++;localBuckets[bucket(m,maxIter)]++;}} - for(let i=0;i<8;i++){wg[i]+=localReasons[i];wgBuckets[i]+=localBuckets[i];} - } - must([...direct].every((v,i)=>v===wg[i]),`reason stats mismatch ${w}x${h}`); - must([...directBuckets].every((v,i)=>v===wgBuckets[i]),`bucket stats mismatch ${w}x${h}`); - cases.push({w,h,maxIter,unknown:direct[0],reasons:Array.from(direct.slice(1,7)),buckets:Array.from(directBuckets)}); -} -must(G.DEEP_PERTURB_POSTSTATS_WGSL.includes('fieldMeta[out]=pack_unknown')&&!G.DEEP_PERTURB_POSTSTATS_WGSL.includes('atomicAdd(&unresolved.remaining'),'post-stats primary contract broken'); -must(G.DEEP_BUCKET_HIST_STATS_WGSL.includes('localReasons')&&G.DEEP_BUCKET_HIST_STATS_WGSL.includes('unresolvedStats'),'histogram stats aggregation contract broken'); -must(/ensureDeepBucketState/.test(script)&&/encodeDeepBucketHistogramStats/.test(script),'production integration contract broken'); -console.log(JSON.stringify({status:'pass',check:'primary-post-stats-workgroup-model',workgroupSize:64,cases},null,2)); diff --git a/tests/v24-reference-recovery-model.mjs b/tests/v24-reference-recovery-model.mjs deleted file mode 100644 index 04c2a07..0000000 --- a/tests/v24-reference-recovery-model.mjs +++ /dev/null @@ -1,15 +0,0 @@ -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'); -const must=(ok,msg)=>{if(!ok)throw new Error(msg)}; -function extract(prefix,suffix){const start=source.indexOf(prefix)+prefix.length,end=source.indexOf(suffix,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});self.onmessage({data});if(!message)throw new Error('worker produced no message');if(message.type==='error')throw new Error(message.error);return message} -const bits=256,ONE=1n<<256n,re=ONE/2n,im=0n,span=34n*ONE/10n,iter=700,width=800,height=600; -const deep=runWorker(extract("function referenceWorkerSource(){return String.raw`","`}\nclass ReferenceService"),{type:'build',id:1,key:'deep-select',bits,re:re.toString(),im:im.toString(),span:span.toString(),width,height,iter}); -must(BigInt(deep.referenceRe)!==re,'Deep candidate selection stayed on short-lived center'); -must(deep.selectionEscape===0,'Deep candidate selection did not find a full-length reference'); -must(deep.refLen===iter,'Deep selected reference is not full-length'); -const fast=runWorker(extract("function fastReferenceWorkerSource(){return String.raw`","`}\nclass FastReferenceService"),{type:'build',id:2,key:'fast-select',sourceBits:bits,targetBits:128,re:re.toString(),im:im.toString(),span:span.toString(),width,height,iter}); -must(BigInt(fast.referenceRe)!==re,'Fast candidate selection stayed on short-lived center'); -must(fast.selectionEscape===0,'Fast candidate selection did not find a full-length reference'); -must(fast.refLen===iter,'Fast selected reference is not full-length'); -console.log(JSON.stringify({status:'pass',check:'reference-recovery-selection',deepReference:deep.referenceRe,fastReference:fast.referenceRe,iter},null,2)); diff --git a/tests/v24-reference-worker.mjs b/tests/v24-reference-worker.mjs deleted file mode 100644 index bcc6cb6..0000000 --- a/tests/v24-reference-worker.mjs +++ /dev/null @@ -1,3 +0,0 @@ -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(/renderMode:'fast'/.test(script)&&/manual-accurate/.test(script)&&/manual-fast/.test(script),'manual fast/accurate backend policy missing'); -must(/\$\('#renderMode'\)\.onchange/.test(script)&&/mandelbrot\.renderMode/.test(script),'manual render mode control/persistence missing'); -must(!/ensureBackendProbe|scheduleBackendProbeAfterPaint|backendProfiles|backendViewProbes|runCrossoverBenchmark|runFixed96Experiment|__MANDEL_DIAG__/.test(script),'obsolete diagnostic/router system remains'); -must(!/DIRECT_DS_WGSL|DIRECT_DS_GUARDED_WGSL|DIRECT_FIXED96/.test(kernels),'diagnostic Direct DS/Fixed96 kernels remain'); -must(/正確 perturbation|DEEP_PERTURB_WGSL/.test(script+kernels)&&/DEEP_CORRECT_WGSL/.test(kernels)&&/correctUnknownFrame/.test(script),'accurate perturbation + sparse DS correction path missing'); -must(/DEEP_BUCKET_HIST_WGSL/.test(kernels)&&/DEEP_BUCKET_PREFIX_WGSL/.test(kernels)&&/DEEP_BUCKET_SCATTER_WGSL/.test(kernels)&&/DEEP_CORRECT_QUEUE_WGSL/.test(kernels),'production bucketed Deep queue path missing'); -must(/DEEP_PERTURB_POSTSTATS_WGSL/.test(kernels)&&/DEEP_BUCKET_HIST_STATS_WGSL/.test(kernels)&&/encodeDeepPostStatsNumeric/.test(script),'post-stats Deep primary missing'); -must(/presentTransform/.test(script)&&/前フレーム再利用/.test(script),'pan frame reprojection reuse missing'); -must(/FAST_PERTURB_WGSL/.test(kernels)&&/fastNeedsExtended/.test(script)&&/FastReferenceService/.test(script)&&/manual-fast-extended/.test(script),'fast deep-coordinate precision extension missing'); -must(/reusableDeepReference/.test(script)&&/referencePixel/.test(script)&&/reference再利用/.test(script),'Deep pan reference reuse missing'); -must(/colorAutoStep/.test(script)&&/colorCycleDir/.test(script)&&/colorShiftDir/.test(script)&&/state\.cycle>=cmax/.test(script)&&/state\.shift>=smax/.test(script),'bouncing color auto-slide missing'); -must(/effectiveColorCycle/.test(script)&&/state\.cycle\*base\/effective/.test(script)&&/sliderToCycle/.test(script)&&/cycleToSlider/.test(script),'zoom-relative/logarithmic color-period control missing'); -must(/mark_fast_unresolved/.test(kernels)&&/REASON_REFERENCE_END/.test(kernels)&&/REASON_REBASE_GAP/.test(kernels)&&/@group\(0\) @binding\(4\) var unresolved/.test(kernels),'Fast unresolved accounting missing'); -must(/provisional_unknown/.test(kernels)&&/fillCount>0\.0/.test(kernels),'UNKNOWN visual neighbor fill missing'); -must(/if\(!deep\)\{const cbg/.test(script)&&/if\(!deep\)\[f\.front,f\.back\]/.test(script)&&/const painted=await r\.recolor\(token,iter\)/.test(script),'deferred Accurate color/commit missing'); -must(/chooseReference/.test(script)&&/referenceCandidates/.test(script)&&/selectionEscape/.test(script),'long-lived reference selection missing'); -must(/max-height:calc\(100dvh - 20px\)/.test(dev)&&/overflow-y:auto/.test(dev),'scrollable bottom panel missing'); -must(/select option\{background:#000;color:#fff\}/.test(dev),'black/white dropdown styling missing'); -must(!/詳細設定・正確な座標|診断情報|backendBench|sparseBench/.test(dev),'removed settings/diagnostics UI remains'); -must(/referenceWorkerSource/.test(script)&&/BigInt\(d\.re\)/.test(script),'BigInt reference worker missing'); -must(/AA_RESOLVE_WGSL/.test(kernels)&&/renderTileRGBA2x/.test(script),'GPU 2x2 export resolve missing'); -must(/class StreamingPng/.test(script)&&/CompressionStream\('deflate'\)/.test(script),'streaming PNG export missing'); -console.log(JSON.stringify({status:'pass',shaderVersion:'24.2.26-baseline-idle-refinement',checks:22},null,2)); diff --git a/tests/v24-sparse-correction-model.mjs b/tests/v24-sparse-correction-model.mjs deleted file mode 100644 index 5086afe..0000000 --- a/tests/v24-sparse-correction-model.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import fs from 'node:fs/promises'; -const corpus=JSON.parse(await fs.readFile(new URL('./scenes.json',import.meta.url),'utf8')); -const sc=corpus.scenes.find(s=>s.id==='swirly-seahorses-z12'); -if(!sc)throw new Error('swirly scene missing'); -const F=Math.fround,U=2**-24; -function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length} -function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q} -function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q} -function align(v,a,b){const d=b-a;return d===0?v:d>0?v<>BigInt(-d)} -function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]} -function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x} -function log2FixedAt(v,b){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-b} -function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:Math.pow(2,l-exp),exp}} -function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]} -function direct(cr,ci,b,limit){const bail=4n<bail)return n+1}return limit} -function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b),bail=16n<bail){refLen=n+1;const rr1=fixedNum(zr,b),ri1=fixedNum(zi,b),hr1=F(rr1),hi1=F(ri1);hi[2*refLen]=hr1;hi[2*refLen+1]=hi1;lo[2*refLen]=F(rr1-hr1);lo[2*refLen+1]=F(ri1-hi1);break}}return{hi,lo,refLen}} -function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]} -function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))} -function primary(ref,s,w,h,x,y,limit){const se0=spanME(s),se={mant:F(se0.mant),exp:se0.exp},dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w);let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);return ea<=1e-3?{kind:'bounded',n}:{kind:'unknown',n}}if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n};return{kind:'unknown',n}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag=ref.refLen)return{kind:'unknown',n};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}} -function q(a,b){const x=F(a+b),e=F(b-F(x-a));return[x,e]} -function sum(a,b){const x=F(a+b),bb=F(x-a),e=F(F(a-F(x-bb))+F(b-bb));return[x,e]} -function prod(a,b){const x=F(a*b),ca=F(4097*a),ah=F(ca-F(ca-a)),al=F(a-ah),cb=F(4097*b),bh=F(cb-F(cb-b)),bl=F(b-bh);let e=F(F(ah*bh)-x);e=F(e+F(ah*bl));e=F(e+F(al*bh));e=F(e+F(al*bl));return[x,e]} -function add(a,b){const t=sum(a[0],b[0]);return q(t[0],F(t[1]+F(a[1]+b[1])))} -function neg(a){return[F(-a[0]),F(-a[1])]} -function sub(a,b){return add(a,neg(b))} -function mul(a,b){const t=prod(a[0],b[0]);let e=F(t[1]+F(a[0]*b[1]));e=F(e+F(a[1]*b[0]));e=F(e+F(a[1]*b[1]));return q(t[0],e)} -function scaleDS(a,b){const t=prod(a[0],b);return q(t[0],F(t[1]+F(a[1]*b)))} -function pow2DS(a,e){if(e<-126)return[0,0];if(e>126)return[F(8.507059e37),0];return[F(a[0]*2**e),F(a[1]*2**e)]} -function cmp(a,b){if(a[0]b[0])return 1;if(a[1]b[1])return 1;return 0} -function cadd(a,b){return[add(a[0],b[0]),add(a[1],b[1])]} -function cmulDS(a,b){return[sub(mul(a[0],b[0]),mul(a[1],b[1])),add(mul(a[0],b[1]),mul(a[1],b[0]))]} -function cscale(a,b){return[scaleDS(a[0],b),scaleDS(a[1],b)]} -function cpow2(a,e){return[pow2DS(a[0],e),pow2DS(a[1],e)]} -function mag2(a){return add(mul(a[0],a[0]),mul(a[1],a[1]))} -function val(a){return F(a[0]+a[1])} -function maxabsDS(a){return Math.max(Math.abs(val(a[0])),Math.abs(val(a[1])))} -function correction(ref,s,w,h,x,y,limit){const se=spanME(s),mh=F(se.mant),sm=[mh,F(se.mant-mh)],iw=1/w,iwh=F(iw),inv=[iwh,F(iw-iwh)],ox=F(x+.5-.5*w),oy=F(.5*h-y-.5),dx=scaleDS(inv,ox),dy=scaleDS(inv,oy),d0=[mul(sm,dx),mul(sm,dy)];let d=d0,wv=[[0,0],[0,0]],e=se.exp,n=0,m=0,ops=0;while(true){if(n>=limit)return{kind:'bounded',n};if(m>ref.refLen)return{kind:'unknown',n};const rp=m*2,r=[[ref.hi[rp],ref.lo[rp]],[ref.hi[rp+1],ref.lo[rp+1]]],delta=cpow2(wv,e),z=cadd(r,delta),mg=mag2(z);if(cmp(mg,[4,0])>0)return{kind:'escaped',n};const dm=mag2(delta);if(m>0&&cmp(dm,[0,0])>0&&cmp(mg,dm)<0){if(se.exp-e<-96)return{kind:'unknown',n};wv=z;d=cpow2(d0,se.exp);e=0;m=0;continue}if(m>=ref.refLen)return{kind:'unknown',n};const linear=cscale(cmulDS(r,wv),2),sq=cpow2(cmulDS(wv,wv),e);wv=cadd(cadd(linear,sq),d);m++;n++;ops++;const mm=Math.max(maxabsDS(wv),maxabsDS(d));if(mm>=1e30||!Number.isFinite(mm))return{kind:'unknown',n};if(mm>65536){wv=cscale(wv,1/65536);d=cscale(d,1/65536);e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wv=cscale(wv,65536);d=cscale(d,65536);e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n}}} -const s=snap(sc),w=61,h=39,limit=2000,ref=buildRef(s,limit);let guardMismatch=0,primaryUnknown=0,primaryFalseEscaped=0,primaryFalseBounded=0,corrected=0,remaining=0,correctionFalseEscaped=0,correctionFalseBounded=0,countMismatch=0,maxCountDiff=0; -for(let y=0;y{if(!ok)throw new Error(msg)}; -must(!/高速<\//.test(h)&&/>正確<\//.test(h)&&!/高速(標準)|正確(深部)/.test(h)&&/id="colorAuto"/.test(h),'manual render labels/color auto UI contract failed'); -must(!/診断情報|benchmarkDialog|sparseDialog|coordReInput/.test(h),'removed diagnostic/exact-coordinate UI still embedded'); -must(/ diff --git a/tests/webgpu-acceptance.js b/tests/webgpu-acceptance.js deleted file mode 100644 index 483df56..0000000 --- a/tests/webgpu-acceptance.js +++ /dev/null @@ -1,21 +0,0 @@ -(()=>{'use strict'; -const out=document.querySelector('#out'),frame=document.querySelector('#app'); -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=[.15,.35,.5,.65,.85].map(t=>Math.min(w-1,Math.max(0,Math.round(t*(w-1))))),ys=[.15,.35,.5,.65,.85].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 wait(ms){return new Promise(r=>setTimeout(r,ms))} -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 wait(50);if(!frame.contentWindow.__MANDEL_TEST__)throw new Error('test hook not available')} -async function numericDeep(api,id,re,im,span,iter){await api.setView({re,im,span,baseIter:iter,adaptive:false,processMode:'standard',renderMode:'accurate'});const st=api.state();if(st.renderMode!=='accurate'||!/正確 reference自動選択|正確 reference再利用/.test(st.engine))throw new Error('accurate mode did not select Deep: '+JSON.stringify(st));const ps=points(st.width,st.height),meta=await api.sampleMeta(ps);let guardMismatch=0,falseEscaped=0,falseBounded=0,unknown=0;for(let i=0;i>>28)&3;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a((m>>>28)&3)===0))throw new Error('forced corrected Deep probe left UNKNOWN'); - await api.setView({re:'-0.75',im:'0',span:'3.4e-20',baseIter:1000,adaptive:false,processMode:'standard',renderMode:'accurate'});const exportSmoke=await api.smokeExportTile({w:48,h:32,strict:true,ss:1}),exportSmokeAA=await api.smokeExportTile({w:48,h:32,strict:true,ss:2});if(exportSmoke.length!==exportSmoke.expected||exportSmokeAA.length!==exportSmokeAA.expected)throw new Error('GPU export tile readback length mismatch');if(exportSmoke.unresolved!==0||exportSmokeAA.unresolved!==0)throw new Error('GPU export smoke left unresolved samples: '+JSON.stringify({exportSmoke,exportSmokeAA})); - const result={status:'pass',kind:'real-webgpu-acceptance',date:new Date().toISOString(),manualModes:true,fastPrecisionExtension:true,panReferenceReuse:true,exportSmoke,exportSmokeAA,report};out.textContent=JSON.stringify(result,null,2);document.title='PASS v24.2.26 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result} -run().catch(e=>{const result={status:'fail',error:String(e&&e.stack||e)};out.textContent=JSON.stringify(result,null,2);document.title='FAIL v24.2.26 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result}); -})(); From 83b874c2a07d03a8b9bbf82eee9aec2d64b90ec2 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Tue, 25 Aug 2026 21:33:45 +0900 Subject: [PATCH 3/3] b --- docs/OPTIMIZATION_PLAN.md | 579 +++++++++++++++++++++ index.html | 733 ++++++++++++++++++++------- tests/KERNEL_HASHES_V24_3_0.json | 39 ++ tests/OPTIMIZATION_BENCHMARK.json | 31 ++ tests/edge_cdp_gate.mjs | 55 ++ tests/optimization_regression.mjs | 306 +++++++++++ tests/optimization_webgpu_corpus.mjs | 122 +++++ tests/production_gate.mjs | 65 +++ tests/static_regression.py | 84 ++- 9 files changed, 1831 insertions(+), 183 deletions(-) create mode 100644 docs/OPTIMIZATION_PLAN.md create mode 100644 tests/KERNEL_HASHES_V24_3_0.json create mode 100644 tests/OPTIMIZATION_BENCHMARK.json create mode 100644 tests/edge_cdp_gate.mjs create mode 100644 tests/optimization_regression.mjs create mode 100644 tests/optimization_webgpu_corpus.mjs create mode 100644 tests/production_gate.mjs diff --git a/docs/OPTIMIZATION_PLAN.md b/docs/OPTIMIZATION_PLAN.md new file mode 100644 index 0000000..a60770f --- /dev/null +++ b/docs/OPTIMIZATION_PLAN.md @@ -0,0 +1,579 @@ +# マンデルブロ集合ビューアー最適化計画 + +対象baseline: `index.html` v24.2.55 (`24.2.55-no-black-hole-async-recovery`) + +実装版: `index.html` v24.3.0 (`24.3.0-optimized-unknown-pipeline`) + +作成日: 2026-08-25 + +## 本番昇格状況(2026-08-25) + +現時点では本番未昇格である。`PIXEL_FRONTIER_PRODUCTION=false` を維持し、候補経路は `?frontier-candidate=1` または自動ゲート実行時だけ有効にする。未検証の active-list engine を Accurate の初期全面描画には使わず、初期描画は bounded strip の既存 Deep kernel、active-list は operation-limit 画素の継続計算だけに限定する。 + +| 条件 | 状態 | 根拠 / 残作業 | +| --- | --- | --- | +| JavaScript 構文、worker 構文、kernel hash | 合格 | `tests/optimization_regression.mjs` | +| 1 dispatch の上限 4,000,000 pixel-iterations | 静的合格 | 初期 Deep、sparse DS、frontier の各契約を回帰テストで固定 | +| Accurate 初期描画が active-list production path を使わない | 静的合格 | `computeFrame()` は bounded `computePerturbFrameTiled()` を使用 | +| UNKNOWN を色で隠さず対象画素を計算する | 静的合格 | operation-limit 全 index を continuation queue に投入。色置換や倍率しきい値は不使用 | +| 早すぎる安定判定の防止 | 候補実装済み | 画素幅から連続的に求めた最低反復数へ到達する前は、2 round 安定でも終了しない | +| exact escape iteration / class mismatch = 0 | 未確認 | 単一ブラウザーセッションで corpus と高精度 oracle を実行する | +| final numeric UNKNOWN = 0 | 未確認 | reset、seahorse、real-axis、報告座標で確認する | +| WGSL error / uncaptured error / device loss = 0 | 未確認 | Windows D3D12 実機を含む最終ブラウザー確認が必要 | +| p50 / p95 性能基準 | 未確認 | 同一セッション A/B を採取し、特に 10^14 付近の初回表示を確認する | +| 通常操作で黒領域が出現・消失しない | 未確認 | 同一近傍の pan / zoom / round-trip hash を確認する | + +`pixelFrontierIterationFloor()` は zoom 倍率で描画方式を切り替えない。`span / canvasWidth` が表す1画素のスケールから最低反復予算を連続的に算出する。これは「少し拡大しただけで未計算領域を黒として確定する」問題を抑えるための下限であり、数学的な集合所属証明ではない。最終的な本番昇格には上表の実機ゲート通過を必須とする。 + +本番判定は `tests/production_gate.mjs` に固定した。候補段階では `node tests/production_gate.mjs preflight`、本番フラグを有効化した後は同じコーパスを再実行して `node tests/production_gate.mjs production` を通す。artifact は同一adapterで採取したbaseline/candidateの対応、異なる2 adapter以上、全12 correctness case、全6 performance caseを各10計測run、3 interaction caseを含まなければ合格しない。10^5と10^8はFAST/Accurateの両方、10^14は800×600でFAST p50 1000 ms・p95 1500 ms未満、Accurate p50 1500 ms・p95 2000 ms未満を追加の絶対条件とする。 + +## 追加修正: 10^14付近の速度とWindows GPU停止 + +実機ログで、Accurate Deepの長い単一dispatchがWindowsのGPU監視時間を超え、`DXGI_ERROR_DEVICE_HUNG`を発生させていたことを確認した。`powerPreference`の警告は原因ではない。 + +- Accurate DeepのActive-list本番化は、正常版との分類経路差になるため撤回する。正確モードはv24.2.55と同じDeep perturbation式へ戻し、1 stripを最大4,000,000 pixel-iterationsへ縮小することでTDRを回避する。 +- Deep/Direct-DS補修はより重い演算なので、1 dispatch相当を最大4,000,000 pixel-iterationsへ制限する。疎Deep補修がこの上限を超える場合も補修を省略せず、2次元タイルのbounded scanですべての数値UNKNOWNを処理する。 +- state/二重queueは最大64 MiBのband workspaceとして再利用し、adapterのbuffer/binding上限を超えない。band間で出力座標を保持するためqueue indexはband-local、meta/smooth indexはframe-globalとする。 +- device loss直後のadapter再作成を1.2秒遅延し、D3D12復旧前の連続した`Device failed at creation`を避ける。 +- FASTの最終フレームから無条件Deep補修を外す。数値UNKNOWNが実際に残った場合だけ局所reference補修と画素精度BigInt補修を同期実行し、数値UNKNOWNが0になるまで新しいframeを公開しない。通常FASTはDeepを待たず、未計算frameも表示しない。 +- 履歴のない深部FASTでは、短いreferenceのpreviewがfinalへ再利用できず二重計算になっていたため、最終品質を1回だけ生成する。既存のstable historyがある操作では従来どおり再投影を即時previewとして使う。 +- 倍率による描画方式の固定閾値は設けない。必要精度を `span / canvasWidth`(複素平面上の1画素幅)から算出し、参照軌道は画素精度+40 guard bit、任意精度補修は同じ精度と+32 bitの二重計算が一致した場合だけ採用する。画素より細かい座標情報は計算しない。数値UNKNOWNは件数上限で打ち切らず、bounded chunkですべて補修してから完成扱いにする。色で未計算を隠す処理や10^100切替は使用しない。 + +### 根本解決: pixel-convergent frontier + +黒領域の膨張には、座標精度不足とは別に「反復上限まで脱出しなかった点を、そのまま集合内部と同じ黒で表示する」問題がある。倍率ごとの固定反復数や固定切替では解決しないため、最終形は次の画素収束方式とする。 + +1. 初回passは現在のadaptive反復数まで全画素を計算する。 +2. `operation-limit` 画素のうち、8近傍に `escaped` がある画素だけをGPU上でfrontier queueへcompactする。黒領域の奥を再計算しない。 +3. frontier画素は保存したperturbation stateからbounded chunkで継続し、新たに脱出した画素だけを更新する。その近傍を次のfrontierへ追加する。 +4. analytic cardioid/bulbまたは検証済み周期性で内部と証明できた画素はfrontierから除外する。単に反復上限へ達しただけの画素は「証明済み内部」へ昇格しない。 +5. 変化した境界のHausdorff距離が1画素未満になり、数値UNKNOWNが0になった時だけframeを完成・history commitする。これにより画素以下の境界情報は追跡しない。 +6. 1 dispatchは既存のpixel-iteration予算内に分割し、操作が入ればgeneration tokenで即時中止する。同一viewの反復延長ではreference/state/queueを再利用し、iteration 0からやり直さない。 + +現在の実装では、この前提となる画素幅基準precision、全残存数値UNKNOWNのbounded補修、未完成frameのpublish禁止までを本番経路へ入れた。frontier continuationは既存のshadow active-listを、実機zero-tolerance corpusとWindows TDR試験に通した後で本番へ昇格する。これが反復上限由来の過大な黒を、全画面高反復の速度低下なしに除く本命経路である。 +- adaptive反復数の64単位上方丸め、BLA既定OFF、同一viewだけのreference再利用は維持する。したがって操作順による黒領域の揺れを抑える決定性は維持し、反復数・誤差guard・分類式は削減しない。 + +## 実装状況(2026-08-25) + +この計画のうち、数値品質を維持したまま静的・モデル検証できる項目をv24.3.0へ実装した。 + +- generationごとにprimary/final/numeric-quiescent/history-commit、submit/sync差分、UNKNOWN数を最大64件記録し、テストhookへprimary/final/numeric-complete待機とmeta/smooth SHA-256取得を追加する。 +- Accurate seedを最大24反復で終了し、未採用画素をreason 1〜5のUNKNOWNとしてDSへ渡す。 +- DSと高精度・高速参照workerで更新後平方を次反復へ引き継ぐ。 +- Direct、Accurate、FAST、Deep、BLAで共通のUNKNOWN post-statsを使い、理由0、1〜5、6を同じ規則で集計する。 +- Deep補正をbucket、prefix、scatter、indirect dispatchの疎queueへ接続し、queue不変条件が崩れた時だけbounded full scanへ戻す。 +- Accurate seed後はUNKNOWN indexだけをcompactし、時間予算に合わせたDirect-DS queue sliceで処理する。selected/enqueued/dispatch/processedまたは範囲不変条件が崩れた場合だけ従来の全画面guard走査へ戻す。 +- failure mapをGPU上で空間タイル集計し、全画面metadata readbackを局所補修ループから除去する。局所referenceは3 workerで並列準備する。 +- failure密度によるFAST→Deep直接昇格、同一viewの検証済みreference再利用、final reference prefetch、BLA table cacheを追加する。近傍viewへのreference流用は黒領域の操作順依存を生じ得るため撤回した。 +- previewの確定済みescape・解析的内点を同一referenceのfinalで再利用し、UNKNOWNだけを再計算する。 +- GPU先行投入を24 ms(直近入力)/40 ms(idle)の予測時間予算で制限する。 +- 初期pipelineをDirect、color、present、UNKNOWN stats、実軸対称copyへ縮小し、Accurate、FAST拡張、Deep、補正、AAを遅延コンパイルする。 +- 最後に残ったreason 1〜5だけをview精度+128 bitと+192 bitで再計算し、両者のescape反復とf32 smoothが一致した結果だけを採用する。 +- 中心虚部が厳密に0の通常画面では、共役対称性により上半分だけを計算して下半分へmetadata/smoothを複製する。 +- BLAはoperation削減率だけでなく、直近baseline、table build、probeを含む推定net時間が5%以上改善する場合だけ有効化する。 +- BLAの不安全分岐は0反復目restartではなく最後の正確checkpointからFAST recurrenceを再開する。 +- exportはadapter limitに応じた256/512 px tileと2〜3個のstaging workspaceを使い、GPU/readbackを重ねる。PNGのrow順は維持し、UI yieldは24 ms経過時だけ行う。 +- UNKNOWN/queue statsはframe単位の3-buffer readback ringへ移し、安定buffer/textureのbind groupをcacheする。 +- stable historyは全画面copyを廃止し、3枚のcolor textureをfront/back/history/spareとして役割回転する。 +- Direct/Deepのactive-list continuation、indirect compaction、FAST/Deepの証明付き内点判定候補をshadow-onlyで追加した。Deep continuationも品質回帰の切り分けを優先して本番採用を撤回した。内点候補は外向き丸めのf32区間演算を使い、候補indexだけを画素精度+40/+72 bit oracleで確認する。 +- 操作ごとに黒領域が揺れる問題への安定化として、adaptive反復数を品質を下げない64反復単位の上方丸めにし、近傍view間の参照軌道流用を廃止した。FASTで数値UNKNOWNが発生した場合は、同一viewの局所referenceと画素精度BigInt補修を完了してから表示し、未完成frameをpresent/commitしない。 + +`tests/optimization_regression.mjs` は、2本のscript構文、36 string/kernel hash、旧新BigInt軌道2,000例、旧新DS軌道600例、Direct active chunk分割1,000例、任意精度worker構文を検査する。この環境ではブラウザ接続とWebGPU adapterを利用できなかったため、実GPU WGSL compile、画面hash、KPI実測は未実施である。 + +`tests/OPTIMIZATION_BENCHMARK.json` のBigInt worker modelでは、240点、700反復、192〜320 bit、7 runの中央値が201.5509 msから115.3558 msになり、42.77%短縮した。この値は参照軌道の平方再利用だけを測るもので、ブラウザや実GPUのend-to-end改善率ではない。 + +BLA replayコードは保持しているが、実測時間で数値backendが揺れることを防ぐためproduction既定値をOFFに戻し、明示的なテストopt-inだけを許可する。export ring、readback/bind cache、history role rotationはproduction経路へ接続済みである。Active-listと証明付き内点判定は`productionEligible:false`のshadow-onlyへ戻し、本番Deepは正常版の式を小さなstripで実行する。 + +### 要件別の実装証拠 + +| 計画項目 | 現在の実装 | 検証証拠 | +| --- | --- | --- | +| P0-1 seed打切り | `DIRECT_ACCURATE_SEED_WGSL` が24反復でreason 2へ移行 | kernel hash、source invariant | +| P0-2 平方再利用 | Direct DS、Deep/Fast reference worker、独立verifyで更新後平方を保持 | BigInt 2,000例、DS 600例の旧新全state一致 | +| P0-3 計測 | generation別primary/final/numeric-quiescent/history、submit/sync、UNKNOWN理由を記録 | `waitForPrimary/Final/NumericComplete`、`fieldHashes`、`gpuDiagnostics` | +| P1-1 疎queue | Deep bucket/prefix/scatter/indirectとAccurate Direct-DS compact queue | selected/enqueued/dispatch/processed不変条件、異常時full scan | +| P1-2 補修routing | failure密度によるFAST→Deep、GPU reason tile map、局所reference | source invariant、reason別post-stats | +| P1-3 実体cache | reference prefix/checkpoint、BLA table、同一pending requestを再利用 | key/coalescing/cancel source invariant | +| P1-4 preview再利用 | 同一view/referenceのknown pixelを保持しUNKNOWNだけfinal再計算 | `unknownOnly` とreference key guard | +| P1-5 時間予算 | 24/40 ms予測budget、adaptive strip/burst、queued DS slice | scheduler source invariant | +| P2-1 failure map | GPU tile集計、3 workerのlocal reference並列準備 | recovery経路に全画面meta readbackなし | +| P2-2 BLA replay | 最後の正確anchorからbaseline recurrenceを再開。production既定OFF、明示opt-inのみ | 7 fallback分岐、zero restartなし、kernel hash | +| P2-3 固定費 | lazy pipeline、3-buffer stats readback、bind cache、history role rotation | 1,000回のtexture role model、source invariant | +| P2-4 export | 256/512 tile、2〜3 staging slot、ordered `allSettled`、24 ms yield | export構造回帰、SS1/SS2 smoke hook | +| P2-5 任意精度 | reason 1〜5を画素精度+40/+72 bitで評価して一致時のみpatch。全残存画素をbounded chunkで処理 | worker構文、受理条件source invariant | +| P3-1 active-list | Direct/Deep state、二重queue、indirect compaction、preview→final resume | Direct chunk 1,000例、GPU zero-tolerance shadow hook | +| P3-2 証明付きskip | 厳密実軸対称copy、外向きf32区間によるcardioid/bulb候補 | 画素精度+40/+72 bit oracle shadow、`productionEligible:false` | + +`tests/optimization_webgpu_corpus.mjs` は上表の実GPU証拠を収集するdriverである。7 correctness case、4 performance scenario、SS1/SS2 exportに加え、近接2 viewと同一view再訪からなるinteraction stability sequenceを定義する。WGSL error、uncaptured error、device loss、coverage UNKNOWN、最終numeric UNKNOWN、history commit、field hash、Direct/Deep active mismatch、内点oracle mismatchをzero-toleranceで検査し、同一view再訪時のmeta/smooth hash一致も必須とする。numeric recoveryが完了しないcaseも即失敗とする。 + +## 結論 + +品質を落とさずに改善できる可能性が高い順序は、次のとおりである。これはprofile前の期待順位であり、Milestone 0のbaseline実測で入れ替える。 + +1. Accurate の f32 seed を24反復で終了させる。現在は「24未満の脱出だけを採用」するにもかかわらず、未採用ピクセルを `maxIter` または脱出まで計算し、その後DSで0反復目から再計算している。 +2. Accurate DSとBigInt参照軌道の反復内で重複計算している更新後の平方を次反復へ引き継ぐ。 +3. すでに存在する Deep UNKNOWN のbucket/queue/indirect補正を本番経路へ接続し、全画面DS補正走査をやめる。 +4. previewで確定した脱出・解析的内点と、同一prefixの参照軌道をfinalへ引き継ぎ、同じピクセルを0反復目から二度計算しない。 +5. 参照軌道とBLA tableを実際の参照点でキャッシュする。同一viewの反復数増加では再利用または延長できるが、近接pan/zoomへの流用は同一view再訪hash gateを通過するまで行わない。 +6. 数値未確定の位置特定を全画面GPU→CPU readbackからGPU側tile集計へ移し、reason別にDS、軌道延長、局所referenceを選ぶ。 +7. GPUへ先行投入する仕事を「strip本数」ではなく予測GPU時間で制限し、キャンセル済みビューの無駄計算を抑える。 +8. 長期的には、一定反復ごとのactive-pixel compactionへ移行し、preview継続、分岐発散削減、細粒度キャンセルを同時に実現する。 + +最初の3項目は、最終的に実行する数値式を変えず、明白な重複または空振り計算だけを減らせる。特に1項目は小さな変更で高い効果が見込まれるため最初に検証するが、実効果は解析的内点、24反復未満の脱出、DS対象ピクセルの比率に依存する。 + +この文書は実装監査に基づく提案であり、ここに記載した性能効果はまだ実GPUで測定した結果ではない。既存の386.9 ms削減値もworker modelであり、canvasを含む実端末フレーム時間ではない。 + +## 目標と非目標 + +目標は次の3点である。 + +- 最終フレームに残る数値未確定ピクセルを最小化する。 +- 入力から新規画像、最終品質、数値補修完了までの時間と、PNG生成時間を短縮する。 +- v24.2.55の解像度、`maxIter`、サンプリング位置、色、数値安全弁を維持し、既知ピクセルをUNKNOWNへ退行させない。 + +速度のためにDPR、反復数、AA sample数、誤差判定の安全余裕を下げることは対象外とする。また、本ビューアーのexport metadataは現在も `membershipCertified:false` であるため、本計画の「品質不変」は数学的な集合所属証明ではなく、現行baselineとの同等性と高精度oracleに対する非退行を意味する。 + +## 用語と計測単位 + +コード上には「計算放棄」という正式な状態名がない。曖昧な集計を避けるため、本計画では次のように分ける。 + +| 区分 | metadata reason | 本計画での扱い | +| --- | ---: | --- | +| 未書込・coverage hole | 0 | バグ検知指標。最終値は必ず0 | +| 数値未確定 | 1〜5 | 「計算放棄ピクセル」の主指標 | +| 反復上限到達 | 6 | 数値故障ではない。失敗率へ加算しない | +| キャンセル済みビューの計算 | metadata外 | GPUへ投入済みだがpublishされない無駄仕事として別集計 | +| 重複計算 | metadata外 | preview→final、seed→DS、primary→recoveryで同じピクセルを再反復した仕事 | + +reason 1〜5は `error-bound`、`escape-uncertain`、`reference-end`、`rebase-gap`、`range` であり、現行の `numericalFailureCount()` と同じ定義である(`index.html:2023-2025`)。reason 6は `maxIter` まで脱出しなかったピクセルで、COLOR passでは不透明な黒になる。数値未確定とは区別する。 + +ピクセル数だけでは、1反復で失敗したピクセルと12000反復後に捨てたピクセルを区別できない。主な性能指標には必ず「pixel-iterationsまたは実際のkernel steps」も併記する。 + +## 現行パイプライン + +```text +入力 + ├─ 操作中: stable historyを再投影 + └─ 操作確定 + ├─ backend選択 + │ ├─ FAST direct f32 → COLOR → PRESENT + │ ├─ FAST perturbation / optional BLA + │ │ └─ coverage repair + COLOR → stats readback + │ │ └─ background recovery予約 → 一次PRESENT + │ │ └─ 60 ms後にlocal refs + │ │ └─ 残存時full Deep + DS + local refs → recolor + │ ├─ Accurate direct: f32 seed → guarded DS → COLOR → PRESENT + │ └─ Accurate Deep: perturbation → stats readback + │ └─ 条件成立時full-screen guarded DS → local refs + │ └─ COLOR → PRESENT + └─ intended contract: 数値未確定が0の時だけstable historyへcommit +``` + +品質面では、未完成frontをhistoryへ昇格させず、通常のpan/zoomでは透明な数値未確定部分を直前のstable frameで補うことが意図された契約である。ただし現行Accurate hybridには未確定を0件と仮定する例外があり、後述の共通post-statsで修正する。最適化後は全backendでこの契約を満たす。 + +## 実装監査で確認した主要ボトルネック + +### 1. Accurate seedが「安価なseed」になっていない + +`DIRECT_ACCURATE_SEED_WGSL` は脱出反復が24未満のピクセルだけを採用するが、loop自体は `maxIter` まで継続する(`index.html:191-200`)。その後、UNKNOWNピクセルはDSで反復0から再計算される(`index.html:1944-1964`)。 + +したがって、解析的に証明できない内点は f32で `maxIter` 回、さらにDSで `maxIter` 回計算される。遅く脱出する境界ピクセルも、f32の脱出まで計算した後にDSで再計算される。既存fuzz契約は「f32 escape iteration < 24」のみを採用するため、seedは24反復に達した時点でUNKNOWNとして終了しても最終DS結果は変わらない(`tests/ACCURATE_SEED_FUZZ.json`)。 + +### 2. 疎なDS補正用GPU queueが存在するが未使用 + +8 bucketのhistogram、prefix、scatter、indirect dispatch、およびqueued DS kernelはすでに実装されている(`index.html:416-800`)。pipelineとencoder helperも起動時に作成される(`index.html:1820-1827`, `1841-1842`, `1863-1867`)。 + +一方、本番の `correctUnknownFrame()` は全画面をrow stripでdispatchし、各strip後に同期する(`index.html:1994-2000`)。shader内で既知ピクセルはearly returnするが、全ピクセル分のinvocation、buffer read、分岐とsubmit/waitは残る。「sparse correction」という名前の判定も、実際には全画面走査を呼んでいる。 + +### 3. previewとfinalが同じ仕事を繰り返す + +historyがないFAST viewではpreviewとfinalの2段階を使う(`index.html:1445-1453`)。しかしfinalではmeta/smoothをclearし、全ピクセルを0反復目から再計算する(`index.html:1897-1939`)。参照軌道cache keyにも `iter` が入るため、previewとfinalで軌道も再構築される(`index.html:1490`, `1513`)。 + +previewで既に脱出したピクセルと解析的に証明された内点は、`maxIter` を増やしても結果が変わらない。同じ参照軌道prefixと同じ数値式を使える場合、これらをfinalで再計算する必要はない。 + +### 4. 局所補修が全画面readbackと直列処理を繰り返す + +`recoverNumericalUnknownTiles()` は各roundでmeta全体をGPUからCPUへ読み戻し、main threadで全画素を走査してtileを作る。最後にも再読込する(`index.html:1986`, `2031-2034`)。その後は、1 tileごとにreference build、GPU correction、stats readbackを順番に待つ。 + +また「広域failureならfull Deepが安い」とする `shouldPromoteFastToDeep()` が定義されているが、呼ばれていない(`index.html:2026-2028`)。実際のFAST background recoveryはfailure密度にかかわらずlocal recoveryから開始し、残ればfull Deepで画面全体を上書きする(`index.html:2050-2073`)。広域failureでは最初のlocal処理が無駄になる。 + +### 5. 参照軌道とBLA tableの再利用範囲が狭い + +Deep/FAST referenceの再利用は同じ `iter` と完全に同じ `span` を要求する(`index.html:2089-2090`)。しかし参照軌道そのものは参照点 `c_ref` に依存し、表示spanには依存しない。参照点が新viewの有効範囲にあり、精度と長さが足りれば、既存の誤差guardを保ったままzoom後にも利用できる。 + +BLA tableも参照軌道から作るが、build keyはview座標・寸法込みであり、近接viewでも再構築される(`index.html:1598`, `1674-1679`, `2110-2114`)。さらに `BlaShadowService.cancelPending()` はPromiseを破棄するだけでworkerをterminateしない。同期的な旧viewのtable build中は、新viewの要求がworker queueで待たされる(`index.html:1594-1600`)。 + +### 6. キャンセルはpublishを止めるが、投入済みGPU仕事は止めない + +token checkにより古い結果のpublishは防いでいるが、WebGPU queueへsubmit済みのcommand bufferはキャンセルできない。現行はDeep 2本、FAST 4本、BLA 8本をまとめて投入してから待つ(`index.html:1897-1925`)。目標strip時間はDeep 18 ms、FAST 22 msであるため、BLAでは理論上かなり長い旧view仕事がqueueに残り得る。 + +`cancelRender()` はJS状態とworker要求を止めるが、GPU queueをpreemptしない(`index.html:2094`)。新viewのcommandは旧viewの仕事の後ろへ並ぶため、無駄計算と入力後latencyの両方に影響する。 + +### 7. 初期化、bind group、history copy、exportに固定費がある + +- 初期化では初期FAST表示に不要なAccurate、Deep、queue、AA pipelineまで一括compileする(`index.html:1811-1834`)。 +- stripごとに同じresource構成のbind groupを作り直す(`index.html:1857-1868`)。 +- stable history更新はfront texture全体をcopyする(`index.html:1982`)。 +- exportは単一readback workspaceでtileを完全直列処理し、各row band後に必ず `requestAnimationFrame` を待つ(`index.html:2276-2289`)。 + +これらは数値品質に触れずに削減できる固定費である。 + +### 8. backend間でUNKNOWN集計が一致していない + +Directはreason 6をmetadataへ書くがstatsを集計しない。より重要なのは、Accurate hybridのDS kernelが `REASON_RANGE` を出し得る一方、orchestrationは完了後に `state.unresolved=0` と無条件設定する点である(`index.html:248`, `2131-2133`)。この場合、数値未確定を見逃してincomplete frameをhistoryへcommitする可能性がある。 + +全backendの最終color前に共通post-statsを通し、reason 0、reason 1〜5、reason 6をmetadata実体から集計する。これは最適化効果を測る前提であると同時に、品質契約の穴を塞ぐ変更でもある。 + +## 期待優先順位付き最適化案 + +| 期待順位 | 案 | 数値未確定 | 生成時間への期待 | キャンセル無駄 | 品質リスク | +| ---: | --- | --- | --- | --- | --- | +| P0 | Accurate seedを24反復で終了 | 同じ | 大(view依存) | 削減 | 低 | +| P0 | DS/BigIntの更新後平方を再利用 | 同じ | 中〜大 | 削減 | 低 | +| P0 | 計測契約とbaselineを固定 | 判定可能にする | 判定可能にする | 判定可能にする | 低 | +| P1 | UNKNOWN統計をpost-pass化 | 同じ | interior-heavyで削減 | 削減 | 低 | +| P1 | 既存の疎queueでDS補正 | 同じか減少 | 大 | 削減 | 低〜中 | +| P1 | failure密度・reason別routing | 減少 | 大 | 削減 | 低 | +| P1 | reference/BLA cache・延長・同一view再利用 | 減少 | 大 | 削減 | 低〜中 | +| P1 | preview確定結果をfinalで再利用 | 同じ | 大 | 削減 | 低〜中 | +| P1 | in-flight GPU時間を動的制限 | 同じ | 操作時に改善 | 大きく削減 | 低 | +| P2 | GPU failure mapと並列local refs | 減少 | 大 | 削減 | 中 | +| P2 | BLA checkpoint replayとnet-time gate | 同じ | BLA viewで改善 | 削減 | 中 | +| P2 | pipeline/export/resource固定費削減 | 同じ | cold load/export改善 | 小幅削減 | 低 | +| P2 | 残存UNKNOWNだけ任意精度fallback | 最終値を0へ | 少数時のみ有利 | 同じ | 中 | +| P3 | active-pixel continuation/compaction | 同じか減少 | hard viewで最大 | 大きく削減 | 高 | +| P3 | 証明付き内点判定・対称性 | 同じ | 対象viewで改善 | 削減 | 中〜高 | + +### P0-1. Accurate seedを `lateThreshold` で打ち切る + +`DIRECT_ACCURATE_SEED_WGSL` の各pixelは次のどちらかだけを行う。 + +- cardioid/period-2 bulbを解析的に証明できれば確定する。 +- 最大24回だけf32反復し、24未満で安全に脱出した場合だけ確定する。それ以外は直ちにUNKNOWNとする。 + +DS passは現在と同じくUNKNOWNを0反復目から計算する。このため最終meta/smoothは現行と同じで、seed側の24回を超える仕事だけが消える。打切りmetadataは後段queueが選べるreason 1〜5(例えば `REASON_ESCAPE_UNCERTAIN`)にし、reason 6やzero sentinelとは区別する。まず単独feature flagで実装し、既存60,000 fuzz、full-frame meta/smooth hash、strip-order testを通す。 + +### P0-2. DSとBigInt軌道で更新後の平方を再利用する + +`DIRECT_DS_CORRECT_WGSL` は、各反復の更新前に `zr²`、`zi²`、`zr*zi` を計算し、更新直後の脱出判定で新しい `zr²` と `zi²` を再計算する。次のloop先頭では同じ新しい平方をもう一度計算する(`index.html:240-246`)。更新後の2平方をstateとして次反復へ渡せば、DS乗算は概ね5回/反復から3回/反復になり、個々の `ds_mul` の入力と演算順は変わらない。 + +Deep/Fast reference workerの `orbit()`、`orbitEscape()`、`verify()` にも、座標更新用と直後のmag用で同じBigInt平方を二度作る箇所がある(`index.html:1479-1483`, `1503-1506`)。丸め済みの更新後平方を次反復へ持ち越す。各変更は旧/新の軌道配列、脱出反復、meta/smoothをbit比較する。 + +### P0-3. 先に計測を直す + +現行の `setView()` は `dirty || rendering` が終わるまでしか待たず、`refinePending` とbackground numeric recoveryを待たない(`index.html:2303`)。`lastRender` だけを比較すると、preview完了と最終補修完了を取り違える。 + +テストhookへ次を追加する。 + +- `waitForPrimary()`、`waitForFinal()`、`waitForNumericComplete()`。 +- generationごとのphase時刻: reference、BLA build/probe、primary、coverage+color、stats readback、DS、local refs、full Deep、recolor、history commit。 +- reason 0、reason 1〜5の内訳、reason 6をprimary/DS/local/finalの各時点で保存。 +- 全backendを共通post-statsへ通し、Accurate hybridで `unresolved=0` を仮定しない。 +- kernel steps: direct、perturbation、DS、BLA、replay、およびキャンセルされたgenerationのsteps。 +- submit、sync wait、readback回数・bytes・時間、reference/BLA cache hit。 +- adapter/browser/実ピクセル数/effective DPR/実効 `maxIter`。 + +`timestamp-query` が利用可能なadapterではGPU timestampを使い、利用できない場合はwall clockと明記する。累積runtime counterではなく、各generationの差分を記録する。 + +### P1-1. Deep/Accurate補正を既存の疎queueへ接続する + +推奨フローは次のとおりである。 + +1. primary Deepはpixelごとのglobal atomicを避けるpost-stats variantを使う。 +2. histogram passでreason 1〜5だけを8個の開始反復bucketへ集計する。 +3. prefixとscatterでUNKNOWN index queueを作る。 +4. `dispatchWorkgroupsIndirect` でqueued `correct_pixel()` だけを実行する。 +5. queue overflow、invalid index、stale entryが1件でもあれば現行full-scanへfallbackする。 + +histogram/prefix/scatter基盤はAccurate f32 seed後のDSにも使う。ただし既存queued kernelはDeep referenceを読むperturbation版 `correct_pixel()` であり、絶対座標を使う `DIRECT_DS_CORRECT_WGSL` へそのまま接続してはならない。Direct-DSの数値式をそのまま持つ別のqueued kernelを作る。seedで確定したピクセルはqueueに入らず、DS invocation自体を発生させない。bucket順により、同じ程度の反復で終わるlaneが隣接し、SIMD divergenceも減る。 + +queue構築は全画面を軽く走査するため、UNKNOWNが非常に多い場合はfull-scanの方が速い可能性がある。adapter別の実測から密度crossoverを決め、queue/full-scanを選ぶ。各backendで元kernelと同じper-pixel arithmeticとoutput semanticsを保ち、出力差を許容しない。 + +同時に、primary hot loopからpixel単位のglobal atomic統計を外す。現在はFAST/Deep/BLAのUNKNOWN 1 pixelにつき最大2回、同じcounterへatomic addする(`index.html:268-270`, `339-347`, `1335-1339`)。特にreason 6が多いinterior-heavy viewでは、長い反復の最後に競合が集中する。Deepにはatomicを除いた `DEEP_PERTURB_POSTSTATS_WGSL` とworkgroup集約histogramが既にあるため、まずこれを接続し、FAST/BLAにも同型のpost-pass集計を用意する。meta/smoothは変更しない。 + +### P1-2. failure密度とreasonで補修経路を選ぶ + +一次statsを得た直後に次のroutingを行う。 + +- failureが `max(60000, 8% of pixels)` 以上なら、既存 `shouldPromoteFastToDeep()` を使ってlocal-firstを省き、full Deepへ直行する。 +- `error-bound` / `escape-uncertain` はまずqueued DSを使う。 +- `reference-end` は「cache prefixが要求iterより短い」場合と「reference自身が脱出して `refLen=escape` になった」場合を分ける。前者だけ同一軌道を延長し、後者は原則として非脱出またはより長く安定する新referenceを選ぶ。脱出後の軌道を無条件に延長しない。 +- `range` / `rebase-gap` は失敗領域に近いlocal referenceを優先する。 +- reason 0は数値補修へ混ぜず、coverage契約だけで処理する。 + +各経路で `recovered / attempted` とmsを記録し、近接viewでは前回の成功経路をpriorとして使う。ただし結果が残る場合は現在と同じDeep/DS/local fallbackを最後まで保持する。 + +### P1-3. 参照軌道とBLA tableを実体ベースで再利用する + +reference cache keyを「要求view」から次の実体へ変更する。 + +- 実際に選択された `referenceRe/referenceIm`。 +- precision bits。 +- orbitの最長計算済み長さ。 +- kernel format/version。 + +短い `iter` 要求にはprefixを返し、長い要求にはworker内に保持したBigInt状態から延長する。preview→finalで同じ軌道を作り直さない。 + +参照点選択でも二重計算を避ける。現在は `orbitEscape()` で候補を評価した後、選ばれた非中心候補を `orbit()` でもう一度0から計算する。候補評価時の最良軌道を保持すれば、選択軌道の再走査を省ける。 + +一方、Deepの独立 `verify()` は通常modeでも外さない。新規orbitは従来どおり検証し、検証済みimmutable orbitをcacheから再利用する時だけ検証結果も再利用する。第一pass中のhashだけへ置き換える案は独立guardの弱体化になるため、別実装の同等以上の検証器を用意できるまで採用しない。 + +再利用判定では `newSpan === oldSpan` を必須にせず、参照点が新view内または安全margin内にあること、精度が十分なこと、orbit長が足りることを確認する。Deepの誤差bound、reference-end、checkpoint検証は残す。guardが不利と判断した場合だけ新しい参照点を作る。 + +BLA tableは `reference buffer/hash + refLen + safety + table format version` でcacheする。viewごとに必要なのはtable再構築ではなく、pixel分布に対する採用probeだけである。旧view buildのキャンセルは、ReferenceServiceと同様のworker terminate/recreate、またはchunk化した協調キャンセルで実際のCPU仕事も止める。 + +### P1-4. previewの確定結果をfinalへ引き継ぐ + +最初の段階ではorbit stateを保存せず、次だけを再利用する。 + +- `FIELD_ESCAPED`: finalの `maxIter` を増やしても最初の脱出反復とsmooth値は変わらない。 +- `FIELD_INTERIOR_PROVEN`: classとsmoothは変わらないが、packed iteration部はpreviewの `maxIter` になっている。final前の軽量patch passで `pack_meta(finalIter, FIELD_INTERIOR_PROVEN)` へ更新してから再利用する。 +- previewのreason 6: finalで再計算が必要。 +- 数値未確定reason 1〜5: finalまたは補修で再計算が必要。 + +Directはfinal kernelへ `unknownOnly` を加えるだけで実現できる。perturbationでは、previewとfinalが同じ最終長referenceのprefix、同じ参照点、同じpixel latticeを使う場合だけ既知pixelを再利用する。条件が1つでも違えば現行full recomputeへ戻す。 + +次段階では、未脱出pixelの `z/w/d/scaleExp/reference index/error` をcheckpointとして保存し、preview末尾からfinalを再開する。ただし浮動小数点演算順を変える可能性があるため、これはP3のactive-list方式と同じ厳しい品質gateを通す。 + +### P1-5. GPU先行投入量を時間予算で制限する + +現在の固定 `burstLimit` を、直近stripのGPU時間EMAから求める。 + +```text +burstLimit = clamp(1, maxBurst, floor(maxInFlightMs / predictedStripMs)) +``` + +初期案として、直近に入力があった時は `maxInFlightMs=24`、十分idleなら40程度から測定を始める。値は品質ではなくsubmit/waitとキャンセルlatencyのtrade-offなので、adapter別に自動調整する。 + +Directの全画面1 dispatchと高反復exportも同じ時間上限でstrip化する。perturbationだけを細分化しても、Directが長時間queueを占有すれば新viewは待たされる。 + +追加で、top-to-bottom固定順ではなく、focus座標と画面中心に近いmacro-tileから処理する。キャンセルされた場合でも、ユーザーが見ている領域に使える仕事の割合が増える。部分表示を行う場合はgeneration stamp付きwork textureを使い、旧generationを絶対にpublishしない。 + +### P2-1. failure mapをGPUで作り、local refsを並列化する + +全meta readbackの代わりに、GPUで固定macro-tileごとの次の小さなsummaryを作る。 + +- reason別件数。 +- `sumX/sumY`、min/max座標。 +- UNKNOWN index rangeまたはcompact queue offset。 + +CPUへ戻すのは数KBのsummaryだけにする。必要なlocal referenceは2〜4 workerのpoolで並列構築し、完成したreferenceごとに1 tileずつ待たず、複数queueをまとめてGPUへsubmitする。補正対象も長方形全体ではなく、そのreferenceへ割り当てたUNKNOWN indexだけにする。 + +近接pan/zoomでは前frameのfailure mapを再投影してcoarse probeの優先候補に使える。ただし最終判定は新frameのguard結果で行う。 + +### P2-2. BLA fallbackを0反復目restartからcheckpoint replayへ変える + +production candidateのBLAは近似chainが不安全になるとbaselineを0反復目から再実行する(`index.html:1320-1323`)。一方、検証用shadow kernelには最後のexact anchorからreplayする実装がある(`index.html:1130-1232`)。このcheckpoint/replayをproductionへ移すと、fallback pixelでBLA以前の仕事を捨てずに済む。 + +BLA採用条件も、現在の `fallback <= 12.5%` と `operation reduction >= 50%` だけでなく、table build、upload、probe、replayを含む予測net msで決める。cache miss時の固定費が節約時間を上回るviewではbaseline FASTを選ぶ。 + +品質gateではclassとescape iterationだけでなくsmooth値と最終RGBAも比較する。approximationが不確かなpixelは必ず既存FAST recurrenceへreplay/fallbackし、BLA由来の新しいUNKNOWNを許さない。 + +### P2-3. 初期化・resource・historyの固定費を減らす + +- cold startではPRESENT、Direct、COLORだけを先にcompileし、FAST extended、Accurate、Deep、queue、AA、診断pipelineは必要時またはidle時にwarm upする。 +- pipeline/frame/contextごとにbind groupとtexture viewをcacheする。 +- 小さなstats readback bufferを毎回create/destroyせずringで再利用する。 +- front/back/historyを3 textureのrole rotationにし、history commit時の全texture copyをなくす。 + +これらはshader結果を変えないため、bit-identical testを要求する。 + +### P2-4. exportをGPU・readback・PNG圧縮のpipelineにする + +現在の単一workspaceを2〜3個のringへ変え、tile N+1のGPU計算、tile Nのmap/copy、前bandのCompressionStream書込を重ねる。PNGのrow順は維持する。 + +各row band後の無条件 `requestAnimationFrame` は、経過時間に応じたyieldへ変える。操作応答性を守る必要がある時だけyieldし、headless/非操作exportでは不要な1 frame待ちを入れない。tile sizeも512固定ではなく、adapter memory limitと目標in-flight時間から選ぶ。 + +2x AAは4 sampleを維持する。サンプル削減は、pixel footprint全体が内点であることを証明できる場合以外は行わない。 + +### P2-5. 最後に残ったUNKNOWNだけを任意精度で解決する + +local referenceにはmode別の `maxRefs` 上限があり、予算を使い切るとreason 1〜5が残り得る(`index.html:2029-2034`)。final Accurate/strict exportでは、残存indexだけをCPU workerの固定小数点または疎なGPU limb演算で `maxIter` まで直接評価し、escapedまたは各backendの現行maxIter metadataへpatchする。Deep/Fastはreason 6、Accurate Direct DSは `FIELD_INTERIOR_LIKELY` という違いを保持する。これにより、画面やtile全体を再計算せずに最終numeric UNKNOWNを0へ近づけられる。 + +これは集合所属の無限反復証明ではない。`maxIter` まで未脱出なら、起点backendの現行分類・metadata policyを変えない。対象が多い場合は先に別reference/full Deepへ戻し、疎な最終手段としてだけ使う。 + +### P3-1. active-pixel continuationへ移行する + +最終的な高反復view向けには、1 pixelを1 dispatchで `maxIter` まで回す方式を次へ置き換える。 + +1. 64〜256反復のchunkだけ実行する。 +2. 脱出・証明済み内点を除き、active pixel indexと継続stateをcompactする。 +3. 次chunkはactive queueだけを実行する。 +4. preview時点のqueue/stateをfinalへそのまま継続する。 + +これにより、早期脱出pixelを後続chunkから除外し、workgroup divergenceを減らし、chunk境界でキャンセルできる。Directでは `z`、Deepでは `w/d/scaleExp/reference index/error` が必要になる。二重queueの論理使用量はactive数に比例するが、通常はworst caseとして全 `P` pixel分のbuffer確保が必要であり、メモリ量もKPIと採用条件に含める。 + +最大の注意点は再開前後で演算順、rescale位置、誤差伝播を変えないことである。baselineとbit-identicalにならない場合は高精度oracleに対する誤差非増加を証明し、既存pathをfallbackとして残す。 + +### P3-2. 証明できる場合だけ計算を省く + +- Deep/FASTでも `c_ref + dc` をdouble-singleまたは区間で再構成し、main cardioid/period-2 bulbの片側安全判定を追加する。 +- center imaginary=0かつsample latticeが厳密に鏡映対称なviewでは、実軸対称性を使って片側のmeta/smoothをcopyする。 +- 周期検出やtile一括fillは、有限精度のepsilon一致だけで内点扱いしない。区間または収縮証明が成立した時だけ確定する。 + +素朴なperiodicity checking、境界だけを調べるrectangle fill、色差だけによるadaptive AAは小さい構造を消す可能性があるため採用しない。 + +## 品質契約 + +最適化を2種類に分ける。 + +### 構造だけを変える最適化 + +seedの早期終了、平方再利用、queue化、post-stats、bind group再利用、export pipelineなど、最終数値式と採用経路を変えない案は次を必須とする。 + +- `fieldMeta`、`fieldSmooth`、最終RGBAがbaselineとbit-identical。 +- known→UNKNOWNが0。 +- final reason 0が0。 +- 数値未確定を含むframeのhistory commitが0。 +- stale generationのpublishが0。 + +### 数値アルゴリズムを変える最適化 + +BLA、continuation、証明付き内点判定に加え、選択referenceや補修経路が変わるcache/routingは次を必須とする。同じreference実体をそのまま再利用するだけなら構造変更としてbit比較する。 + +- baselineで既知のpixelをUNKNOWNへ戻さない。 +- baselineとcandidateの双方が既知なら、classはbaselineおよび高精度oracleと一致する。 +- baseline UNKNOWNをcandidateがescapedまたはreason 6へ解決する差分は改善として許可し、raw classification mismatchへ加算しない。 +- exact同値を主張する経路ではfirst escape iteration mismatchが0。 +- BLA derived UNKNOWNとinternal failureが0。 +- smooth/RGBAのoracle誤差がbaseline以下。単なる画像平均ではなく最大誤差と差分pixel数を保存する。 +- final numeric UNKNOWNは各caseでbaseline以下。baselineが0ならcandidateも0。 +- 異常時は同一viewでbaselineへ自動fallbackする。 + +既存Accurate hybrid記録にはreset viewで2/1200のescape iteration mismatchがあるため、少なくともこれを悪化させず、目標は0とする。SwiftShaderの時間値はcorrectness用の相対参考であり、実GPU性能gateには使わない。 + +## KPI + +画素数を `P = width * height`、reason別件数を `U_r` とする。 + +- `coverage_hole_ppm = 1e6 * U_0 / P`。hard targetはfinalで0。 +- `numeric_unknown_ppm = 1e6 * sum(U_1..U_5) / P`。 +- `operation_limit_ppm = 1e6 * U_6 / P`。情報指標であり失敗率ではない。 +- `recovery_rate = (U_primary - U_final) / max(1, U_primary)`。 +- `recovery_cost_ms_per_1k = (T_recovery_quiescent - T_primary) / recovered_kpixels`。 +- `cancelled_work_ratio = cancelled_kernel_steps / all_kernel_steps`。 +- `duplicate_work_ratio = repeated_kernel_steps / all_kernel_steps`。 + +latencyは最低でも次の5点を分ける。 + +- `T_feedback`: inputから再投影presentまで。 +- `T_primary`: 同一viewの最初の新規frameまで。 +- `T_final`: finalIter frameのpresentまで。 +- `T_recovery_quiescent`: background補修が成功、予算終了、またはfallback完了で停止するまで。数値未確定が残る場合も記録する。 +- `T_history_commit`: 数値未確定0のstable frameがhistoryへcommitされるまで。未確定が残って到達しないrunは、打切り時刻でcensored dataとして扱う。 + +p50、p95、maxを保存し、reference build、BLA build/probe、primary、DS、local refs、readback、color、export compressionの内訳も残す。 + +## ベンチマーク行列 + +### correctness corpus + +- reset: center `(-0.5, 0)`, span `3.4`。 +- seahorse-low: `(-0.743643887037151, 0.13182590420533)`, span `0.1`。 +- cusp-low: `(0.25, 0)`, span `0.85`。 +- classic Seahorse deep: span `3.4e-13`, 2900 iterations。 +- BLAが有利なviewと、BLA probeがOFFになるSeahorse/Misiurewicz hard view。 +- `directPixelRatio` の4直前/直後、32直前/直後。 +- reason 1〜5をそれぞれ誘発する固定seed。 +- 実軸対称、interior-heavy、boundary-heavy、reference-end-heavyの各view。 + +### performance corpus + +- 320×220 / 750 iter: 既存Accurate比較の継続用。 +- 654×690 / 2900 iter: 既存BLA prep scenarioとの接続用。 +- 1920×1080、および対象端末で可能なら4K。 +- 900 / 1500 / 3000 / 6000 / 12000 iter。 +- cold load、warm same view、same-span pan、small zoom、distant jump。 +- FAST/Accurate、BLA forced on/off、連続pan/zoomのcancel storm。 +- export SS1/SS2、balanced/strict。 + +`processMode` はpixel budgetとiteration係数を同時に変えるため、異なるmodeのraw msを直接比較しない。必ず実ピクセル数、DPR、iterを記録する。 + +実機は低電力integrated、現行integrated、discrete GPUを最低構成とし、SwiftShaderはcorrectnessだけに使う。各caseはwarmup後にbaseline/candidateを同一sessionで交互に10〜20回実行し、medianとp95を比較する。 + +## 合格基準 + +### hard quality/safety gate + +- 全caseでfinal reason 0 = 0。 +- known→UNKNOWN、stale publish、不完全history commit = 0。 +- baseline既知pixelのknown→UNKNOWN = 0。双方既知pixelのclass mismatch = 0。 +- baseline UNKNOWNの解決は許可し、その結果が高精度oracleと矛盾しない。 +- exact経路のescape iteration mismatch = 0。 +- final numeric UNKNOWNは全caseでbaseline以下。strict/validate golden corpusの目標は0。 +- reference checkpoint mismatch、strip/full mismatch、WGSL error、uncaptured error、device loss = 0。 +- operation-limitは別集計し、数値未確定削減として数えない。 + +### performance gateの初期値 + +以下は実測後に調整する導入基準であり、達成値ではない。 + +- 対象hard viewの `T_primary` と `T_recovery_quiescent` のp50を20%以上、p95を15%以上短縮。`T_history_commit` 到達runも別集計で非退行とする。 +- 非対象viewでp50 +5%、p95 +10%を超える退行を出さない。 +- failure corpusのprimary numeric UNKNOWN中央値を50%以上、finalを90%以上削減し、各caseでは非増加。 +- BLA enabled viewはbuild+probe込みでbaselineより10%以上速い。BLA rejected/offの追加費用は5%または8 ms以下。 +- 1 burstのp95はFAST 25 ms以下、Deep 22 ms以下、max 50 ms以下を初期目標とする。 +- readback bytes、readback回数、correction sync waitsはbaseline以下。 + +## 実装ロードマップ + +### Milestone 0: 観測とbaseline固定 + +1. generation単位のmetricsと3種類のUNKNOWN集計を追加する。 +2. `waitForFinal()` と `waitForNumericComplete()` をテストhookへ追加する。 +3. meta/smooth/RGBAのgolden artifactと操作traceを作る。 +4. 実GPUでcold/warm/cancel/export baselineを採る。 + +### Milestone 1: 小変更で重複仕事を除く + +1. Accurate seedを24反復で終了する。 +2. Accurate DSとreference workerで更新後平方を再利用する。 +3. 全backendのUNKNOWNを共通post-statsで集計し、Deep/FAST/BLA primaryのpixel単位atomicを外す。 +4. `shouldPromoteFastToDeep()` を実経路へ接続する。 +5. reference/BLA cache keyを実体ベースへ変更し、旧BLA buildを実際にキャンセルする。 +6. stats readback bufferとbind groupを再利用する。 + +各変更を単独flagでA/Bし、bit-identicalと性能gateを確認する。 + +### Milestone 2: 疎queueとpreview再利用 + +1. Deep補正を既存bucket/indirect queueへ接続する。 +2. 同じcompaction基盤と新しいDirect-DS queued kernelをAccurate seed後へ接続する。 +3. previewのknown pixelと最終長reference prefixをfinalへ引き継ぐ。 +4. in-flight時間budgetを導入する。 + +queue異常時は現行full-scan、reuse条件不成立時は現行full recomputeへ戻す。 + +### Milestone 3: recoveryとBLA/exportのpipeline化 + +1. GPU failure summaryとUNKNOWN index queueを実装する。 +2. local reference worker poolとbatched correctionを実装する。 +3. production BLAへcheckpoint replayとnet-time gateを移す。 +4. export staging ringとtime-based yieldを実装する。 +5. strict/export向けの疎な任意精度最終fallbackを実装する。 + +### Milestone 4: active-list engine + +chunk continuation、compaction、証明付き内点判定、厳密対称性をshadow-onlyで導入する。golden full-frame比較、複数adapter canary、kill switchを通過したbackendだけを標準へ昇格する。 + +## 展開とrollback + +- 新経路はすべてfeature flag既定OFFから開始する。 +- 数値kernel/BLA変更は最初にshadow-onlyでbaselineと同時計算する。 +- adapter/browser bucket単位で10%→25%→50%→100%へ段階展開する。 +- hard quality gateはzero toleranceとし、1件でも違反したbucketは即baselineへ戻す。 +- default化後も旧path、kill switch、kernel hashを最低1 release残す。 +- mismatchまたは新しいUNKNOWN reasonを起こしたviewは座標、span、iter、adapter情報と共にgolden corpusへ追加する。 + +## 採用しない近道 + +次は生成時間を短く見せられるが、品質不変の条件に反するため採用しない。 + +- adaptive `maxIter`、DPR、export解像度、AA sample数を下げる。 +- error boundやBLA safetyを緩め、失敗を内点または脱出扱いする。 +- reason 6を「計算放棄」と数え、黒のまま補修済みと見なす。 +- 再投影画像の色が近いという理由だけで、新viewの数値metaを再利用する。 +- 未証明のperiodicity、rectangle border fill、色差ベースAAで内部sampleを省く。 +- BLA/近似経路からbaseline fallbackとgeneration tokenを外す。 + +この順序なら、まず明白な二重計算と未使用の疎処理を取り除き、その後に参照選択とGPU schedulingを改善できる。画質を守る責務は各最適化のfallbackと自動比較に持たせ、速度と数値未確定率のどちらか一方だけを改善したように見せない。 diff --git a/index.html b/index.html index 93170eb..d00eefe 100644 --- a/index.html +++ b/index.html @@ -1,11 +1,11 @@ - + -Mandelbrot Deep Zoom v24.2.46 Stable Frame + Color UI +Mandelbrot Deep Zoom v24.3.0 Optimized UNKNOWN Pipeline