This commit is contained in:
33333-33333 2026-09-29 19:09:04 +09:00
commit 18de4054a7
64 changed files with 29977 additions and 71276 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
# Optional local Edge profile used for GPU smoke tests.
.test-edge/
tests/.browser-smoke.generated.html

24
README.md Normal file
View file

@ -0,0 +1,24 @@
# Mandelbrot Viewer — runtime-cost removal build
Production source of truth is `index.html`.
This build starts from the integrated five-improvement viewer and applies the A/B-supported runtime-cost removals. The numerical architecture remains direct f32 / perturbation + series / sparse recovery / finite continuation / CPU fallback.
## Validation
```bash
node tests/regression.mjs
node experiments/ab/variant_check.mjs index.html
node experiments/ab/supplement.mjs
node experiments/integrated_bench.mjs docs/removal-pass/integrated-bench.json
```
The optional real-browser WebGPU test is `tests/browser_smoke.mjs`; it requires Chromium with a working WebGPU backend.
## Current records
- `docs/removal-pass/IMPLEMENTATION.md` — why each runtime cost was removed or retained
- `docs/removal-pass/results.json` — structural checks, hashes, and A/B evidence
- `docs/removal-pass/integrated-bench.json` — post-removal CPU/surrogate benchmark
Historical investigation records remain under `docs/performance-cost-audit/` and `docs/integrated-improvements/`.

View file

@ -1,28 +0,0 @@
# BLA・監査整理(2026-09-05)
BLAの計算経路、テーブル構築Worker、キャッシュ、GPU probe、shadow比較、canary適用、遅延コンパイル、タイマー、状態表示、切替APIを除去した。
比較専用のDirect/Deep active-list実験と内部点判定実験、未使用の全画面Deep候補、重複した統計シェーダーも削除した。描画完了後に比較計算を開始する処理や、URLから自動監査を起動する処理は残していない。
既存の変更に含まれていたfrontierの本番有効化は維持した。通常のDirect、FAST perturbation、Accurate DS、Deep perturbation、未脱出画素の継続計算、数値UNKNOWNの補修、世代キャンセル、GPU処理量制限、完成画像の履歴保持は維持している。Strictは数値判定の設定として利用できる。
詳細な世代別計測と検証APIは `?test` を指定した場合だけ有効になる。描画時間に応じて処理を分割するための計測と、GPUエラーの報告は通常時も維持する。
旧版別のハッシュ、保存済みベンチマーク、複数の昇格ゲート、重複したPython/JavaScript静的監査は廃止した。検証は `tests/regression.mjs` と `tests/browser_smoke.mjs` にまとめ、現行シェーダーのハッシュだけを保持する。過去の計画・結果はGit履歴から参照できる。
## 回帰テスト
- アプリとシェーダー生成のJavaScript構文。
- 維持した23本のGPUシェーダーが整理前と完全一致。
- Direct/FAST拡張/Accurate DS/Deepの選択と、引数削除後のフレーム振り分け。
- 数値エラーと反復上限画素の分類、既存のGPU strip処理量制限。
- 3種類のWorkerによる参照軌道生成・数値補修。
- 通常起動で検証APIが公開されず、テスト起動で利用できること。
## 実GPU確認
独立した一時プロファイルのEdgeをheadlessモード、320×240で実行。初期表示、深部FAST、Accurate DS、Deepで数値UNKNOWNゼロ、GPUエラーなし、描画完了を確認した。初期表示ではパン往復のmeta/smoothハッシュ一致、通常/2倍サンプリングの出力タイルも確認した。同じSeahorse座標のFASTとDeepでも最終meta/smoothハッシュが一致した。
深部FAST(Seahorse、span 3.4e-13、初期512反復)は継続計算で32768反復まで進み、最終描画に約36.7秒かかった。比較前後の同一条件ベンチマークではないため、速度改善率は主張しない。BLAとは別の継続計算・数値補修の負荷は残っている。
2026-09-06追記:上の「数値UNKNOWNゼロ」は当時のカウンター値であり、全画素の終端誤差検査や厳密な分類の証明ではない。その後のコード精査で、継続経路の目標終端検査と処理量上限に不足を確認した。[改善案](PERFORMANCE_PLAN.md)と[実装設計](PERFORMANCE_IMPLEMENTATION.md)に修正方針・テストの不足範囲を記載した。過去の測定値自体は変更していない。

View file

@ -1,69 +0,0 @@
# 軽量化の実装と確認結果
更新日:2026-09-06。対象:[index.html](../index.html)。[改善案](PERFORMANCE_PLAN.md) と [実装設計](PERFORMANCE_IMPLEMENTATION.md) に基づく変更。
この文書は軽量化実装時点の記録。以後の色相・時間表示の修正と正確モードの削除は[ビューワー修正](VIEWER_FIXES.md)を参照。
## 実装した処理
| 計画 | 変更 | 確認方法 |
| --- | --- | --- |
| A:判定と上限 | 目標の最後の更新後に脱出・誤差・参照範囲を確認する。正常な未脱出は実到達反復数を記録して継続キューへ残す。Direct DSも未脱出をreason 6へ統一した | 実GPUの目標境界・誤差超過・参照末尾テスト |
| A:分割 | DSの適応バッチを毎回400万pixel-iterationsへ制限。幅1行でも上限を超える場合は横に分割する。PNGの重いタイルも分割する | 幅320~3840、350~150000反復の上限・全画素被覆テスト |
| B:同期 | 継続を1~4 passずつ適応投入し、最後の件数コピーをmapする。直前の全queue待ちは省く | 0・1・63・64・65件、奇数/偶数pass、次目標への再投入 |
| B:CPU補修 | 比較済みの高精度結果を次の比較へ使う。最大8軌道を5軌道にする。1~2 Workerへ画素数・反復数・精度に応じて小分けに投入し、完成した結果をまとめてGPUへ渡す | 実Workerの軌道呼び出し数と採用条件を確認 |
| C:状態保持 | 数値補修で正常画素を再初期化しない。metaだけ回復した画素を別に保持し、それらだけ次目標で再計算する。Deepの初期計算も同じ継続stateを作る | キューの保持・コンパクトslot・描画回帰テスト |
| C:参照 | 同一の原点・実精度の軌道をWorkerで末尾延長する。基準軌道と高精度検証軌道は独立したBigInt終端を持つ。GPU側も容量内では末尾だけ転送する | 延長結果と独立再生成のbyte一致、精度変更時のID分離 |
| D:失敗転送 | 既存のGPU histogram/prefix/scatterをタイル補修へ使う。CPU補修へ渡す際も失敗indexのみ読み戻す | タイル内のindex集合をGPU出力と厳密比較 |
| D:結果反映 | CPU結果をGPU scatterで反映する。書き込み前に描画世代を確認し、GPU側でも現在のmetaが数値失敗であることを確認する | 古い世代の拒否、確定画素・反復待ち画素の保持 |
| E:内点 | 主カージオイドと周期2円に矩形全体が入ることをBigIntの整数区間で確認し、該当タイルを継続対象から除く | 256 bitの境界内外・境界をまたぐ矩形、実軸のGPU描画 |
| E:優先順位 | 大きく重い継続ラウンドでは既存境界を先に進め、残りも同じ目標まで必ず進める | 実コントローラーのテストで両グループの目標到達・index全件保持を確認 |
| E:反復予算 | 「反復上限」で画素精度から独立した有限予算を指定できる。自動設定では従来の停止方針を維持する。指定値はURLと表示履歴へ保存する | 初期反復512/4096から指定4096への到達、未脱出全画素のmeta、PNGの予算一致 |
| E:表示とメモリ | 現在の画素格子の暫定結果を公開する。完成履歴へは入れない。疎な候補はコンパクトslot、密な候補は32 byteのstateを使う | 暫定PNG拒否、キャンセル、slotと画素indexの一致 |
## 数値・品質の扱い
数値故障、有限反復での未脱出、証明できた内点を区別する。誤差guardを緩めたり、未確定画素を周囲の色で埋めて成功件数へ算入したりしない。
既定の「自動」では、従来の最低反復予算と安定ラウンド判定を維持した。「反復上限」を指定すると、その有限予算まで必要な全画素を処理して終了する。優先順位は表示順を変えるもので、低優先度の画素を省くものではない。`pixelBits × 256` や安定2回を内点証明とは扱わず、結果には有限予算の方針を記録する。一般周期成分の包含証明や新しい近似加速器は、元の計画どおり研究候補として残している。
参照が実際に脱出した場合や採用精度が変わった場合は、同じstateを無条件には使えない。その場合の再初期化と、少数の補修による不要な全体再初期化は別に扱う。
初期描画用の参照と補修用の参照を分離し、同一のテスト入力では繰り返し描画・FAST/Deepのmetaとsmoothのbyte一致を確認した。独立BigIntサンプルでは脱出分類・脱出反復数を厳密比較し、f32の再構成とlog2を含むsmoothは8 ULP以内を確認する。このサンプル検査を全画素の数学的証明とはしない。
主参照のCPUキャッシュは8 MiB、局所参照は2 Worker×2 MiBを基本上限とし、上限より大きな単独参照は1本だけ保持する。GPU参照は最大3種類。activeの密なstateは32 byte/画素、疎なslotは40 byte/候補とindex対応表を持ち、後者が実際に小さくなる場合だけ使う。密なframeでは既存の画素数上限とdeviceのbinding上限を使い、タイルごとに生存stateを捨てる方式は採らない。
通常の同一解像度PNGは補修済みのframeをコピーする。別解像度・複数sample出力は独立したタイル計算であり、残った未確定sample数を従来どおり出力メタデータへ記録する。任意の出力解像度・座標の数値失敗0は保証していない。
## 検証
検証用Edgeは専用プロファイル、1インスタンス・1タブで直列実行する。常時監査は追加せず、検証APIと詳細計測は `?test` のみ。
```sh
node tests/regression.mjs
node tests/browser_smoke.mjs 9333 all
```
CPUテスト:構文、シェーダー固定値、ルーティング、分割の被覆と上限、Workerの参照延長・精度再利用、内点区間、古い世代の拒否。
GPUテスト:終端判定、queueの全index、コンパクトstate、scatterの対象保護、6つの描画座標、独立BigIntサンプル、パン往復、補修中のキャンセル、PNGの符号化と復号。
最終版で全回帰テストが通過した。640×480の実画面で「反復上限」を4096へ変更し、URLへの保存と再読み込み後の復元も確認した。検証終了後、専用Edgeとそのプロファイルを終了・削除し、残存Edgeプロセスがないことを確認した。
## 測定結果
Intel Gen-9の実GPU、Edge、320×240、標準負荷、自動の反復上限。Seahorseは `re=-0.743643887037151, im=0.13182590420533, span=3.4e-13`、初期512反復。各ケースを1回ウォームアップした後、同じタブで3回測定した。
| ケース | 3回の完成時間 (ms) | 中央値 (ms) | 暫定表示までの中央値 (ms) | 最終反復 | 数値失敗 | 未脱出 |
| --- | --- | --- | --- | --- | --- | --- |
| 初期表示 | 211.2 / 214.4 / 205.0 | 211.2 | 9.8 | 4096 | 0 | 1174 |
| Seahorse FAST | 12002.7 / 11637.2 / 11577.7 | 11637.2 | 55.9 | 32768 | 0 | 1 |
| Seahorse Deep | 11828.2 / 11164.0 / 11153.8 | 11164.0 | 84.7 | 32768 | 0 | 1 |
Seahorseでは最終的に残った1画素も32768反復へ到達しており、数値故障とは別の未脱出である。3回とも数値bufferのhashが一致した。FAST/Deep間でも一致した。
FASTの継続バッチは64~65回、主な数値readbackは約4.00 MB、CPU補修は3618軌道。Deepは64~65回、約4.31 MB、3618軌道。仕事量の集計は入口件数×chunkの上界であり、早期脱出を差し引いた実反復数ではない。初期化157185画素には参照原点を変更した際の再初期化を含む。最終active workspaceは疎な1候補用48 byteだが、これは描画中の最大使用量やframe全体のメモリではない。
[前回記録](CLEANUP.md)のFAST約36.7秒・Deep約41.3秒に対し、今回の同じ解像度・座標の中央値は約11.6秒・11.2秒だった。ただし今回は終端判定と補修も修正しており、旧版との数学的処理条件を完全に固定した速度比較ではない。全座標で同じ倍率の高速化を約束する値ではなく、p95も算出していない。
回帰用の実軸と既知の黒領域は64×48で確認した。独立サンプルはSeahorse FAST/Deep・実軸・黒領域の各12画素。PNGは320×240の全画素を復号比較し、不一致0だった。処理中の操作で古いCPU補修が新しいframeを上書きせず、暫定frameが完成PNGへ入らないことも確認した。

File diff suppressed because it is too large Load diff

View file

@ -1,178 +0,0 @@
# 追加の軽量化・高品質化の実装計画
2026-09-08。B・Cを導入した現在のHTMLを読み直し、3案を検証用コピーで実測した。**本体への追加実装はまだ行っていない。** BLA、反復数削減、精度低下、数値失敗の塗りつぶしは使っていない。
ユーザーの「低性能PCなので、多少表示が重くても問題ない」という方針を反映し、**高画質では細部の改善を優先する**。軽量化で追加計算の負担を減らし、計算完了・キャンセル・パンへの応答を維持する。
## 結論と実装順
| 順序 | 実装候補 | 実測した効果 | 判断 |
| --- | --- | --- | --- |
| 1 | CPU補修の二乗に対する丸めを簡略化 | 深拡大の中央値8.99秒→7.99秒、約11.1%短縮 | 小さな変更として先に導入。ほかの視点の高速化は未確認 |
| 2 | 未脱出画素の継続計算に限り、1 dispatchの仕事量上限を400万→800万へ | 2倍解像度の中間倍率で14.17秒→12.42秒、約12.4%短縮 | 導入候補。1回の処理待ちは長くなるため操作応答を確認する |
| 3 | 「高画質」の縦横倍率を1.5倍→2倍へ | 4倍解像度の参考画像との差が約18~22%減少 | 画質優先で採用候補。計算量・メモリー増を明示する |
案3の計測では案1を併用し、案2の計測では案1+案3を共通の土台にした。**案ごとの短縮率を足し合わせない。** 解像度を上げた版が現行より軽くなった、という結果ではない。
## 読み直して確認した現在の処理
対象は [index.html](../index.html)。基準HTMLのSHA-256は `a0806944469b20152fefafe04a23e158044e127864cd9c0eef9c96894a8f5b4a`。
- `precisionFallbackWorkerSource()` の `pixel()` は、B・C導入後も各反復で3回、符号を扱う汎用の丸め関数を呼ぶ。このうち実部・虚部の二乗は必ず非負。
- `continueOperationLimitActive()` は `min(256, 残り反復, floor(4000000 / active画素数))` を使う。active画素が増えると1回で進む反復数が減り、状態の読み書きと件数の読戻しが増える。
- 1バッチにまとめるdispatch数は最大4。処理時間が10 ms未満なら増やし、32 msを超えたら1に戻す制御が既にある。
- 高画質は縦横1.5倍、画素上限2,359,296、反復上限32,768。`resize()` はさらに端末メモリーとGPUのバッファ・テクスチャ上限を適用する。
- 彩色後のcanvasをCSSサイズへ縮小表示している。旧「精細」の近隣画素を平均する `edgeAA` は通常操作で無効。今回も有効にしていない。
## 測定条件
Windows、Edge 152、Intel Gen-9。ブラウザーが報告するメモリー8 GB、論理プロセッサー8。専用Edge 1インスタンス・1タブで順番に実行し、時間比較中は編集や別テストを停止した。OSのバックグラウンド処理や発熱を完全に固定した測定ではない。
表示領域320×240、devicePixelRatio=1、座標精度448 bit、初期512反復、自動反復増分なし、彩色「昼夜」、色相アニメーションなし。標準画質の上限は16,384反復、高画質・参考画像は32,768反復で固定した。
| 視点 | 中心(実部, 虚部) | 表示幅 |
| --- | --- | --- |
| 初期表示 | -0.5, 0 | 3.4 |
| 深拡大 | -0.743643887037151, 0.13182590420533 | 3.4e-13 |
| 中間倍率 | 同上 | 1e-6 |
各条件1回のウォームアップ後3回測定し、2巡目は比較順を反転した。表は完成時間の中央値、括弧内は最小~最大。完成後の読戻し・ハッシュ・PNG検査は完成時間に含めない。参考画像だけは1回取得した。
## 案1:二乗の丸めを簡略化する
`pixel()` の次の2箇所を変更する。虚数成分の積に対する正負対称の丸めは維持する。
```js
// 現在
zr2 = round(zr * zr);
zi2 = round(zi * zi);
// 候補
zr2 = (zr * zr + half) >> B;
zi2 = (zi * zi + half) >> B;
```
`zr * zr` と `zi * zi` はBigIntの非負整数なので、元の丸め関数の負数分岐へ入らない。関数呼出しと不要な符号判定を省ける。シフト幅・丸め定数・2精度の照合・反復上限を変える必要はない。
標準画質320×240、単位ms:
| 視点 | 現行B+C | 案1 |
| --- | ---: | ---: |
| 初期表示 | 580.5(543.6~585.0) | 641.1(576.1~644.6) |
| 深拡大 | 8,985.2(8,177.0~9,162.4) | 7,989.3(7,711.2~8,793.8) |
| 中間倍率 | 2,899.3(2,799.1~2,998.7) | 3,051.6(2,852.9~3,184.8) |
深拡大は各巡で現行より短く、中央値で11.1%短縮した。初期表示・中間倍率の中央値は長くなっており、全視点での高速化は主張しない。結果はCPU高精度補修が多い場面を狙う判断材料とする。
比較用24描画すべてで、全画素のmeta・smoothのハッシュが一致。分類・脱出反復数・理由コード・連続彩色値のビット列を保持した。数値失敗0、指定上限への到達、PNG復号不一致0も確認した。
## 案2:継続計算の処理単位を増やす
対象を `continueOperationLimitActive()` の上限だけに限定する。従来の `PIXEL_FRONTIER_WORK=4000000` は局所補修などにも使われるため、その定数を一括変更しない。
```js
const ACTIVE_CONTINUATION_WORK = 8000000;
const chunk = Math.min(
256, remaining, Math.floor(ACTIVE_CONTINUATION_WORK / before)
);
```
画素数が多い場合に1回で進む反復数を増やし、途中状態の書戻しとGPU完了待ちを減らす。上限到達・誤差判定・画素の選別は変えない。最大256反復、最大4 dispatchのバッチ、処理時間に応じたバッチ数調整、世代トークンの確認は維持する。
案1+案3を共通の土台にした高画質640×480、単位ms:
| 視点 | 400万/dispatch | 800万/dispatch |
| --- | ---: | ---: |
| 初期表示 | 5,633.2(5,555.8~5,713.4) | 5,815.5(5,737.7~6,016.0) |
| 中間倍率 | 14,169.1(13,688.2~14,839.1) | 12,416.5(11,998.3~12,928.6) |
中間倍率の中央値は12.4%短縮。完了待ちの回数は606/631/701回から419/459/464回へ減り、中央値で約27.3%減った。初期表示は待ち回数が3回のままで、改善していない。
| 中間倍率のバッチ待ち時間 | 400万 | 800万 |
| --- | ---: | ---: |
| 各描画内の95パーセンタイル、3回の値 | 17.6 / 17.7 / 17.9 ms | 26.1 / 24.8 / 23.8 ms |
| 測定3回中の最大 | 28.8 ms | 45.4 ms |
これはGPU投入から件数読戻しまでの壁時計時間で、GPU timestampではない。1バッチには複数dispatchを含む場合がある。**完成時間は短くなるが、1回の処理待ちは長くなる**。これをそのままアプリ全体のキャンセル応答時間とは扱わない。
比較用16描画すべてで全画素ハッシュが一致し、数値失敗0、32,768反復への到達、PNG不一致0を確認した。各dispatchの仕事量も対応する400万/800万以下だった。
正式実装では継続用800万と補修用400万を別々に検査する。既存テストの「400万以下」を一律に「800万以下」へ緩めず、補修側の上限違反を検出できるようにする。
## 案3:「高画質」を縦横2倍へ
実際に計算するサンプル数を増やす。表示済みの隣接画素をぼかす方法は使わない。高速・標準の設定と高画質32,768反復の上限は維持する。
今回の試作は案1を併用し、高画質を倍率2、画素上限4,194,304にした。320×240の表示で、現行480×360から640×480へ増える。**画素数は現行高画質の約1.78倍**。
単位ms:
| 視点 | 現行高画質1.5倍 | 案1+2倍 |
| --- | ---: | ---: |
| 初期表示 | 3,184.7(3,177.8~3,303.0) | 5,448.0(5,270.0~5,515.2) |
| 中間倍率 | 7,573.1(7,278.2~7,746.4) | 13,701.5(13,658.1~13,716.9) |
計算時間は約1.71倍/1.81倍になった。案2を加えた場合の効果は前節の別測定を参照。これらの別実験を混ぜて厳密な総合高速化率を算出しない。
### 画質の数値比較
同じ座標・反復上限で1280×960の4倍解像度画像を作り、各4×4画素をRGB値の面積平均で320×240へ縮小したものを参考画像とした。現行・候補は、UIを隠した実際のブラウザー表示のスクリーンショットを比較に使った。
RGB各成分0~255の差からRMSEを求める。低いほど参考画像に近い。境界付近の指標は、参考画像の4×4サンプル内でいずれかのRGB成分の最大差が32を超える表示画素だけを対象とした。評価対象は初期表示4,346画素、中間倍率17,027画素。
| 視点 | 全体RMSE:現行→2倍 | 誤差減少 | 境界付近RMSE:現行→2倍 | PSNR:現行→2倍 |
| --- | ---: | ---: | ---: | ---: |
| 初期表示 | 6.834→5.341 | 約21.9% | 28.612→22.413 | 31.44→33.58 dB |
| 中間倍率 | 19.455→15.861 | 約18.5% | 41.303→33.671 | 22.35→24.12 dB |
両視点で、全体と境界付近の誤差が減った。比較画像でも細かな点や境界のギザつきが減っていることを確認した。**「画質が何%上がった」という普遍的尺度ではなく、この参考画像との差の減少率**である。
参考画像も有限反復・有限サンプル数の描画であり、集合境界の厳密解ではない。RGB平均は今回の表示比較のための基準で、線形光空間での色再現性を評価したものではない。別の座標・配色・DPRで同じ改善率になるとは限らない。
| 現行高画質 | 案1+2倍 | 4倍解像度からの参考画像 |
| --- | --- | --- |
| ![初期表示・現行](quality-plan-images/reset-current.png) | ![初期表示・2倍](quality-plan-images/reset-high2.png) | ![初期表示・参考](quality-plan-images/reset-reference.png) |
| ![中間倍率・現行](quality-plan-images/mid-current.png) | ![中間倍率・2倍](quality-plan-images/mid-high2.png) | ![中間倍率・参考](quality-plan-images/mid-reference.png) |
参考画像の完成時間は初期表示約20.4秒、中間倍率約166.1秒だった。中間倍率は最初の試行で検証APIの120秒の待機上限に達した。計算条件を変えず、参考画像の観測時間だけ延ばして再取得した。これは通常描画が120秒で停止する仕様という意味ではない。
途中の診断では92万画素以上の未脱出画素が継続計算に残っていた。400万の仕事量上限では、これだけで1 dispatchの反復数が4程度になる。大きい画像で状態の読み書き・往復が増える点が、案2を追加した理由である。4倍表示を通常設定へ追加する計画にはしない。
## 実装時の負荷と画素上限
高画質は倍率2を候補にするが、端末メモリー・GPU制限による縮小を維持する。試作の画素上限4,194,304は、GPUのstorage buffer上限÷40などでさらに制限される。各GPUバッファが上限内でも、プロセス全体のメモリーが小さいことを保証するわけではない。
640×480では設定上限が効かないため、今回確認できたのは倍率変更の効果。**大きい画面で画素上限を引き上げる際のピークメモリーと性能は未測定**。正式実装では1100×720以上、DPR=2、低メモリー条件でも確認する。
画質を優先する方針に従い、多少の時間増は許容する。GPU制限は超えず、完成済み画像の再投影、現在の世代以外の結果の拒否、補修中のキャンセルを保つ。上限の引上げでメモリー圧迫や操作不能が出る場合は、倍率2を維持しつつ画素上限を現行値へ戻す判断をする。近隣色の平均や反復削減で完成扱いにはしない。
## 実装・検証の手順
1. **案1を単独実装。** Workerの二乗2箇所だけを変更し、正負を扱う積の丸めと2精度照合は残す。CPU回帰と現行版とのmeta・smooth全画素一致を確認する。
2. **案2を独立した上限定数で実装。** 継続計算と数値補修の仕事量を区別し、既存のバッチ数調整とトークン確認を残す。高画質の大量active画素、0/1/端数候補、目標反復直前のケースを確認する。色相変化中のパン・ズーム・補修中のキャンセルを実操作で検査する。
3. **高画質の倍率を2へ変更。** 高速・標準、共有URLの3段階、反復予算を維持する。大画面・DPR=2・メモリー上限を検査し、画素数上限を決める。彩色だけの操作で数値再計算が増えないことも確認する。
4. **組合せを再測定。** 実装前後で座標・反復予算・端末・表示サイズを固定する。画質を変えない比較は全画素ビット一致、解像度を変える比較は参考画像との誤差とPNGで評価する。完成時間、処理待ち時間、数値失敗、キャンセル、メモリーを別々に記録する。
通常の確認には `node tests/regression.mjs` と `node tests/browser_smoke.mjs 9333 all` を使う。数値シェーダーを変えない案なので、固定ハッシュを再生成して通す必要はない。新たな詳細計測は検証モード内に置き、通常表示へ監査ループを追加しない。
今回は案の作成と隔離した試作の測定まで。案2を加えた版の全画素一致は確認したが、その版での操作応答・大画面メモリー・全回帰は正式実装時の確認事項であり、確認済みとはしていない。
## 再現方法と証拠
基準HTMLのハッシュが上記と一致する作業ツリーで、専用Edgeのデバッグポート9333と1タブを使う。
```sh
node tests/quality_plan.mjs 9333 all
node tests/active_work_plan.mjs 9333
```
参考画像だけを取り直す場合は、既存JSONと比較画像を保持したまま次を実行する。完成済みの時間比較をやり直さない。
```sh
node tests/quality_plan.mjs 9333 reference
```
- [速度・画質の生データ](NEXT_QUALITY_PLAN_DATA.json):案1の24描画、案3の16描画、参考画像2枚、画像誤差4件。各条件の1回目は `round=-1` のウォームアップ。
- [処理単位の生データ](NEXT_ACTIVE_WORK_DATA.json):案2の16描画と各バッチの仕事量・待ち時間。
- [比較用スクリプト](../tests/quality_plan.mjs)、[処理単位のスクリプト](../tests/active_work_plan.mjs):本体からコピーを作り、変更箇所を限定して比較する。終了時にコピーを削除する。
成功した比較用描画は合計58件(ウォームアップを含む、起動時の描画は除く)。それぞれ数値失敗0、有限上限での完了、PNG不一致0を検査した。同じサンプル位置を使う速度比較では全画素ハッシュが一致した。試験後に検証用Edgeと専用プロファイルを終了・削除し、残存Edgeプロセス0を確認した。

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,134 +0,0 @@
# 品質を保持する追加軽量化の実測
2026-09-07。画質設定を3段階へ統合した `index.html` を基準に、実際のビューワーで候補を比較した。BLAは使用しない。以下の候補比較は導入前の記録。ユーザーの指示を受け、現在はBとCを本体へ導入済み。組合せの実装結果を末尾に追記した。
優先候補は**高精度補修の丸め定数の再利用**。深拡大の完成時間中央値が11.25秒から8.06秒へ28.4%短縮し、計算結果の全画素が一致した。初期表示・中間倍率の高速化は確認できていない。全画面集計の遅延は処理量を減らせるが、今回の完成時間の改善は小さい。
## 比較条件
解像度320×240、標準画質、反復上限16,384、初期512反復、座標精度448 bit、色相アニメーション無効で固定した。画質設定を下げる、反復を減らす、数値失敗を黒へ置き換える方法は今回の軽量化に含めない。
Intel Gen-9、Windows、Edge 152、ブラウザーが報告するメモリー8 GB/論理プロセッサー8。専用Edge 1インスタンス・1タブを再利用した。各候補・各視点を1回ウォームアップし、3回測定した。測定順の一方的な偏りを減らすため、2巡目は候補の順番を反転した。ページを開き直すため参照キャッシュは候補間で共有しない。パイプライン作成・起動時の初期描画は比較用描画の前に済ませる。
PC上の別作業が測定へ干渉する懸念を受け、ユーザーが操作を止めると申し出た後に全比較を最初からやり直した。計測中はエージェント側でも編集・別テストを停止し、測定スクリプトだけを順次実行した。以下の表とJSONはすべて再測定の結果で、前回の値は混ぜていない。OSのバックグラウンド処理を停止・固定した測定ではない。
完成時間はアプリの描画開始から最終GPU処理と数値補修の完了まで。検証用の全画素読戻し・ハッシュ計算・PNG出力は完成後に実施し、完成時間に含めない。処理時間には他プロセス・発熱・Worker実行順による変動がある。3回の中央値と範囲を用い、統計的な優位性や他機種での速度を保証する結果とは扱わない。
| 視点 | 中心(実部, 虚部) | 表示幅 |
| --- | --- | --- |
| 初期表示 | -0.5, 0 | 3.4 |
| 深拡大 | -0.743643887037151, 0.13182590420533 | 3.4e-13 |
| 中間倍率 | 同上 | 1e-6 |
## 候補A:継続計算の反復単位を増やす
`continueOperationLimitActive()` の1 dispatchあたりの反復数上限を256から512/1024へ変更する。候補画素数×反復数は従来の4,000,000以下を維持し、反復到達目標・計算式・補修条件は変えない。候補が少ない段階のGPU投入と件数読戻しを減らせる可能性がある。
一度に処理する反復が増えると、そのGPU処理を途中で止められない時間も増える。大きい値を一律に採用する前に、処理中のパン・ズームと端末別の遅延を確認する必要がある。
完成時間の中央値(括弧内は最小~最大、単位ms):
| 視点 | 現行256 | 512 | 1024 |
| --- | ---: | ---: | ---: |
| 初期表示 | 789.7(610.3~881.4) | 734.3(730.9~805.9) | 749.7(681.9~857.8) |
| 深拡大 | 12,731.1(12,391.3~12,819.7) | 12,593.2(12,397.9~14,137.2) | 12,135.0(12,009.9~13,744.3) |
| 中間倍率 | 3,068.7(2,704.7~3,244.9) | 2,929.0(2,701.9~2,993.0) | 2,680.5(2,676.9~2,790.0) |
**一律変更は保留する。** 1024では中間倍率の中央値が12.7%、深拡大が4.7%短縮した。一方、深拡大の最も遅い回は現行より遅く、すべての表示位置で安定して短くなるとは言えない。候補数による仕事量上限が先に効く段階やCPU補修の負荷は、この定数を上げても解消しない。導入するなら疎な継続段階を対象とし、操作応答の悪化がないことを先に確認する。
全36描画(ウォームアップ9回を含む)でmetaとsmoothのハッシュが一致し、PNG復号の不一致は0。[生データ](PERFORMANCE_CANDIDATES_DATA.json)の `submits`/`syncs` はページ起動以来の累積値であり、視点単独の処理回数としては使わない。
## 候補B:CPU補修後の全画面集計をまとめる
`applyPrecisionFallback()` は少量の補修結果をGPUへ反映するたびに、全画面の未確定画素を集計している。この集計だけを遅延し、`readUnresolvedStats()` が統計を読む直前に最新の画素データから集計する。補修結果のscatterとGPU完了待ちは維持する。数値判定や処理対象の間引きは行わない。
遅延中の集計を必要とする呼出元がないこと、キャンセルやframe交換で別の画像の統計を使わないことが実装上の要点になる。検証用コピーでは対象frameを記録し、読戻し時に同じframeであることを確認する。
完成時間の中央値(括弧内は最小~最大、単位ms):
| 視点 | 現行 | 集計を遅延 |
| --- | ---: | ---: |
| 初期表示 | 748.4(642.9~792.0) | 757.1(721.0~776.8) |
| 深拡大 | 12,037.5(11,928.3~12,410.7) | 11,916.3(11,904.3~12,139.1) |
| 中間倍率 | 2,778.9(2,728.3~2,809.8) | 2,676.2(2,649.3~3,038.9) |
深拡大の全画面集計呼出回数は現行255/244/244回から20/20/20回へ減った。中央値で91.8%の削減。一方、深拡大の完成時間短縮は約1.0%にとどまり、処理量削減率をそのまま高速化率とは扱えない。初期表示と中間倍率の集計回数はそれぞれ11回・18回のままなので、その時間差も集計削減の効果とは断定しない。
**優先度は2番目。** GPU集計の無駄を減らす効果は確認できたが、完成時間を大幅に短縮する主施策にはしない。全24描画(ウォームアップ6回を含む)の品質検査が通過。[生データ](PERFORMANCE_STATS_DATA.json)に視点単独の集計回数 `statsPasses` と処理回数を保存した。
## 候補C:高精度補修の丸め定数をループ外へ出す
CPU高精度補修Workerの `pixel()` は、各反復で3回 `roundShift()` を呼ぶ。現在は同じ精度の計算中も、シフト幅のBigInt変換と丸め用の半単位 `1n << (bits - 1)` を毎回作っている。これを軌道の開始時に1回だけ作り、同じ正負対称の丸め式を使う候補を比較する。
計算精度・反復数・2精度の照合条件は維持する。異なる精度の軌道では定数を作り直すため、精度を上げたときに古い丸め幅を使うこともない。GPUシェーダーと並列Worker数は変えない。
完成時間の中央値(括弧内は最小~最大、単位ms):
| 視点 | 現行 | 定数を再利用 |
| --- | ---: | ---: |
| 初期表示 | 746.8(696.5~824.4) | 761.6(648.6~811.9) |
| 深拡大 | 11,251.4(11,048.5~11,715.3) | 8,055.0(7,684.2~8,119.4) |
| 中間倍率 | 2,851.5(2,659.2~2,930.6) | 3,060.1(2,985.8~3,079.0) |
深拡大は3回とも現行の最速回より短く、中央値で28.4%短縮した。初期表示は約2.0%、中間倍率は約7.3%長く、これらの表示位置で速度改善を主張しない。改善はCPU高精度補修が多い場面を狙うものとする。
補修結果を32 msなどの条件でまとめて反映する既存処理があるため、CPU補修が速くなると反映・集計の回数も変わる。深拡大の集計呼出回数は現行226/236/223回から150/156/127回になった。28.4%はアプリ全体の実測であり、BigIntの丸め命令だけの速度差ではない。
**実装する場合はこの候補を最初に単独導入する。** 正負の丸め式を保ったまま不変量を軌道ループの外へ出せるので、計算品質を落とす必要がない。全24描画(ウォームアップ6回を含む)でmeta・smoothの全画素ハッシュ一致、数値失敗0、PNG不一致0を確認した。[生データ](PERFORMANCE_ROUNDING_DATA.json)を参照。
案作成時点では3案の組合せ効果は未測定だった。特に候補BとCは補修結果反映の回数に関係するため、各短縮率を足し合わせない。導入時は通常の回帰に加え、高画質・大きい画面・補修中のキャンセル・色相変化中のパンを再確認する方針とした。その後のB+C実装と検証は下記を参照。
## 品質の確認方法
候補ごとに完成画像の全画素metaとsmoothのSHA-256を基準版と比較する。分類・到達反復・理由コード・連続彩色値のビット列が一致することを採用条件とする。解像度・反復上限・完成状態・数値失敗0・dispatch仕事量上限も確認し、PNGを復号して表示元のRGBAと一致することを検査する。
これは指定した視点での現行品質の保持を確認する方法であり、マンデルブロ集合の全座標で数学的な正しさを証明する検査ではない。
## 再現
専用ブラウザーをデバッグポート9333で起動し、タブを1つだけにして実行する。
導入前の比較を再現する場合は、変更前のHTMLを取得して第5引数に渡す。履歴の基準コミットは `0c61354a5a27f4680bae22638ec67e0dce34b6a6`。次のコマンドはバイト列を直接保存するため、PowerShellのリダイレクトによる文字コード変更を避けられる。
```sh
node -e "require('fs').mkdirSync('.test-edge',{recursive:true});require('fs').writeFileSync('.test-edge/bc-baseline.html',require('child_process').execFileSync('git',['show','0c61354a5a27f4680bae22638ec67e0dce34b6a6:index.html']))"
node tests/performance_candidates.mjs 9333 .test-edge/chunk-rerun.json chunk .test-edge/bc-baseline.html
node tests/performance_candidates.mjs 9333 .test-edge/stats-rerun.json stats .test-edge/bc-baseline.html
node tests/performance_candidates.mjs 9333 .test-edge/rounding-rerun.json rounding .test-edge/bc-baseline.html
```
スクリプトは基準HTMLのSHA-256、機器情報、座標、ウォームアップと測定の生データを保存する。候補HTMLは `.test-edge/candidates/` に生成し、終了時に削除する。数値結果の不一致が出た場合は成功扱いにせず停止する。終了後は検証用ブラウザーを閉じる。
今回の再測定は比較対象84描画(測定63回、ウォームアップ21回)を完了した。専用Edgeと一時プロファイルは終了・削除済み。基準版のCPU回帰検査も通過している。
## B+Cの実装結果
ユーザーの指示を受け、`index.html` へBとCを実装した。Aの反復単位256は維持している。
Bは補修を反映したframeへ集計待ちのフラグを付け、統計の読戻し直前に一度だけ集計する。試作でrendererがframeを参照していた形から、frame自身がフラグを持つ形へ整理した。補修の反映とGPU完了待ちは維持し、画像交換で待機状態を持ち越さない。Cは軌道ごとにシフト幅と丸め定数を作り、正負の丸め規則を保って再利用する。
反復上限・解像度・数値判定・GPUシェーダーは変更していない。BLAも使用していない。
変更前と組合せ版を同じEdge・同じタブで順番に測定した。各視点1回のウォームアップ後3回、標準320×240/16,384反復の条件は候補比較と同じ。計測中は追加の編集・別テストを停止した。完成時間の中央値(括弧内は最小~最大、単位ms):
| 視点 | 変更前 | B+C |
| --- | ---: | ---: |
| 初期表示 | 661.6(618.5~682.1) | 577.8(572.1~662.5) |
| 深拡大 | 13,321.1(11,383.8~13,439.1) | 9,940.8(7,957.5~12,002.6) |
| 中間倍率 | 3,285.7(3,159.2~3,288.0) | 2,766.7(2,733.8~3,502.1) |
深拡大の中央値は25.4%短縮した。全画面集計は変更前229/256/258回から20/20/20回へ減った。時間の範囲は重なるため、毎回同じ速度向上を保証するものではない。初期表示と中間倍率の中央値も短くなったが、集計回数はそれぞれ11回・18回のままなので、これらをBの効果とは断定しない。単独候補の測定値とは別の測定であり、短縮率は足し合わせていない。
比較用24描画でmeta・smoothの全画素ハッシュが一致した。加えて、高画質の深拡大480×360、デスクトップ高画質1650×1080についても変更前との全画素一致、数値失敗0、PNG復号不一致0を確認した。実際の高画質表示もスクリーンショットで確認した。[生データ](PERFORMANCE_BC_DATA.json)に変更前・実装版のSHA-256と結果を保存した。
実GPUの追加検査では、複数の補修結果を1回で集計し、読み直しだけでは再集計しないこと、脱出済み・反復上限画素を上書きしないこと、古い世代の補修を拒否すること、別frameへ待機状態を移さないことを確認する。
```sh
node tests/regression.mjs
node tests/browser_smoke.mjs 9333 all
node tests/performance_candidates.mjs 9333 .test-edge/bc-rerun.json combined .test-edge/bc-baseline.html
```
`combined` は標準画質の3視点比較に加え、高画質の深拡大・大きい画面の一致検査も行う。通常起動にこれらの検証処理を追加してはいない。
CPU回帰と実GPUの `all` が通過した。画質3段階、彩色変更時の数値再計算抑止、色相変化中のパン、補修中のキャンセル、PNG、独立高精度サンプルも確認した。検査を連続実行した際に残っていた診断用設定と評価変数を検査ごとに分離した。検証用Edgeと専用プロファイルは終了・削除済み。

File diff suppressed because it is too large Load diff

View file

@ -1,194 +0,0 @@
# 軽量化の実装設計と精査結果
更新日:2026-09-06。[改善案](PERFORMANCE_PLAN.md)の続き。以下は承認前の設計と精査記録。採用した実装、検証結果、残る数値上の制約は [実装結果](IMPLEMENTATION_RESULTS.md) を参照。
## 1. 精査で確定した修正
| 論点 | 現在のコードで確認した内容 | 案への反映 |
| --- | --- | --- |
| 参照の強制更新 | `ReferenceService.request(..., fresh=false)` があり、trueで該当cacheを削除する。実コードを使った小さなCPU確認でも再投入された | 「引数がなく無効」を撤回。同じ入力から同じ短い参照を再選択し得る問題として扱う |
| 局所参照の位置 | `FAILURE_TILE_MAP_WGSL` が失敗indexの最小値を求め、`readNumericalFailureTiles()` が代表点へ変換する | 既に失敗画素を使う。重心選択と誤記せず、代替候補・回収実績を改善対象とする |
| reason 4・7 | 4は定義・集計、7は名前がある。現行シェーダーにそれらを失敗として書く経路は確認できない | 現在の発生原因や改善効果に算入しない |
| 継続の終端 | `DEEP_ACTIVE_CONTINUE_WGSL` の終端分岐はtarget>=150001が必要だが、通常の目標は最大150000 | 目標到達の判定とstateの寿命を分ける |
| 最後の1反復 | 継続loopは更新後のnがstopに達すると、次回の脱出判定より先に終了する | 結果採用時に最終stateの脱出・誤差を確認する |
| DS queueの上限 | `computeAccurateDirectHybrid()` が初期batchを時間に応じて拡大し、元の仕事量上限へ戻していない | 適応後のclampと横分割を前提にしてからまとめ投入する |
| 現行の計測値 | `session.pixelIterations` は入口active数×chunkの加算 | 実反復数と呼ばず、仕事量上界として比較する |
| 現行の「正確」 | Direct DS/Deep DS補修はDSで計算するが、全丸め誤差を区間伝播する証明ではない | 数値採用・有限回未脱出・内点証明を分ける |
照合対象は現行 [index.html](../index.html)。精査時のSHA-256は `045a5de85b816808ea8f54c5a390196b7436b3c7802267aadcd7bc1baa7c6792`。この値は照合した版を識別する記録で、今後の編集を禁止するゲートではない。
## 2. 計算状態の契約
既存のmetaを表示用の最小情報として残し、継続可能なstate・参照の識別・仕事の所属を分ける。reasonやclassを増やすこと自体を目的にしない。
| 論理状態 | 必須情報 | 次の処理 |
| --- | --- | --- |
| 未着手 | 画素index、描画世代 | 初期計算 |
| 継続可能 | 実到達n、参照内のm、差分、指数、誤差、参照ID | 同じ数値方式で次の有界chunk |
| 数値補修待ち | 失敗理由、画素index、最後に有効だったstateの有無 | 理由に合う参照/精度で補修 |
| 目標まで未脱出 | 採用済みのn、誤差確認の状態、継続stateの有無 | 次の反復目標を待つ。集合内の証明とはしない |
| 脱出を採用 | 脱出反復数、smooth、採用条件 | 数値計算から除外し、配色変更だけに応答 |
| 内点を確認 | 確認した方式と対象画素 | 継続から除外 |
各画素が同時に複数の仕事へ所属しないことを基本にする。画素単位の所有者は `継続/補修/保留/数値処理終了` のどれか1つ。集計はこの排他的な所属を数え、active件数とmetaのUNKNOWN総数をそのまま足し合わせない。
`n` は処理した反復数、`m` は参照軌道内の位置であり、片方から他方を推定しない。画素ごとに到達nが違う可能性があるため、sessionの単一progressを全画素の到達証明として使わない。
### 目標到達時の処理
1. 最後の更新後のstateから `z_n` を再構成する。参照範囲外なら補修へ回し、末尾indexを丸めて代用しない。
2. 脱出条件を満たすか、境界を誤差込みで判定できるかを確認する。確定できない境界は補修へ回す。
3. 未脱出なら現在の目標に対する誤差条件を確認し、metaへ実到達nを記録する。
4. 採用できた未脱出画素はstateを保存し、次の目標に使える保留対象として保持する。脱出・内点は除外する。
5. 誤差条件を満たさないstateを「正常な継続state」として再利用しない。前chunk等の有効stateがある場合だけそこから補修できるものとする。
P0では既存の誤差閾値を無条件に緩めず、未実行だった採用処理を到達可能にする。DSやCPU補修にも採用条件の限界を残す。数学的保証を強める場合は、別の数値方式変更として評価する。
## 3. GPUスケジューラーの具体案
同じ描画世代・参照ID・数値方式・誤差方針の画素を1グループとして処理する。最初は既存の2本のactive queueを使い、複数の常駐エンジンを増やさない。
### まとめ投入の上限
```text
upper = このまとまりの入口のactive件数
workCap = 継続カーネル1回の上限(初期案:4,000,000)
chunk = min(256, floor(workCap / upper), この目標までの残り反復)
batchPasses = 1~4(初期は1、短い完了時間を確認して増加)
upper == 0 または残り反復 == 0 → 反復を投入せず目標の完了処理へ
floor(workCap / upper) == 0 → 対象を分割する。chunkを1へ丸めて上限を超えない
batchPasses × upper × chunk → まとまり全体の仕事量上限でも制限
```
この上界が有効なのは、まとまりの途中で対象を追加せず、各画素を高々1回だけ次queueへ戻す場合である。補修済み画素の再参加・別グループの統合はまとまりの境界で行う。nの違う画素の残り反復はカーネル内でも切り詰める。
```text
世代を確認
同じ参照のbufferを確保
2~4回以下の「件数→indirect引数→有界継続」をencode
最後の件数・失敗要約だけをstagingへcopy
submit
staging.mapAsyncの成立を待つ
世代を再確認して要約を採用、unmap
表示更新・入力処理へ制御を返す
残件があれば次のまとまりへ
```
CPU時間による初期調整目標は、1まとまりの完了を数ms~十数ms程度に収めること。ただしmapを含む経過時間は純GPU時間ではない。過去の実測が長いときは回数か仕事量を減らし、数値失敗を捨てて短くしない。初回に4回を固定投入したり、毎回時間計測用の追加同期を挟んだりしない。
専用参照・uniform・queue・stagingは、そのまとまりが使い終わるまで破棄/上書きしない。`mapAsync` が成立したstagingは `unmap` までGPUへ再投入しない。単一stagingを直列再利用する実装から始め、重ね合わせが必要になった場合だけ少数ringへ広げる。
GPUへ既に投入した処理を世代tokenだけで取り消すことはできない。入力時は追加投入を止め、旧世代の結果を新しい画素バッファへ書き込まない。参照の破棄は、それを使う処理の完了後に行う。
### 他の重い経路にも適用する上限
`computeAccurateDirectHybrid()` のDS queueは、初期値と適応後の値の両方を `maxPixels=floor(workCap/iter)` で制限する。workgroupが64でも仕事件数を最低64にする必要はなく、端数laneをguardすれば1~63画素も処理できる。
全幅1行でも予算を超えるDeep stripは、既存の `correctUnknownFrameScan()` と同様にx方向も分割する。処理量は論理画素数×反復上限で数え、丸められたworkgroup数だけを根拠にしない。デバイスのbuffer・binding・dispatchサイズ上限は、この仕事量上限とは別に満たす。
## 4. 補修と参照を再利用する手順
### 正常画素を巻き戻さない
失敗したindexを別queueへ移す際、残る画素のstateとqueueを保持する。補修から戻る情報は次の2種類を区別する。
- **meta・smoothのみ**:脱出なら計算終了。未脱出ならstate未提供として保留し、この画素だけを必要に応じて再計算する。現行のDS/CPU補修は主にこちら。
- **有効な継続state付き**:同じ参照・数値方式のグループへ戻せる。異なる局所参照なら、その参照に属する別グループへ入れる。
局所参照で回復したstateを、元の全体参照のstateとして扱わない。全体の `setDeepContext()` を変更して別グループの処理と競合させず、encode時に使う参照bufferを明示する。原点が変わる場合の差分変換は、この最初の実装には含めない。
### 参照軌道の末尾延長
参照IDは原点・実際の計算精度・数値方式で識別し、反復目標だけの増加では同じIDを維持する。精度の自動再試行でbit数が増えた場合は、要求精度ではなく採用した精度に基づいて別IDにする。
Workerは参照本体の `zr, zi, zr², zi², n` と、検査用の高精度軌道の独立した終端stateを保存する。精度Pの丸め済み終端を単にP+32へ拡張して検査の初期値にしない。両者の検査済みprefixを維持し、それぞれのstateから末尾を進める。
参照が既に脱出した場合は単純延長を続けず、別候補か局所参照へ進む。同じ候補を同じ条件で何度も選び直さない。prefixの検査点配置は長さに依存しない規則へ揃えるか、新目標で必要になる過去の検査点も確認する。GPU側も容量が足りる範囲では末尾だけを転送し、容量拡張時は旧参照の利用完了を待つ。
### CPU補修の小分け化
最初の候補設定は同時1~2 Worker、1ジョブ最大16~64画素に加え `件数×反復数` による上限を設ける。これは固定の最適値ではない。精度bit数・実行時間も見てジョブを縮小し、少数でも長い画素がUIの次の仕事を妨げないようにする。
同じ精度の結果再利用は最初に導入できる。Workerへ渡した入力の世代・座標・解像度・反復予算・精度が同じ場合だけ再利用し、前回の高精度結果を次の比較へ渡す。
結果は完了したジョブから受け取るが、GPUへの反映は1か所で直列化する。writeBuffer/scatterの**前**に世代と対象metaを確認し、同時進行するGPU補修が確定させた画素を古いCPU結果で上書きしない。現在の `applyPrecisionFallback()` 内ではtoken確認がwriteBuffer後にあるため、呼び出し側の確認だけに依存する非同期化を避ける。
回収できなかった画素は、小さな保留リストと次の手段を保持する。時間予算を使い切ったことを `numeric-complete` として報告しない。
## 5. 未脱出の反復予算と表示
最初のP0~P2では、変更前に採用されていた反復方針を可能な範囲で固定して比較する。ただしP0で新しい失敗や目標境界の脱出を正しく認識した場合、その差を性能回帰と誤認しない。
P3の候補は、同じ大域目標の中で「境界周辺・孤立成分・長時間残件」を別優先度にする方式から始める。全候補が目標に到達したかはGPUで `未到達件数` を集約する。低優先度の残件にも割当を設け、速く終わる画素ばかりを処理し続けない。これだけでは総反復数は減らないが、表示の改善順と処理継続性を制御できる。
総反復数を減らす変更は、検証できた内点の除外か、明示した有限予算への停止として別に扱う。内点判定を増やす場合は適用領域を限定し、軌道の近接や少数回の無変化だけで内点へ昇格しない。
表示・履歴・出力の意味は次のように統一する。
| 表示段階 | 条件 | 履歴・PNG |
| --- | --- | --- |
| 現在視点の暫定表示 | 計算済みタイルを順次表示。未処理領域があることを状態として保持 | 完成履歴へ保存しない |
| 数値補修が完了した予算画像 | 被覆漏れ0、数値補修待ち0、残る未脱出は到達nを記録 | 既定の反復方針を満たすまでは暫定。明示的な暫定出力を設けるなら予算を付記 |
| 現在の品質方針で完了 | 必要な全候補が目標と数値条件を満たし、品質方針の停止条件を満たす | 完成履歴へ保存できる。集合内の完全証明とは別 |
同一画素格子のタイル表示を先に検討する。低解像度の全体previewを追加する場合、半画素中心のずれを確認し、単に同じ画像範囲という理由で最終画素の計算へ流用しない。暫定表示で初回待ちが改善しても、数値補修完了の時間を別途示す。
## 6. 実装単位と確認ケース
| 単位 | 対象 | 採用するための確認 |
| --- | --- | --- |
| A:判定と上限 | 継続の目標終端、meta更新、DS/Deepの分割 | 最後の反復での脱出、誤差超過、未脱出の再投入、全分割の被覆を確認 |
| B:同期とBigInt重複 | 小さなまとめ投入、精度ペア再利用 | Aと同一入力・予算で分類、脱出反復、smooth、到達nが一致。同期や軌道数が減る |
| C:stateと参照の寿命 | 補修queue分離、参照末尾延長 | 少数の補修で正常stateが0に戻らず、参照変更・精度変更を混同しない |
| D:補修転送 | タイル別GPU queue、scatter反映 | 件数だけでなくindexの被覆を確認。重複・古い世代・確定画素への上書きがない |
| E:予算と表示 | 内点判定、優先度、暫定表示 | 未到達を完了扱いしない。操作と出力で品質方針が一致する |
必要な確認は既存の2つのテストファイルへ足す。常時監査や別の昇格システムは作らない。
| ケース | 確認する差分 | 現行テストの不足 |
| --- | --- | --- |
| 目標最後の1回で脱出する単一画素 | 更新後の脱出反復が目標内に記録される。例:c=1なら半径2を越えるのは3反復目 | Workerの参照は半径4で生成されるため、既存のWorker確認だけでGPUの目標境界は証明できない |
| 目標到達時の誤差超過・未脱出・参照末尾 | 誤差検査が走る。正常未脱出だけが継続可能。参照外を読まない | スモークはmetaの反復到達・この分岐を直接検査しない |
| 0/1/63/64/65件、奇数回/偶数回のまとめ投入 | queue交換、端数lane、空queue、同じ画素の重複処理がない | 現行テストは複数pass化のqueue内容を確認しない |
| 広いcanvas・高反復・適応batch拡大 | `logicalCount×iterationBound` がカーネル別上限内、横分割で全画素を1回ずつ処理 | 現行のstrip検査は最小1行の上限超過を許容する |
| 大半が正常で1画素だけ補修、参照の原点/精度変更 | 正常な進捗保持、補修画素だけの再参加、参照IDの分離 | ハッシュだけでは無駄な再計算を検出できない |
| CPU補修中のパン・再ズーム・戻る操作 | 古い世代がwriteBuffer前に破棄される。最新画面の結果だけが残る | 現行のパン往復は各回の完了を待つため、処理中の競合を検査しない |
| 同じ深部座標のFAST/Deepと独立した少数画素 | 共通バグを含む一致と、独立な高精度確認を区別 | 比較関数は主に不一致画素だけをCPU確認する |
| 暫定表示中・完了後のPNG | 配列長だけでなく、同じ座標・予算の数値結果を出力する | 現行スモークは小さなRGBAタイルの長さ・checksumで、PNG全体の検証ではない |
正確性用の小ケースを先に実行し、性能ケースは初期表示とSeahorseのFAST/Deepを基本にする。主カージオイド/周期2円の境界変更時は `re=-0.75, im=0, span=0.02` の実軸付近を追加する。既知の黒領域問題は次の入力を固定小解像度で使用する。
```json
{
"re": "-29466147000382485924219538765424656674342940730553342389512209954887705938978413587",
"im": "5179018789925933872641942859702187326563010594882200888329237785122225251258352",
"span": "250637324516887125119843167990565159991251418774166207381437546036409",
"bits": 273,
"baseIter": 350,
"adaptive": true,
"processMode": "standard",
"renderMode": "accurate",
"fixed": true
}
```
出典はGit履歴の `tests/optimization_webgpu_corpus.mjs` にある `reported-black-regression`。`fixed:true` の整数座標なので10進実数として読み替えない。今回はこのケースをGPU実行していない。
## 7. 性能評価と中止条件
同じadapter、canvas画素数、座標、初期反復、最終品質方針で比較する。固定予算での実行効率と、予算を変える方式は別の結果にする。ブラウザー起動直後のコンパイル込み時間と、同じpipelineを使う描画時間も分ける。
ウォームアップ1回+計測3回を最初の上限とし、中央値と全3件の値を保存する。ばらつきが大きく結論が出ない場合は「未判定」とし、同一条件が整ってから必要なケースだけ再測定する。多数の測定を自動で走らせ続けない。
追加する計測は、理由別失敗数、未到達数、GPU往復回数、readback byte数、仕事量上界、再初期化した画素数、BigInt軌道数の小さな集計に絞る。性能測定中に全画面のoracle比較を走らせない。独立した画素確認は正確性用の別実行にする。
数値補修完了時間が悪化する変更は、初回表示だけの改善と区別する。仕事量が減ってもメモリ増加や同期で時間が増える場合は、その段階の変更を採用しない。数値判定を緩めて性能値を合わせない。
GPUエラー・device loss・画素欠落・古い世代の書込みがあれば、そのケースを中止して原因を確認する。重いケースのtimeout後に、新しいEdgeを追加起動して続けない。検証用Edgeは1インスタンス・1タブ・直列実行とし、起動したプロファイルを識別して終了まで管理する。
## 8. 計画作成時点の成果と未実施事項
今回実施したのは、現行コードとの照合、旧案の誤記修正、参照キャッシュの小さな実コード確認、仕事量の数値例の再計算、実装設計の追記である。終端分岐・queue・精度の確認方法を具体化したが、新しい数値方式やスケジューラーを実装・GPU検証したわけではない。
参照サービスのfresh処理、代表点選択、予約reason、処理量上限、現行テストの限界は本文へ反映済み。期待効果は仮説として残し、改善率や任意座標での未確定ゼロを検証済みとは記載していない。新たなEdgeは起動していない。

View file

@ -1,205 +0,0 @@
# 未確定画素を減らしながら軽量化する案
作成日:2026-09-05。精査・追記:2026-09-06。以下はBLA除去後のコードを調査した時点の計画。承認後の変更と測定結果は [実装結果](IMPLEMENTATION_RESULTS.md) を参照。
実装時の状態遷移・処理量上限・手順・確認ケースは [実装設計](PERFORMANCE_IMPLEMENTATION.md) にまとめた。
## 推奨方針
まず **計算済みの状態を使い続けること、GPUとの往復を減らすこと、失敗した画素だけを適切な精度で補修すること** に集中する。反復回数や誤差判定を一律に削って軽くする方針は採らない。
現状はBLAを除去しても、未脱出画素の全件継続、補修後のやり直し、細かいGPU完了待ち、各画素のBigInt再計算が残っている。ここを直す方が、別の近似加速器を追加するより先である。
最初の実装単位は次の3つを推奨する。
1. 未脱出画素の到達反復数・誤差を正しく記録し、未確定の分類を揃える。
2. 継続計算のGPU投入を小さなまとまりにし、まとまりごとの読戻しにする。
3. 補修が必要な画素だけを別キューへ移し、正常な画素の状態と参照軌道を保持する。
P0の判定修正は正しい結果へ直す変更であり、従来と結果が変わる可能性がある。P0後の結果を基準として、P1では最終反復数の方針を維持し、同じ計算結果を少ない再計算・同期で得る。その後に、内点の早期確定と画素ごとの反復予算を導入する。
## 1. 「計算不能」を分けて扱う
ここでいうセルは画素を指す。現在の `FIELD_UNKNOWN` は複数の状態を含むため、総数だけを最小化すると誤判定を増やしやすい。
| 状態 | 現行の表現 | 目指す扱い |
| --- | --- | --- |
| まだ計算していない | reason 0 | 最終画像で0。タイルの被覆漏れを防ぐ |
| 精度・参照軌道の都合で結果を採用できない | reason 1~5 | 最優先で減らす。局所参照・精度昇格・高精度補修へ回す |
| 現在の反復数まで脱出していない | reason 6、または `FIELD_INTERIOR_LIKELY` | 数値故障とは分ける。到達反復数を記録し、継続または内点判定へ回す |
| DS感度を表す予約名 | reason 7(名前のみ。現行シェーダーに出力経路なし) | 現在の失敗原因とは数えない。将来出力するなら補修対象・集計まで同時に定義する |
| 内点と確認できた | `FIELD_INTERIOR_PROVEN` | 継続キューから安全に除く |
**有限回の未脱出だけで集合内と証明したことにはならない。** また、異なる2精度で結果が一致したことは有用な確認だが、一般の境界画素について数学的な証明そのものではない。Direct DSとDeep DS補修も、全丸め誤差の区間を伝播して脱出を証明する実装ではない。全座標で「厳密分類・未確定0・短時間」を保証する目標は置かない。
reason 4の `rebase-gap` も、定義と集計は残るが現行シェーダーに失敗を書き込む経路は見当たらない。rangeやreference-endの実測件数と、予約理由を区別する。現在の統計配列の要素7は `corrected` 用なので、reason 7をそこへ足す変更はできない。
目標は、同じ表示精度・反復方針での数値失敗を可能な限り0へ近づけ、残った未脱出画素には正直な状態と継続手段を持たせること。UNKNOWNを黒や周囲の色へ置き換えるだけでは改善と数えない。
## 2. 現状から分かった負荷
根拠は [index.html](../index.html) のコードと [前回の確認結果](CLEANUP.md)。今回、追加のEdge起動や速度測定は行っていない。2026-09-06の精査では、参照サービスの実コードを使ったキャッシュ更新確認と、小さなCPU計算で数値例を確認した。以下のボトルネック順位はコードからの推定で、GPU時間・同期時間・CPU時間の内訳は未計測である。
| 箇所 | 確認できた処理 | 問題・軽量化の方向 |
| --- | --- | --- |
| `refinePixelFrontier()`、`operationLimitIndices()` | reason 6の全画素を候補にする。8近傍だけを選ぶ `operationLimitFrontier()` は本経路から呼ばれていない | 名前に反して境界だけの計算ではない。内点判定と優先順位が必要 |
| `pixelFrontierIterationFloor()` | 画素幅のビット数×256を最低反復予算とする | 空間精度と必要脱出反復数を強く結び付けたヒューリスティック。深部では大きな追加計算になる |
| `DEEP_ACTIVE_RESUME_INIT_WGSL` | `w=0, n=0, m=0` から開始する | 初期描画で計算した反復を、継続経路で再び計算する |
| `refinePixelFrontier()` の `restart` | 数値補修があると、次の周回で残存候補全体のsessionを作り直すことがある | 少数の失敗で正常な画素まで再計算する |
| `continueOperationLimitActive()` | 原則1 dispatchごとにsubmit、全queue完了待ち、4 byte件数のmapを行う | GPUとCPUの細かい往復。GPUに次の仕事が届くまで空く可能性がある |
| `ReferenceService.requestFixed()`、参照Worker | 反復目標が変わりキャッシュにない場合、参照を再生成して検証する | 同一原点・同一精度なら末尾延長で済ませられる余地がある |
| `recoverNumericalUnknownTiles()` | 局所参照のバッチごとに全画面metaを読戻し、CPUで所属画素を抽出 | 失敗が疎でも全画面転送・走査が発生する |
| `precisionFallbackWorkerSource()` | 最大4試行で2精度ずつ、各画素を反復0から計算する | 高精度計算が重複する。同じ精度の計算結果を再利用できる |
| `recoverResidualPrecision()` | 残件をWorker数で割り、実質全残件を一度に投入して `Promise.all` で待つ | 小さな費用上限で区切られておらず、長いCPU負荷になり得る |
| `applyPrecisionFallback()` | Workerの結果ごとに反映と全画面統計を実行 | 細切れのアップロード・統計・完了待ちをまとめられる |
| `qualityStages()`、`renderQualityStage()` | 深部は原則1回の最終描画。補修と継続が完了するまで新規frameの公開を待つ | 完了時間がそのまま初回表示待ちになる。履歴のない座標ジャンプで特に目立つ |
### 具体的な規模
前回の320×240、Seahorse、span `3.4e-13`、初期512反復では、FASTは32768反復まで進んで約36.7秒かかった。FASTとDeepの最終meta・smoothは一致したが、これは両者共通の停止条件・数値判定まで正しいという証明ではない。
この条件の画素幅は約49.74 bit相当で、現在の最低反復予算は12736。目標を倍増するため最低予算を初めて越えるのは16384で、さらに安定判定によって32768などへ進む。512から32768は反復目標で64倍であり、実際の総演算量が常に64倍という意味ではない。
現在のchunkは、残り反復数による切り詰めを除くと `max(1, min(256, floor(4,000,000 / activeCount)))`。全画素が継続対象のまま32768反復まで進む仮定では、320×240で少なくとも約631 dispatch、800×600で4096 dispatchになる。各目標境界、補修、参照生成の追加費用は含まない。実際には脱出によって対象が減るため、この例を実測dispatch数として扱わない。ここでいうdispatch数は反復カーネルの回数で、別にindirect引数を準備する小さなdispatchが各回にある。
## 3. 最初に直すべき判定上の問題
### 継続経路の終端誤差検査が通常の目標で実行されない
`DEEP_ACTIVE_CONTINUE_WGSL` の未脱出時の終端処理は `p.targetIter >= p.maxIter` を条件とする。一方、呼び出し側は `p.maxIter=150001`、通常の目標は最大150000である。そのため、この分岐の誤差検査と反復数のmeta更新には通常到達しない。脱出・range等の他の検査は別途存在する。
提案は、「次へ継続するための状態保存」と「今の目標まで計算した結果の判定」を分離すること。各目標境界で実際の `n` と誤差を記録し、必要な画素だけを補修へ回す。既存の終端分岐を単に有効化すると `return` によって継続キューから画素が脱落するため、state保存と再投入を含めて設計する。
さらにloop先頭で `s.n>=stop` を先に判定するため、最後の更新で到達した `z_target` の脱出判定は次の継続まで遅れる。表示をその目標で確定するなら、最終stateを評価してから未脱出として採用する必要がある。中間chunkではなく、結果を採用する目標境界で確認する。
この修正で今まで見えていなかった数値UNKNOWNが一時的に増える可能性がある。改善の評価は、その増加を隠さず、正しい判定後の補修コストと残件数で行う。
### 未脱出の表現と停止条件を揃える
Direct DSは未脱出を `FIELD_INTERIOR_LIKELY` として返す一方、別経路ではreason 6となり、継続対象が異なる。同じ予算で比較するには、数値的に判定できたこと、集合内であること、追加反復を待っていることを別々に扱う必要がある。
また、`pixelEscapeAdvance()` の「新しい脱出が既存の脱出画素の近傍内に収まる」という条件と2回の安定周回は、将来の孤立した脱出や1画素未満の誤差を保証しない。現行の実用的な停止条件として記録し、厳密な内点証明とは呼ばない。
### 処理量上限は全経路で統一されていない
400万pixel-iterationsは現行の継続・疎補修の設計値であり、すべてのdispatchに適用できている保証ではない。`computeAccurateDirectHybrid()` のDS queueは初期batchを上限から決めるが、時間による拡大後に同じ上限でclampしていない。12000反復では333画素から500画素へ増えるだけで600万になる。
`initialNumericRows()` は最小1行なので、`幅×反復数` が予算より大きい場合は1行でも超える。既存の `regression.mjs` はこの例外を許容する検査で、全経路400万以下の証拠にはならない。重いDS/Deep経路は必要なら横方向も分割し、どの適応処理でも最終的に上限へ戻す。FASTの4800万等とは別のカーネル別予算として管理する。GPUの経過時間は演算内容にも依存するので、画素×反復数だけで停止防止を保証しない。
## 4. 軽量化案と優先順位
| 優先 | 案 | 期待する効果 | 未確定を増やさない条件 |
| --- | --- | --- | --- |
| P0 | 上記の状態・誤差判定と処理量上限を整理 | 評価基準と分割の前提を正す | 未検査の画素を確定扱いせず、上限超過時はさらに分割する |
| P1 | GPU投入をまとめ、読戻しを間引く | 同期・submit・JS呼び出しを削減 | 各dispatchの上限と投入総量を維持 |
| P1 | 正常画素のstateと参照軌道を保持 | 反復0からの重複計算を削減 | 参照原点・精度・計算式が同一であること |
| P1 | 数値失敗キューをGPU上で作る | 全画面readback・CPU走査を削減 | キュー被覆とoverflow時の処理を保証 |
| P2 | 失敗理由別に補修を選ぶ | 回収できない補修の反復を削減 | 失敗した画素には次の補修手段を残す |
| P2 | BigIntの重複計算と一括投入を減らす | CPU負荷・最長待ち時間を削減 | 2精度比較と未採用画素の追跡を維持 |
| P3 | 安全な内点判定を加える | 未脱出キュー自体を縮小 | 判定に失敗した点は通常計算へ戻す |
| P3 | 画素・タイルごとに反復予算を配分 | 一律の深い反復を削減 | 優先度の低い未確定画素を捨てない |
| P4 | 同一視点の段階表示とメモリ上限 | 初回待ち・メモリ圧迫を軽減 | 暫定表示を完成履歴へ昇格させない |
### P1-A:GPU主導の継続と小さなまとめ投入
`encodeDeepActiveChunks()` は既に複数passを符号化できる。最初は2~4回程度の有界dispatchをまとめ、件数を読むのはまとまりの最後だけにする。GPU上の件数からindirect dispatchを構築し、対象0の後続passは何も処理しないようにする。
補修画素の再投入がないまとまりの中ではactive数は増えない。その入口の件数を保守的な上限に使えば、毎回CPUへ戻らず各dispatchの処理量を制限できる。再投入するときはこの前提を更新する。
4 byteの読戻し自体のために `mapAsync()` の前で全queueの完了を重ねて待つ必要はない。対象バッファのGPU使用完了はmapの成立で扱える。ただし、mapの成立は他バッファや別のpromiseの完了を意味しない。投入量の制御や別用途の完了待ちは分けて残す。[WebGPU仕様のbuffer mapping](https://gpuweb.github.io/gpuweb/#buffer-mapping)
継続・高コスト補修の各dispatchを400万pixel-iterations以内へ実際に制限したうえで、GPUへ先行投入する時間・総仕事量にも上限を置く。まとめる回数を増やしてWindowsのGPU停止やキャンセル遅延を再発させない。uniformの値がpassごとに変わる場合は専用offsetや別バッファで保持し、submit前の同じuniform領域への上書きで全passが最後の値を見る構成にしない。[WebGPU開発者による複数passのuniform書き込みの説明](https://github.com/gpuweb/gpuweb/discussions/2509)
### P1-B:正常なstateと参照軌道を使い続ける
最初に補修キューと継続キューを分離する。数値失敗した画素だけを継続キューから外し、正常画素の `d, w, scaleExp, n, m, errScaled` を保持する。補修した画素は、その結果と参照に対応したstateで再参加させる。stateを復元できない画素に限って再計算する。
次に、同一原点・同一精度の参照Workerに末尾stateを保持し、目標増加時は軌道の末尾を延長する。既に検査したprefixは同じ条件のまま保持し、新しく追加した部分を検査する。精度変更や原点変更時は別の参照として扱う。
初期Deep描画からの継続state保存はその次に行う。FAST・Direct・DSのstateをDeepへそのまま移植しない。実装を共通化する場合も、丸め順・誤差・参照の契約を揃えてから採用する。
参照が短い場合の `refs.request(..., true)` は、現行の第5引数 `fresh` によって該当キャッシュを削除する。旧案の「引数がなく無効」という記述は誤りだった。ただし同じ入力の処理中promiseは再利用され、再生成しても決定的な候補探索は同じ原点を選び得る。改善対象はfreshの追加ではなく、試した原点の記録、失敗分布に基づく別候補の選択、十分な長さを得られない場合の処理である。
### P1-C:GPU内で失敗画素を集める
reason別・タイル別の件数、prefix sum、画素indexのscatterをGPU上で作り、局所参照ごとの範囲をそのまま補修へ渡す。既存の疎キューを拡張する形を優先し、別の監査基盤は追加しない。
CPUへ必要なのは参照を選ぶための小さなタイル要約と、最終的にBigIntへ渡す少数の失敗indexである。全画面metaの読戻しは通常の補修ループから外し、任意の検証や例外処理に限定する。
overflowや被覆漏れの検査はGPUで集計してまとまりの最後に確認する。件数確認を減らすことと、未処理画素を黙って捨てることを混同しない。
### P2-A:失敗理由ごとの補修
| 理由・分布 | 最初に試す処理 | 次の処理 |
| --- | --- | --- |
| reference-end | 同一原点の軌道延長。実際に脱出する参照なら失敗領域内で参照を再選択 | 局所参照で再計算 |
| error-boundが局所に集中 | 既存参照でのDS補修か、回収実績のある局所参照を選ぶ | 改善しなければ高精度補修 |
| escape-uncertain | 該当画素だけ精度を上げる | 必要な画素だけBigInt |
| range | スケーリングと座標差の表現を確認 | 指数を持つ表現または高精度補修 |
| 広く密集した失敗 | タイル単位で参照・精度を選び直す | 大量の画素別BigInt投入を避ける |
現在も `readNumericalFailureTiles()` はタイル内の最小indexの失敗画素を代表点に使っている。`sumX/sumY` という名前だが重心の総和ではなく、その代表点×件数である。改善は、この既存代表点を基準に失敗の密集度・到達反復・過去の回収費用から少数の代替候補を選ぶこと。単に「中心から失敗画素へ変更する」という新規機能ではない。参照変更でグリッチを減らせる一方、近い参照を選ぶだけで誤差を保証できるわけではない。[摂動法のグリッチと参照選択](https://mathr.co.uk/blog/2014-03-31_perturbation_glitches.html)
reason 4・7を将来使う場合の経路は予約設計として別途定義する。発生していない理由に専用の常時計算を追加しない。
補修1回で回収できた画素数、残件数、費用を少数の集計値で保持する。回収の止まった手段を同じ条件で繰り返さず、次の手段へ送る。時間切れの画素は保留として残し、補修完了数へ加算しない。
### P2-B:BigInt補修を小さくする
現在の精度比較は `P/P+32 → P+32/P+64 → …` で、隣り合う試行が同じ精度の軌道を再計算する。前回の高精度側の結果を次の低精度側へ使えば、最大4試行の8軌道を5軌道に減らせる。最初の比較で成功する画素にはこの削減は発生しない。
Workerへの仕事は全残件の等分ではなく、小さな画素数・推定pixel-iterationsで区切る。まず1~2 Workerを基本にして空いたWorkerへ次のバッチを渡し、端末の余裕がある場合だけ増やす。入力が来たら追加投入を止め、長い処理は必要に応じてWorker終了で取り消す。
同時期に返った結果のindex・meta・smoothをまとめて転送し、GPU scatterで反映する。全画面統計はWorker結果ごとではなく適用バッチごとに1回。2精度で採用できない画素は失敗キューに残す。
### P3-A:継続が不要な内点を安全に除く
Directにある主カージオイド・周期2円の判定を、適用可能なタイル・参照表現へ広げる。深い座標を単純にf32へ丸めて判定せず、高精度座標または誤差を含む区間で領域内と確認できた場合だけ採用する。領域から遠いタイルでは判定自体を省ける。[Cardioid and bulb checking](https://mathr.co.uk/blog/2022-11-19_cardioid_and_bulb_checking.html)
さらに一般の周期成分を対象とするなら、周期候補の検出と内点の確認を分ける。有限精度で同じ値になった、近い位置へ戻った、という理由だけでは確定しない。包含・収縮等を確認できる方式は後段の研究候補とし、全画素で毎反復行わず長く残る画素に絞る。
主カージオイドと周期2円だけでは、Seahorseや小さなコピー内部の問題を全部解決できない。効果の対象範囲を区別する。
### P3-B:反復予算を空間精度と切り離す
画素幅は座標の必要精度を決める。一方、外部の点が何回で脱出するかは軌道に依存するため、`pixelBits × 256` は必要反復数の証明ではない。
一律の倍増を置き換える候補は、境界近傍、孤立した未脱出成分、長時間残る画素を別キューで管理し、変化の多い部分を優先する方式。低優先度の未確定画素にも一定量を配り、孤立した遅い脱出を永久に取り残さない。8近傍だけへ対象を限定する変更は行わない。
この変更は従来と最終反復数・停止条件が変わり得る。P1の純粋な実行効率改善と分けて評価する。通常閲覧の有限予算と、明示的な高品質計算の扱いを決め、到達予算と未確定数を内部状態に残す。
### P4:表示待ちとメモリ負荷を抑える
最終的な未確定を減らす処理を継続しつつ、現在の視点で計算した暫定結果を段階表示する案を検討する。全体の低解像度計算を単純に追加すると二重計算になるため、同一画素格子のタイル完成表示か、最終計算へ再利用できる段階処理を優先する。暫定画像は完成履歴にしない。
現在のactive用stateは全画面に32 byte/画素、2本のqueueは合計8 byte/画素。800×600ならこの部分だけで約18.3 MiBあり、meta・smooth・texture・参照は別に必要になる。候補数が少ないときは密なslot配列、候補が多いときは上限付きタイルworkspaceを使い分ける。ただし画素indexとslotの対応、世代、queue境界を明確にする。slot方式でindex対応表を追加すれば、その分のメモリも増える。タイル処理でも生存stateを破棄して毎回0から計算してはならない。
## 5. 実装・確認の進め方
| 段階 | 変更範囲 | 確認すること |
| --- | --- | --- |
| 0 | 終端判定・未確定分類・処理量上限・最低限の集計 | 正常画素と未検査画素を区別できる。継続画素がqueueから消えず、適応batchも上限内 |
| 1 | 同期のまとめ、BigIntの同精度結果再利用 | 同じ予算で脱出分類・反復数・smoothが一致。同期数と重複軌道数が減る |
| 2 | 失敗キュー分離、正常state保持、参照末尾延長 | 補修後も正常画素の進捗が戻らない。数値UNKNOWNが減るか増えない |
| 3 | GPUでのタイル別queueと補修選択 | 全画面読戻しbyte数、BigInt対象数、補修失敗の再試行が減る |
| 4 | 内点判定と反復予算・段階表示 | 内点の誤確定を防ぐ。遅い脱出・孤立成分・操作往復で破綻しない |
自動監査を描画ループへ戻さない。計測は `?test` の明示実行で、初回表示までの時間、数値補修完了までの時間、未確定の理由別件数、仕事量上界、再初期化数、GPU読戻し回数・byte数、CPU補修数を1ケース1レコードへ集約する。現在の `activeCount×chunk` は早期脱出分も含む上界で、実行した反復数そのものではない。実反復を測るならテスト時のみGPU内で集約し、区別して記録する。
テストは既存の `regression.mjs` と `browser_smoke.mjs` を必要な分だけ拡張する。初期表示、同じSeahorse座標のFAST/Deep、実軸付近、既知の黒領域問題の座標を小さな代表セットとする。最初は同じadapter・解像度・予算でウォームアップ1回+計測3回。少数回の測定からp95は主張しない。
GPU制御は **検証用Edgeを1インスタンス、タブを1つ** に限定し、ケースを直列実行する。Edge内部の複数プロセスはあり得るが、ケースごとにブラウザーやプロファイルを増やさない。終了時に今回の検証用ブラウザーを閉じ、通常利用のブラウザーは終了対象にしない。
採用条件は、同条件での数値失敗の減少または非増加、計測した時間・仕事量の削減、既知の表示破綻がないこと。P0で従来未検査の失敗を可視化した直後は、修正後の正しい状態を比較基準とする。速度改善率・任意座標での未確定0は実装前に約束しない。
既存テストはこの案の採用条件をまだすべて検査しない。特に `numericalFailures=0` とFAST/Deep一致だけではP0の終端検査や共通する誤りを検出できない。既存の比較関数は不一致箇所だけを高精度で確認するため、一致した画素の独立確認も少数必要である。追加する確認ケースと、現行テストが未対応の範囲は実装設計に明記した。
## 6. 後回しにする案
- BLA、series approximation等の別の近似加速器の再導入。まず既存計算の重複・同期・補修を直す。
- 全画面を常にDS/BigIntにする変更。高精度が必要な画素へ限定する。
- Worker・Edge・GPU投入数の無制限な増加。
- 誤差guardの緩和、未確定画素の着色だけで件数を減らす変更。
- 常時shadow比較、フレームごとの全面ハッシュ、新しい多段階昇格ゲート。
未使用の `encodeDeepBucketHistogramStats()` や旧背景補修経路などの追加整理は可能だが、実行されていないコードの削除だけで数十秒の描画時間は解消しない。次の実装では、上記の実際に実行される仕事量と待ち合わせを優先する。

View file

@ -1,879 +0,0 @@
{
"date": "2026-09-07T06:53:28.990Z",
"experiment": "rounding",
"sourceSha256": "7db5cbd5c7f802a3d1db3d90861d8cccdc2a353cb12312907c1afb94d4e08020",
"viewport": {
"width": 320,
"height": 240,
"deviceScaleFactor": 1
},
"quality": "standard",
"iterations": 16384,
"variants": [
256,
"hoisted-rounding"
],
"cases": [
{
"id": "reset",
"re": "-0.5",
"im": "0",
"span": "3.4"
},
{
"id": "deep",
"re": "-0.743643887037151",
"im": "0.13182590420533",
"span": "3.4e-13"
},
{
"id": "mid",
"re": "-0.743643887037151",
"im": "0.13182590420533",
"span": "1e-6"
}
],
"runs": [
{
"round": -1,
"chunk": 256,
"case": "reset",
"ms": 774.7000000476837,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": -1,
"chunk": 256,
"case": "deep",
"ms": 12634.5,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 410,
"syncs": 401,
"renderSubmits": 371,
"renderSyncs": 367,
"statsPasses": 247,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": -1,
"chunk": 256,
"case": "mid",
"ms": 3024.899999856949,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 581,
"syncs": 566,
"renderSubmits": 171,
"renderSyncs": 165,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": -1,
"chunk": "hoisted-rounding",
"case": "reset",
"ms": 780.2000000476837,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": -1,
"chunk": "hoisted-rounding",
"case": "deep",
"ms": 7691.5,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 304,
"syncs": 295,
"renderSubmits": 265,
"renderSyncs": 261,
"statsPasses": 143,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": -1,
"chunk": "hoisted-rounding",
"case": "mid",
"ms": 2900.399999856949,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 482,
"syncs": 467,
"renderSubmits": 178,
"renderSyncs": 172,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": 256,
"case": "reset",
"ms": 746.8000001907349,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": 256,
"case": "deep",
"ms": 11251.399999856949,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 388,
"syncs": 379,
"renderSubmits": 349,
"renderSyncs": 345,
"statsPasses": 226,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": 256,
"case": "mid",
"ms": 2851.5,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 557,
"syncs": 542,
"renderSubmits": 169,
"renderSyncs": 163,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": "hoisted-rounding",
"case": "reset",
"ms": 761.6000001430511,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": "hoisted-rounding",
"case": "deep",
"ms": 8055,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 315,
"syncs": 306,
"renderSubmits": 276,
"renderSyncs": 272,
"statsPasses": 150,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": "hoisted-rounding",
"case": "mid",
"ms": 3079,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 487,
"syncs": 472,
"renderSubmits": 172,
"renderSyncs": 166,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": "hoisted-rounding",
"case": "reset",
"ms": 811.9000000953674,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": "hoisted-rounding",
"case": "deep",
"ms": 8119.399999856949,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 317,
"syncs": 308,
"renderSubmits": 278,
"renderSyncs": 274,
"statsPasses": 156,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": "hoisted-rounding",
"case": "mid",
"ms": 2985.800000190735,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 488,
"syncs": 473,
"renderSubmits": 171,
"renderSyncs": 165,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": 256,
"case": "reset",
"ms": 824.4000000953674,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": 256,
"case": "deep",
"ms": 11715.299999952316,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 397,
"syncs": 388,
"renderSubmits": 358,
"renderSyncs": 354,
"statsPasses": 236,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": 256,
"case": "mid",
"ms": 2930.5999999046326,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 576,
"syncs": 561,
"renderSubmits": 179,
"renderSyncs": 173,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": 256,
"case": "reset",
"ms": 696.5,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": 256,
"case": "deep",
"ms": 11048.5,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 384,
"syncs": 375,
"renderSubmits": 345,
"renderSyncs": 341,
"statsPasses": 223,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": 256,
"case": "mid",
"ms": 2659.2000000476837,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 553,
"syncs": 538,
"renderSubmits": 169,
"renderSyncs": 163,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": "hoisted-rounding",
"case": "reset",
"ms": 648.6000001430511,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": "hoisted-rounding",
"case": "deep",
"ms": 7684.200000047684,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 287,
"syncs": 278,
"renderSubmits": 248,
"renderSyncs": 244,
"statsPasses": 127,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": "hoisted-rounding",
"case": "mid",
"ms": 3060.0999999046326,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 462,
"syncs": 447,
"renderSubmits": 175,
"renderSyncs": 169,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
}
],
"environment": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0",
"deviceMemory": 8,
"hardwareConcurrency": 8,
"gpu": {
"adapterInfo": {
"vendor": "intel",
"architecture": "gen-9",
"device": "",
"description": ""
},
"adapterLimits": {
"maxBufferSize": 2147483648,
"maxStorageBufferBindingSize": 2147483644,
"maxComputeWorkgroupsPerDimension": 65535,
"maxTextureDimension2D": 16384
},
"compilation": [
{
"label": "direct",
"messages": []
},
{
"label": "unknown-stats",
"messages": []
},
{
"label": "real-axis-symmetry-copy",
"messages": []
},
{
"label": "color",
"messages": []
},
{
"label": "present",
"messages": []
},
{
"label": "active-prepare-production",
"messages": []
},
{
"label": "deep-active-resume-init-production",
"messages": []
},
{
"label": "deep-active-continue-production",
"messages": []
},
{
"label": "deep-compact-init",
"messages": []
},
{
"label": "deep-compact-continue",
"messages": []
},
{
"label": "deep",
"messages": []
},
{
"label": "deep-post-stats-production",
"messages": []
},
{
"label": "deep-bucket-histogram-production",
"messages": []
},
{
"label": "deep-bucket-prefix-production",
"messages": []
},
{
"label": "deep-bucket-scatter-production",
"messages": []
},
{
"label": "deep-correction",
"messages": []
},
{
"label": "deep-correction-queued-production",
"messages": []
},
{
"label": "failure-tile-map",
"messages": []
}
],
"uncapturedErrors": [],
"lossReason": "",
"activeGpuStage": "",
"lastCompletedGpuStage": "pixel frontier post-stats"
}
}
}

View file

@ -1,879 +0,0 @@
{
"date": "2026-09-07T06:51:15.620Z",
"experiment": "stats",
"sourceSha256": "7db5cbd5c7f802a3d1db3d90861d8cccdc2a353cb12312907c1afb94d4e08020",
"viewport": {
"width": 320,
"height": 240,
"deviceScaleFactor": 1
},
"quality": "standard",
"iterations": 16384,
"variants": [
256,
"deferred-stats"
],
"cases": [
{
"id": "reset",
"re": "-0.5",
"im": "0",
"span": "3.4"
},
{
"id": "deep",
"re": "-0.743643887037151",
"im": "0.13182590420533",
"span": "3.4e-13"
},
{
"id": "mid",
"re": "-0.743643887037151",
"im": "0.13182590420533",
"span": "1e-6"
}
],
"runs": [
{
"round": -1,
"chunk": 256,
"case": "reset",
"ms": 721.1000001430511,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": -1,
"chunk": 256,
"case": "deep",
"ms": 12872.099999904633,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 419,
"syncs": 410,
"renderSubmits": 380,
"renderSyncs": 376,
"statsPasses": 256,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": -1,
"chunk": 256,
"case": "mid",
"ms": 3186.5,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 597,
"syncs": 582,
"renderSubmits": 178,
"renderSyncs": 172,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": -1,
"chunk": "deferred-stats",
"case": "reset",
"ms": 777.5999999046326,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": -1,
"chunk": "deferred-stats",
"case": "deep",
"ms": 12699.600000143051,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 418,
"syncs": 409,
"renderSubmits": 379,
"renderSyncs": 375,
"statsPasses": 20,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": -1,
"chunk": "deferred-stats",
"case": "mid",
"ms": 3130,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 593,
"syncs": 578,
"renderSubmits": 175,
"renderSyncs": 169,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": 256,
"case": "reset",
"ms": 792,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": 256,
"case": "deep",
"ms": 12410.699999809265,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 417,
"syncs": 408,
"renderSubmits": 378,
"renderSyncs": 374,
"statsPasses": 255,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": 256,
"case": "mid",
"ms": 2728.2999999523163,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 588,
"syncs": 573,
"renderSubmits": 171,
"renderSyncs": 165,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": "deferred-stats",
"case": "reset",
"ms": 757.0999999046326,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": "deferred-stats",
"case": "deep",
"ms": 11916.299999952316,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 402,
"syncs": 393,
"renderSubmits": 363,
"renderSyncs": 359,
"statsPasses": 20,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 0,
"chunk": "deferred-stats",
"case": "mid",
"ms": 2676.2000000476837,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 571,
"syncs": 556,
"renderSubmits": 169,
"renderSyncs": 163,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": "deferred-stats",
"case": "reset",
"ms": 721,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": "deferred-stats",
"case": "deep",
"ms": 12139.099999904633,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 409,
"syncs": 400,
"renderSubmits": 370,
"renderSyncs": 366,
"statsPasses": 20,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": "deferred-stats",
"case": "mid",
"ms": 2649.2999999523163,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 580,
"syncs": 565,
"renderSubmits": 171,
"renderSyncs": 165,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": 256,
"case": "reset",
"ms": 642.9000000953674,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": 256,
"case": "deep",
"ms": 11928.299999952316,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 406,
"syncs": 397,
"renderSubmits": 367,
"renderSyncs": 363,
"statsPasses": 244,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 1,
"chunk": 256,
"case": "mid",
"ms": 2809.7999999523163,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 575,
"syncs": 560,
"renderSubmits": 169,
"renderSyncs": 163,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": 256,
"case": "reset",
"ms": 748.3999998569489,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": 256,
"case": "deep",
"ms": 12037.5,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 405,
"syncs": 396,
"renderSubmits": 366,
"renderSyncs": 362,
"statsPasses": 244,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": 256,
"case": "mid",
"ms": 2778.899999856949,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 574,
"syncs": 559,
"renderSubmits": 169,
"renderSyncs": 163,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": "deferred-stats",
"case": "reset",
"ms": 776.8000001907349,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 39,
"syncs": 34,
"renderSubmits": 26,
"renderSyncs": 22,
"statsPasses": 11,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "e46506bf6f588119c432eaf9506b72e33211a8622d2143cdb64b3fee12751d92",
"smoothSha256": "a653eae1e09fcca512e8bfacb7022bd01c7fac50808de2bca1864b9382cf31d9"
},
"png": {
"width": 320,
"height": 240,
"bytes": 41250,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": "deferred-stats",
"case": "deep",
"ms": 11904.300000190735,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 400,
"syncs": 391,
"renderSubmits": 361,
"renderSyncs": 357,
"statsPasses": 20,
"workBound": 3997696,
"hashes": {
"pixels": 76800,
"metaSha256": "0a19df20b4e73803fd5299dd8f388414317240a861436b278b62a966c69c8087",
"smoothSha256": "b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e"
},
"png": {
"width": 320,
"height": 240,
"bytes": 152907,
"mismatches": 0,
"iter": 16384
},
"errors": []
},
{
"round": 2,
"chunk": "deferred-stats",
"case": "mid",
"ms": 3038.9000000953674,
"width": 320,
"height": 240,
"budget": 16384,
"numericalFailures": 0,
"provisional": false,
"status": "complete",
"submits": 584,
"syncs": 569,
"renderSubmits": 184,
"renderSyncs": 178,
"statsPasses": 18,
"workBound": 3999744,
"hashes": {
"pixels": 76800,
"metaSha256": "458a50b255ea9bb7815e56d4267ca30bb1ce266ee138c8d1ee789d661422c169",
"smoothSha256": "1d967e370c665469a4b0c67a7bda5e9af0cdf592346537021a67e21bc6227a5f"
},
"png": {
"width": 320,
"height": 240,
"bytes": 46304,
"mismatches": 0,
"iter": 16384
},
"errors": []
}
],
"environment": {
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/152.0.0.0 Safari/537.36 Edg/152.0.0.0",
"deviceMemory": 8,
"hardwareConcurrency": 8,
"gpu": {
"adapterInfo": {
"vendor": "intel",
"architecture": "gen-9",
"device": "",
"description": ""
},
"adapterLimits": {
"maxBufferSize": 2147483648,
"maxStorageBufferBindingSize": 2147483644,
"maxComputeWorkgroupsPerDimension": 65535,
"maxTextureDimension2D": 16384
},
"compilation": [
{
"label": "direct",
"messages": []
},
{
"label": "unknown-stats",
"messages": []
},
{
"label": "real-axis-symmetry-copy",
"messages": []
},
{
"label": "color",
"messages": []
},
{
"label": "present",
"messages": []
},
{
"label": "active-prepare-production",
"messages": []
},
{
"label": "deep-active-resume-init-production",
"messages": []
},
{
"label": "deep-active-continue-production",
"messages": []
},
{
"label": "deep-compact-init",
"messages": []
},
{
"label": "deep-compact-continue",
"messages": []
},
{
"label": "deep",
"messages": []
},
{
"label": "deep-post-stats-production",
"messages": []
},
{
"label": "deep-bucket-histogram-production",
"messages": []
},
{
"label": "deep-bucket-prefix-production",
"messages": []
},
{
"label": "deep-bucket-scatter-production",
"messages": []
},
{
"label": "deep-correction",
"messages": []
},
{
"label": "deep-correction-queued-production",
"messages": []
},
{
"label": "failure-tile-map",
"messages": []
}
],
"uncapturedErrors": [],
"lossReason": "",
"activeGpuStage": "",
"lastCompletedGpuStage": "pixel frontier post-stats"
}
}
}

View file

@ -1,46 +0,0 @@
# Mandelbrot WebGPU
`index.html` をWebGPU対応のEdge/Chromeで開く。外部ライブラリやビルドは不要。
- 描画は高速方式に統一。画素間隔に応じてDirect f32または参照軌道を使うFAST perturbationへ自動で切り替える。旧「正確」の共有URLもこの方式で開く。
- パン/ズーム、配色変更、URLによる表示位置の共有、PNG出力に対応。
- 数値未確定画素の補修と未脱出画素の継続計算を終えてから、完成画像を履歴へ保存する。
- 計算中は現在の視点の暫定画像を表示する。「画質」は高速・標準・高画質の3段階で、解像度と反復上限をまとめて変更する。未脱出を集合内の証明とは扱わない。
- 画面と同じ解像度・1 sampleのPNGは、補修済みの完成画像をそのまま保存する。
- 色相変化は表示済みの数値結果を使い、計算中も色を更新する。停止ボタンまたは色相位置の手動操作で停止する。
- 彩色は「ピンクと黒」を追加し、「桃翠」「ネオン」「紅碧」「深海」を削除した。彩色の変更は計算中でも表示済みの数値結果を使い、数値再計算を起動しない。
- 描画時間は初期描画から精細化・数値補修・最終GPU処理までの合計。計算中は経過時間、完了時は確定した時間を表示する。
BLAと比較専用の監査処理は削除済み。[整理内容と検証範囲](CLEANUP.md)を参照。
[軽量化の実装結果と測定値](IMPLEMENTATION_RESULTS.md)を参照。承認前の[改善案](PERFORMANCE_PLAN.md)と[実装設計](PERFORMANCE_IMPLEMENTATION.md)も残している。
色相・時間表示の修正と描画方式統合については[ビューワー修正](VIEWER_FIXES.md)を参照。
BLAを使わず現行品質を保持する追加候補の比較と、補修集計・丸め定数再利用の実装結果は[追加軽量化の実測](PERFORMANCE_CANDIDATES.md)を参照。
2026-09-08の追加調査は[軽量化・高品質化の実装計画](NEXT_QUALITY_PLAN.md)を参照。二乗の丸め、継続計算の処理単位、高画質2倍解像度を実測し、比較画像も掲載した。
## 検証
Node.js 22以降で実行する。追加パッケージは不要。
```sh
node tests/regression.mjs
```
構文、現行シェーダーのハッシュ、バックエンド選択、フレーム振り分け、Worker、補修対象と処理量上限を確認する。ハッシュファイルを無条件に再生成してテストを通さないこと。
実GPUのスモークテストは、独立した一時プロファイルのEdge/Chromiumをリモートデバッグポート9333で起動して実行する。
```sh
node tests/browser_smoke.mjs 9333 all
```
`all` は `reset`、`fast`、`mid`、`legacy`、`axis`、`black`、`kernels`、`cancel`、`budget`、`ui`、`palette`、`pan`、`quality` に置き換えて単独実行できる。描画6ケース(旧方式指定の移行を含む)、キューの被覆、独立高精度サンプル、計算中のキャンセル、指定反復予算、PNGの復号、色相変化、時間表示、彩色変更時の数値再計算の抑止、画質ごとの解像度と有限反復での完了を確認する。
起動時は拡張機能と同期を無効にし、専用プロファイルのタブを1つだけにする。テストはそのタブを再利用し、最後に `about:blank` へ戻す。終了後は検証用ブラウザーを閉じる。
測定が必要な場合だけ `node tests/browser_smoke.mjs 9333 bench` を実行する。初期表示・Seahorse FASTを各1回ウォームアップ+3回測定する。通常の回帰確認ではベンチマークを繰り返さない。
検証APIと詳細計測は `index.html?test` でのみ有効。通常起動では監査計算を自動実行しない。

View file

@ -1,79 +0,0 @@
# 色相・描画時間・描画方式の修正
対象:`index.html`。2026-09-06。
## 色相変化
従来は数値計算中の色変更を保留し、精細化終了時に保留分を実行する処理も抜けていた。精細化終了後に最新の色を必ず反映するようにした。
自動色相変化中は、公開済みの数値結果だけを別のGPUバッファへ保持し、独立した色用テクスチャへ描く。計算中のバッファやfront/backを変更せず、GPUキューの順序を保って表示色を更新する。位置が変わった際は保存した視点に応じて再投影する。新しい暫定結果の公開時に色用の数値結果も更新する。
追加領域は自動色相変化を使う間だけ確保する(12 byte/画素+32 byte、320×240で約0.88 MiB)。停止後の色反映または次の結果公開で解放する。数値再計算はせず、色更新は最大約20回/秒。GPUが処理中の場合のフレームレートは機器の負荷に依存する。
## 描画時間
従来の表示は最後の処理段階の時間だった。初期描画・精細化待ち・参照作成・数値補修・最終GPU処理を含む、1回の生成全体の経過時間へ変更した。描画中は250 ms間隔で更新し、完了時にその場で確定する。色変更では計測をやり直さない。
途中キャンセルとエラーを完了と区別し、古い生成からの完了通知は新しい計測を終了させない。起動時に初期化完了通知から同じ視点を重複生成する処理も抑止した。画面への物理的な走査時間は計測に含まない。
## 描画方式の統合
独立した「正確」モードと専用Direct DSの4シェーダー、専用パイプライン、全画面Deep初期計算、未使用の背景Deep切り替えを削除した。描画方式の選択UIはなくし、従来の高速方式へ統一した。旧URLの `rm=accurate` は高速方式で復元し、旧ローカル設定も除去する。
高速方式でも使うBigInt参照、局所参照、DS補修、CPU高精度補修、未脱出画素の継続計算は維持する。これらの数値シェーダーは変更していない。名前としての「正確」を外すことで、有限反復の描画を数学的な全画素証明と誤解させない。任意の座標・反復予算での数値失敗0を保証するものではない。
中間倍率の確認で、自動反復の150000回へ正常到達しても安定判定を満たさないと画像を完成扱いにできない問題も見つかった。残った全画素の到達反復数を確認できた場合は有限予算の結果として公開し、「反復上限で完了」と表示する。未脱出は未脱出のまま記録し、内点へ置き換えない。到達不足や数値故障は引き続き完成扱いにしない。
## 検証
`node tests/regression.mjs` と、専用Edge 1インスタンス・1タブで `node tests/browser_smoke.mjs 9333 all` を使用する。色相テストはボタンの状態だけでなく、表示に使うGPUテクスチャの画素変化と数値バッファの不変性を確認する。長い数値補修中の色変更、停止、手動操作、完成時の保留色反映、通常描画との数値一致も対象。
時間表示は段階間の待ちを含む合計、数秒かかる実描画の完了直後の表示、色変更で時間が変わらないことを確認する。旧設定と共有URLの移行、描画6ケース、独立BigIntサンプル、キャンセル、PNGも従来の回帰テストに含める。
追加した中間倍率(Seahorse、span=1e-6)と旧Deep専用の黒領域は、それぞれ12サンプルの分類と脱出反復数が独立BigInt計算に一致した。一方、連続色付け用smooth値の差は最大約0.0367反復分/0.0516反復分だった。両ケースではsmoothのULP精度を保証せず、偏差をテスト出力に記録する。高速Seahorseと実軸の8 ULP以内という検査は維持している。旧正確モードと同じ色の数値精度を維持したという主張ではない。
最終版でCPU回帰と実GPUの `all` が通過した。6描画ケースの数値失敗は0。1100×720の実画面でも自動色相変化による色の変化、描画完了直後の「2.32 s」、描画方式の選択肢がないことを確認した。検証用Edgeと専用プロファイルは終了・削除済みで、残存Edgeプロセスは0だった。
## 2026-09-07:配色整理と彩色変更の応答
「桃翠」「ネオン」「紅碧」「深海」をUIと色シェーダーから削除し、「ピンクと黒」(ID 19)を追加した。残る配色のIDは維持し、削除した配色を指定する旧URLは「昼夜」に戻す。
通常の彩色変更ハンドラー自体は数値再計算を起動していなかった。しかし計算中の彩色変更は、自動色相変化を有効にしたときだけ使える公開済みデータに依存し、無効時は計算終了まで待たされていた。計算中にも公開済みデータを保持することで、通常の彩色変更・色相位置・色周期も数値計算とは独立して反映する。連続入力は描画フレームごとに最新値へまとめる。
この変更に伴い、上記の追加領域は自動色相変化中に加えて数値計算中にも保持する。完成後、自動色相変化が無効で保留中の彩色もなければ解放する。追加容量は12 byte/画素+32 byteで変わらない。
色だけが違うURLへの移動・表示履歴の復元は再彩色だけにし、描画サイズが変わらないresize通知からの数値再計算も抑止した。計算用シェーダーは変更していない。
`node tests/browser_smoke.mjs 9333 palette` にて、320×240の彩色反映は完成後約19.1 ms、実際の数値補修中約9.3 msだった(各1回の測定、機器や負荷により変動)。数値投入回数・生成ID・計算結果が彩色変更で変わらず、計算中の彩色変更でも最終数値ハッシュが一致し、PNGの画素不一致が0であることを確認した。
CPU回帰と実GPUの `palette`・`ui` が通過。1100×720の実画面で新配色を確認し、削除済み配色のURLを開いても再計算せず「昼夜」へ戻ることも確認した。
その後「ピンクと黒」の暗部をプラム色へ調整。深いプラムからモーヴ、くすんだローズ、淡いピンクへ移る5色のグラデーションとし、この配色の未脱出・内点表示も紫がかった暗色にした。数値判定は変更せず、CPU回帰と1100×720の実GPU表示で確認した。
## 色相変化中のパン
彩色が遅いと、新しい色相変更が待機中に届き続け、再彩色ループが `recoloring=true` のままパン後の数値計算開始を阻害していた。1回の彩色で必ず処理を返し、視点更新があるときは数値計算を優先するようにした。
ドラッグ中は再投影による移動表示を優先し、自動色相の描画更新は操作終了後に再開する。数値計算中の自動彩色は最大10回/秒、静止中は従来の最大20回/秒。公開済みデータを使う彩色も未完了のGPU処理を1件までに制限し、遅いGPUへ処理を積み続けない。色相変化のオン状態は維持する。
320×240で同一のパンを実マウスイベントから実行した結果、完了までオフ約343 ms、オン約352 msだった。彩色へ意図的に100 msの遅延を加えた検査でも約677 msで完了し、3条件の数値ハッシュが一致した。各1回の測定であり、機器・座標に依存する。再現検査は `node tests/browser_smoke.mjs 9333 pan`。
プラムの暗部も再調整し、未脱出・内点は `#0e0a13`、グラデーションの最暗部は `#140e1a` とした。ローズと淡いピンクの色は維持する。
## 画質設定への統合
負荷設定と反復上限を「画質」へ統合した。画面のデバイスピクセル数を基準に、次の解像度倍率と反復上限を適用する。
| 画質 | 縦横の倍率 | 画素数上限 | 反復上限 |
| --- | ---: | ---: | ---: |
| 高速 | 0.75倍 | 524,288 | 4,096 |
| 標準 | 1倍 | 1,048,576 | 16,384 |
| 高画質 | 1.5倍 | 2,359,296 | 32,768 |
高画質は標準の最大2.25倍の画素を実際に計算する。旧「精細」の近隣画素を平均する処理は使わない。端末メモリーが4 GB以下の場合は画素数上限を半減し、GPUのバッファ・テクスチャ制限も適用する。
旧「精細」「保守的」の補修回数増加とStrict経路を通常操作から外し、補修方針を標準へ統一した。反復上限へ到達した未脱出画素を有限反復の結果として完了できるようにし、長い自動継続を避ける。到達不足や数値故障を完了へ偽装することはない。高画質は計算量が増えるため、標準より完了に時間がかかる。
画質は共有URLの `q` とローカル設定へ保存する。旧「精細」「保守的」は高画質へ、旧反復上限の指定は対応する有限予算の画質へ移行する。画質変更時は進行中の計算をキャンセルし、新しい解像度で描画する。彩色だけの変更では数値計算をやり直さない。
CPU回帰と実GPUの `quality` 検査で、初期表示・Seahorse深拡大・中間倍率の3視点×3画質が完了した。320×240の表示領域で描画サイズは高速240×180、標準320×240、高画質480×360。9ケースとも数値失敗0、暫定状態の解消、上限到達、PNG一致を確認した。設定復元と計算中の画質切り替えも通過。これは検査した視点での結果であり、任意の座標での完了時間を保証するものではない。

View file

@ -0,0 +1,83 @@
# Final structure cleanup — 2026-09-12
## Intent
Finish the structural cleanup after the component pruning pass without changing Mandelbrot numerical behavior. The production HTML is now the single source of truth for both release and regression.
## Changes
### Single source of truth
- Removed the persistent `tests/index.test.html` copy.
- `tests/regression.mjs` now parses and executes production `index.html` directly.
- `tests/browser_smoke.mjs` generates a temporary instrumented page from production `index.html`, then removes it even when browser connection fails.
- Added a regression assertion that a persistent test HTML copy must not reappear.
### Correctness fixes found during cleanup
- Fixed `cancelRender()` export cancellation/generation update. The production-only typo `truestate.token++` is gone; export cancellation and `state.token++` are separate operations.
- Fixed the render cancellation/error path that called the nonexistent `returnfinishRenderClock(...)` token. Cancelled generations now close the render clock correctly and return.
### Removed write-only state and UI churn
Removed production state that had no readers:
- `lastEngine`
- `frontierRounds`
- `frontierPixels`
- `frontierEscaped`
- `frontierIter`
- `frontierMinimumIter`
- `frontierConverged`
- `gpuUnavailable`
- `qualityIter`
- `refineMs`
- `lastRender`
- `gpuStage`
Progress-only `lastEngine` assignments and their no-op `updateStats()` calls were removed. The user-visible status continues to derive from `drawState`, unresolved counts, temporal fill, render clock, and GPU error state.
### Renderer diagnostics
Removed retained diagnostics that were never consumed:
- `this.adapter`
- `this.adapterInfo`
- `this.compilation`
- `this.uncapturedErrors`
Shader compilation errors are still checked and thrown immediately. The latest uncaptured GPU error is still copied to `state.gpuError`. Device-loss stage tracking stays in renderer-local `activeGpuStage` / `lastCompletedGpuStage`.
### Fixed-point / WGSL dead definitions
- Removed production-only unused `fromDec()` and `decimalRequiredBits()` helpers. Test decimal conversion now lives only in the test harness.
- Removed unused WGSL `FIELD_INTERIOR_LIKELY`.
- All 19 surviving kernels are byte-identical to the pre-cleanup kernels after deleting that single unused constant line.
- Simplified stale reference-selector comments and redundant return logic.
### Export metadata
Both export paths now call one `buildExportMetadata()` function. Common fields now have one definition for:
- finite-budget completion
- membership certification
- precision policy
- iteration policy
- view coordinates
- palette description
- renderer/shader version
- pixel contract
Path-specific tile/ring fields are added only when applicable.
### Small dead-code cleanup
- Removed dead `dispatchWork` local after correction accounting was deleted.
- Removed dead `sampleCount`, `lastUi`, and final-frame `elapsed` locals.
- Renamed stale `reusePreview` to `reusePriorField` and the no-frame `PREVIEW` draw-state label to `PENDING`.
## Verification
`node tests/regression.mjs` passes and directly executes production JS. It covers routing, finite-budget semantics, staged operation-limit scoring, exact interior proof, integer-grid history reuse, nearby reference reuse, Worker lifecycle, correction pipeline count, export cancellation/generation behavior, common export metadata, pinned WGSL hashes, and CPU worker smoke tests.
The optional browser smoke script passes syntax validation. A real WebGPU browser run is not claimed in this environment.

View file

@ -0,0 +1,44 @@
{
"date": "2026-09-12",
"baseline": {
"artifact": "mandelbrot-cleaned-all/index.html",
"bytes": 212483,
"gzip9_bytes_shell": 53779
},
"final": {
"index_bytes": 208576,
"gzip9_bytes_shell": 52563,
"sha256": "8355da57e2f546c4e1cbeb2237e9dbfb65168b6d4c254929a2b02078c086a935",
"wgsl_kernels": 19,
"persistent_test_html": false,
"definition_only_locals_or_functions": 0
},
"change": {
"index_bytes": -3907,
"index_percent": -1.8387,
"gzip9_bytes_shell": -1216,
"gzip9_percent": -2.2616
},
"kernel_equivalence": {
"kernel_object_keys_including_version": 20,
"byte_identical_without_normalization": 9,
"byte_identical_after_removing_old_unused_FIELD_INTERIOR_LIKELY_line": 20,
"behavioral_kernel_changes": 0
},
"regression": {
"status": "PASS",
"runs_after_warmup": 12,
"median_ms": 54.8059,
"min_ms": 47.6742,
"max_ms": 71.0208
},
"browser_smoke": {
"script_syntax": "PASS",
"temporary_file_cleanup_on_connection_failure": "PASS",
"real_webgpu_run": "not available in this environment"
},
"fixed_bugs": [
"cancelRender production typo truestate.token++",
"render error/cancellation typo returnfinishRenderClock(...)"
]
}

View file

@ -0,0 +1,153 @@
# 5改善案の本体統合と再評価
## 対象
前回評価表の「Numeric tile cache」より上にあった次の5案を `index.html` に統合した。
1. 一般周期検出
2. perturbation series approximation
3. sparse active state
4. dynamic multi-reference
5. CPU Worker fallback
`Numeric tile cache` 以下は今回の対象外。基準は統合前の `index.html`(SHA-256 `8355da57...086a935`)。
## 実装
### 1. 周期検出
- direct f32 kernel に Brent 型 O(1) candidate search を追加。
- 単純な「近いから内部」判定は再評価で危険と判明したため、候補後に **2周期分の再一致**と **周期乗数 `|μ| < 0.95`** を確認してから `FIELD_INTERIOR_HEURISTIC=2` にする。
- `FIELD_INTERIOR_PROVEN=3` とは分離し、heuristic が1画素でもあれば `membershipCertified=false`。
- strict/export では無効。
- FAST perturbation 上の周期早期停止は撤回。forced-direct 診断で deep seahorse に false candidate が残ったため、deep path は series / continuation に任せる。
### 2. Series approximation
- reference worker で 1〜6次係数を更新。
- GPU に送るのは 1〜4次係数。5・6次は truncation error の安全側判定に使用。
- validity 条件を満たす最大 jump を reference ごとに計算し、FAST perturbation の初期反復を4次多項式評価へ置換。
- strict/export では series jump を無効化し、従来 recurrence を使う。
### 3. Sparse active state
旧 continuation は state/queues/mark をほぼ全画素サイズで持っていた。新構成は以下。
- `DeepState`: active slot ごと 32 B
- queue A/B: active slot ID
- `pixelMap`: active slot → pixel index
- membership: 1 bit/pixel bitset
- 初期 capacity: `active * 1.25 + 256`(全画素上限)
- correction により新しい `OPERATION_LIMIT` が生じた場合は、slot allocator を複雑化せず metadata から compact session を再構築する。
### 4. Dynamic multi-reference
- failure-guided reference recovery を最大3 passまで反復。
- 既に試した reference identity は再試行しない。
- 1 pass の改善が `max(8 pixels, 2%)` 未満なら打ち切る。
- 固定 2x2 / 3x3 配置ではなく、既存 failure sample + scorer を使って必要箇所だけ reference を追加する。
### 5. CPU Worker fallback
- WebGPU 非対応/初期化失敗時に最大4 Workerで direct Mandelbrot を描画。
- cardioid / period-2 bulb、有限 iteration、既存 palette の主要表示をサポート。
- JS `Number` で adjacent pixel を区別できない deep zoom では停止し、誤った deep image を出さない。
- CPU fallback の `OPERATION_LIMIT` と周期 heuristic は未確定扱い。WebGPU の high-precision path の代替ではなく互換経路。
## 再評価結果
### 周期検出
96×64、maxIter=8192、candidate は 65,536 iteration まで escape を追跡。
| view | production backend | 反復仕事量削減 | false candidate |
|---|---|---:|---:|
| overview | direct | 84.9% | 0 |
| period3-bulb | direct | 99.09% | 0 |
| period3-core | direct | 99.12% | 0 |
| seahorse-boundary | direct | 0% | 0 |
| exterior | direct | 80.0% | 0 |
| seahorse-deep | fast-extended | **周期停止を使用しない** | — |
forced-direct では seahorse-deep に false candidate が残るため、FAST 側を無効化した判断は維持する。
### Series approximation
production fast-reference worker の実出力を使用し、4次 series と直接 perturbation の delta を8点で比較。
| span | jump | 最大絶対誤差 |
|---:|---:|---:|
| 1e-5 | 53 | 7.11e-9 |
| 1e-7 | 446 | 7.74e-9 |
| 1e-9 | 930 | 7.46e-9 |
| 1e-11 | 1037 | 2.28e-9 |
reference build の一部として係数を作るため CPU 側の追加仕事はあるが、deep zoom ほど jump が大きい。実 GPU frame time はこの環境では測れていない。
### Sparse active state
standard 1,048,576 px の continuation workspace モデル。
| active ratio | 旧 | 新 | 削減 |
|---:|---:|---:|---:|
| 100% | 44.0 MiB | 44.13 MiB | -0.28% |
| 25% | 44.0 MiB | 13.89 MiB | 68.4% |
| 10% | 44.0 MiB | 5.64 MiB | 87.2% |
| 1% | 44.0 MiB | 0.686 MiB | 98.4% |
active がほぼ全画面なら pixel map + bitset の分だけわずかに悪化する。狙いは sparse frontier であり、その条件では効果が大きい。
### Dynamic multi-reference
production は failure-guided 最大3追加 reference。GPU end-to-end の `A_num` は実 GPU 不在で測れないため、前回と同じ f32 perturbation locality surrogate を再実行した。
seahorse span=1e-7:
- center 1 ref: class mismatch 2.578%
- center + 2x2 (5 refs): **1.445%**
- center + 3x3 (9 refs): 1.953%
reference を無制限に増やすのは逆効果になり得るため、「失敗箇所から選ぶ + 3 pass cap」は妥当。
### CPU fallback
production `cpuFallbackWorkerSource()` 自体を Node worker shim で実行。640×360、350 iter、7 run median。
- 1 Worker: 約 97.2 ms
- 4 Workers: 約 36.0 ms
- speedup: **2.70×**
- escaped / OP_LIMIT / heuristic / work count は 1 Worker と4 Workerで一致。
これはブラウザ 2D canvas compositing を含まない worker compute 値。
## 回帰・サイズ
- JS syntax: PASS
- production regression: **5/5 PASS**
- regression median: 約 0.08 s
- WGSL kernels: 19本、hash 更新済み
- `index.html`: 208,576 → 223,672 bytes(+7.24%)
- gzip -9: 52,563 → 57,198 bytes(+8.82%)
## 実 GPU 検証の限界
コンテナの Headless Chromium + SwiftShader は EGL 初期化に失敗し、WebGPU smoke を開始できなかった。そのため以下は未測定。
- stable frame p50/p95
- GPU series jump の実時間短縮
- sparse scatter/gather の実 GPU overhead
- dynamic multi-reference 導入後の production `A_num`
したがって **CPU/数値 surrogate を GPU frame-time として扱わない**。
## 結論
| 改善 | 統合判断 | 再評価 |
|---|---|---|
| 周期検出 | 採用(directのみ) | 高周期内部で非常に有効。FASTでは安全性不足のため無効 |
| Series | 採用(interactive) | deepほど jump が増え、有望 |
| Sparse active state | 採用 | active≪100%ならメモリ効果が明確 |
| Dynamic multi-reference | 採用(最大3 pass) | reference増加は非単調。failure-guided capが妥当 |
| CPU fallback | 採用 | 互換性改善。4 Workerで約2.7×、deep precision代替ではない |
次に実機で確認すべき優先順位は **series GPU frame time → sparse continuation latency → multi-reference の `A_num`**。

View file

@ -0,0 +1,6 @@
# Integrated improvements
- `IMPLEMENTATION.md`: 実装内容、設計判断、再評価、限界
- `results.json`: 再現用の生データ
- benchmark: `../../experiments/integrated_bench.mjs`
- regression: `../../tests/regression.mjs`

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,426 @@
{
"date": "2026-09-29",
"scope": [
"direct periodicity heuristic",
"perturbation series approximation",
"sparse active continuation state",
"dynamic failure-guided multi-reference",
"CPU Worker fallback"
],
"baseline": {
"path": "../mandelbrot_work/index.html",
"sha256": "8355da57e2f546c4e1cbeb2237e9dbfb65168b6d4c254929a2b02078c086a935",
"bytes": 208576,
"gzip9Bytes": 52702
},
"integrated": {
"sha256": "544820c5024cf6d56e012db6d1bec3ddfcef5d833691b890baa64b26c5c2ee5a",
"bytes": 223672,
"gzip9Bytes": 57375
},
"regression": {
"passes": 5,
"timesSeconds": [
0.08,
0.09,
0.07,
0.07,
0.09
],
"medianSeconds": 0.08,
"status": "PASS",
"kernelCount": 19
},
"webGpuBrowserSmoke": {
"status": "UNAVAILABLE_IN_CONTAINER",
"reason": "Headless Chromium SwiftShader/EGL initialization failed before WebGPU could run; no real GPU frame-latency claim is made."
},
"benchmarks": {
"date": "2026-09-29T08:28:46.101Z",
"periodicity": {
"w": 96,
"h": 64,
"maxIter": 8192,
"oracleIter": 65536,
"rows": [
{
"view": "overview",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 884420,
"heuristicWork": 133404,
"workReduction": 0.849162162773343,
"candidates": 96,
"falseCandidates": 0,
"baseLimit": 104,
"heuristicLimit": 8,
"diagnosticIfForcedDirect": null,
"ms": 68.786555
},
{
"view": "period3-bulb",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 50331648,
"heuristicWork": 460156,
"workReduction": 0.9908575216929117,
"candidates": 6144,
"falseCandidates": 0,
"baseLimit": 6144,
"heuristicLimit": 0,
"diagnosticIfForcedDirect": null,
"ms": 3092.6467549999998
},
{
"view": "period3-core",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 50331648,
"heuristicWork": 442395,
"workReduction": 0.991210401058197,
"candidates": 6144,
"falseCandidates": 0,
"baseLimit": 6144,
"heuristicLimit": 0,
"diagnosticIfForcedDirect": null,
"ms": 3061.9720069999994
},
{
"view": "seahorse-boundary",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 410111,
"heuristicWork": 410111,
"workReduction": 0,
"candidates": 0,
"falseCandidates": 0,
"baseLimit": 0,
"heuristicLimit": 0,
"diagnosticIfForcedDirect": null,
"ms": 17.153881000000183
},
{
"view": "seahorse-deep",
"productionBackend": "fast-extended",
"pixels": 6144,
"baseWork": 36579905,
"heuristicWork": 6167354,
"workReduction": null,
"candidates": 0,
"falseCandidates": 0,
"baseLimit": 4304,
"heuristicLimit": 4304,
"diagnosticIfForcedDirect": {
"workReduction": 0.8314004916087125,
"candidates": 4125,
"falseCandidates": 12
},
"ms": 2289.795112
},
{
"view": "exterior",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 686432,
"heuristicWork": 137063,
"workReduction": 0.8003254510279241,
"candidates": 72,
"falseCandidates": 0,
"baseLimit": 79,
"heuristicLimit": 7,
"diagnosticIfForcedDirect": null,
"ms": 37.95325000000048
}
],
"note": "Production DIRECT_F32 rule with two attracting-cycle confirmations. FAST/perturbation periodic early-stop is intentionally disabled."
},
"series": {
"rows": [
{
"span": 1e-05,
"jump": 53,
"errorLog2": -22.11596292311006,
"maxAbsError": 7.112067409017996e-09,
"maxRelativeError": 9.415623690713784e-05,
"refLen": 1200,
"buildMs": 32.31590200000028
},
{
"span": 1e-07,
"jump": 446,
"errorLog2": -22.302471602690176,
"maxAbsError": 7.736805567567409e-09,
"maxRelativeError": 0.0031991190750368034,
"refLen": 1200,
"buildMs": 33.50016399999913
},
{
"span": 1e-09,
"jump": 930,
"errorLog2": -22.038152635655024,
"maxAbsError": 7.459933177532973e-09,
"maxRelativeError": 1.015603159369818e-05,
"refLen": 1200,
"buildMs": 29.815043000000514
},
{
"span": 1e-11,
"jump": 1037,
"errorLog2": -23.96377927072575,
"maxAbsError": 2.2791360188558774e-09,
"maxRelativeError": 4.948137247650717e-06,
"refLen": 1200,
"buildMs": 37.88702799999919
}
],
"note": "Production fast-reference worker output. Error compares packed 4th-order series against direct perturbation over 8 screen-domain deltas."
},
"sparseActiveState": {
"rows": [
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 1,
"capacity": 524288,
"oldBytes": 23068672,
"newBytes": 23134208,
"reduction": -0.0028409090909091717
},
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 0.25,
"capacity": 164096,
"oldBytes": 23068672,
"newBytes": 7285760,
"reduction": 0.6841708096590908
},
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 0.1,
"capacity": 65792,
"oldBytes": 23068672,
"newBytes": 2960384,
"reduction": 0.8716708096590909
},
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 0.01,
"capacity": 6810,
"oldBytes": 23068672,
"newBytes": 365176,
"reduction": 0.9841700467196378
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 1,
"capacity": 1048576,
"oldBytes": 46137344,
"newBytes": 46268416,
"reduction": -0.0028409090909091717
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 0.25,
"capacity": 327936,
"oldBytes": 46137344,
"newBytes": 14560256,
"reduction": 0.6844149502840908
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 0.1,
"capacity": 131328,
"oldBytes": 46137344,
"newBytes": 5909504,
"reduction": 0.8719149502840909
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 0.01,
"capacity": 13364,
"oldBytes": 46137344,
"newBytes": 719088,
"reduction": 0.9844141873446378
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 1,
"capacity": 4194304,
"oldBytes": 184549376,
"newBytes": 185073664,
"reduction": -0.0028409090909091717
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 0.25,
"capacity": 1310976,
"oldBytes": 184549376,
"newBytes": 58207232,
"reduction": 0.6845980557528408
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 0.1,
"capacity": 524544,
"oldBytes": 184549376,
"newBytes": 23604224,
"reduction": 0.8720980557528409
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 0.01,
"capacity": 52685,
"oldBytes": 184549376,
"newBytes": 2842428,
"reduction": 0.9845980080691251
}
],
"note": "Production allocation: 32 B state + 2x4 B queues + 4 B pixel map per active slot, plus 1-bit membership bitmap. Capacity adds 25% + 256 headroom."
},
"multiReference": {
"maxGuidedPasses": 3,
"w": 64,
"h": 40,
"maxIter": 1200,
"rows": [
{
"view": "seahorse-1e-5",
"grid": 1,
"references": 1,
"total": 2560,
"refEnd": 0,
"mismatchClass": 3,
"mismatchIter": 155,
"mismatchRate": 0.001171875
},
{
"view": "seahorse-1e-5",
"grid": 2,
"references": 5,
"total": 2560,
"refEnd": 0,
"mismatchClass": 3,
"mismatchIter": 155,
"mismatchRate": 0.001171875
},
{
"view": "seahorse-1e-5",
"grid": 3,
"references": 9,
"total": 2560,
"refEnd": 0,
"mismatchClass": 3,
"mismatchIter": 155,
"mismatchRate": 0.001171875
},
{
"view": "seahorse-1e-7",
"grid": 1,
"references": 1,
"total": 2560,
"refEnd": 0,
"mismatchClass": 66,
"mismatchIter": 74,
"mismatchRate": 0.02578125
},
{
"view": "seahorse-1e-7",
"grid": 2,
"references": 5,
"total": 2560,
"refEnd": 0,
"mismatchClass": 37,
"mismatchIter": 41,
"mismatchRate": 0.014453125
},
{
"view": "seahorse-1e-7",
"grid": 3,
"references": 9,
"total": 2560,
"refEnd": 0,
"mismatchClass": 50,
"mismatchIter": 67,
"mismatchRate": 0.01953125
}
],
"note": "CPU f32 perturbation locality surrogate. Production selects up to three additional references from failure clusters instead of fixed grids."
},
"cpuFallback": {
"w": 640,
"h": 360,
"iter": 350,
"threads": 4,
"one": {
"medianMs": 97.22207600000002,
"times": [
115.15600200000154,
86.49102299999868,
97.22207600000002,
97.75022499999977,
95.21002200000112,
105.08147800000006,
81.41742200000044
],
"work": 2658084,
"escaped": 176648,
"operationLimit": 3394,
"heuristicInterior": 1654
},
"multi": {
"medianMs": 36.02884199999971,
"times": [
100.63864199999989,
61.91339699999844,
50.152170999999726,
34.4066870000006,
34.23579900000004,
36.02884199999971,
34.373131000000285
],
"work": 2658084,
"escaped": 176648,
"operationLimit": 3394,
"heuristicInterior": 1654
},
"speedup": 2.698451312978663,
"workMatch": true,
"escapedMatch": true,
"operationLimitMatch": true,
"heuristicMatch": true,
"note": "Production CPU fallback Worker source executed under node:worker_threads shim."
},
"sourceAudit": {
"heuristicClass": true,
"strictDisablesPeriodicity": true,
"seriesBinding": true,
"compactPixelMap": true,
"multiReferencePasses": true,
"cpuFallback": true
},
"environment": {
"node": "v22.16.0",
"cpus": 5,
"platform": "linux",
"arch": "x64"
}
},
"assessment": {
"periodicity": "ADOPT_DIRECT_ONLY",
"seriesApproximation": "ADOPT_EXPERIMENTAL_INTERACTIVE",
"sparseActiveState": "ADOPT",
"dynamicMultiReference": "ADOPT_CAPPED_FAILURE_GUIDED",
"cpuFallback": "ADOPT_COMPATIBILITY_PATH"
}
}

View file

@ -0,0 +1,137 @@
# Performance / Memory Cost Audit
## Purpose
Audit the integrated Mandelbrot viewer for processing that is low-value, redundant, or harmful from a speed / memory perspective. This audit intentionally **does not remove or alter any production behavior** in `index.html`.
Production source audited:
- `index.html`
- SHA-256: `544820c5024cf6d56e012db6d1bec3ddfcef5d833691b890baa64b26c5c2ee5a`
- Regression: PASS, 19 WGSL kernels
Measurements are CPU/static surrogates unless explicitly stated. Real WebGPU frame timing is not available in this container because the browser WebGPU backend cannot initialize successfully here.
## Conclusions
### A — High-confidence waste / harmful cost
| ID | Finding | Cost | Benefit observed | Confidence |
|---|---|---|---|---|
| A1 | Numeric history is recopied after every accepted recolor | 8 B/px copy per recolor | Numeric data did not change | Very high |
| A2 | `cpuFrameRgba` retains the assembled CPU fallback frame but is never read | 4 B/px JS heap retained | None | Very high |
| A3 | `certifyInterior()` runs a full-frame unknown-statistics pass and waits for GPU completion before primary compute | Full meta scan + queue synchronization | Partial proof/history-seed caller does not consume these stats | Very high |
| A4 | Strict direct rendering still updates Brent-cycle state even though periodicity classification is disabled | Per-iteration ALU/register state | None in strict mode | Very high |
| A5 | Export workspace survives export completion | Up to ~24 MiB GPU memory with 512 tiles / 3 slots | None while not exporting | Very high |
| A6 | Deep active/correction workspaces survive the hard frame that required them | active workspace up to ~44.1 MiB standard / ~176.5 MiB high; dense correction queue 4/16 MiB | None on later easy frames until reuse/resize | High |
### B — Strong optimization candidates, but keep until GPU A/B validation
| ID | Finding | Evidence | Assessment |
|---|---|---|---|
| B1 | Numeric history is always resident although exact-grid pan reuse is restrictive | 8 B/px resident. Under common budget-scaled resolutions, integer CSS pans 1–100 px produced 0/100 exact-grid reuse hits in the static alignment test | Likely over-allocated; make lazy/telemetry-driven rather than delete immediately |
| B2 | CPU exact cardioid/bulb tile proof runs for every numerical frame | 1024×768: ~4.8–6.1 ms median; 2048²: ~25–28 ms median. Four of six fixed corpus views certified 0 pixels | Add cheap spatial gate; direct backend especially needs A/B comparison |
| B3 | `fast-coverage-repair` runs a full-screen FAST pass after tiled coverage | Tile loops cover the full compute domain; mode 2 only repairs reason=0 untouched metadata | Likely redundant defensive pass; instrument reason=0 count before removal |
| B4 | `captureColorSource()` snapshots `meta+smooth` during rendering | 8 B/px copy per snapshot; provisional snapshots can occur every 125 ms. Snapshot allocation is 12 B/px | Useful only for live recoloring while render is in flight; gate more aggressively |
| B5 | Color-auto idle mode retains a color snapshot | standard ~12 MiB, high ~48 MiB at preset caps | Idle color-auto recolor path uses current field, not `recolorPublished()` | Snapshot should be render-only/on-demand |
| B6 | Adaptive initial-iteration probe uses high-precision scoring before every adaptive frame | Fixed corpus: ~1.3–12.5 ms and all six chose 350. Exploratory views can choose 512, so feature is not useless | Cache/gate/replace with cheaper trigger; do not delete blindly |
| B7 | Active continuation can replay already-performed iterations | initial OP_LIMIT session starts state at n=0; reference change resets progress to 0; recovered pixels can rebuild whole active session | Potentially large on the hardest views; add replay counters before redesign |
### C — Expensive but currently justified / not a deletion target
- General periodicity detection: CPU surrogate showed ~98% iteration reduction in period-3 interiors. It can add overhead on boundary-only views, but removal is not justified.
- Sparse numerical correction and reason bucketing: it adds full scans/queue construction, but isolates expensive DS/BigInt work. Needs GPU A/B before structural change.
- Stable texture reprojection: consumes one history texture but directly improves interaction latency.
- Reference cache / guided multi-reference: memory cost is bounded and prior evaluation showed reduced perturbation mismatch.
- Series approximation: small reference metadata cost with large deep-zoom iteration skip in prior evaluation.
## Key measurements
### Recolor numeric-history copy bandwidth
`commitHistory()` always calls `commitNumericHistory()`. Therefore a color-only update copies unchanged `meta` and `smooth` buffers (8 B/px). Color auto can request a pass every 50 ms while idle.
| preset cap | unchanged numeric copy / recolor | at 20 Hz |
|---|---:|---:|
| fast, 524,288 px | 4 MiB | 80 MiB/s |
| standard, 1,048,576 px | 8 MiB | 160 MiB/s |
| high, 4,194,304 px | 32 MiB | 640 MiB/s |
This is in addition to the actual color pass bandwidth.
### Color snapshot
`captureColorSource()` allocates/copies:
- meta: 4 B/px
- smooth: 4 B/px
- texture: 4 B/px
Resident cost = 12 B/px: ~6 MiB fast, 12 MiB standard, 48 MiB high at preset caps. A provisional snapshot copies 8 B/px and can be refreshed at up to 8 Hz, equivalent to ~32 / 64 / 256 MiB/s at the preset caps.
### Exact interior tile proof
Seven-run median on this host:
| view | 1024×768 | certified | 2048×2048 | certified |
|---|---:|---:|---:|---:|
| overview | ~6.1 ms | 7.3% | ~25.5 ms | 8.6% |
| period3 bulb | ~5.1 ms | 0% | ~26.5 ms | 0% |
| period3 core | ~4.8 ms | 0% | ~28.1 ms | 0% |
| seahorse boundary | ~6.0 ms | 0% | ~25.1 ms | 0% |
| seahorse deep | ~5.6 ms | 0% | ~26.1 ms | 0% |
The exact proof is still useful in views that overlap the main cardioid / period-2 bulb, especially on the perturbation path. The issue is unconditional invocation, not the proof mechanism itself.
### Exact-grid numeric-history reuse
The history seed requires identical span and an approximately integer render-pixel shift. When the resolution is pixel-budget limited, render pixels often do not align with CSS pixels.
Examples from the current resize rule, testing integer CSS horizontal pans of 1–100 px:
| screen / quality | render size | reuse hits |
|---|---:|---:|
| 1920×1080 / fast | 965×543 | 0/100 |
| 1920×1080 / standard | 1365×768 | 0/100 |
| 1920×1080 / high | 2731×1536 | 0/100 |
| 1024×768@1× / standard | 1024×768 | 100/100 |
This is a static alignment test, not real interaction telemetry. It supports making numeric history conditional rather than proving that it has no value.
### Persistent workspaces
- Sparse active workspace, worst full-active capacity: ~44.1 MiB standard, ~176.5 MiB high.
- At 10% active: ~5.64 MiB standard, ~22.51 MiB high.
- Dense deep correction queue: 4 MiB standard, 16 MiB high.
- Export workspace at 512 tile, depth 3: ~24 MiB retained after export.
- Non-AA export currently allocates four 512² sample textures, ~4 MiB per slot, although 1× rendering does not need the four AA sample textures.
## Static code locations
- `index.html:1299-1300`: pre-primary unknown stats + synchronization in `certifyInterior()`.
- `index.html:1383-1384`: full numeric-history copy.
- `index.html:1416`: `commitHistory()` always commits numeric history.
- `index.html:1822`: color-only recolor promotes history and therefore triggers numeric copy.
- `index.html:1718`: `state.cpuFrameRgba=rgba`; the state value has no reader.
- `index.html:162-163`: cycle state still updated in strict direct mode.
- `index.html:1349-1350`: tiled FAST coverage followed by full-screen coverage-repair pass.
- `index.html:1751-1756`: provisional color snapshots at 125 ms throttle.
- `index.html:1774`: old completed field snapshot at every numerical render start.
- `index.html:1866`: idle color-auto snapshot allocation.
- `index.html:1218`: active workspace persists unless another active allocation request causes a >4× shrink.
- `index.html:1217`: full-pixel dense correction queue persists once allocated.
- `index.html:1222-1226, 1898-1944`: export workspace lifetime extends beyond export job.
- `index.html:1306-1307, 1648, 1654`: active continuation reinitialization/rebuild can reset progress.
## Recommended instrumentation before deleting anything
1. Add counters for numeric-history `attempt / hit / reused pixels / bytes copied`.
2. Count reason-0 pixels immediately before `fast-coverage-repair`; if consistently zero, remove/disable that pass in an A/B branch.
3. Record `certifiedInteriorTiles` CPU ms and certified ratio per frame; gate when hit rate stays zero.
4. Record active-continuation `reinitCount` and `replayedPixelIterations`.
5. Record peak/resident bytes for active, correction, export, color snapshot and release-on-idle experiments.
6. Separate color-history promotion from numeric-history promotion and A/B color-auto frame time.
## Production integrity
No production deletion or behavioral modification was made by this audit. Regression was rerun after inspection and passed.

View file

@ -0,0 +1,234 @@
# Mandelbrot viewer performance-cost A/B test
Date: 2026-09-29
## Scope
This A/B test follows the performance-cost audit. Production `index.html` was **not changed**. A is the current production implementation; B variants live under `experiments/ab/variants/` and disable, defer, or gate one candidate operation at a time.
Production SHA-256 before/after testing:
`544820c5024cf6d56e012db6d1bec3ddfcef5d833691b890baa64b26c5c2ee5a`
The existing regression suite passes on production. The combined high-confidence B variant and the individual structural B variants used for gating/lazy allocation also pass the same regression suite; all retain 19 WGSL kernels.
## Measurement limitation
A usable WebGPU backend could not be started in the container (Chromium/SwiftShader/EGL). Therefore this report deliberately separates:
1. **CPU measured time/work**: exact-interior BigInt proof, adaptive-iteration probe, CPU surrogates for strict-cycle and continuation replay.
2. **Exact traffic/residency counts from the production buffer sizes and dispatch extents**: copies, full-frame scans, allocations.
3. **Structural/regression validation**: B variants parse and pass the production regression suite.
No result below is presented as measured GPU frame time unless explicitly stated. Real-device WebGPU p50/p95 remains a follow-up requirement.
## Summary
| Candidate | A/B result | Decision from this test |
|---|---|---|
| Recolor numeric-history copy | B removes 8 B/pixel copy with unchanged numeric field | **Strongly supported** |
| Unread `cpuFrameRgba` retention | B removes 4 B/pixel retained JS data; no reader exists | **Strongly supported** |
| Pre-primary unknown stats after exact interior | B removes one full-frame `fieldMeta` scan; primary path computes stats later | **Strongly supported** |
| Strict direct periodicity-state updates | CPU surrogate median B change **+2.82% slower** | **Do not prioritize for speed** |
| FAST full-screen coverage repair | 36 layout cases + 200 semantic randomized cases: **0 holes** | **Strongly supported**, real GPU A/B still desirable |
| Idle color-auto `colorSource` snapshot | B avoids 12 B/pixel idle snapshot; idle recolor does not consume it | **Strongly supported** |
| Export workspace lifetime | B releases ~24 MiB max workspace after export | **Memory win**, trades for reallocation on next export |
| Exact-interior spatial gate | Fixed corpus proof CPU time reduced ~40% at 1024×768 and ~34% at 2048²; 240 random views: 0 gate false-negatives | **Strongly supported** |
| Conditional numeric-history residency | Budget-scaled 1920×1080 cases had 0–0.6% exact-grid hit rate; native-grid cases 100% | **Supported conditionally**, not blanket removal |
| Lazy export AA samples | 1× export avoids **12 MiB** of four unused AA sample textures across a 3-slot ring | **Strongly supported** |
| Adaptive initial-iteration probe removal | 3/48 survey views selected >350; forcing 350 raised predicted work by **+7.2%, +25.3%, +33.1%** | **Do not remove**; cache/gate instead |
| Continuation restart from n=0 | Resume-state surrogate reduced work **7.6–9.8%**, identical classifications | **Real waste**, but requires state-preserving redesign |
| Deep workspace release after easy frame | Up to ~44.1 MiB standard / ~176.5 MiB high can remain resident | **Memory-lifetime candidate**; real GPU allocation cost unmeasured |
## Results in detail
### 1. Recolor numeric-history copy
Current recolor promotes color history and also copies unchanged `meta/smooth` numeric history. B separates the two responsibilities.
Exact avoided copy per accepted recolor:
- fast cap: 4 MiB
- standard cap: 8 MiB
- high cap: 32 MiB
At 20 Hz color cycling, that corresponds to 80 / 160 / 640 MiB/s of avoidable numeric-history copying at the respective caps. This is a traffic count, not a GPU-time measurement.
Variant: `b01-recolor-no-numeric-copy.html`
### 2. Unread CPU RGBA retention
`cpuFrameRgba` is assigned after CPU fallback rendering but has no subsequent reader in production. B drops the assignment.
Avoided retained JS memory at cap:
- fast: 2 MiB
- standard: 4 MiB
- high: 16 MiB
Variant: `b02-cpu-no-rgba-retention.html`
### 3. Exact-interior pre-primary unknown-stat pass
When exact tile certification only partially covers the frame, production immediately runs a full-frame unknown-stat scan and synchronization before the primary numerical path. The partial-path result is not consumed; the primary path computes the required statistics later.
B removes this pre-primary stats scan while preserving the fully-certified shortcut.
Minimum avoided `fieldMeta` read per pass:
- fast: 2 MiB
- standard: 4 MiB
- high: 16 MiB
This excludes atomics, dispatch overhead, and synchronization cost, so it is a lower bound on removed work.
Variant: `b03-certify-no-preprimary-stats.html`
### 4. Strict direct periodicity-state updates
The strict path does not accept heuristic periodicity, yet production still maintains Brent state. A CPU surrogate compared identical numerical output with and without those state updates.
Across six fixed views, B was not consistently faster: median elapsed change was **+2.82%** (slower), mean +5.47%. One deep case improved by 1.48%, but the result is not robust.
Conclusion: semantically redundant state exists, but this A/B test does **not** justify touching it for performance. GPU register pressure could behave differently; real WebGPU measurement would be needed.
Variant: `b04-strict-no-cycle-state.html`
### 5. FAST coverage repair
Production performs a second full-frame FAST pass intended to fill `reason=0` holes after tiled FAST rendering.
Coverage simulation tested 36 combinations of dimensions, iteration budgets and symmetry modes; all produced **0 holes**. A second semantic randomized test seeded mixed proven/unknown metadata over 200 cases and also found **0 cases with holes**.
Representative work avoided if the repair pass is unnecessary:
- 1365×768: 1,050,624 shader invocations; at least 4,193,280 bytes of metadata reads
- 2731×1536: 4,202,496 invocations; at least 16,779,264 bytes of metadata reads
Variant: `b05-fast-no-coverage-repair.html`
### 6. Idle color-auto snapshot
Starting color cycling while no render is in progress allocates/copies `colorSource`, but idle recolor reads the current numerical field and does not consume that snapshot. Render-time snapshot behavior is preserved in B.
Avoided idle snapshot residency:
- fast: 6 MiB
- standard: 12 MiB
- high: 48 MiB
Variant: `b06-idle-colorauto-no-snapshot.html`
### 7. Export workspace lifetime
The export ring remains allocated after export. With 512-sized slots and ring depth 3, the tested maximum workspace is approximately **24 MiB**. B destroys it in `finally`, reducing post-export residency to zero. The cost is allocation of three slots on a subsequent export.
This is a clear memory trade-off, not a proven speed win.
Variant: `b07-export-release-workspace.html`
### 8. Exact-interior spatial gate
The expensive BigInt tile proof only needs to run when the viewport can intersect the main cardioid or period-2 bulb. B uses a conservative bounding-box gate before tile proof.
The cardioid gate was corrected during the A/B test: the main cardioid reaches `x = 3/8`, so the conservative box is `x ∈ [-3/4, 3/8]`, `y ∈ [-2/3, 2/3]`. The period-2 bulb box is `x ∈ [-5/4, -3/4]`, `y ∈ [-1/4, 1/4]`.
Random validation: 240 views, 160 gate-negative, **0 false negatives** versus the current exact tile proof.
Fixed-corpus aggregate CPU proof time:
- 1024×768: A 41.64 ms → B 25.16 ms, about **39.6% reduction**
- 2048²: A 177.70 ms → B 117.08 ms, about **34.1% reduction**
The largest wins were period-3 views that cannot intersect either analytically certifiable component: about 7–10 ms saved at 1024×768 and 25–30 ms at 2048².
Variant: `b08-exact-interior-spatial-gate.html`
### 9. Conditional numeric-history residency
Exact-grid history reuse requires render-pixel translation to line up exactly with CSS/device motion. A deterministic horizontal-pan survey over offsets -500…500 showed:
| Configuration | Reuse hits | A residency | B policy |
|---|---:|---:|---:|
| 1920×1080 fast → 965×543 | 0.2% | ~4.0 MiB | 0 |
| 1920×1080 standard → 1365×768 | 0.6% | ~8.0 MiB | 0 |
| 1920×1080 high → 2731×1536 | 0% | ~32.0 MiB | 0 |
| 1024×768 native standard | 100% | 6 MiB | keep |
| 1024×768 DPR2 native ratio | 100% | 24 MiB | keep |
B keeps numeric history only for simple reduced render/CSS scale ratios (denominator ≤ 8). This preserves the clearly useful native-grid cases while avoiding permanent allocation where reuse is nearly impossible.
Variant: `b09-conditional-numeric-history.html`
### 10. Lazy export AA sample textures
Production allocates four 512² RGBA8 AA sample textures per export slot even for 1× export, although only 2× AA uses them. Four textures are 4 MiB per slot; with ring depth 3 this is **12 MiB** of unnecessary 1× export allocation.
B creates AA sample textures lazily only for 2× AA. Regression passes.
Variant: `b10-export-aa-samples-lazy.html`
### 11. Adaptive initial-iteration probe
The fixed six-view corpus always selected the base 350 iterations, at a measured probe cost of roughly 1.4–17.4 ms per view. That initially suggested deletion.
A broader deterministic survey of 48 views disproved the blanket-removal hypothesis. Three views (6.25%) selected more than 350. Forcing 350 increased the surrogate work metric by:
- +7.21%
- +33.14%
- +25.25%
Conclusion: **do not delete this probe globally**. Better candidates are caching by nearby view, a cheaper trigger before the full probe, or reducing sample count where confidence is high.
### 12. Continuation replay
Current continuation reconstructs active state from `n=0`; a state-resume surrogate instead kept the base-350 state and continued to 4096. The classifications matched in every tested view.
Work reduction by view: **7.63% to 9.79%**, median about **7.87%**.
This is genuine redundant work, but production perturbation/reference changes complicate state validity. It is a redesign candidate, not a safe line deletion.
## Recommended next actions
### High-confidence changes to consider integrating
1. Separate recolor history promotion from numeric-history copy.
2. Stop retaining unused CPU RGBA output.
3. Remove the partial exact-interior pre-primary stats pass.
4. Remove/disable FAST coverage repair after adding a diagnostic hole counter for one release cycle.
5. Avoid idle color-auto snapshots.
6. Release export workspace after export, subject to acceptable next-export allocation latency.
7. Add the conservative exact-interior spatial gate.
8. Make export AA sample textures lazy.
### Conditional changes
- Allocate numeric history only when exact-grid reuse has realistic geometry.
- Release large deep workspaces after returning to an easy frame or after a memory-pressure/idle policy; do not churn them every frame.
### Do not remove based on this test
- Adaptive initial-iteration probe.
- Strict periodicity state solely for speed; no measurable CPU benefit was established.
### Requires structural work
- Preserve valid continuation state so OPERATION_LIMIT pixels do not replay their first ~350 iterations.
## Reproduction
From project root:
```bash
node tests/regression.mjs
node experiments/ab/ab_test.mjs
node experiments/ab/supplement.mjs
node experiments/ab/variant_check.mjs
```
Raw data:
- `docs/performance-cost-audit/ab/results.json`
- `docs/performance-cost-audit/ab/supplement.json`
All B variants are experimental. Production was not modified by this A/B test.

View file

@ -0,0 +1 @@
544820c5024cf6d56e012db6d1bec3ddfcef5d833691b890baa64b26c5c2ee5a index.html

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
{
"interiorGateRandom": {
"cases": 240,
"gateFalse": 160,
"falseNeg": 0,
"totalProofInFalseNeg": 0
},
"fastCoverageSemantic": {
"cases": 200,
"casesWithHoles": 0
}
}

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View file

@ -0,0 +1,148 @@
# Runtime cost removal pass
Date: 2026-09-29
## Purpose
This build applies every removal/reduction that the preceding A/B pass supported strongly enough to change without redesigning numerical state. The original integrated build remains separately available at `/mnt/data/mandelbrot_integrated`; this directory is the experimental production-equivalent removal build.
Production source of truth in this build: `index.html`.
## Applied changes
### 1. Recolor no longer copies numeric history
Color-only recolor now promotes only color history. `meta/smooth` numeric history is not recopied when the numerical field did not change.
A/B traffic avoided per accepted recolor at the preset caps:
- fast: 4 MiB
- standard: 8 MiB
- high: 32 MiB
At 20 Hz color cycling this removes 80 / 160 / 640 MiB/s of avoidable copy traffic respectively. These are byte counts, not measured GPU frame-time gains.
### 2. Removed unread CPU fallback RGBA retention
The CPU fallback still creates the RGBA array needed for `ImageData`, but no longer stores a second long-lived reference in `state.cpuFrameRgba`. The state property itself was removed.
Avoided retained JS memory at the pixel caps:
- fast: 2 MiB
- standard: 4 MiB
- high: 16 MiB
### 3. Removed partial exact-interior pre-primary unknown scan
`certifyInterior()` no longer runs a full-frame unknown-stat pass immediately before the primary numerical pass. The primary path computes the required statistics later. A fully certified frame now uses known-zero stats directly instead of a readback.
Minimum avoided `fieldMeta` read per partial certification:
- fast: 2 MiB
- standard: 4 MiB
- high: 16 MiB
### 4. Removed FAST coverage-repair pass and its dead remnants
The second full-screen FAST numerical pass that attempted to repair `reason=0` holes was removed. Validation before removal found zero holes in 36 layout cases plus 200 randomized semantic cases.
After integration, two dead remnants were also removed:
- the post-tile `numericParams` upload whose only purpose was coverage-repair mode 2;
- the coverage-hole mode-2 branch in `FAST_PERTURB_WGSL`.
Representative eliminated shader invocations:
- 1365×768: 1,050,624 invocations
- 2731×1536: 4,202,496 invocations
The FAST kernel hash changed only because this unreachable coverage-repair branch was deleted. `tests/kernel_hashes.json` was updated accordingly.
### 5. Removed idle color-auto snapshot allocation
Turning on automatic hue cycling while no render is running no longer captures `colorSource`. Idle recolor does not consume that snapshot. Render-time snapshot behavior remains because it is used to keep visual continuity while a new numerical frame is in flight.
Avoided idle snapshot residency at caps:
- fast: 6 MiB
- standard: 12 MiB
- high: 48 MiB
### 6. Export workspace is released after every export
`runExport()` now destroys the reusable export ring in `finally`. This removes roughly 24 MiB of post-export residency in the tested 512×512 / depth-3 configuration. The next export must allocate the ring again; real-device allocation latency was not measurable in this environment.
### 7. Exact-interior proof is spatially gated
Before the BigInt tile proof, the exact viewport is tested against conservative bounding boxes for the main cardioid and period-2 bulb. If neither can intersect the viewport, the proof is skipped.
A/B aggregate CPU proof time on the fixed corpus:
- 1024×768: 41.64 ms → 25.16 ms (~39.6% reduction)
- 2048²: 177.70 ms → 117.08 ms (~34.1% reduction)
Random validation: 240 views, 160 gate-negative, 0 false negatives versus the existing exact tile proof.
### 8. Numeric history is no longer permanently resident when exact-grid reuse is unrealistic
`historyMeta/historySmooth` are allocated only when the render-to-CSS scale ratio has a reduced denominator ≤ 8. Native-grid cases retain the feature; common pixel-budget-scaled cases do not keep 8 B/pixel of history that almost never matches integer-grid pan reuse.
Measured deterministic pan survey:
- 1920×1080 → 965×543: 0.2% reuse, history removed (~4 MiB)
- 1920×1080 → 1365×768: 0.6% reuse, history removed (~8 MiB)
- 1920×1080 → 2731×1536: 0% reuse, history removed (~32 MiB)
- 1024×768 native: 100% reuse, history retained
- 1024×768 DPR2/native ratio: 100% reuse, history retained
This is intentionally conditional rather than blanket deletion.
### 9. Export AA sample textures are lazy
The four RGBA8 sample textures used only by 2× AA export are no longer allocated by 1× export. At 512² and ring depth 3, 1× export avoids 12 MiB of peak sample-texture allocation.
## Deliberately not removed
The following were investigated but are not part of this deletion pass:
- **Adaptive initial-iteration probe**: 3/48 survey views selected more than 350 iterations; forcing 350 increased the surrogate work metric by +7.2%, +25.3%, and +33.1%.
- **Strict periodicity-state updates**: the CPU A/B did not establish a speed win; median B was slower.
- **Continuation restart from n=0**: confirmed redundant work, but fixing it requires carrying valid continuation state across primary/reference transitions rather than deleting code.
- **Deep/correction workspace lifetime**: potentially large memory saving, but unconditional release can cause GPU allocation churn; no real WebGPU allocation timing was available, so it remains unchanged in this pass.
## Validation
Commands:
```bash
node tests/regression.mjs
node experiments/ab/variant_check.mjs index.html
node experiments/ab/supplement.mjs
node experiments/integrated_bench.mjs docs/removal-pass/integrated-bench.json
```
Results:
- regression: PASS
- JavaScript syntax: PASS
- WGSL kernels: 19 + bundle version
- exact-interior gate randomized test: 0 false negatives / 240 views
- FAST coverage semantic test: 0 hole cases / 200 randomized cases
- integrated CPU benchmark: completed; existing periodicity/series/sparse/multi-reference/CPU-fallback paths remain operational
Real WebGPU frame-time p50/p95 remains unmeasured because the container cannot start a usable WebGPU adapter.
## Source-size impact
Compared with the preceding integrated build:
- `index.html`: 223,672 → 224,559 bytes (+887 bytes)
- gzip-9: 57,375 → 57,744 bytes (+369 bytes)
The file is slightly larger because the spatial and allocation eligibility gates add code. The intended gains are runtime computation, transfer traffic, and memory residency rather than download size.
## Integrity
Removal-build `index.html` SHA-256:
`e69b91ead027927d977a065d27d9e27629110122df20aa470eab877e23f77ac7`

View file

@ -0,0 +1,163 @@
--- /mnt/data/mandelbrot_integrated/index.html 2026-09-29 09:15:05.708978122 +0000
+++ index.html 2026-09-29 09:26:37.218893530 +0000
@@ -268,9 +268,6 @@
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;
@@ -915,7 +912,7 @@
};
function normalizeQuality(value){return value==='fast'||value==='power'?'fast':value==='high'||value==='fine'||value==='validate'?'high':'standard'}
function applyQuality(value){state.quality=normalizeQuality(value);state.continuationBudget=QUALITY_PRESETS[state.quality].iterations}
-const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,continuationBudget:16384,quality:'standard',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,renderClock:null,drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,unresolved:0,unknownReasons:null,gpuError:'',gpuInitFailed:false,lastInteraction:performance.now(),uiHidden:false,temporalFill:false,deferNumericPublish:false,heuristicInterior:0,cpuFrameRgba:null};
+const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,continuationBudget:16384,quality:'standard',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,renderClock:null,drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,unresolved:0,unknownReasons:null,gpuError:'',gpuInitFailed:false,lastInteraction:performance.now(),uiHidden:false,temporalFill:false,deferNumericPublish:false,heuristicInterior:0};
let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
// ── exact fixed-point view state ─────────────────────────────────────────
@@ -1171,6 +1168,12 @@
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}
+
+function numericHistoryScaleEligible(w,h){
+ const cw=Math.max(1,Math.round(canvas.clientWidth||w)),ch=Math.max(1,Math.round(canvas.clientHeight||h));
+ const gcd=(a,b)=>{a=Math.abs(a);b=Math.abs(b);while(b){const t=a%b;a=b;b=t}return a||1};
+ return cw/gcd(w,cw)<=8&&ch/gcd(h,ch)<=8;
+}
class WebGpuRenderer{
constructor(adapter,device){
this.device=device;
@@ -1210,7 +1213,7 @@
frameDestroy(){this.releaseColorSource();if(!this.frame)return;this.clearBindGroupCache();for(const ring of Object.values(this.frame.readbackRings||{}))for(const b of ring.buffers)destroy(b);for(const slot of this.frame.precisionUploadRing?.slots||[])destroy(slot.buffer);for(const k of ['meta','smooth','historyMeta','historySmooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead','deepActiveMark','deepActivePixel','numericParams','statsParams','symmetryParams','colorParams','presentParams'])destroy(this.frame[k]);for(const t of new Set([this.frame.front,this.frame.back,this.frame.history,this.frame.spare]))destroy(t);this.frame=null;this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false;this.numericHistoryView=null;this.numericHistoryIter=0;this.numericHistoryReady=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,colorUsage=T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,front=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'front-color'}),back=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'back-color'}),history=d.createTexture({size:[w,h],format:'rgba8unorm',usage:colorUsage,label:'stable-history'});
- 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'),historyMeta:buf(d,n*4,B.COPY_SRC|B.COPY_DST,'stable-history-meta'),historySmooth:buf(d,n*4,B.COPY_SRC|B.COPY_DST,'stable-history-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,deepActiveCapacity:0,deepActiveState:null,deepActiveQueueA:null,deepActiveQueueB:null,deepActiveCountA:null,deepActiveCountB:null,deepActiveIndirect:null,deepActiveRead:null,deepActiveMark:null,deepActivePixel:null,readbackRings:{},numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),statsParams:buf(d,16,B.UNIFORM|B.COPY_DST,'unknown-stats-params'),symmetryParams:buf(d,16,B.UNIFORM|B.COPY_DST,'symmetry-copy-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,48,B.UNIFORM|B.COPY_DST,'present-params'),front,back,history,spare:history};return this.frame;
+ const keepNumericHistory=numericHistoryScaleEligible(w,h);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'),historyMeta:keepNumericHistory?buf(d,n*4,B.COPY_SRC|B.COPY_DST,'stable-history-meta'):null,historySmooth:keepNumericHistory?buf(d,n*4,B.COPY_SRC|B.COPY_DST,'stable-history-smooth'):null,unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,deepActiveCapacity:0,deepActiveState:null,deepActiveQueueA:null,deepActiveQueueB:null,deepActiveCountA:null,deepActiveCountB:null,deepActiveIndirect:null,deepActiveRead:null,deepActiveMark:null,deepActivePixel:null,readbackRings:{},numericParams:buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,'numeric-params'),statsParams:buf(d,16,B.UNIFORM|B.COPY_DST,'unknown-stats-params'),symmetryParams:buf(d,16,B.UNIFORM|B.COPY_DST,'symmetry-copy-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,48,B.UNIFORM|B.COPY_DST,'present-params'),front,back,history,spare:history};return this.frame;
}
frameReadback(kind,size){const f=this.frame;if(!f)throw new Error('frame readback requested without a frame');let ring=f.readbackRings[kind];if(!ring){const B=GPUBufferUsage;ring=f.readbackRings[kind]={cursor:0,buffers:Array.from({length:3},(_,i)=>buf(this.device,size,B.COPY_DST|B.MAP_READ,kind+'-readback-'+i))}}for(let tries=0;tries<ring.buffers.length;tries++){const i=ring.cursor++%ring.buffers.length,b=ring.buffers[i];if(!b.mapState||b.mapState==='unmapped')return b}throw new Error(kind+' readback ring is saturated')}
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}
@@ -1221,8 +1224,9 @@
destroyExportWorkspace(ws){if(!ws)return;this.clearBindGroupCache();for(const k of ['meta','smooth','cbuf','tex','read'])destroy(ws[k]);for(const b of ws.unresolveds)destroy(b);for(const b of ws.pbufs)destroy(b);for(const t of ws.samples)destroy(t)}
ensureExportWorkspace(slot=0,S=this.exportTileSize()){
const old=this.exportWs[slot];if(old&&old.size===S)return old;if(old)this.destroyExportWorkspace(old);const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,n=S*S,bpr=S*4,pixelBytes=bpr*S,label='export-'+slot+'-';
- const ws={slot,size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,label+'unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,label+'numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,label+'color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:label+'sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:label+'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,label+'readback')};this.exportWs[slot]=ws;return ws;
+ const ws={slot,size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,label+'smooth'),unresolveds:Array.from({length:4},(_,i)=>buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,label+'unresolved-'+i)),pbufs:Array.from({length:4},(_,i)=>buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST,label+'numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,label+'color'),samples:[],tex:d.createTexture({label:label+'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,label+'readback')};this.exportWs[slot]=ws;return ws;
}
+ ensureExportAaSamples(ws){if(ws.samples.length===4)return ws.samples;const d=this.device,T=GPUTextureUsage;for(let i=0;i<4;i++)ws.samples.push(d.createTexture({label:'export-'+ws.slot+'-sample-'+i,size:[ws.size,ws.size],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING}));return ws.samples}
exportWorkspaceDestroy(){for(const ws of this.exportWs)this.destroyExportWorkspace(ws);this.exportWs=[]}
setDeepContext(ctx){
if(this.deepCtx?.key===ctx.key&&this.deepCtx.refLen>=ctx.refLen)return;
@@ -1296,7 +1300,6 @@
if(input)d.queue.writeBuffer(input,0,mask);const e=d.createCommandEncoder({label:'history-seed-and-exact-interior'});if(historySeed)this.encodeNumericHistorySeed(e,historySeed);else if(clear){e.clearBuffer(f.meta);e.clearBuffer(f.smooth)}
if(pixels){const bg=d.createBindGroup({layout:this.interiorMask.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:input}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]});
const pass=e.beginComputePass();pass.setPipeline(this.interiorMask);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end()}
- this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
return await this.submitStage(e,historySeed?'numeric history + exposed interior':'exact interior before primary',token,'correction')===null?0:pixels;
}finally{destroy(input)}
}
@@ -1347,7 +1350,7 @@
const tileWidth=this.numericTileShape(f.w,iter).width;let rows=this.initialNumericRows(tileWidth,iter),burstCount=0,burstStart=performance.now(),lastStage='',predictedStripMs=22,burstLimit=this.numericBurstLimit(predictedStripMs),burstPredictedMs=0,lastCompletedRows=rows;
const flushBurst=async remaining=>{if(!burstCount)return true;const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;rows=this.adaptNumericRows(lastCompletedRows,ms,remaining);predictedStripMs=.65*predictedStripMs+.35*ms;burstLimit=this.numericBurstLimit(predictedStripMs);burstCount=0;burstPredictedMs=0;burstStart=performance.now();return true};
for(let y=0;y<computeH;){if(token!==state.token)return false;const th=Math.min(rows,computeH-y);lastCompletedRows=th;for(let x=0;x<f.w;x+=tileWidth){if(token!==state.token)return false;const tw=Math.min(tileWidth,f.w-x),base=y*f.w+x,params=this.fastParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,refX,refY,f.w,base,reuseResolved?1:0);d.queue.writeBuffer(f.numericParams,0,params);const e=d.createCommandEncoder({label:'fast-numeric-strip-'+y});if(x===0&&y===0){if(historySeed)this.encodeNumericHistorySeed(e,historySeed);else if(!reusePriorField&&!preserveInterior){e.clearBuffer(f.meta);e.clearBuffer(f.smooth)}}this.encodeFastPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});lastStage='fast primary rows '+y+'..'+(y+th-1);this.submitStageDeferred(e,lastStage,'numeric');burstCount++;burstPredictedMs+=predictedStripMs;const budgetMs=this.numericInFlightBudgetMs();if(burstCount>=burstLimit||burstPredictedMs>=budgetMs||performance.now()-burstStart>=budgetMs){if(!await flushBurst(computeH-(y+th)))return false}}y+=th}
- if(!await flushBurst(0))return false;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));if(!deferColor)d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder({label:deferColor?'fast-coverage-repair':'fast-coverage-repair-and-color'});if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeFastNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});if(!deferColor){const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),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,deferColor?'fast coverage only':'fast coverage + color',token,'numeric');if(ms===null)return false;if(!deferColor){[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey}return true
+ if(!await flushBurst(0))return false;if(!deferColor)d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const e=d.createCommandEncoder({label:deferColor?'fast-post-stats':'fast-post-stats-and-color'});if(mirror)this.encodeSymmetryCopy(e,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});if(!deferColor){const cbg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),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,deferColor?'fast post-stats only':'fast post-stats + color',token,'numeric');if(ms===null)return false;if(!deferColor){[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey}return true
}
async computeFrame(snap,iter,token,referencePixel=null,fastExtended=false,fastContext=null,preserveInterior=false,preseededHistory=false,deferColor=false){
await this.ready;if(fastExtended)await this.ensureFastPipelines();if(token!==state.token)return false;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device,colorStyle=colorStyleSnapshot(),colorKey=colorStyleKey(colorStyle);
@@ -1381,7 +1384,7 @@
return true;
}
commitNumericHistory(view=state.frameView,iter=state.fieldView?.iter??maxIter()){
- const f=this.frame;if(!f||!view||state.fieldView?.complete===false)return false;const e=this.device.createCommandEncoder({label:'stable-numeric-history'});e.copyBufferToBuffer(f.meta,0,f.historyMeta,0,f.n*4);e.copyBufferToBuffer(f.smooth,0,f.historySmooth,0,f.n*4);this.device.queue.submit([e.finish()]);this.numericHistoryView={...view,w:f.w,h:f.h};this.numericHistoryIter=iter;this.numericHistoryReady=true;return true;
+ const f=this.frame;if(!f||!f.historyMeta||!f.historySmooth||!view||state.fieldView?.complete===false){this.numericHistoryReady=false;return false}const e=this.device.createCommandEncoder({label:'stable-numeric-history'});e.copyBufferToBuffer(f.meta,0,f.historyMeta,0,f.n*4);e.copyBufferToBuffer(f.smooth,0,f.historySmooth,0,f.n*4);this.device.queue.submit([e.finish()]);this.numericHistoryView={...view,w:f.w,h:f.h};this.numericHistoryIter=iter;this.numericHistoryReady=true;return true;
}
captureColorSource(view=state.frameView,needed=state.colorAuto||state.rendering){
this.colorSourceVisible=false;
@@ -1413,7 +1416,7 @@
}
async recolor(token,iter=state.fieldView?.iter??maxIter()){await this.ready;if(token!==state.token||!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=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:this.cachedTextureView(this.colorTarget(f))}]),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;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)}}
- commitHistory(view=state.frameView){if(!this.frame||!view||state.fieldView?.complete===false)return false;const f=this.frame;if(f.history!==f.front){f.history=f.front;this.clearBindGroupCache()}this.historyView=view;this.historyColorKey=this.frontColorKey;this.historyReady=true;this.commitNumericHistory(view,state.fieldView?.iter??maxIter());return true}
+ commitHistory(view=state.frameView,commitNumeric=true){if(!this.frame||!view||state.fieldView?.complete===false)return false;const f=this.frame;if(f.history!==f.front){f.history=f.front;this.clearBindGroupCache()}this.historyView=view;this.historyColorKey=this.frontColorKey;this.historyReady=true;if(commitNumeric)this.commitNumericHistory(view,state.fieldView?.iter??maxIter());return true}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const colored=this.colorSourceVisible&&this.colorSource;if(colored)transform=this.presentTransform(colored.view);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=!!(!colored&&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=this.cachedBindGroup(this.present,[{binding:0,resource:interactive?this.reprojectSampler:this.sampler},{binding:1,resource:this.cachedTextureView(colored?colored.texture:f.front)},{binding:2,resource:this.cachedTextureView(f.history)},{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<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readNumericalIndices(token){
@@ -1504,7 +1507,7 @@
}
async renderTileRGBA2x({snap,iter,fastContext=null,fullW,fullH,tileX,tileY,w,h,strict=false,workspaceSlot=0,workspaceSize=this.exportTileSize()}){
if(w*h*Math.max(1,iter)>48000000){const shape=this.numericTileShape(w,iter),out=new Uint8ClampedArray(w*h*4),generation=state.token;let unresolved=0;for(let y=0;y<h;y+=shape.rows)for(let x=0;x<w;x+=shape.width){if(generation!==state.token||exportJob.cancelled&&exportJob.active)throw new Error('cancelled');const tw=Math.min(shape.width,w-x),th=Math.min(shape.rows,h-y),part=await this.renderTileRGBA2x({snap,iter,fastContext,fullW,fullH,strict,workspaceSlot,workspaceSize,tileX:tileX+x,tileY:tileY+y,w:tw,h:th});for(let row=0;row<th;row++)out.set(part.rgba.subarray(row*tw*4,(row+1)*tw*4),((y+row)*w+x)*4);unresolved+=part.unresolved}return{rgba:out,unresolved}}
- await this.ready;await this.ensureAaPipeline();const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!!fastContext;if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,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(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,w,0,0,strict?1:0));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,strict?1:0));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolved=0;for(let si=0;si<4;si++)unresolved+=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4)[0]||0;ws.read.unmap();return{rgba:out,unresolved}
+ await this.ready;await this.ensureAaPipeline();const ws=this.ensureExportWorkspace(workspaceSlot,workspaceSize);this.ensureExportAaSamples(ws);if(w>ws.size||h>ws.size)throw new Error('export tile exceeds reusable workspace');const fastExtended=!!fastContext;if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,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(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,w,0,0,strict?1:0));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,strict?1:0));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end()}const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h,statsBytes=4*UNRESOLVED_BYTES;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});for(let si=0;si<4;si++)encoder.copyBufferToBuffer(ws.unresolveds[si],0,ws.read,pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+statsBytes);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+statsBytes)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);let unresolved=0;for(let si=0;si<4;si++)unresolved+=new Uint32Array(raw.buffer,raw.byteOffset+pixelBytes+si*UNRESOLVED_BYTES,UNRESOLVED_BYTES/4)[0]||0;ws.read.unmap();return{rgba:out,unresolved}
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext();this.destroyFastContext()}
}
@@ -1514,6 +1517,16 @@
// sparse/moderate failures go directly to local references.
// Integer interval tests over exact rational pixel coordinates. No f32
// rounding, periodic-orbit guess, or agreement of two precisions is a proof.
+
+function knownInteriorViewportMayOverlap(snap,width,height){
+ const unit=1n<<BigInt(snap.bits),bw=BigInt(Math.max(1,width)),bh=BigInt(Math.max(1,height));
+ const xLo=2n*snap.re-snap.span,xHi=2n*snap.re+snap.span,xDen=2n*unit;
+ const yLo=2n*bw*snap.im-snap.span*bh,yHi=2n*bw*snap.im+snap.span*bh,yDen=2n*bw*unit;
+ const axis=(lo,hi,den,lp,lq,hp,hq)=>hi*BigInt(lq)>=BigInt(lp)*den&&lo*BigInt(hq)<=BigInt(hp)*den;
+ const cardioid=axis(xLo,xHi,xDen,-3,4,3,8)&&axis(yLo,yHi,yDen,-2,3,2,3);
+ const bulb=axis(xLo,xHi,xDen,-5,4,-3,4)&&axis(yLo,yHi,yDen,-1,4,1,4);
+ return cardioid||bulb;
+}
function certifiedInteriorTiles(snap,width,height,tile=32,historySeed=null){
const columns=Math.ceil(width/tile),rows=Math.ceil(height/tile),mask=new Uint32Array(4+columns*rows);
mask.set([width,height,tile,columns]);let pixels=0;
@@ -1715,7 +1728,7 @@
constructor(){this.workers=[];this.serial=0;this.pending=new Map();this.failed=false;this.ctx=null}
ensure(){if(this.workers.length)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.ctx=canvas.getContext('2d');if(!this.ctx)throw new Error('2D canvas unavailable');const count=Math.max(1,Math.min(4,(Number(navigator.hardwareConcurrency)||4)-1));for(let i=0;i<count;i++){const url=URL.createObjectURL(new Blob([cpuFallbackWorkerSource()],{type:'text/javascript'})),w=new Worker(url);w.__url=url;w.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);d.type==='error'?p.reject(new Error(d.error)):p.resolve(d)};w.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'CPU fallback worker error'));this.pending.clear()};this.workers.push(w)}return true}catch(e){this.failed=true;return false}}
request(worker,payload){const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});worker.postMessage({...payload,id})})}
- async render(snap,iter,token){if(!this.ensure())throw new Error('CPU fallback workerを作成できません');const w=canvas.width,h=canvas.height,re=fixedNum(snap.re,snap.bits),im=fixedNum(snap.im,snap.bits),span=fixedNum(snap.span,snap.bits),step=span/Math.max(1,w);if(!Number.isFinite(re)||!Number.isFinite(im)||!Number.isFinite(span)||span<=0||step===0||(re!==0&&re+step===re)||(im!==0&&im+step===im))throw new Error('CPU fallback の倍精度範囲を超えています');const style=colorStyleSnapshot(),jobs=[],rows=Math.ceil(h/this.workers.length);for(let i=0;i<this.workers.length;i++){const y0=i*rows,y1=Math.min(h,y0+rows);if(y0<y1)jobs.push(this.request(this.workers[i],{type:'render',w,h,y0,y1,maxIter:iter,re,im,span,style}))}const parts=await Promise.all(jobs);if(token!==state.token)throw new Error('cancelled');const rgba=new Uint8ClampedArray(w*h*4);let operationLimit=0,heuristicInterior=0,work=0,escaped=0;for(const part of parts){rgba.set(new Uint8ClampedArray(part.rgba),part.y0*w*4);operationLimit+=part.operationLimit||0;heuristicInterior+=part.heuristicInterior||0;work+=part.work||0;escaped+=part.escaped||0}this.ctx.putImageData(new ImageData(rgba,w,h),0,0);state.cpuFrameRgba=rgba;return{operationLimit,heuristicInterior,work,escaped,workers:this.workers.length}}
+ async render(snap,iter,token){if(!this.ensure())throw new Error('CPU fallback workerを作成できません');const w=canvas.width,h=canvas.height,re=fixedNum(snap.re,snap.bits),im=fixedNum(snap.im,snap.bits),span=fixedNum(snap.span,snap.bits),step=span/Math.max(1,w);if(!Number.isFinite(re)||!Number.isFinite(im)||!Number.isFinite(span)||span<=0||step===0||(re!==0&&re+step===re)||(im!==0&&im+step===im))throw new Error('CPU fallback の倍精度範囲を超えています');const style=colorStyleSnapshot(),jobs=[],rows=Math.ceil(h/this.workers.length);for(let i=0;i<this.workers.length;i++){const y0=i*rows,y1=Math.min(h,y0+rows);if(y0<y1)jobs.push(this.request(this.workers[i],{type:'render',w,h,y0,y1,maxIter:iter,re,im,span,style}))}const parts=await Promise.all(jobs);if(token!==state.token)throw new Error('cancelled');const rgba=new Uint8ClampedArray(w*h*4);let operationLimit=0,heuristicInterior=0,work=0,escaped=0;for(const part of parts){rgba.set(new Uint8ClampedArray(part.rgba),part.y0*w*4);operationLimit+=part.operationLimit||0;heuristicInterior+=part.heuristicInterior||0;work+=part.work||0;escaped+=part.escaped||0}this.ctx.putImageData(new ImageData(rgba,w,h),0,0);return{operationLimit,heuristicInterior,work,escaped,workers:this.workers.length}}
cancelPending(reason='cancelled'){for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();this.destroyWorkers()}
destroyWorkers(){for(const w of this.workers){try{w.terminate()}catch{}try{URL.revokeObjectURL(w.__url)}catch{}}this.workers.length=0}
destroy(){this.cancelPending('destroyed');this.ctx=null}
@@ -1759,9 +1772,9 @@
}
async function renderFinalFrame(r,snap,iter,token){
let decision=chooseBackend(snap,canvas.width),fastExtended=decision.fastExtended;
- const historySeed=fastExtended?r.numericHistorySeed(snap,iter,false,true):null,proof=certifiedInteriorTiles(snap,canvas.width,canvas.height,32,historySeed);let certified=0,preserveInterior=false;
+ const historySeed=fastExtended?r.numericHistorySeed(snap,iter,false,true):null,proof=knownInteriorViewportMayOverlap(snap,canvas.width,canvas.height)?certifiedInteriorTiles(snap,canvas.width,canvas.height,32,historySeed):{mask:null,pixels:0};let certified=0,preserveInterior=false;
if(proof.pixels||historySeed){certified=await r.certifyInterior(snap,token,true,proof,historySeed);if(token!==state.token)return null;preserveInterior=certified>0||!!historySeed}
- if(certified===canvas.width*canvas.height){decision={backend:'exact-interior',fastExtended:false,reason:'exact-full-interior'};const stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.heuristicInterior=0;const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;const frontier={stats,iter,minimumIter:iter,rounds:0,processed:0,escaped:0,converged:true,finiteBudgetComplete:true,policy:'exact-interior-proof',membershipCertified:true,applied:true};state.frameView=snap;state.fieldView={...snap,iter,frontier,w:canvas.width,h:canvas.height,fastExtended:false,backend:'exact-interior',referenceKey:'',referenceReused:false,complete:true,membershipCertified:true};r.captureColorSource(snap,state.colorAuto||state.recolorPending);r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});return{decision,referenceReused:false,referencePixel:null,certified}}
+ if(certified===canvas.width*canvas.height){decision={backend:'exact-interior',fastExtended:false,reason:'exact-full-interior'};const stats={total:0,heuristicInterior:0,reasons:{errorBound:0,escapeUncertain:0,referenceEnd:0,rebaseGap:0,range:0,operationLimit:0}};state.unresolved=0;state.unknownReasons=stats.reasons;state.heuristicInterior=0;const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;const frontier={stats,iter,minimumIter:iter,rounds:0,processed:0,escaped:0,converged:true,finiteBudgetComplete:true,policy:'exact-interior-proof',membershipCertified:true,applied:true};state.frameView=snap;state.fieldView={...snap,iter,frontier,w:canvas.width,h:canvas.height,fastExtended:false,backend:'exact-interior',referenceKey:'',referenceReused:false,complete:true,membershipCertified:true};r.captureColorSource(snap,state.colorAuto||state.recolorPending);r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});return{decision,referenceReused:false,referencePixel:null,certified}}
let fastCtx=null,referencePixel=null,referenceReused=false;if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true}else{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)}}
const deferColor=!!(r.historyReady&&canUseStableReprojection());state.deferNumericPublish=deferColor;const ok=await r.computeFrame(snap,iter,token,referencePixel,fastExtended,fastCtx,preserveInterior,!!historySeed,deferColor);if(!ok||token!==state.token){state.deferNumericPublish=false;return null}if(!deferColor&&!await presentProvisional(r,snap,iter,token,true)){state.deferNumericPublish=false;return null}
let stats=await r.readUnresolvedStats();if(token!==state.token)return null;if(fastExtended){const initialFailed=numericalFailureCount(stats);if(initialFailed>0){const ordered=await recoverNumericalCostOrdered(r,snap,iter,token,stats);if(!ordered||token!==state.token){state.deferNumericPublish=false;return null}stats=ordered.stats}state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.heuristicInterior=stats.heuristicInterior||0;if(initialFailed>0&&!deferColor){const painted=await r.recolor(token,iter);if(!painted||token!==state.token){state.deferNumericPublish=false;return null}}}else{state.unresolved=stats.total;state.unknownReasons=stats.reasons}
@@ -1819,7 +1832,7 @@
// 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&&numericFrameComplete()){renderer.commitHistory(state.frameView);state.temporalFill=false}
+ if(revision===state.colorRevision&&numericFrameComplete()){renderer.commitHistory(state.frameView,false);state.temporalFill=false}
updateStats();
// Yield ownership after one pass: a stream of hue changes must not keep
// recoloring=true forever and prevent a pending pan from computing.
@@ -1863,7 +1876,7 @@
function syncColorAutoButton(){const b=$('colorAuto');b.textContent=state.colorAuto?'色相変化停止':'色相変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')}
function stopColorAuto(repaint=true){state.colorAuto=false;colorAutoLast=0;if(colorAutoRaf){cancelAnimationFrame(colorAutoRaf);colorAutoRaf=0}syncColorAutoButton();if(repaint)requestRecolor()}
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(!state.pointerActive&&!state.wheelActive&&now-colorAutoPaint>=(state.rendering?100:50)){colorAutoPaint=now;requestRecolor()}colorAutoRaf=requestAnimationFrame(colorAutoStep)}
-$('colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf){if(!state.rendering)renderer?.captureColorSource();colorAutoRaf=requestAnimationFrame(colorAutoStep);}else if(!state.colorAuto)stopColorAuto()};
+$('colorAuto').onclick=()=>{state.colorAuto=!state.colorAuto;syncColorAutoButton();if(state.colorAuto&&!colorAutoRaf){colorAutoRaf=requestAnimationFrame(colorAutoStep);}else if(!state.colorAuto)stopColorAuto()};
$('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()});
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()}
@@ -1940,7 +1953,7 @@
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');
$('exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
- finally{exportJob.active=false;$('exportStart').disabled=false}
+ finally{r.exportWorkspaceDestroy();exportJob.active=false;$('exportStart').disabled=false}
}
$('png').onclick=()=>{const d=$('exportDialog');$('exportWidth').value=String(canvas.width);$('exportScale').value='1';$('exportProgress').hidden=true;$('exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('exportWidth').value=String(exportDimensions().w)};$('exportStart').onclick=runExport;$('exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('exportStatus').textContent='中止しています…'}else $('exportDialog').close()};$('exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+(state.fieldView?.complete===false?'-preview':'')+'.png')},'image/png');

View file

@ -0,0 +1,381 @@
{
"date": "2026-09-29T09:27:34.132Z",
"periodicity": {
"w": 96,
"h": 64,
"maxIter": 8192,
"oracleIter": 65536,
"rows": [
{
"view": "overview",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 884420,
"heuristicWork": 133404,
"workReduction": 0.849162162773343,
"candidates": 96,
"falseCandidates": 0,
"baseLimit": 104,
"heuristicLimit": 8,
"diagnosticIfForcedDirect": null,
"ms": 107.84037399999998
},
{
"view": "period3-bulb",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 50331648,
"heuristicWork": 460156,
"workReduction": 0.9908575216929117,
"candidates": 6144,
"falseCandidates": 0,
"baseLimit": 6144,
"heuristicLimit": 0,
"diagnosticIfForcedDirect": null,
"ms": 4596.538675
},
{
"view": "period3-core",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 50331648,
"heuristicWork": 442395,
"workReduction": 0.991210401058197,
"candidates": 6144,
"falseCandidates": 0,
"baseLimit": 6144,
"heuristicLimit": 0,
"diagnosticIfForcedDirect": null,
"ms": 3849.7766140000003
},
{
"view": "seahorse-boundary",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 410111,
"heuristicWork": 410111,
"workReduction": 0,
"candidates": 0,
"falseCandidates": 0,
"baseLimit": 0,
"heuristicLimit": 0,
"diagnosticIfForcedDirect": null,
"ms": 28.11920800000007
},
{
"view": "seahorse-deep",
"productionBackend": "fast-extended",
"pixels": 6144,
"baseWork": 36579905,
"heuristicWork": 6167354,
"workReduction": null,
"candidates": 0,
"falseCandidates": 0,
"baseLimit": 4304,
"heuristicLimit": 4304,
"diagnosticIfForcedDirect": {
"workReduction": 0.8314004916087125,
"candidates": 4125,
"falseCandidates": 12
},
"ms": 2697.9715419999993
},
{
"view": "exterior",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 686432,
"heuristicWork": 137063,
"workReduction": 0.8003254510279241,
"candidates": 72,
"falseCandidates": 0,
"baseLimit": 79,
"heuristicLimit": 7,
"diagnosticIfForcedDirect": null,
"ms": 44.91841799999929
}
],
"note": "Production DIRECT_F32 rule with two attracting-cycle confirmations. FAST/perturbation periodic early-stop is intentionally disabled."
},
"series": {
"rows": [
{
"span": 0.00001,
"jump": 53,
"errorLog2": -22.11596292311006,
"maxAbsError": 7.112067409017996e-9,
"maxRelativeError": 0.00009415623690713784,
"refLen": 1200,
"buildMs": 38.390750999999
},
{
"span": 1e-7,
"jump": 446,
"errorLog2": -22.302471602690176,
"maxAbsError": 7.736805567567409e-9,
"maxRelativeError": 0.0031991190750368034,
"refLen": 1200,
"buildMs": 35.28093799999988
},
{
"span": 1e-9,
"jump": 930,
"errorLog2": -22.038152635655024,
"maxAbsError": 7.459933177532973e-9,
"maxRelativeError": 0.00001015603159369818,
"refLen": 1200,
"buildMs": 40.962877999998454
},
{
"span": 1e-11,
"jump": 1037,
"errorLog2": -23.96377927072575,
"maxAbsError": 2.2791360188558774e-9,
"maxRelativeError": 0.000004948137247650717,
"refLen": 1200,
"buildMs": 42.88623700000062
}
],
"note": "Production fast-reference worker output. Error compares packed 4th-order series against direct perturbation over 8 screen-domain deltas."
},
"sparseActiveState": {
"rows": [
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 1,
"capacity": 524288,
"oldBytes": 23068672,
"newBytes": 23134208,
"reduction": -0.0028409090909091717
},
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 0.25,
"capacity": 164096,
"oldBytes": 23068672,
"newBytes": 7285760,
"reduction": 0.6841708096590908
},
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 0.1,
"capacity": 65792,
"oldBytes": 23068672,
"newBytes": 2960384,
"reduction": 0.8716708096590909
},
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 0.01,
"capacity": 6810,
"oldBytes": 23068672,
"newBytes": 365176,
"reduction": 0.9841700467196378
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 1,
"capacity": 1048576,
"oldBytes": 46137344,
"newBytes": 46268416,
"reduction": -0.0028409090909091717
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 0.25,
"capacity": 327936,
"oldBytes": 46137344,
"newBytes": 14560256,
"reduction": 0.6844149502840908
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 0.1,
"capacity": 131328,
"oldBytes": 46137344,
"newBytes": 5909504,
"reduction": 0.8719149502840909
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 0.01,
"capacity": 13364,
"oldBytes": 46137344,
"newBytes": 719088,
"reduction": 0.9844141873446378
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 1,
"capacity": 4194304,
"oldBytes": 184549376,
"newBytes": 185073664,
"reduction": -0.0028409090909091717
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 0.25,
"capacity": 1310976,
"oldBytes": 184549376,
"newBytes": 58207232,
"reduction": 0.6845980557528408
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 0.1,
"capacity": 524544,
"oldBytes": 184549376,
"newBytes": 23604224,
"reduction": 0.8720980557528409
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 0.01,
"capacity": 52685,
"oldBytes": 184549376,
"newBytes": 2842428,
"reduction": 0.9845980080691251
}
],
"note": "Production allocation: 32 B state + 2x4 B queues + 4 B pixel map per active slot, plus 1-bit membership bitmap. Capacity adds 25% + 256 headroom."
},
"multiReference": {
"maxGuidedPasses": 3,
"w": 64,
"h": 40,
"maxIter": 1200,
"rows": [
{
"view": "seahorse-1e-5",
"grid": 1,
"references": 1,
"total": 2560,
"refEnd": 0,
"mismatchClass": 3,
"mismatchIter": 155,
"mismatchRate": 0.001171875
},
{
"view": "seahorse-1e-5",
"grid": 2,
"references": 5,
"total": 2560,
"refEnd": 0,
"mismatchClass": 3,
"mismatchIter": 155,
"mismatchRate": 0.001171875
},
{
"view": "seahorse-1e-5",
"grid": 3,
"references": 9,
"total": 2560,
"refEnd": 0,
"mismatchClass": 3,
"mismatchIter": 155,
"mismatchRate": 0.001171875
},
{
"view": "seahorse-1e-7",
"grid": 1,
"references": 1,
"total": 2560,
"refEnd": 0,
"mismatchClass": 66,
"mismatchIter": 74,
"mismatchRate": 0.02578125
},
{
"view": "seahorse-1e-7",
"grid": 2,
"references": 5,
"total": 2560,
"refEnd": 0,
"mismatchClass": 37,
"mismatchIter": 41,
"mismatchRate": 0.014453125
},
{
"view": "seahorse-1e-7",
"grid": 3,
"references": 9,
"total": 2560,
"refEnd": 0,
"mismatchClass": 50,
"mismatchIter": 67,
"mismatchRate": 0.01953125
}
],
"note": "CPU f32 perturbation locality surrogate. Production selects up to three additional references from failure clusters instead of fixed grids."
},
"cpuFallback": {
"w": 640,
"h": 360,
"iter": 350,
"threads": 4,
"one": {
"medianMs": 142.60004300000037,
"times": [
149.66868299999987,
152.4433539999991,
129.93585699999858,
119.70592199999919,
129.10428200000024,
142.60004300000037,
150.36682700000165
],
"work": 2658084,
"escaped": 176648,
"operationLimit": 3394,
"heuristicInterior": 1654
},
"multi": {
"medianMs": 58.8322790000002,
"times": [
132.8346849999998,
62.70222600000125,
52.98496299999897,
54.89045199999964,
60.89404900000045,
58.8322790000002,
43.25380600000062
],
"work": 2658084,
"escaped": 176648,
"operationLimit": 3394,
"heuristicInterior": 1654
},
"speedup": 2.423840201736871,
"workMatch": true,
"escapedMatch": true,
"operationLimitMatch": true,
"heuristicMatch": true,
"note": "Production CPU fallback Worker source executed under node:worker_threads shim."
},
"sourceAudit": {
"heuristicClass": true,
"strictDisablesPeriodicity": true,
"seriesBinding": true,
"compactPixelMap": true,
"multiReferencePasses": true,
"cpuFallback": true
},
"environment": {
"node": "v22.16.0",
"cpus": 5,
"platform": "linux",
"arch": "x64"
}
}

View file

@ -0,0 +1,574 @@
{
"date": "2026-09-29",
"base": {
"sha256": "544820c5024cf6d56e012db6d1bec3ddfcef5d833691b890baa64b26c5c2ee5a",
"bytes": 223672,
"gzip9": 57375
},
"pruned": {
"sha256": "e69b91ead027927d977a065d27d9e27629110122df20aa470eab877e23f77ac7",
"bytes": 224559,
"gzip9": 57744
},
"validation": {
"regression": "PASS",
"kernelCount": 19,
"variantSyntax": "PASS",
"interiorGateRandom": {
"cases": 240,
"gateFalse": 160,
"falseNeg": 0,
"totalProofInFalseNeg": 0
},
"fastCoverageSemantic": {
"cases": 200,
"casesWithHoles": 0
}
},
"structural": {
"cpuFrameRgbaRemoved": true,
"fastCoverageRepairRemoved": true,
"idleColorAutoSnapshotRemoved": true,
"exactInteriorSpatialGate": true,
"conditionalNumericHistory": true,
"lazyExportAaSamples": true,
"exportWorkspaceReleased": true,
"recolorNumericHistorySeparated": true,
"prePrimaryUnknownStatsRemoved": true,
"deadFastMode2ParamWriteRemoved": true
},
"priorABEvidence": {
"fastCoverageCases": [
{
"w": 965,
"h": 543,
"iter": 350,
"mirror": false,
"tileW": 965,
"initialRows": 142,
"holes": 0,
"repairThreads": 526592,
"minimumMetaReadBytes": 2095980
},
{
"w": 965,
"h": 543,
"iter": 350,
"mirror": true,
"tileW": 965,
"initialRows": 142,
"holes": 0,
"repairThreads": 526592,
"minimumMetaReadBytes": 2095980
},
{
"w": 965,
"h": 543,
"iter": 4096,
"mirror": false,
"tileW": 965,
"initialRows": 12,
"holes": 0,
"repairThreads": 526592,
"minimumMetaReadBytes": 2095980
},
{
"w": 965,
"h": 543,
"iter": 4096,
"mirror": true,
"tileW": 965,
"initialRows": 12,
"holes": 0,
"repairThreads": 526592,
"minimumMetaReadBytes": 2095980
},
{
"w": 965,
"h": 543,
"iter": 16384,
"mirror": false,
"tileW": 965,
"initialRows": 3,
"holes": 0,
"repairThreads": 526592,
"minimumMetaReadBytes": 2095980
},
{
"w": 965,
"h": 543,
"iter": 16384,
"mirror": true,
"tileW": 965,
"initialRows": 3,
"holes": 0,
"repairThreads": 526592,
"minimumMetaReadBytes": 2095980
},
{
"w": 1365,
"h": 768,
"iter": 350,
"mirror": false,
"tileW": 1365,
"initialRows": 100,
"holes": 0,
"repairThreads": 1050624,
"minimumMetaReadBytes": 4193280
},
{
"w": 1365,
"h": 768,
"iter": 350,
"mirror": true,
"tileW": 1365,
"initialRows": 100,
"holes": 0,
"repairThreads": 1050624,
"minimumMetaReadBytes": 4193280
},
{
"w": 1365,
"h": 768,
"iter": 4096,
"mirror": false,
"tileW": 1365,
"initialRows": 8,
"holes": 0,
"repairThreads": 1050624,
"minimumMetaReadBytes": 4193280
},
{
"w": 1365,
"h": 768,
"iter": 4096,
"mirror": true,
"tileW": 1365,
"initialRows": 8,
"holes": 0,
"repairThreads": 1050624,
"minimumMetaReadBytes": 4193280
},
{
"w": 1365,
"h": 768,
"iter": 16384,
"mirror": false,
"tileW": 1365,
"initialRows": 2,
"holes": 0,
"repairThreads": 1050624,
"minimumMetaReadBytes": 4193280
},
{
"w": 1365,
"h": 768,
"iter": 16384,
"mirror": true,
"tileW": 1365,
"initialRows": 2,
"holes": 0,
"repairThreads": 1050624,
"minimumMetaReadBytes": 4193280
},
{
"w": 1024,
"h": 768,
"iter": 350,
"mirror": false,
"tileW": 1024,
"initialRows": 133,
"holes": 0,
"repairThreads": 786432,
"minimumMetaReadBytes": 3145728
},
{
"w": 1024,
"h": 768,
"iter": 350,
"mirror": true,
"tileW": 1024,
"initialRows": 133,
"holes": 0,
"repairThreads": 786432,
"minimumMetaReadBytes": 3145728
},
{
"w": 1024,
"h": 768,
"iter": 4096,
"mirror": false,
"tileW": 1024,
"initialRows": 11,
"holes": 0,
"repairThreads": 786432,
"minimumMetaReadBytes": 3145728
},
{
"w": 1024,
"h": 768,
"iter": 4096,
"mirror": true,
"tileW": 1024,
"initialRows": 11,
"holes": 0,
"repairThreads": 786432,
"minimumMetaReadBytes": 3145728
},
{
"w": 1024,
"h": 768,
"iter": 16384,
"mirror": false,
"tileW": 1024,
"initialRows": 2,
"holes": 0,
"repairThreads": 786432,
"minimumMetaReadBytes": 3145728
},
{
"w": 1024,
"h": 768,
"iter": 16384,
"mirror": true,
"tileW": 1024,
"initialRows": 2,
"holes": 0,
"repairThreads": 786432,
"minimumMetaReadBytes": 3145728
},
{
"w": 2731,
"h": 1536,
"iter": 350,
"mirror": false,
"tileW": 2731,
"initialRows": 50,
"holes": 0,
"repairThreads": 4202496,
"minimumMetaReadBytes": 16779264
},
{
"w": 2731,
"h": 1536,
"iter": 350,
"mirror": true,
"tileW": 2731,
"initialRows": 50,
"holes": 0,
"repairThreads": 4202496,
"minimumMetaReadBytes": 16779264
},
{
"w": 2731,
"h": 1536,
"iter": 4096,
"mirror": false,
"tileW": 2731,
"initialRows": 4,
"holes": 0,
"repairThreads": 4202496,
"minimumMetaReadBytes": 16779264
},
{
"w": 2731,
"h": 1536,
"iter": 4096,
"mirror": true,
"tileW": 2731,
"initialRows": 4,
"holes": 0,
"repairThreads": 4202496,
"minimumMetaReadBytes": 16779264
},
{
"w": 2731,
"h": 1536,
"iter": 16384,
"mirror": false,
"tileW": 2731,
"initialRows": 1,
"holes": 0,
"repairThreads": 4202496,
"minimumMetaReadBytes": 16779264
},
{
"w": 2731,
"h": 1536,
"iter": 16384,
"mirror": true,
"tileW": 2731,
"initialRows": 1,
"holes": 0,
"repairThreads": 4202496,
"minimumMetaReadBytes": 16779264
},
{
"w": 2048,
"h": 2048,
"iter": 350,
"mirror": false,
"tileW": 2048,
"initialRows": 66,
"holes": 0,
"repairThreads": 4194304,
"minimumMetaReadBytes": 16777216
},
{
"w": 2048,
"h": 2048,
"iter": 350,
"mirror": true,
"tileW": 2048,
"initialRows": 66,
"holes": 0,
"repairThreads": 4194304,
"minimumMetaReadBytes": 16777216
},
{
"w": 2048,
"h": 2048,
"iter": 4096,
"mirror": false,
"tileW": 2048,
"initialRows": 5,
"holes": 0,
"repairThreads": 4194304,
"minimumMetaReadBytes": 16777216
},
{
"w": 2048,
"h": 2048,
"iter": 4096,
"mirror": true,
"tileW": 2048,
"initialRows": 5,
"holes": 0,
"repairThreads": 4194304,
"minimumMetaReadBytes": 16777216
},
{
"w": 2048,
"h": 2048,
"iter": 16384,
"mirror": false,
"tileW": 2048,
"initialRows": 1,
"holes": 0,
"repairThreads": 4194304,
"minimumMetaReadBytes": 16777216
},
{
"w": 2048,
"h": 2048,
"iter": 16384,
"mirror": true,
"tileW": 2048,
"initialRows": 1,
"holes": 0,
"repairThreads": 4194304,
"minimumMetaReadBytes": 16777216
},
{
"w": 513,
"h": 511,
"iter": 350,
"mirror": false,
"tileW": 513,
"initialRows": 267,
"holes": 0,
"repairThreads": 266240,
"minimumMetaReadBytes": 1048572
},
{
"w": 513,
"h": 511,
"iter": 350,
"mirror": true,
"tileW": 513,
"initialRows": 267,
"holes": 0,
"repairThreads": 266240,
"minimumMetaReadBytes": 1048572
},
{
"w": 513,
"h": 511,
"iter": 4096,
"mirror": false,
"tileW": 513,
"initialRows": 22,
"holes": 0,
"repairThreads": 266240,
"minimumMetaReadBytes": 1048572
},
{
"w": 513,
"h": 511,
"iter": 4096,
"mirror": true,
"tileW": 513,
"initialRows": 22,
"holes": 0,
"repairThreads": 266240,
"minimumMetaReadBytes": 1048572
},
{
"w": 513,
"h": 511,
"iter": 16384,
"mirror": false,
"tileW": 513,
"initialRows": 5,
"holes": 0,
"repairThreads": 266240,
"minimumMetaReadBytes": 1048572
},
{
"w": 513,
"h": 511,
"iter": 16384,
"mirror": true,
"tileW": 513,
"initialRows": 5,
"holes": 0,
"repairThreads": 266240,
"minimumMetaReadBytes": 1048572
}
],
"numericHistory": [
{
"name": "1920x1080-fast",
"cssW": 1920,
"cssH": 1080,
"w": 965,
"h": 543,
"hits": 2,
"total": 1000,
"hitRate": 0.002,
"meanReuseRatio": 0.8,
"reducedScaleDenominator": 384,
"policyEligible": false,
"aResidentBytes": 4191960,
"bResidentBytes": 0
},
{
"name": "1920x1080-standard",
"cssW": 1920,
"cssH": 1080,
"w": 1365,
"h": 768,
"hits": 6,
"total": 1000,
"hitRate": 0.006,
"meanReuseRatio": 0.8666666666666667,
"reducedScaleDenominator": 128,
"policyEligible": false,
"aResidentBytes": 8386560,
"bResidentBytes": 0
},
{
"name": "1920x1080-high",
"cssW": 1920,
"cssH": 1080,
"w": 2731,
"h": 1536,
"hits": 0,
"total": 1000,
"hitRate": 0,
"meanReuseRatio": 0,
"reducedScaleDenominator": 1920,
"policyEligible": false,
"aResidentBytes": 33558528,
"bResidentBytes": 0
},
{
"name": "1024x768-standard",
"cssW": 1024,
"cssH": 768,
"w": 1024,
"h": 768,
"hits": 1000,
"total": 1000,
"hitRate": 1,
"meanReuseRatio": 0.75537109375,
"reducedScaleDenominator": 1,
"policyEligible": true,
"aResidentBytes": 6291456,
"bResidentBytes": 6291456
},
{
"name": "1024x768-dpr2",
"cssW": 1024,
"cssH": 768,
"w": 2048,
"h": 1536,
"hits": 1000,
"total": 1000,
"hitRate": 1,
"meanReuseRatio": 0.75537109375,
"reducedScaleDenominator": 1,
"policyEligible": true,
"aResidentBytes": 25165824,
"bResidentBytes": 25165824
}
],
"memoryTraffic": {
"rows": [
{
"preset": "fast",
"pixels": 524288,
"recolorNumericCopyA": 4194304,
"recolorNumericCopyB": 0,
"cpuRgbaRetainedA": 2097152,
"cpuRgbaRetainedB": 0,
"idleColorSnapshotA": 6291456,
"idleColorSnapshotB": 0,
"unknownPrepassMetaReadA": 2097152,
"unknownPrepassMetaReadB": 0,
"fastRepairMetaReadA": 2097152,
"fastRepairMetaReadB": 0
},
{
"preset": "standard",
"pixels": 1048576,
"recolorNumericCopyA": 8388608,
"recolorNumericCopyB": 0,
"cpuRgbaRetainedA": 4194304,
"cpuRgbaRetainedB": 0,
"idleColorSnapshotA": 12582912,
"idleColorSnapshotB": 0,
"unknownPrepassMetaReadA": 4194304,
"unknownPrepassMetaReadB": 0,
"fastRepairMetaReadA": 4194304,
"fastRepairMetaReadB": 0
},
{
"preset": "high",
"pixels": 4194304,
"recolorNumericCopyA": 33554432,
"recolorNumericCopyB": 0,
"cpuRgbaRetainedA": 16777216,
"cpuRgbaRetainedB": 0,
"idleColorSnapshotA": 50331648,
"idleColorSnapshotB": 0,
"unknownPrepassMetaReadA": 16777216,
"unknownPrepassMetaReadB": 0,
"fastRepairMetaReadA": 16777216,
"fastRepairMetaReadB": 0
}
],
"export": {
"retainedA": 25165824,
"retainedB": 0,
"reallocationSlotsB": 3
}
}
},
"notes": [
"Real WebGPU frame-time p50/p95 remains unavailable in this container.",
"Adaptive initial-iteration probe was retained because blanket removal regressed 3/48 survey views.",
"Strict periodicity-state updates were retained because CPU A/B did not establish a speed win.",
"Continuation replay and deep-workspace lifetime were not changed because they require redesign or real-GPU allocation measurements rather than safe deletion."
]
}

View file

@ -0,0 +1,57 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
import {performance} from 'node:perf_hooks';
import {createHash} from 'node:crypto';
const root=new URL('../../',import.meta.url);
const html=await fs.readFile(new URL('index.html',root),'utf8');
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
const element={width:1024,height:768,clientWidth:1024,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:this.clientWidth,height:this.clientHeight}}};
const app={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:8,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:1024,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
const cut=scripts[1].indexOf('// ── boot / teardown');
vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.abInternal={state,canvas,snapshot,maxIter,pixelPrecisionBits,adaptiveProbeSources,adaptiveProbeChoice,referenceWorkerSource,certifiedInteriorTiles,integerGridReuseShift,WebGpuRenderer,fixedNum};})();`,app);
const I=app.abInternal;
const corpus=JSON.parse(await fs.readFile(new URL('experiments/benchmark-corpus.json',root),'utf8')).views;
const median=a=>{const b=[...a].sort((x,y)=>x-y),m=b.length>>1;return b.length%2?b[m]:(b[m-1]+b[m])/2};
const mean=a=>a.reduce((s,x)=>s+x,0)/Math.max(1,a.length);
function fromDec(s,bits=256){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [mant,es='0']=s.toLowerCase().split('e'),exp=parseInt(es,10)||0,[i='0',fr='']=mant.split('.');let digits=(i+fr).replace(/^0+(?=\d)/,'')||'0',places=fr.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
function snapOf(v,bits=256){return{bits,re:fromDec(v.re,bits),im:fromDec(v.im,bits),span:fromDec(v.span,bits)}}
function setView(v,w=1024,h=768){I.canvas.width=w;I.canvas.height=h;I.canvas.clientWidth=w;I.canvas.clientHeight=h;const s=snapOf(v);Object.assign(I.state,s);I.state.adaptive=true;I.state.baseIter=350;I.state.continuationBudget=16384;return s}
// A/B 1: strict direct path: current A maintains Brent cycle state even though strict disables classification.
const f=Math.fround;
function analyticF32(cr,ci){cr=f(cr);ci=f(ci);const 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*5.960464477539063e-8*f(f(Math.abs(lhs)+Math.abs(rhs))+1));if(lhs<f(rhs-margin))return true;const x2=f(cr+1),bulb=f(f(x2*x2)+y2),bm=f(16*5.960464477539063e-8*f(f(Math.abs(bulb)+.0625)+1));return bulb<f(.0625-bm)}
function coordF32(view,x,y,w,h){const scale=f(f(view.span)/f(w));return[f(f(view.re)+f(f(f(x+.5)-f(.5*w))*scale)),f(f(view.im)+f(f(f(.5*h)-f(y+.5))*scale))]}
function strictA(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0),n=0,cycleR=f(0),cycleI=f(0),cyclePower=1,cycleLam=0;while(n<maxIter){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;if(f(f(zr*zr)+f(zi*zi))>4)return n;cycleLam++;if(cycleLam>=cyclePower){cycleR=zr;cycleI=zi;cyclePower=Math.min(65536,cyclePower*2);cycleLam=0}}return maxIter}
function strictB(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0),n=0;while(n<maxIter){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;if(f(f(zr*zr)+f(zi*zi))>4)return n}return maxIter}
function benchStrict(){const w=64,h=40,maxIter=4096,rows=[];for(const v of corpus){const pts=[];for(let y=0;y<h;y++)for(let x=0;x<w;x++)pts.push(coordF32(v,x,y,w,h));const run=fn=>{let sum=0;const t0=performance.now();for(const p of pts)sum+=fn(p[0],p[1],maxIter);return{ms:performance.now()-t0,sum}};run(strictA);run(strictB);const A=[],B=[];for(let k=0;k<5;k++){A.push(run(strictA));B.push(run(strictB))}if(A.at(-1).sum!==B.at(-1).sum)throw new Error('strict A/B result mismatch '+v.name);const a=median(A.map(x=>x.ms)),b=median(B.map(x=>x.ms));rows.push({view:v.name,aMs:a,bMs:b,speedup:a/b,changePct:(b/a-1)*100,iterationChecksum:A.at(-1).sum})}return rows}
// A/B 2: exact interior proof with a conservative cheap viewport bounding-box gate.
// Main cardioid is inside x[-3/4,1/4], |y|<3sqrt(3)/8<2/3. Period-2 bulb is inside x[-5/4,-3/4], |y|<=1/4.
function maybeOverlapsKnownInterior(v,w,h){const re=Number(v.re),im=Number(v.im),span=Number(v.span),halfX=span/2,halfY=span*h/(2*w),xl=re-halfX,xh=re+halfX,yl=im-halfY,yh=im+halfY;const hit=(ax,bx,ay,by)=>!(xh<ax||xl>bx||yh<ay||yl>by);return hit(-.75,.375,-2/3,2/3)||hit(-1.25,-.75,-.25,.25)}
function benchInteriorGate(){const sizes=[[1024,768],[2048,2048]],rows=[];for(const [w,h] of sizes){for(const v of corpus){const snap=setView(v,w,h);const runsA=[],runsB=[];let aRes,bRes;for(let k=0;k<7;k++){let t=performance.now();aRes=I.certifiedInteriorTiles(snap,w,h,32,null);runsA.push(performance.now()-t);t=performance.now();bRes=maybeOverlapsKnownInterior(v,w,h)?I.certifiedInteriorTiles(snap,w,h,32,null):{pixels:0};runsB.push(performance.now()-t)}if(aRes.pixels!==bRes.pixels)throw new Error('interior gate false negative '+v.name);rows.push({view:v.name,w,h,gate:maybeOverlapsKnownInterior(v,w,h),certified:aRes.pixels,aMs:median(runsA),bMs:median(runsB),savedMs:median(runsA)-median(runsB)})}}return rows}
// A/B 3: adaptive probe vs always base iteration. Fixed corpus + deterministic exploratory survey.
function scorerVM(){let result=null;const c={performance,postMessage:m=>{result=m},BigInt,Float32Array,Float64Array,ArrayBuffer,DataView,Math,Number,String,Map,Set,Uint32Array};c.self=c;vm.runInNewContext(I.referenceWorkerSource(),c);return{score(snap,iter,w,h,candidates){result=null;const targetBits=Math.max(I.pixelPrecisionBits(snap,w,40)+32,...candidates.map(x=>x.precisionBits||0));c.self.onmessage({data:{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,candidates:candidates.map(x=>({re:x.re.toString(),im:x.im.toString()}))}});if(result?.type==='error')throw new Error(result.error);return result}}}
function probeOne(v,sc,w=1024,h=768){const snap=setView(v,w,h),iter=I.maxIter(),sources=I.adaptiveProbeSources(snap,w,h),t0=performance.now(),res=sc.score(snap,iter,w,h,sources),wall=performance.now()-t0,choice=I.adaptiveProbeChoice(res.escapes,iter,I.state.baseIter),baseWork=choice.work.find(x=>x.initial===I.state.baseIter)?.total??null,chosenWork=choice.work.find(x=>x.initial===choice.iter)?.total??null;return{iter,chosen:choice.iter,probeMs:wall,baseWork,chosenWork,workPenaltyAlwaysBase:chosenWork?baseWork/chosenWork:1,escapes:res.escapes}}
function exploratoryViews(){let seed=0x12345678;const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/4294967296};const anchors=[[-.75,0],[-.743643887, .131825904],[-.1011,.9563],[-1.25066,.02012],[-.16,1.0405],[-.1225611669,.7448617666],[-.5,0]];const spans=[2,1,.4,.1,.03,.01,.003,.001];const out=[];for(let i=0;i<48;i++){const a=anchors[i%anchors.length],sp=spans[(i*3)%spans.length],j=sp*.35;out.push({name:'survey-'+i,re:a[0]+(rnd()-.5)*j,im:a[1]+(rnd()-.5)*j,span:sp})}return out}
function benchProbe(){const sc=scorerVM(),fixed=corpus.map(v=>({view:v.name,...probeOne(v,sc)})),survey=[];for(const v of exploratoryViews())survey.push({view:v.name,...v,...probeOne(v,sc,640,480)});return{fixed,survey,nonBase:survey.filter(x=>x.chosen!==350)}}
// A/B 4: FAST full-screen repair. Tiled post-stats pass covers all compute pixels; symmetry covers the other half.
function coverageCase(w,h,iter,mirror){const r=Object.create(I.WebGpuRenderer.prototype),shape=r.numericTileShape(w,iter),tileW=shape.width,baseRows=r.initialNumericRows(w,iter),seen=new Uint8Array(w*h),computeH=mirror?Math.ceil(h/2):h;let rows=baseRows,y=0,step=0;while(y<computeH){const th=Math.min(rows,computeH-y);for(let x=0;x<w;x+=tileW){const tw=Math.min(tileW,w-x);for(let yy=0;yy<th;yy++)seen.fill(1,(y+yy)*w+x,(y+yy)*w+x+tw)}y+=th;step++;if(step%3===0)rows=Math.max(1,Math.min(computeH-y||1,Math.ceil(rows*.73)));else if(step%4===0)rows=Math.max(1,Math.min(computeH-y||1,rows+1))}if(mirror){for(let yy=0;yy<Math.floor(h/2);yy++){const my=h-1-yy;for(let x=0;x<w;x++)if(seen[yy*w+x])seen[my*w+x]=1}}let holes=0;for(const v of seen)if(!v)holes++;return{w,h,iter,mirror,tileW,initialRows:baseRows,holes,repairThreads:Math.ceil(w/8)*Math.ceil(h/8)*64,minimumMetaReadBytes:w*h*4}}
function benchCoverage(){const cases=[];for(const [w,h] of [[965,543],[1365,768],[1024,768],[2731,1536],[2048,2048],[513,511]])for(const iter of [350,4096,16384])for(const mirror of [false,true])cases.push(coverageCase(w,h,iter,mirror));if(cases.some(x=>x.holes))throw new Error('coverage hole found');return cases}
// A/B 5: numeric history exact-grid reuse hit survey and conditional-residency policy.
function historyPanSurvey(){const configs=[{name:'1920x1080-fast',cssW:1920,cssH:1080,w:965,h:543},{name:'1920x1080-standard',cssW:1920,cssH:1080,w:1365,h:768},{name:'1920x1080-high',cssW:1920,cssH:1080,w:2731,h:1536},{name:'1024x768-standard',cssW:1024,cssH:768,w:1024,h:768},{name:'1024x768-dpr2',cssW:1024,cssH:768,w:2048,h:1536}],rows=[];const base=snapOf(corpus[0]);for(const c of configs){let hits=0,total=0,reused=0;const old={...base,w:c.w,h:c.h};for(let dx=-500;dx<=500;dx++){if(dx===0)continue;const re=old.re-old.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(c.cssW*1e6)),next={...old,re};const sh=I.integerGridReuseShift(old,next,c.w,c.h);total++;if(sh){hits++;reused+=sh.pixels}}const g=(a,b)=>b?g(b,a%b):Math.abs(a),den=c.cssW/g(c.w,c.cssW),policyEligible=den<=8;rows.push({...c,hits,total,hitRate:hits/total,meanReuseRatio:hits?reused/hits/(c.w*c.h):0,reducedScaleDenominator:den,policyEligible,aResidentBytes:8*c.w*c.h,bResidentBytes:policyEligible?8*c.w*c.h:0})}return rows}
// A/B 6: continuation replay surrogate (direct arithmetic): A restarts survivors from zero; B resumes saved z.
function analytic64(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;if(q*(q+x)<.25*y2)return true;const x2=cr+1;return x2*x2+y2<.0625}
function continueFrom(cr,ci,maxIter,zr=0,zi=0,n=0){let work=0;for(;n<maxIter;n++){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;work++;if(zr*zr+zi*zi>4)return{escaped:true,n:n+1,zr,zi,work}}return{escaped:false,n,zr,zi,work}}
function continuationReplay(){const w=64,h=40,base=350,target=4096,rows=[];for(const v of corpus){let workA=0,workB=0,survivors=0,mismatch=0;for(let y=0;y<h;y++)for(let x=0;x<w;x++){const scale=v.span/w,cr=v.re+(x+.5-w/2)*scale,ci=v.im+(h/2-y-.5)*scale;if(analytic64(cr,ci))continue;const p=continueFrom(cr,ci,base);workA+=p.work;workB+=p.work;if(!p.escaped){survivors++;const a=continueFrom(cr,ci,target);const b=continueFrom(cr,ci,target,p.zr,p.zi,p.n);workA+=a.work;workB+=b.work;if(a.escaped!==b.escaped||a.n!==b.n)mismatch++}}rows.push({view:v.name,survivors,workA,workB,workReduction:1-workB/Math.max(1,workA),mismatch})}return rows}
function memoryTraffic(){const presets={fast:524288,standard:1048576,high:4194304};const rows=[];for(const [preset,n] of Object.entries(presets)){rows.push({preset,pixels:n,recolorNumericCopyA:8*n,recolorNumericCopyB:0,cpuRgbaRetainedA:4*n,cpuRgbaRetainedB:0,idleColorSnapshotA:12*n,idleColorSnapshotB:0,unknownPrepassMetaReadA:4*n,unknownPrepassMetaReadB:0,fastRepairMetaReadA:4*n,fastRepairMetaReadB:0})}const export512PerSlot=(512*512)*(4+4+4*4+4+4),activeStdFull=1048576*(32+4+4+4)+Math.ceil(1048576/32)*4+24,activeHighFull=4194304*(32+4+4+4)+Math.ceil(4194304/32)*4+24;return{rows,export:{retainedA:3*export512PerSlot,retainedB:0,reallocationSlotsB:3},deep:{standardFullA:activeStdFull,highFullA:activeHighFull,releaseAfterEasyB:0}}}
const results={date:new Date().toISOString(),source:{sha256:createHash('sha256').update(html).digest('hex'),webGpuTiming:'unavailable in container; Chromium exposes no usable WebGPU adapter'},strictCycle:benchStrict(),interiorGate:benchInteriorGate(),adaptiveProbe:benchProbe(),fastCoverage:benchCoverage(),numericHistory:historyPanSurvey(),continuationReplay:continuationReplay(),memoryTraffic:memoryTraffic()};
console.log(JSON.stringify(results,null,2));
if(process.argv[2])await fs.writeFile(process.argv[2],JSON.stringify(results,null,2)+'\n');

View file

@ -0,0 +1,8 @@
import fs from 'node:fs/promises';import vm from 'node:vm';import {performance} from 'node:perf_hooks';
const html=await fs.readFile(new URL('../../index.html',import.meta.url),'utf8'),scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]),kc={};vm.runInNewContext(scripts[0],kc);const element={width:256,height:192,clientWidth:256,clientHeight:192,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},getBoundingClientRect(){return{left:0,top:0,width:256,height:192}}};const app={MANDEL_WEBGPU_KERNELS:kc.MANDEL_WEBGPU_KERNELS,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:8,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:256,innerHeight:192,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};const cut=scripts[1].indexOf('// ── boot / teardown');vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.x={certifiedInteriorTiles};})();`,app);const C=app.x;
function fromDec(s,bits=256){s=String(s);let neg=s[0]=='-';if(neg)s=s.slice(1);const [m,e0='0']=s.toLowerCase().split('e'),e=+e0||0,[i='0',f='']=m.split('.');let d=(i+f).replace(/^0+(?=\d)/,'')||'0',p=f.length-e;if(p<0){d+='0'.repeat(-p);p=0}let den=10n**BigInt(p),v=(BigInt(d)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
function gate(v,w,h){const re=v.re,im=v.im,span=v.span,unit=1n<<BigInt(v.bits),bw=BigInt(w),bh=BigInt(h),xLo=2n*re-span,xHi=2n*re+span,xDen=2n*unit,yLo=2n*bw*im-span*bh,yHi=2n*bw*im+span*bh,yDen=2n*bw*unit,axis=(lo,hi,den,lp,lq,hp,hq)=>hi*BigInt(lq)>=BigInt(lp)*den&&lo*BigInt(hq)<=BigInt(hp)*den;return axis(xLo,xHi,xDen,-3,4,3,8)&&axis(yLo,yHi,yDen,-2,3,2,3)||axis(xLo,xHi,xDen,-5,4,-3,4)&&axis(yLo,yHi,yDen,-1,4,1,4)}
let seed=0x9e3779b9;const rnd=()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/2**32};let gateFalse=0,falseNeg=0,totalProof=0;for(let i=0;i<240;i++){const span=10**(-4+rnd()*4.7),v={bits:256,re:fromDec(-2+rnd()*3),im:fromDec(-1.2+rnd()*2.4),span:fromDec(span)},g=gate(v,256,192),p=C.certifiedInteriorTiles(v,256,192,32,null).pixels;if(!g)gateFalse++;if(!g&&p){falseNeg++;totalProof+=p}}
// FAST semantic coverage with random seeded/proven/unknown metadata. 0 means untouched hole; any invocation writes nonzero unless a nonzero seed/proof is intentionally retained.
let fastFalse=0,fastCases=0;for(let c=0;c<200;c++){const w=1+Math.floor(rnd()*700),h=1+Math.floor(rnd()*500),mirror=rnd()<.5,computeH=mirror?Math.ceil(h/2):h,meta=new Uint32Array(w*h);for(let i=0;i<meta.length;i++){const q=rnd();meta[i]=q<.1?(3<<28):q<.2?(1<<28)|5:q<.3?123:0}const tileW=1+Math.floor(rnd()*Math.min(w,200)),rows=1+Math.floor(rnd()*Math.min(computeH,80));for(let y=0;y<computeH;y+=rows)for(let x=0;x<w;x+=tileW){const tw=Math.min(tileW,w-x),th=Math.min(rows,computeH-y);for(let yy=0;yy<th;yy++)for(let xx=0;xx<tw;xx++){const i=(y+yy)*w+x+xx,cls=meta[i]>>>28;if(cls===3)continue;if(cls!==0)continue;meta[i]=(1<<28)|1}}if(mirror)for(let y=0;y<Math.floor(h/2);y++)for(let x=0;x<w;x++)meta[(h-1-y)*w+x]=meta[y*w+x]||1;let holes=0;for(const m of meta)if(m===0)holes++;if(holes)fastFalse++;fastCases++}
console.log(JSON.stringify({interiorGateRandom:{cases:240,gateFalse,falseNeg,totalProofInFalseNeg:totalProof},fastCoverageSemantic:{cases:fastCases,casesWithHoles:fastFalse}},null,2));

View file

@ -0,0 +1,13 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
for (const file of process.argv.slice(2)) {
const html=await fs.readFile(file,'utf8');
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
if(scripts.length!==2) throw new Error(file+': expected 2 scripts, got '+scripts.length);
for(const s of scripts)new vm.Script(s);
const kc={}; vm.runInNewContext(scripts[0],kc);
const kernels=kc.MANDEL_WEBGPU_KERNELS;
if(!kernels||Object.keys(kernels).length!==20)throw new Error(file+': kernel bundle malformed');
for(const k of ['DIRECT_F32_WGSL','FAST_PERTURB_WGSL','UNKNOWN_STATS_WGSL','COLOR_WGSL'])if(typeof kernels[k]!=='string')throw new Error(file+': missing '+k);
console.log('PASS',file.split('/').at(-1),'JS syntax, 19 WGSL kernels + version');
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
{
"purpose": "Fixed surrogate corpus for evaluating proposed Mandelbrot viewer improvements without changing production index.html.",
"views": [
{"name":"overview","re":-0.5,"im":0,"span":3.4},
{"name":"period3-bulb","re":-0.1225611668766536,"im":0.7448617666197442,"span":0.10},
{"name":"period3-core","re":-0.1225611668766536,"im":0.7448617666197442,"span":0.025},
{"name":"seahorse-boundary","re":-0.743643887037151,"im":0.13182590420533,"span":0.004},
{"name":"seahorse-deep","re":-0.743643887037151,"im":0.13182590420533,"span":0.000002},
{"name":"exterior","re":0.7,"im":0.2,"span":1.2}
],
"notes": [
"CPU tests are surrogates for algorithmic work and classification behavior, not WebGPU frame-time benchmarks.",
"Deep-zoom tests that depend on arbitrary precision are represented by perturbation/series locality tests rather than full production-equivalent rendering.",
"Do not change these views when comparing future revisions; add new named views instead."
]
}

View file

@ -0,0 +1,40 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
import {performance} from 'node:perf_hooks';
const html=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
const element={width:1024,height:768,clientWidth:1024,clientHeight:768,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:this.clientWidth,height:this.clientHeight}}};
const app={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:8,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:1024,innerHeight:768,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
const cut=scripts[1].indexOf('// ── boot / teardown');
vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.auditInternal={state,canvas,snapshot,maxIter,zoomExp,pixelPrecisionBits,adaptiveProbeSources,adaptiveProbeChoice,referenceWorkerSource,certifiedInteriorTiles,integerGridReuseShift,fmtFixed,updateStats};})();`,app);
const I=app.auditInternal;
const corpus=JSON.parse(await fs.readFile(new URL('./benchmark-corpus.json',import.meta.url),'utf8')).views;
const median=a=>{const b=[...a].sort((x,y)=>x-y),m=b.length>>1;return b.length%2?b[m]:(b[m-1]+b[m])/2};
function fromDec(s,bits=256){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [mant,es='0']=s.toLowerCase().split('e'),exp=parseInt(es,10)||0,[i='0',fr='']=mant.split('.');let digits=(i+fr).replace(/^0+(?=\d)/,'')||'0',places=fr.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
function snapOf(v,bits=256){return{bits,re:fromDec(v.re,bits),im:fromDec(v.im,bits),span:fromDec(v.span,bits)}}
function setView(v,w=1024,h=768){I.canvas.width=w;I.canvas.height=h;I.canvas.clientWidth=w;I.canvas.clientHeight=h;const s=snapOf(v);Object.assign(I.state,s);return s}
function benchInteriorProof(){const sizes=[[1024,768],[2048,2048]],rows=[];for(const [w,h] of sizes){for(const v of corpus){const snap=setView(v,w,h);const times=[];let r;for(let k=0;k<7;k++){const t0=performance.now();r=I.certifiedInteriorTiles(snap,w,h,32,null);times.push(performance.now()-t0)}rows.push({view:v.name,w,h,pixels:w*h,certified:r.pixels,certifiedRatio:r.pixels/(w*h),medianMs:median(times),times})}}return rows}
function runScoreWorker(snap,iter,w,h,candidates){let result;const c={performance,postMessage:m=>{result=m},BigInt,Float32Array,Float64Array,ArrayBuffer,DataView,Math,Number,String,Map,Set,Uint32Array};c.self=c;vm.runInNewContext(I.referenceWorkerSource(),c);const targetBits=Math.max(I.pixelPrecisionBits(snap,w,40)+32,...candidates.map(x=>x.precisionBits||0));c.self.onmessage({data:{type:'score',id:1,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width:w,height:h,iter,candidates:candidates.map(x=>({re:x.re.toString(),im:x.im.toString()}))}});if(result?.type==='error')throw new Error(result.error);return result}
function benchAdaptiveProbe(){const w=1024,h=768,rows=[];for(const v of corpus){const snap=setView(v,w,h),adaptiveIter=I.maxIter(),candidates=I.adaptiveProbeSources(snap,w,h);const times=[];let res;for(let k=0;k<3;k++){const t0=performance.now();res=runScoreWorker(snap,adaptiveIter,w,h,candidates);times.push(performance.now()-t0)}const choice=I.adaptiveProbeChoice(res.escapes,adaptiveIter,I.state.baseIter);rows.push({view:v.name,adaptiveIter,chosenInitialIter:choice.iter,targetBits:I.pixelPrecisionBits(snap,w,40)+32,samples:candidates.length,scoreReportedMs:res.scoreMs,wallMedianMs:median(times),escapeSamples:res.escapes,workChoices:choice.work})}return rows}
const f=Math.fround;
function analyticF32(cr,ci){cr=f(cr);ci=f(ci);const 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*5.960464477539063e-8*f(f(Math.abs(lhs)+Math.abs(rhs))+1));if(lhs<f(rhs-margin))return true;const x2=f(cr+1),bulb=f(f(x2*x2)+y2),bm=f(16*5.960464477539063e-8*f(f(Math.abs(bulb)+.0625)+1));return bulb<f(.0625-bm)}
function coordF32(view,x,y,w,h){const scale=f(f(view.span)/f(w)),cr=f(f(view.re)+f(f(f(x+.5)-f(.5*w))*scale)),ci=f(f(view.im)+f(f(f(.5*h)-f(y+.5))*scale));return[cr,ci]}
function cmul(a,b){return[f(f(a[0]*b[0])-f(a[1]*b[1])),f(f(a[0]*b[1])+f(a[1]*b[0]))]}
function directBase(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0);for(let n=1;n<=maxIter;n++){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);if(f(f(zr*zr)+f(zi*zi))>4)return n}return maxIter}
function directPeriodic(cr,ci,maxIter){if(analyticF32(cr,ci))return 0;let zr=f(0),zi=f(0),n=0,cycleR=f(0),cycleI=f(0),cyclePower=1,cycleLam=0,candPeriod=0,candAge=0,candR=f(0),candI=f(0),candMul=[f(1),f(0)],candRepeats=0;while(n<maxIter){const prev=[zr,zi],zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(2*zr*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;if(f(f(zr*zr)+f(zi*zi))>4)return n;if(candPeriod>0){candMul=cmul(candMul,[f(2*prev[0]),f(2*prev[1])]);candAge++;if(candAge>=candPeriod){const tol=f(4*5.960464477539063e-8*f(1+Math.max(Math.abs(zr),Math.abs(zi)))),close=Math.max(Math.abs(f(zr-candR)),Math.abs(f(zi-candI)))<=tol,attracting=f(f(candMul[0]*candMul[0])+f(candMul[1]*candMul[1]))<.9025;if(close&&attracting){candRepeats++;if(candRepeats>=2)return n;candR=zr;candI=zi;candAge=0;candMul=[f(1),f(0)]}else{candPeriod=0;candRepeats=0}}}if(candPeriod===0&&n>=64){const tol=f(4*5.960464477539063e-8*f(1+Math.max(Math.abs(zr),Math.abs(zi))));if(Math.max(Math.abs(f(zr-cycleR)),Math.abs(f(zi-cycleI)))<=tol){const period=cycleLam+1;if(period<=2048){candPeriod=period;candAge=0;candR=zr;candI=zi;candMul=[f(1),f(0)];candRepeats=0}}}cycleLam++;if(cycleLam>=cyclePower){cycleR=zr;cycleI=zi;cyclePower=Math.min(65536,cyclePower*2);cycleLam=0}}return maxIter}
function benchPeriodicityRuntime(){const w=64,h=40,maxIter=4096,rows=[];for(const v of corpus){const pts=[];for(let y=0;y<h;y++)for(let x=0;x<w;x++)pts.push(coordF32(v,x,y,w,h));const run=fn=>{let sum=0;const t0=performance.now();for(const [cr,ci] of pts)sum+=fn(cr,ci,maxIter);return{ms:performance.now()-t0,sum}};run(directBase);run(directPeriodic);const b=[],p=[];for(let k=0;k<4;k++){b.push(run(directBase));p.push(run(directPeriodic))}rows.push({view:v.name,baseMedianMs:median(b.map(x=>x.ms)),periodicMedianMs:median(p.map(x=>x.ms)),runtimeRatio:median(p.map(x=>x.ms))/median(b.map(x=>x.ms)),baseIterationSum:b.at(-1).sum,periodicIterationSum:p.at(-1).sum,iterationReduction:1-p.at(-1).sum/Math.max(1,b.at(-1).sum)})}return rows}
function memoryAudit(){const presets={fast:524288,standard:1048576,high:4194304},rows=[];for(const [preset,n] of Object.entries(presets)){rows.push({preset,pixels:n,baseFrameBytes:28*n,numericHistoryBytes:8*n,colorSourceBytes:12*n,deepCorrectionQueueBytes:4*n,cpuFallbackRgbaBytes:4*n,colorAutoNumericHistoryCopyBytesPerSecond:8*n*20,provisionalSnapshotCopyBytesPerSecondAt8Hz:8*n*8})}const export512PerSlot=(512*512)*(4+4+4*4+4+4); // meta,smooth,4 samples,resolve,read
const export256PerSlot=(256*256)*(4+4+4*4+4+4);
return{rows,export:{tile512PerSlotBytes:export512PerSlot,tile512Depth3Bytes:3*export512PerSlot,tile256PerSlotBytes:export256PerSlot,tile256Depth2Bytes:2*export256PerSlot,nonAaUnusedSampleTextureBytesPer512Slot:4*512*512*4}}}
function gridReusePanAudit(){const rows=[];for(const dpr of [1,1.25,1.5,2])for(const cssW of [800,1024,1365]){const w=Math.round(cssW*dpr),h=Math.round(w*.75),old={...snapOf(corpus[0]),w,h};for(const dx of [1,3,10,37,100]){const denom=BigInt(Math.round(cssW*1e6)),re=old.re-old.span*BigInt(Math.round(dx*1e6))/denom,newv={...old,re};const sh=I.integerGridReuseShift(old,newv,w,h);rows.push({dpr,cssW,w,dx,reused:!!sh,sx:sh?.sx??null,pixels:sh?.pixels??0,ratio:sh?sh.pixels/(w*h):0})}}return rows}
const results={date:new Date().toISOString(),sourceSha256:null,interiorProof:benchInteriorProof(),adaptiveProbe:benchAdaptiveProbe(),periodicityRuntimeSurrogate:benchPeriodicityRuntime(),memory:memoryAudit(),gridReusePan:gridReusePanAudit(),staticFindings:{certifyInteriorRunsUnusedUnknownStats:/encodeUnknownStats\(e,\{meta:f\.meta,unresolved:f\.unresolved,n:f\.n\}\);\s*return await this\.submitStage\(e,historySeed\?/.test(html),numericHistoryAlwaysAllocated:/historyMeta:buf\(d,n\*4/.test(html)&&/historySmooth:buf\(d,n\*4/.test(html),commitHistoryAlwaysCommitsNumeric:/this\.commitNumericHistory\(view,state\.fieldView\?\.iter/.test(html),colorAuto20Hz:/state\.rendering\?100:50/.test(html),colorSourceSnapshotCopiesNumeric:/copyBufferToBuffer\(f\.meta,0,source\.meta,0,f\.n\*4\)/.test(html)&&/copyBufferToBuffer\(f\.smooth,0,source\.smooth,0,f\.n\*4\)/.test(html),cpuFallbackRgbaUnused:(html.match(/cpuFrameRgba/g)||[]).length===2,exportWorkspacePersists:!/exportWorkspaceDestroy\(\)/.test(html.slice(html.indexOf('async function runExport'),html.indexOf("$('png').onclick"))),fastCoverageRepair:/fast-coverage-repair/.test(html)&&/this\.encodeFastNumeric\(e,\{pbuf:f\.numericParams/.test(html)}};
console.log(JSON.stringify(results,null,2));
if(process.argv[2])await fs.writeFile(process.argv[2],JSON.stringify(results,null,2)+'\n');

View file

@ -0,0 +1,46 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
import os from 'node:os';
import {Worker} from 'node:worker_threads';
import {performance} from 'node:perf_hooks';
const html=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}};
const app={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>'',revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:800,innerHeight:600,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}};
const cut=scripts[1].indexOf('// ── boot / teardown');
vm.runInNewContext(scripts[1].slice(0,cut)+`\nglobalThis.internal={fastReferenceWorkerSource,cpuFallbackWorkerSource,chooseBackend};})();`,app);
const internal=app.internal;
const corpus=JSON.parse(await fs.readFile(new URL('./benchmark-corpus.json',import.meta.url),'utf8')).views;
const median=a=>{const b=[...a].sort((x,y)=>x-y),m=b.length>>1;return b.length%2?b[m]:(b[m-1]+b[m])/2};
const f=Math.fround;
function analyticF32(cr,ci){cr=f(cr);ci=f(ci);const 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*5.960464477539063e-8*f(f(Math.abs(lhs)+Math.abs(rhs))+1));if(lhs<f(rhs-margin))return true;const x2=f(cr+1),bulb=f(f(x2*x2)+y2),bm=f(16*5.960464477539063e-8*f(f(Math.abs(bulb)+.0625)+1));return bulb<f(.0625-bm)}
function coordF32(view,x,y,w,h){const scale=f(f(view.span)/f(w)),cr=f(f(view.re)+f(f(f(x+.5)-f(.5*w))*scale)),ci=f(f(view.im)+f(f(f(.5*h)-f(y+.5))*scale));return[cr,ci]}
function cMulF(a,b){return[f(f(a[0]*b[0])-f(a[1]*b[1])),f(f(a[0]*b[1])+f(a[1]*b[0]))]}
function iterateProd(cr,ci,maxIter,periodic){if(analyticF32(cr,ci))return{cls:'analytic',work:0,iter:0};let zr=f(0),zi=f(0),n=0,cycleR=f(0),cycleI=f(0),power=1,lam=0,candPeriod=0,candAge=0,candR=0,candI=0,candMul=[1,0],candRepeats=0;while(n<maxIter){const prev=[zr,zi],zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(f(2*zr)*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;const mag=f(f(zr*zr)+f(zi*zi));if(mag>4)return{cls:'escaped',work:n,iter:n};if(periodic&&candPeriod>0){candMul=cMulF(candMul,[f(2*prev[0]),f(2*prev[1])]);candAge++;if(candAge>=candPeriod){const tol=f(4*5.960464477539063e-8*f(1+Math.max(Math.abs(zr),Math.abs(zi)))),close=Math.max(Math.abs(f(zr-candR)),Math.abs(f(zi-candI)))<=tol,attracting=f(f(candMul[0]*candMul[0])+f(candMul[1]*candMul[1]))<.9025;if(close&&attracting){candRepeats++;if(candRepeats>=2)return{cls:'heuristic',work:n,iter:n};candR=zr;candI=zi;candAge=0;candMul=[1,0]}else{candPeriod=0;candRepeats=0}}}if(periodic&&candPeriod===0&&n>=64){const tol=f(4*5.960464477539063e-8*f(1+Math.max(Math.abs(zr),Math.abs(zi))));if(Math.max(Math.abs(f(zr-cycleR)),Math.abs(f(zi-cycleI)))<=tol){const period=lam+1;if(period<=2048){candPeriod=period;candAge=0;candR=zr;candI=zi;candMul=[1,0];candRepeats=0}}}lam++;if(lam>=power){cycleR=zr;cycleI=zi;power=Math.min(65536,power*2);lam=0}}return{cls:'limit',work:maxIter,iter:maxIter}}
function iterateOracle(cr,ci,maxIter){let zr=0,zi=0;for(let n=1;n<=maxIter;n++){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;if(zr*zr+zi*zi>4)return n}return 0}
function periodicityBench(){const w=96,h=64,maxIter=8192,oracleIter=65536,rows=[];for(const view of corpus){const snap={bits:256,re:fromDec(String(view.re)),im:fromDec(String(view.im)),span:fromDec(String(view.span))},backend=internal.chooseBackend(snap,800).backend;let baseWork=0,heurWork=0,candidates=0,falseCandidates=0,baseLimit=0,heurLimit=0;const t0=performance.now();for(let y=0;y<h;y++)for(let x=0;x<w;x++){const [cr,ci]=coordF32(view,x,y,w,h),b=iterateProd(cr,ci,maxIter,false),q=iterateProd(cr,ci,maxIter,true);baseWork+=b.work;heurWork+=q.work;if(b.cls==='limit')baseLimit++;if(q.cls==='limit')heurLimit++;if(q.cls==='heuristic'){candidates++;if(iterateOracle(cr,ci,oracleIter)>q.iter)falseCandidates++}}rows.push({view:view.name,productionBackend:backend,pixels:w*h,baseWork,heuristicWork:heurWork,workReduction:backend==='direct'?1-heurWork/Math.max(1,baseWork):null,candidates:backend==='direct'?candidates:0,falseCandidates:backend==='direct'?falseCandidates:0,baseLimit,heuristicLimit:backend==='direct'?heurLimit:baseLimit,diagnosticIfForcedDirect:backend==='direct'?null:{workReduction:1-heurWork/Math.max(1,baseWork),candidates,falseCandidates},ms:performance.now()-t0})}return{w,h,maxIter,oracleIter,rows,note:'Production DIRECT_F32 rule with two attracting-cycle confirmations. FAST/perturbation periodic early-stop is intentionally disabled.'}}
function fromDec(s,bits=256){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [mant,es='0']=s.toLowerCase().split('e'),exp=parseInt(es,10)||0,[i='0',fr='']=mant.split('.');let digits=(i+fr).replace(/^0+(?=\d)/,'')||'0',places=fr.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
function workerBuildFast(span){let result;const c={performance,postMessage:m=>{result=m},BigInt,Float32Array,Float64Array,ArrayBuffer,DataView,Math,Number,String,Map,Set,Uint32Array};c.self=c;vm.runInNewContext(internal.fastReferenceWorkerSource(),c);c.self.onmessage({data:{type:'build',id:1,key:'bench',re:fromDec('-0.743643887037151').toString(),im:fromDec('0.13182590420533').toString(),span:fromDec(span).toString(),sourceBits:256,targetBits:320,width:800,height:600,iter:1200}});if(result?.type==='error')throw new Error(result.error);return result}
function cMul(a,b){return[a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]]}
function refsAt(buf,n){const d=new DataView(buf),o=n*16;return[d.getFloat32(o,true)+d.getFloat32(o+8,true),d.getFloat32(o+4,true)+d.getFloat32(o+12,true)]}
function evalSeries(coeffs,dc){let sum=[0,0],pow=[1,0];for(let k=0;k<4;k++){pow=cMul(pow,dc);const a=[coeffs[k*2],coeffs[k*2+1]],t=cMul(a,pow);sum=[sum[0]+t[0],sum[1]+t[1]]}return sum}
function directDelta(refs,jump,dc){let d=[0,0];for(let n=0;n<jump;n++){const z=refsAt(refs,n),lin=cMul([2*z[0],2*z[1]],d),sq=cMul(d,d);d=[lin[0]+sq[0]+dc[0],lin[1]+sq[1]+dc[1]]}return d}
function seriesBench(){const spans=['1e-5','1e-7','1e-9','1e-11'],rows=[];for(const spanText of spans){const r=workerBuildFast(spanText),span=Number(spanText),samples=[[-.5,-.375],[-.5,.375],[.5,-.375],[.5,.375],[0,-.375],[0,.375],[-.5,0],[.5,0]];let maxAbsError=0,maxRelError=0;for(const [x,y] of samples){const dc=[x*span,y*span],p=evalSeries(r.series.coeffs,dc),e=directDelta(r.refs,r.series.jump,dc),ae=Math.hypot(p[0]-e[0],p[1]-e[1]),re=ae/Math.max(1e-30,Math.hypot(e[0],e[1]));maxAbsError=Math.max(maxAbsError,ae);maxRelError=Math.max(maxRelError,re)}rows.push({span:Number(spanText),jump:r.series.jump,errorLog2:r.series.errorLog2,maxAbsError,maxRelativeError:maxRelError,refLen:r.refLen,buildMs:r.buildMs})}return{rows,note:'Production fast-reference worker output. Error compares packed 4th-order series against direct perturbation over 8 screen-domain deltas.'}}
function sparseMemoryBench(){const presets={fast:524288,standard:1048576,high:4194304},rows=[];for(const [preset,n] of Object.entries(presets))for(const activeRatio of [1,.25,.1,.01]){const oldBytes=44*n,capacity=Math.min(n,Math.max(Math.ceil(n*activeRatio),Math.ceil(n*activeRatio*1.25)+256)),newBytes=44*capacity+4*Math.ceil(n/32),reduction=1-newBytes/oldBytes;rows.push({preset,pixels:n,activeRatio,capacity,oldBytes,newBytes,reduction})}return{rows,note:'Production allocation: 32 B state + 2x4 B queues + 4 B pixel map per active slot, plus 1-bit membership bitmap. Capacity adds 25% + 256 headroom.'}}
// Same perturbation locality surrogate as the first evaluation; production now requests up to 3 failure-guided references instead of one.
function analytic(cr,ci){const y2=ci*ci,x=cr-.25,q=x*x+y2;return q*(q+x)<.25*y2||(cr+1)*(cr+1)+y2<.0625}
function iterate(cr,ci,maxIter){if(analytic(cr,ci))return{cls:'analytic',iter:0};let zr=0,zi=0;for(let n=1;n<=maxIter;n++){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;if(zr*zr+zi*zi>4)return{cls:'escaped',iter:n}}return{cls:'limit',iter:maxIter}}
function coord(view,x,y,w,h){const scale=view.span/w;return[view.re+(x+.5-w/2)*scale,view.im+(h/2-y-.5)*scale]}
function mMul(ar,ai,br,bi){return[ar*br-ai*bi,ar*bi+ai*br]}
function mFMul(ar,ai,br,bi){return[f(f(ar*br)-f(ai*bi)),f(f(ar*bi)+f(ai*br))]}
function buildReference(cr,ci,maxIter){const re=new Float64Array(maxIter+1),im=new Float64Array(maxIter+1),hiRe=new Float32Array(maxIter+1),hiIm=new Float32Array(maxIter+1),loRe=new Float32Array(maxIter+1),loIm=new Float32Array(maxIter+1);let zr=0,zi=0,valid=maxIter;for(let n=0;n<=maxIter;n++){re[n]=zr;im[n]=zi;hiRe[n]=f(zr);hiIm[n]=f(zi);loRe[n]=f(zr-hiRe[n]);loIm[n]=f(zi-hiIm[n]);if(n===maxIter)break;const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;if(!Number.isFinite(zr+zi)||Math.hypot(zr,zi)>1e18){valid=n;break}}return{cr,ci,re,im,hiRe,hiIm,loRe,loIm,valid}}
function mPerturbEscape(cRe,cIm,ref,maxIter){if(ref.valid<maxIter)return{cls:'reference-end',iter:ref.valid};let wr=f(0),wi=f(0),dr=f(cRe-ref.cr),di=f(cIm-ref.ci);for(let n=0;n<maxIter;n++){const zr=ref.hiRe[n]+ref.loRe[n]+wr,zi=ref.hiIm[n]+ref.loIm[n]+wi,mag=zr*zr+zi*zi;if(mag>4)return{cls:'escaped',iter:n};const [lr,li]=mFMul(ref.hiRe[n],ref.hiIm[n],wr,wi),[lr2,li2]=mFMul(ref.loRe[n],ref.loIm[n],wr,wi),[sr,si]=mFMul(wr,wi,wr,wi);wr=f(f(2*f(lr+lr2))+sr+dr);wi=f(f(2*f(li+li2))+si+di)}const n=maxIter,zr=ref.hiRe[n]+ref.loRe[n]+wr,zi=ref.hiIm[n]+ref.loIm[n]+wi;return zr*zr+zi*zi>4?{cls:'escaped',iter:n}:{cls:'limit',iter:maxIter}}
function perturbGrid(view,w,h,maxIter,grid){const refs=[],seen=new Set(),add=(x,y)=>{const key=`${x.toFixed(6)}:${y.toFixed(6)}`;if(seen.has(key))return;seen.add(key);const [cr,ci]=coord(view,x,y,w,h);refs.push({...buildReference(cr,ci,maxIter),x,y})};add((w-1)/2,(h-1)/2);if(grid>1)for(let gy=0;gy<grid;gy++)for(let gx=0;gx<grid;gx++)add((gx+.5)*w/grid-.5,(gy+.5)*h/grid-.5);let mismatchClass=0,mismatchIter=0,refEnd=0,total=0;for(let y=0;y<h;y++)for(let x=0;x<w;x++){const [cr,ci]=coord(view,x,y,w,h),truth=iterate(cr,ci,maxIter);if(truth.cls==='analytic')continue;let ref=null,best=Infinity;for(const r of refs){if(r.valid<maxIter)continue;const d=(x-r.x)**2+(y-r.y)**2;if(d<best){best=d;ref=r}}if(!ref){refEnd++;continue}const p=mPerturbEscape(cr,ci,ref,maxIter);total++;const tc=truth.cls==='escaped'?'escaped':'limit';if(p.cls!==tc)mismatchClass++;else if(tc==='escaped'&&Math.abs(p.iter-truth.iter)>1)mismatchIter++}return{grid,references:refs.length,total,refEnd,mismatchClass,mismatchIter,mismatchRate:mismatchClass/Math.max(1,total)}}
function multiReferenceBench(){const w=64,h=40,maxIter=1200,views=[{name:'seahorse-1e-5',re:-.743643887037151,im:.13182590420533,span:1e-5},{name:'seahorse-1e-7',re:-.743643887037151,im:.13182590420533,span:1e-7}],rows=[];for(const v of views)for(const grid of [1,2,3])rows.push({view:v.name,...perturbGrid(v,w,h,maxIter,grid)});return{maxGuidedPasses:(html.match(/FAILURE_GUIDED_REF_MAX_PASSES=(\d+)/)||[])[1]|0,w,h,maxIter,rows,note:'CPU f32 perturbation locality surrogate. Production selects up to three additional references from failure clusters instead of fixed grids.'}}
function makeNodeWorkerSource(){return `const {parentPort}=require('node:worker_threads');globalThis.self=globalThis;globalThis.postMessage=(m,t)=>parentPort.postMessage(m,t);${internal.cpuFallbackWorkerSource()}\nparentPort.on('message',d=>self.onmessage({data:d}));`}
async function cpuFallbackBench(){const source=makeNodeWorkerSource(),w=640,h=360,iter=350,threads=Math.min(4,Math.max(2,os.cpus().length));const payload=(y0,y1,id=1)=>({type:'render',id,w,h,y0,y1,maxIter:iter,re:-.5,im:0,span:3.4,style:{palette:0,cycle:1.0,shift:0}});async function run(count){const workers=Array.from({length:count},()=>new Worker(source,{eval:true}));const once=()=>Promise.all(workers.map((wk,i)=>new Promise((resolve,reject)=>{wk.once('message',resolve);wk.once('error',reject);wk.postMessage(payload(Math.floor(h*i/count),Math.floor(h*(i+1)/count),i+1))})));await once();const times=[];let parts;for(let k=0;k<7;k++){const t0=performance.now();parts=await once();times.push(performance.now()-t0)}await Promise.all(workers.map(w=>w.terminate()));return{medianMs:median(times),times,work:parts.reduce((s,p)=>s+p.work,0),escaped:parts.reduce((s,p)=>s+p.escaped,0),operationLimit:parts.reduce((s,p)=>s+p.operationLimit,0),heuristicInterior:parts.reduce((s,p)=>s+p.heuristicInterior,0)}}const one=await run(1),multi=await run(threads);return{w,h,iter,threads,one,multi,speedup:one.medianMs/multi.medianMs,workMatch:one.work===multi.work,escapedMatch:one.escaped===multi.escaped,operationLimitMatch:one.operationLimit===multi.operationLimit,heuristicMatch:one.heuristicInterior===multi.heuristicInterior,note:'Production CPU fallback Worker source executed under node:worker_threads shim.'}}
const results={date:new Date().toISOString(),periodicity:periodicityBench(),series:seriesBench(),sparseActiveState:sparseMemoryBench(),multiReference:multiReferenceBench(),cpuFallback:await cpuFallbackBench(),sourceAudit:{heuristicClass:/FIELD_INTERIOR_HEURISTIC:u32=2u/.test(html),strictDisablesPeriodicity:/candPeriod==0u && n>=64u/.test(html)&&!/cycleReady/.test(kernels.FAST_PERTURB_WGSL),seriesBinding:/binding\(5\).*series:SeriesData/s.test(html),compactPixelMap:/deepActivePixel/.test(html)&&/pixelMap/.test(kernels.DEEP_ACTIVE_CONTINUE_WGSL),multiReferencePasses:/FAILURE_GUIDED_REF_MAX_PASSES=3/.test(html),cpuFallback:/class CpuFallbackRenderer/.test(html)},environment:{node:process.version,cpus:os.cpus().length,platform:process.platform,arch:process.arch}};
console.log(JSON.stringify(results,null,2));
if(process.argv[2])await fs.writeFile(process.argv[2],JSON.stringify(results,null,2)+'\n');

1276
index.html

File diff suppressed because it is too large Load diff

34
tests/README.md Normal file
View file

@ -0,0 +1,34 @@
# Regression tests
通常の回帰確認:
```bash
node tests/regression.mjs
```
`regression.mjs` は **production `index.html` を唯一のsource of truthとして直接読み込みます**。恒久的なtest用HTMLコピーはありません。
高速スイートで固定する項目:
- production JavaScript構文
- 19本の現行WGSL kernel SHA-256
- direct / FAST routing と有限continuation budget
- operation-limitが数値failureやmembership証明として扱われないこと
- 12点adaptive initialIter probe
- 12→必要時+12 operation-limit scoring(winner 1本だけbuild)
- exact interior certificate / integer-grid numeric history reuse
- guided/deep GPU-resident referenceのnearby reuse参加
- idle Workerの確実なterminate/revoke
- correction lazy compileが実使用5 pipelineだけであること
- cancelRenderのexport取消・generation更新
- export metadata共通builder
- reference / FAST reference / precision fallback Workerの小規模数値回帰
- 削除済みlegacy/dead subsystem・write-only stateがbundleへ再混入しないこと
実ブラウザ/WebGPU E2Eが必要な場合のみ:
```bash
node tests/browser_smoke.mjs 9333
```
`browser_smoke.mjs` はproduction `index.html`から一時的な診断HTMLを生成し、終了時に削除します。テスト用アプリ本体のコピーは保持しません。

View file

@ -1,43 +0,0 @@
// Compare only active-continuation dispatch depth at the same high resolution.
// node tests/active_work_plan.mjs [port=9333] [work|shards]
import assert from 'node:assert/strict';
import {readFile,writeFile,mkdir,rm} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import {setTimeout as sleep} from 'node:timers/promises';
const root=new URL('../',import.meta.url),directory=new URL('.test-edge/active-work/',root),source=await readFile(new URL('index.html',root),'utf8');
const hash=x=>createHash('sha256').update(x).digest('hex');
const mode=process.argv[3]||'work';assert.ok(['work','shards'].includes(mode));
let base=source;
for(const v of ['zr','zi']){const old=`${v}2=round(${v}*${v});`;assert.equal(base.split(old).length,2);base=base.replace(old,`${v}2=(${v}*${v}+half)>>B;`)}
base=base.replace('high:{scale:1.5,pixels:2359296,iterations:32768}','high:{scale:2,pixels:4194304,iterations:32768}');
const point='const elapsed=performance.now()-started,work=before*chunk;';assert.equal(base.split(point).length,2);
base=base.replace(point,point+'(globalThis.__activeTimes ||= []).push({elapsed,work,chunk,before,passes,after:session.active});');
const dispatch='const chunk=Math.min(256,remaining,Math.floor(PIXEL_FRONTIER_WORK/before));';assert.equal(base.split(dispatch).length,2);
const splitPoint='const start=session.progress,survivors=[],d=this.device;';assert.equal(base.split(splitPoint).length,2);
const splitCode='const bounded=[];for(const group of groups)for(let i=0;i<group.length;i+=65536)bounded.push(group.slice(i,i+65536));groups.splice(0,groups.length,...bounded);';
const variants=mode==='shards'?{work4:base,shards64:base.replace(splitPoint,splitCode+splitPoint)}:{work4:base,work8:base.replace(dispatch,'const chunk=Math.min(256,remaining,Math.floor(8000000/before));')};
const variantIds=Object.keys(variants),dataPath=mode==='shards'?'docs/NEXT_SHARD_DATA.json':'docs/NEXT_ACTIVE_WORK_DATA.json';
await mkdir(directory,{recursive:true});for(const [id,html] of Object.entries(variants))await writeFile(new URL(id+'.html',directory),html);
const pages=(await(await fetch(`http://127.0.0.1:${Number(process.argv[2]||9333)}/json/list`)).json()).filter(p=>p.type==='page');assert.equal(pages.length,1);
const ws=new WebSocket(pages[0].webSocketDebuggerUrl);await new Promise(r=>ws.addEventListener('open',r,{once:true}));let serial=0;const jobs=new Map();
ws.addEventListener('message',e=>{const m=JSON.parse(e.data),j=jobs.get(m.id);if(j){jobs.delete(m.id);clearTimeout(j.timer);m.error?j.reject(new Error(JSON.stringify(m.error))):j.resolve(m.result)}});
function command(method,params={}){return new Promise((resolve,reject)=>{const id=++serial,timer=setTimeout(()=>reject(new Error(method+' timeout')),240000);jobs.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}))})}
async function evaluate(expression){const r=await command('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(r.exceptionDetails)throw new Error(JSON.stringify(r.exceptionDetails));return r.result.value}
const cases=[{id:'reset',re:'-0.5',im:'0',span:'3.4'},{id:'mid',re:'-0.743643887037151',im:'0.13182590420533',span:'1e-6'}],data={date:new Date().toISOString(),sourceSha256:hash(source),variants:Object.fromEntries(Object.entries(variants).map(([id,html])=>[id,hash(html)])),viewport:{width:320,height:240,deviceScaleFactor:1},quality:'high',cases,runs:[]},expected=new Map();
try{
await command('Emulation.setDeviceMetricsOverride',{...data.viewport,mobile:false});
for(let round=-1;round<3;round++)for(const variant of round===1?[...variantIds].reverse():variantIds){
await command('Page.navigate',{url:new URL(variant+'.html?test',directory).href});
for(let i=0;i<200;i++){if(await evaluate('!!globalThis.__MANDEL_TEST__'))break;await sleep(50)}await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
if(!data.environment)data.environment=await evaluate('({userAgent:navigator.userAgent,gpu:__MANDEL_TEST__.gpuDiagnostics()})');
for(const c of cases){
const result=await evaluate(`(async()=>{globalThis.__activeTimes=[];await __MANDEL_TEST__.setView(${JSON.stringify({...c,bits:448,baseIter:512,adaptive:false,quality:'high'})});const s=await __MANDEL_TEST__.waitForNumericComplete(),r=__MANDEL_TEST__.kernelAccess().renderer,field=await r.readFieldAll();let invalid=0;for(const m of field.meta)if((m&0xfffff)>32768||((m>>>28)===0&&((m>>>20)!==6||(m&0xfffff)!==32768)))invalid++;return{ms:s.lastRender,width:s.width,height:s.height,budget:s.continuationBudget,status:s.renderClock.status,failures:s.numericalFailures,invalid,hashes:await __MANDEL_TEST__.fieldHashes(),png:await __MANDEL_TEST__.pngRoundTrip(),batches:globalThis.__activeTimes,errors:__MANDEL_TEST__.gpuDiagnostics().uncapturedErrors}})()`);
assert.equal(result.width,640);assert.equal(result.height,480);assert.equal(result.budget,32768);assert.equal(result.status,'complete');assert.equal(result.failures,0);assert.equal(result.invalid,0);assert.equal(result.png.mismatches,0);assert.deepEqual(result.errors,[]);
assert.ok(result.batches.every(b=>b.work<=(variant==='work8'?8000000:4000000)));const h=JSON.stringify(result.hashes);if(expected.has(c.id))assert.equal(h,expected.get(c.id));else expected.set(c.id,h);
const times=result.batches.map(x=>x.elapsed).sort((a,b)=>a-b);result.batchCount=times.length;result.batchP95=times[Math.floor((times.length-1)*.95)];result.batchMax=times.at(-1);result.minChunk=Math.min(...result.batches.map(b=>b.chunk));
data.runs.push({round,variant,case:c.id,...result});await writeFile(new URL(dataPath,root),JSON.stringify(data,null,2)+'\n');
console.log(JSON.stringify({round,variant,case:c.id,ms:result.ms,batches:result.batchCount,p95:result.batchP95,max:result.batchMax}));
}
}
console.log('PASS: active scheduling preserves every field bit, finite target, PNG and measured work limit');
}finally{await command('Page.navigate',{url:'about:blank'}).catch(()=>{});ws.close();for(const id of Object.keys(variants))await rm(new URL(id+'.html',directory),{force:true})}

View file

@ -1,444 +1,68 @@
// Connect to a separate Chromium/Edge instance with remote debugging enabled.
// Usage: node tests/browser_smoke.mjs [port=9333] [reset|fast|mid|legacy|axis|black|ui|all]
// Optional real-browser WebGPU smoke test.
// Starts from the production index.html and injects a temporary test-only API.
// Usage: node tests/browser_smoke.mjs [port=9333]
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import {fileURLToPath,pathToFileURL} from 'node:url';
import {setTimeout as sleep} from 'node:timers/promises';
const port=Number(process.argv[2]||9333),selection=process.argv[3]||'all';
const url=new URL('../index.html?test',import.meta.url).href;
const port=Number(process.argv[2]||9333);
const prodUrl=new URL('../index.html',import.meta.url),generatedUrl=new URL('./.browser-smoke.generated.html',import.meta.url);
const prodHtml=await fs.readFile(prodUrl,'utf8');
const marker='// ── boot / teardown';
assert.equal(prodHtml.includes(marker),true,'boot marker missing');
const hook=String.raw`
function testDecimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function testFromDec(s,bits){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v}
async function waitForTestState(predicate,label,timeout=120000){const start=performance.now();for(;;){if(state.drawState==='ERROR')throw new Error(state.gpuError||'render failed');if(predicate())return true;if(performance.now()-start>=timeout)throw new Error(label+' timeout');if(state.dirty&&!state.rendering)schedule();await new Promise(resolve=>setTimeout(resolve,20))}}
globalThis.__MANDEL_TEST__={
async ensureGpuReady(){const r=renderer||await initRenderer();return{ready:!!r,navigatorGpu:!!navigator.gpu,renderer:!!renderer,gpuInitFailed:state.gpuInitFailed,gpuError:state.gpuError}},
async setView({re,im,span,bits,baseIter=350,adaptive=false,continuationBudget=0,quality=null,fixed=false}){const ready=await this.ensureGpuReady();if(!ready.ready)throw new Error('WebGPU initialization failed: '+JSON.stringify(ready));cancelRender();state.bits=bits||Math.max(256,testDecimalRequiredBits(re),testDecimalRequiredBits(im),testDecimalRequiredBits(span));state.re=fixed?BigInt(re):testFromDec(re,state.bits);state.im=fixed?BigInt(im):testFromDec(im,state.bits);state.span=fixed?BigInt(span):testFromDec(span,state.bits);state.baseIter=baseIter;state.adaptive=adaptive;state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(continuationBudget)));state.quality='standard';if(quality)applyQuality(quality);else if(!state.continuationBudget)applyQuality('standard');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 this.state()},
async waitForFinal({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&state.fieldView?.complete===true&&state.renderClock?.status==='complete','final',timeout);return this.state()},
async waitForNumericComplete({timeout=120000}={}){const result=await this.waitForFinal({timeout});if(result.numericalFailures)throw new Error('numeric recovery ended with '+result.numericalFailures+' unresolved failures');return result},
async sampleMeta(points){if(!renderer?.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);return Array.from(await renderer.readMeta(idx))},
state:()=>{const reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter(),effectiveIter:state.fieldView?.iter??maxIter(),continuationBudget:state.continuationBudget,quality:state.quality,rendering:state.rendering,renderClock:state.renderClock?{...state.renderClock}:null,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,finiteBudgetComplete:!!state.fieldView?.frontier?.finiteBudgetComplete,membershipCertified:!!state.fieldView?.membershipCertified,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version}},
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 smokeExportTile({w=48,h=32,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),fastExtended=fastNeedsExtended(snap,w),fastContext=fastExtended?await fastRefs.request(snap,iter,w,h):null,args={snap,iter,fastContext,fullW:w,fullH:h,tileX:0,tileY:0,w,h},result=ss===2?await renderer.renderTileRGBA2x(args):await renderer.renderTileRGBA({...args,sampleX:.5,sampleY:.5,edgeAA:false}),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,ss,unresolved:result.unresolved||0}}
};
`;
const generatedHtml=prodHtml.replace(marker,hook+'\n'+marker);
await fs.writeFile(generatedUrl,generatedHtml);
const viewerUrl=pathToFileURL(fileURLToPath(generatedUrl)).href;
let ws=null;
try{
const response=await fetch(`http://127.0.0.1:${port}/json/list`);
assert.ok(response.ok,'Remote debugging endpoint unavailable');
const pages=(await response.json()).filter(t=>t.type==='page');
assert.equal(pages.length,1,'Use one dedicated test tab');
const target=pages[0],ws=new WebSocket(target.webSocketDebuggerUrl);
ws=new WebSocket(pages[0].webSocketDebuggerUrl);
await new Promise((resolve,reject)=>{ws.addEventListener('open',resolve,{once:true});ws.addEventListener('error',reject,{once:true})});
let serial=0;const pending=new Map(),errors=[];
let serial=0;const pending=new Map(),exceptions=[];
ws.addEventListener('message',event=>{
const m=JSON.parse(event.data);
if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);
if(m.method==='Runtime.exceptionThrown')exceptions.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);
const p=pending.get(m.id);if(!p)return;pending.delete(m.id);clearTimeout(p.timer);
if(m.error)p.reject(new Error(JSON.stringify(m.error)));else p.resolve(m.result);
});
function command(method,params={}){
const id=++serial;
return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{pending.delete(id);reject(new Error(method+' timeout'))},240000);pending.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}))});
}
async function evaluate(expression){
const result=await command('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});
if(result.exceptionDetails)throw new Error(result.exceptionDetails.exception?.description||result.exceptionDetails.text);
return result.result.value;
}
const cases=[
{id:'reset',re:'-0.5',im:'0',span:'3.4',renderMode:'fast',backend:'direct'},
{id:'fast',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',renderMode:'fast',backend:'fast-extended'},
{id:'mid',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000001',renderMode:'fast',backend:'fast-extended'},
{id:'legacy',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',renderMode:'accurate',backend:'fast-extended'},
{id:'axis',re:'-0.75',im:'0',span:'0.02',renderMode:'fast',backend:'direct'},
{id:'black',re:'-29466147000382485924219538765424656674342940730553342389512209954887705938978413587',im:'5179018789925933872641942859702187326563010594882200888329237785122225251258352',span:'250637324516887125119843167990565159991251418774166207381437546036409',bits:273,baseIter:350,adaptive:true,fixed:true,renderMode:'accurate',backend:'fast-extended'}
];
assert.ok(['all','kernels','cancel','bench','budget','ui','palette','pan','quality'].includes(selection)||cases.some(c=>c.id===selection),'Unknown case');
// Execute the real production kernels on small, fully specified states.
// This isolates terminal and queue behavior from render convergence heuristics.
async function kernelCases(){
const {renderer:r}=globalThis.__MANDEL_TEST__.kernelAccess();
await r.ensureDeepActivePipelines();
const d=r.device,B=GPUBufferUsage,created=[],reports=[];
const check=(value,message)=>{if(!value)throw new Error(message)};
const buffer=(size,usage)=>{const b=d.createBuffer({size:Math.max(4,size),usage});created.push(b);return b};
async function read(b,bytes){const staging=buffer(bytes,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(b,0,staging,0,bytes);d.queue.submit([e.finish()]);await staging.mapAsync(GPUMapMode.READ);const out=staging.getMappedRange().slice(0);staging.unmap();return out}
async function run({count=1,target=3,chunk=1,passes=3,orbit=[0,0,0,0],error=0,m=0,n=0}){
const capacity=Math.max(1,count),params=new ArrayBuffer(96),p=new DataView(params);
[capacity,1,capacity,1,0,0,150000,orbit.length-1,0,0,capacity,0].forEach((v,i)=>p.setUint32(i*4,v,true));
p.setInt32(52,0,true);p.setUint32(84,target,true);p.setUint32(88,chunk,true);
const pb=buffer(96,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pb,0,params);
const refs=buffer(orbit.length*16,B.STORAGE|B.COPY_DST),refData=new Float32Array(orbit.length*4);orbit.forEach((v,i)=>refData[i*4]=v);d.queue.writeBuffer(refs,0,refData);
const states=buffer(capacity*32,B.STORAGE|B.COPY_DST|B.COPY_SRC),initial=new ArrayBuffer(capacity*32),sv=new DataView(initial);
for(let i=0;i<count;i++){sv.setUint32(i*32+20,n,true);sv.setUint32(i*32+24,m,true);sv.setFloat32(i*32+28,error,true)}d.queue.writeBuffer(states,0,initial);
const queues=[0,1].map(()=>buffer(capacity*4,B.STORAGE|B.COPY_DST|B.COPY_SRC)),counts=[0,1].map(()=>buffer(4,B.STORAGE|B.COPY_DST|B.COPY_SRC)),indirect=buffer(12,B.STORAGE|B.INDIRECT|B.COPY_DST);
if(count)d.queue.writeBuffer(queues[0],0,Uint32Array.from({length:count},(_,i)=>i));d.queue.writeBuffer(counts[0],0,Uint32Array.of(count));
const meta=buffer(capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buffer(capacity*4,B.STORAGE|B.COPY_SRC);
const e=d.createCommandEncoder();let input=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB:states,queues,counts,indirect,meta,smooth,input:0,passes,refsB:refs});d.queue.submit([e.finish()]);
const active=new Uint32Array(await read(counts[input],4))[0],field=new Uint32Array(await read(meta,capacity*4));
const live=active?Array.from(new Uint32Array(await read(queues[input],active*4))).sort((a,b)=>a-b):[];
return{active,field,live,states:new DataView(await read(states,capacity*32)),async resume(nextTarget,nextPasses){p.setUint32(84,nextTarget,true);d.queue.writeBuffer(pb,0,params);const e=d.createCommandEncoder();input=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB:states,queues,counts,indirect,meta,smooth,input,passes:nextPasses,refsB:refs});d.queue.submit([e.finish()]);return{active:new Uint32Array(await read(counts[input],4))[0],field:new Uint32Array(await read(meta,capacity*4))}}};
}
try{
let x=await run({orbit:[0,1,2,5]});check(x.active===0&&x.field[0]===(1<<28|3),'escape exactly at target');
x=await run({target:1,n:1,m:1,passes:1,error:.002});check(x.active===0&&x.field[0]===(1<<20|1),'terminal error guard');
x=await run({target:2,passes:2,orbit:[0]});check(x.active===0&&x.field[0]===(3<<20),'reference exhaustion');
x=await run({target:1,passes:1,orbit:[0],m:1});check(x.active===0&&x.field[0]===(3<<20),'reference index guard');
for(const count of [0,1,63,64,65])for(const passes of [1,2,3,4]){
const target=passes;x=await run({count,target,passes,orbit:Array(10).fill(0)});
check(x.active===count,'queue count');check(x.live.every((v,i)=>v===i),'queue membership');
for(let i=0;i<count;i++){check(x.field[i]===(6<<20|target),'target metadata');check(x.states.getUint32(i*32+20,true)===target,'retained iteration')}
const next=await x.resume(target+1,1);check(next.active===count,'target resubmission');
for(let i=0;i<count;i++)check(next.field[i]===(6<<20|target+1),'next target metadata');
}
reports.push('target escape/error/reference bounds; 0/1/63/64/65 queues; 1–4 passes; retained states');
await r.ensureCorrectionPipelines();await r.ensureCompactActivePipelines();
{
const width=17,height=5,n=width*height,initial=Uint32Array.from({length:n},(_,i)=>i%7===6?1<<28:(i%7)<<20|i);
const p=new Uint32Array(24);p.set([9,3,width,height,3,1,100,0,0,2,width,width+3]);
const pb=buffer(96,B.UNIFORM|B.COPY_DST),meta=buffer(n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),buckets=buffer(96,B.STORAGE|B.COPY_DST),stats=buffer(32,B.STORAGE|B.COPY_SRC|B.COPY_DST),queue=buffer(n*4,B.STORAGE|B.COPY_SRC),indirect=buffer(16,B.STORAGE|B.INDIRECT);
d.queue.writeBuffer(pb,0,p);d.queue.writeBuffer(meta,0,initial);const e=d.createCommandEncoder();
r.encodeDeepBucketHistogram(e,{pbuf:pb,meta,bucketState:buckets,w:9,h:3});r.encodeDeepBucketPrefix(e,{bucketState:buckets,queueStats:stats,indirect});r.encodeDeepBucketScatter(e,{pbuf:pb,meta,bucketState:buckets,queueStats:stats,queue,w:9,h:3});d.queue.submit([e.finish()]);
const q=new Uint32Array(await read(stats,32)),expected=[];
for(let y=1;y<4;y++)for(let x=3;x<12;x++){const i=y*width+x;if(i%7>=1&&i%7<=5)expected.push(i)}
check(q[0]===expected.length&&q[1]===0&&q[2]===expected.length,'tile queue counts');
const actual=Array.from(new Uint32Array(await read(queue,q[0]*4))).sort((a,b)=>a-b);check(JSON.stringify(actual)===JSON.stringify(expected),'tile queue exact membership');
if(!r.precisionScatter)r.precisionScatter=await r.makeCompute(await r.module('precision-scatter-test',globalThis.MANDEL_WEBGPU_KERNELS.PRECISION_SCATTER_WGSL));
const results=new Uint32Array(n*4);for(let i=0;i<n;i++)results.set([i,1<<28|7,0,i%2],i*4);
const input=buffer(results.byteLength,B.STORAGE|B.COPY_DST),sm=buffer(n*4,B.STORAGE);d.queue.writeBuffer(input,0,results);
const enc=d.createCommandEncoder(),bg=d.createBindGroup({layout:r.precisionScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:input}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:sm}}]}),pass=enc.beginComputePass();pass.setPipeline(r.precisionScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(2);pass.end();d.queue.submit([enc.finish()]);
const applied=new Uint32Array(await read(meta,n*4));for(let i=0;i<n;i++)check(applied[i]===(i%2&&i%7>=1&&i%7<=5?(1<<28|7):initial[i]),'scatter preserves completed/operation-limit pixels');
}
{
const pb=buffer(96,B.UNIFORM|B.COPY_DST),p=new Uint32Array(24);p.set([9,1,9,1,0,0,150000,4,0,3,9,0]);p[21]=3;p[22]=1;d.queue.writeBuffer(pb,0,p);
const stateB=buffer(3*40,B.STORAGE|B.COPY_SRC),queues=[0,1].map(()=>buffer(12,B.STORAGE|B.COPY_SRC|B.COPY_DST)),counts=[0,1].map(()=>buffer(4,B.STORAGE|B.COPY_SRC|B.COPY_DST)),indirect=buffer(12,B.STORAGE|B.INDIRECT),refsB=buffer(5*16,B.STORAGE),meta=buffer(9*4,B.STORAGE|B.COPY_SRC),sm=buffer(9*4,B.STORAGE);
d.queue.writeBuffer(queues[0],0,Uint32Array.of(8,2,5));d.queue.writeBuffer(counts[0],0,Uint32Array.of(3));
const e=d.createCommandEncoder();r.encodeDeepActiveResumeInit(e,{pbuf:pb,stateB,queue:queues[0],count:3,pipeline:r.deepCompactInit});
const output=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB,queues,counts,indirect,meta,smooth:sm,input:0,passes:3,refsB,pipeline:r.deepCompactContinue});d.queue.submit([e.finish()]);
const field=new Uint32Array(await read(meta,36)),states=new DataView(await read(stateB,120));
for(let i=0;i<9;i++)check(field[i]===([8,2,5].includes(i)?6<<20|3:0),'compact pixel mapping');
for(let i=0;i<3;i++){check(states.getUint32(i*40+32,true)===[8,2,5][i],'compact slot identity');check(states.getUint32(i*40+20,true)===3,'compact state retained')}
check(new Uint32Array(await read(counts[output],4))[0]===3,'compact queue retained');
}
reports.push('tile failure queue exact membership; scatter ownership; compact slot identity');
{
const width=17,height=5,n=width*height,whole=buffer(n*4,B.STORAGE|B.COPY_SRC),pieces=buffer(n*4,B.STORAGE|B.COPY_SRC),sm=buffer(n*4,B.STORAGE);
const unit=1n<<256n,snap={bits:256,re:-unit/2n,im:0n,span:34n*unit/10n},e=d.createCommandEncoder();
const encode=(meta,x,y,w,h)=>{const pb=buffer(96,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pb,0,r.directParams(w,h,width,height,x,y,100,snap,.5,.5,0,width,y*width+x));r.encodeDirectNumeric(e,{pbuf:pb,meta,smooth:sm,w,h})};
encode(whole,0,0,width,height);
for(let y=0;y<height;y+=2)for(let x=0;x<width;x+=6)encode(pieces,x,y,Math.min(6,width-x),Math.min(2,height-y));
d.queue.submit([e.finish()]);const expected=new Uint32Array(await read(whole,n*4)),actual=new Uint32Array(await read(pieces,n*4));
check(expected.every((v,i)=>v!==0&&v===actual[i]),'direct tiles reproduce whole-frame coordinates');
}
reports.push('direct tile coordinates and full pixel coverage');
{
// Real GPU scatters must preserve terminal pixels and defer aggregation
// until the consumer asks, without leaking pending work to another frame.
const originalFrame=r.frame,originalStats=r.encodeUnknownStats,fixtures=[];let aggregates=0;
const fixture=()=>{const f={w:5,h:1,n:5,meta:buffer(20,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth:buffer(20,B.STORAGE|B.COPY_SRC|B.COPY_DST),unresolved:buffer(32,B.STORAGE|B.COPY_SRC|B.COPY_DST),statsParams:buffer(16,B.UNIFORM|B.COPY_DST),readbackRings:{}};fixtures.push(f);return f};
const first=fixture(),second=fixture(),token=__MANDEL_TEST__.kernelAccess().token;
const result=indices=>({indices:Uint32Array.from(indices),meta:new Uint32Array(indices.length).fill(1<<28|9),smooth:new Float32Array(indices.length).fill(8.5),accepted:new Uint8Array(indices.length).fill(1)});
try{
r.frame=first;d.queue.writeBuffer(first.meta,0,Uint32Array.of(1<<20|10,2<<20|10,6<<20|20,1<<28|3,1<<20|10));
r.encodeUnknownStats=function(...args){aggregates++;return originalStats.apply(this,args)};
await r.applyPrecisionFallback(result([0,2,3]),token);
await r.applyPrecisionFallback(result([1]),token);
check(aggregates===0,'scatter batches must not scan the whole frame');
let stats=await r.readUnresolvedStats();
check(aggregates===1&&stats.total===2&&stats.reasons.errorBound===1&&stats.reasons.escapeUncertain===0&&stats.reasons.operationLimit===1,'one fresh aggregation after both scatters');
const field=new Uint32Array(await read(first.meta,20));
check(field[0]===(1<<28|9)&&field[1]===(1<<28|9)&&field[2]===(6<<20|20)&&field[3]===(1<<28|3),'deferred statistics preserve terminal metadata');
await r.readUnresolvedStats();check(aggregates===1,'unchanged field reuses statistics');
check(await r.applyPrecisionFallback(result([4]),token-1)===null,'cancelled generation cannot apply a repair');
check(!first.precisionStatsDirty,'rejected repair cannot invalidate statistics');
await r.applyPrecisionFallback(result([4]),token);
r.frame=second;d.queue.writeBuffer(second.meta,0,new Uint32Array(5).fill(1<<28|3));
stats=await r.readUnresolvedStats();check(stats.total===0&&aggregates===1,'frame replacement does not inherit pending statistics');
r.frame=first;stats=await r.readUnresolvedStats();
check(stats.total===1&&stats.reasons.operationLimit===1&&aggregates===2,'pending original frame remains independently refreshable');
}finally{
r.frame=originalFrame;r.encodeUnknownStats=originalStats;r.clearBindGroupCache();
for(const f of fixtures)for(const ring of Object.values(f.readbackRings))for(const b of ring.buffers)created.push(b);
}
reports.push('deferred repair statistics: batching, exact totals, terminal ownership, stale token, frame isolation');
}
return reports;
}finally{await d.queue.onSubmittedWorkDone();for(const b of created)b.destroy()}
}
// Read the actual texture selected by presentation, including animation during
// numeric work. UI text alone cannot prove that colors are changing.
async function displayedColorHash(){
const r=globalThis.__MANDEL_TEST__.kernelAccess().renderer,f=r.frame,d=r.device;
const texture=r.colorSourceVisible?r.colorSource.texture:f.front;
const pitch=Math.ceil(f.w*4/256)*256,b=d.createBuffer({size:pitch*f.h,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ});
try{
const e=d.createCommandEncoder();e.copyTextureToBuffer({texture},{buffer:b,bytesPerRow:pitch},[f.w,f.h]);d.queue.submit([e.finish()]);
await b.mapAsync(GPUMapMode.READ);const data=new Uint8Array(b.getMappedRange()),pixels=new Uint8Array(f.n*4);
for(let y=0;y<f.h;y++)pixels.set(data.subarray(y*pitch,y*pitch+f.w*4),y*f.w*4);
const digest=await crypto.subtle.digest('SHA-256',pixels);b.unmap();return Array.from(new Uint8Array(digest),x=>x.toString(16).padStart(2,'0')).join('');
}finally{b.destroy()}
}
try{
await command('Runtime.enable');
await command('Emulation.setDeviceMetricsOverride',{width:320,height:240,deviceScaleFactor:1,mobile:false});
await command('Page.navigate',{url});
for(let i=0;i<100;i++){if(await evaluate('!!globalThis.__MANDEL_TEST__'))break;await sleep(100)}
const ready=await evaluate('globalThis.__MANDEL_TEST__.ensureGpuReady()');
assert.equal(ready.ready,true,JSON.stringify(ready));
if(selection==='quality'||selection==='all'){
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
assert.deepEqual(await evaluate('Array.from(document.querySelector("#quality").options,o=>o.textContent)'),['高速','標準','高画質']);
assert.equal(await evaluate('!!document.querySelector("#processMode, #continuationBudget")'),false);
const reports=[];
for(const scenario of [cases[0],cases[1],cases[2]]){
// The mid view contains many surviving pixels; this exercised unfinished
// high-quality frames in the old automatic/Strict policies.
await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...scenario,bits:448,baseIter:512,adaptive:false,quality:'fast'})})`);
for(const [quality,width,height,budget] of [['fast',240,180,4096],['standard',320,240,16384],['high',480,360,32768]]){
const result=await evaluate(`(async()=>{const e=document.querySelector('#quality');e.value='${quality}';e.dispatchEvent(new Event('change'));const s=await __MANDEL_TEST__.waitForNumericComplete({timeout:180000});const r=__MANDEL_TEST__.kernelAccess().renderer,field=await r.readFieldAll();let invalid=0;for(const p of field.meta){if((p&0xfffff)>${budget})invalid++;if((p>>>28)===0&&((p>>>20)!==6||(p&0xfffff)!==${budget}))invalid++}return{s,invalid,front:r.frontColorKey,png:await __MANDEL_TEST__.pngRoundTrip()}})()`);
assert.equal(result.s.width,width);assert.equal(result.s.height,height);assert.equal(result.s.continuationBudget,budget);
assert.equal(result.s.numericalFailures,0);assert.equal(result.s.provisional,false);assert.equal(result.s.renderClock.status,'complete');assert.equal(result.invalid,0);
assert.equal(result.front.split(':')[3],'0','no neighborhood blur');assert.equal(result.png.mismatches,0);
reports.push({case:scenario.id,quality,width,height,budget,ms:result.s.lastRender});
console.log(JSON.stringify({quality:reports.at(-1)}));
}
}
// New URL/local preferences restore the quality and its resolution.
await command('Page.reload');await sleep(150);await evaluate('__MANDEL_TEST__.waitForNumericComplete({timeout:180000})');
assert.equal(await evaluate('__MANDEL_TEST__.state().quality'),'high');
assert.equal(await evaluate('document.querySelector("#quality").value'),'high');
assert.equal(await evaluate('new URLSearchParams(location.hash.slice(1)).get("q")'),'high');
// Cancel a high-resolution calculation via the actual selector.
await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[0],bits:448,baseIter:512,adaptive:false,quality:'fast'})})`);
const work=evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false,quality:'high'})})`).then(()=>true,()=>false);
for(let i=0;i<500;i++){if(await evaluate('__MANDEL_TEST__.state().rendering'))break;await sleep(10)}
await evaluate('const e=document.querySelector("#quality");e.value="fast";e.dispatchEvent(new Event("change"))');
await work;const final=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
assert.equal(final.quality,'fast');assert.equal(final.width,240);assert.equal(final.continuationBudget,4096);
console.log('PASS: three quality presets, real supersampling, finite completion, PNG, URL reload, cancellation');
}
if(selection==='pan'||selection==='all'){
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
await evaluate('if(!document.body.classList.contains("ui-hidden"))document.querySelector("#uiToggle").click()');
const reports=[];let expected;
for(const mode of ['off','on','slow-color']){
await evaluate('if(__MANDEL_TEST__.state().colorAuto)document.querySelector("#colorAuto").click()');
await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[0],bits:448,baseIter:512,adaptive:false})})`);
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
if(mode==='slow-color')await evaluate('const r=__MANDEL_TEST__.kernelAccess().renderer;globalThis.originalRecolor=r.recolor;r.recolor=async function(...args){await new Promise(done=>setTimeout(done,100));return originalRecolor.apply(this,args)}');
if(mode!=='off'){
await evaluate('document.querySelector("#colorAuto").click()');
if(mode==='slow-color')for(let i=0;i<100;i++){if(await evaluate('__MANDEL_TEST__.state().recoloring'))break;await sleep(5)}
}
const start=performance.now();
await command('Input.dispatchMouseEvent',{type:'mousePressed',x:160,y:120,button:'left',buttons:1,clickCount:1});
await command('Input.dispatchMouseEvent',{type:'mouseMoved',x:176,y:120,button:'left',buttons:1});
await command('Input.dispatchMouseEvent',{type:'mouseReleased',x:176,y:120,button:'left',buttons:0,clickCount:1});
await sleep(130);
const final=await evaluate('__MANDEL_TEST__.waitForNumericComplete({timeout:10000})');
const elapsed=performance.now()-start,hash=await evaluate('__MANDEL_TEST__.fieldHashes()');
if(expected)assert.deepEqual(hash,expected,'animated pan must calculate the same view');expected=hash;
if(mode!=='off')assert.equal(final.colorAuto,true,'animation stays enabled');
if(mode==='slow-color')await evaluate('__MANDEL_TEST__.kernelAccess().renderer.recolor=originalRecolor;delete globalThis.originalRecolor');
reports.push({mode,elapsedMs:elapsed,renderMs:final.lastRender});
}
assert.ok(reports[1].elapsedMs<reports[0].elapsedMs*2.5+750,'animation must not starve pan');
assert.ok(reports[2].elapsedMs<reports[0].elapsedMs*2.5+1000,'slow color producer must yield to pan');
const bounded=await evaluate('(async()=>{const r=__MANDEL_TEST__.kernelAccess().renderer;await r.device.queue.onSubmittedWorkDone();return Array.from({length:20},()=>r.recolorPublished())})()');
assert.equal(bounded.filter(Boolean).length,1,'at most one published color pass in flight');
await evaluate('document.querySelector("#colorAuto").click()');
console.log(JSON.stringify({pan:reports}));
}
if(selection==='palette'||selection==='all'){
// Earlier numeric diagnostics may use a raw automatic budget that the
// production quality URL cannot encode. Start this UI test with a preset.
await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[0],bits:448,baseIter:512,adaptive:false,quality:'standard'})})`);
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
const options=await evaluate('Array.from(document.querySelector("#palette").options,o=>({id:Number(o.value),name:o.textContent}))');
assert.deepEqual(options.map(o=>o.id),[0,1,2,3,5,7,10,13,15,19]);assert.equal(options.at(-1).name,'ピンクと黒');
const before=await evaluate('({s:__MANDEL_TEST__.state()})');
const hashes=await evaluate('__MANDEL_TEST__.fieldHashes()');
const oldColor=await evaluate(`(${displayedColorHash.toString()})()`);
const latency=await evaluate(`(async()=>{const t=performance.now(),e=document.querySelector('#palette');e.value='19';e.dispatchEvent(new Event('change'));while(__MANDEL_TEST__.state().recolorPending||__MANDEL_TEST__.state().recoloring)await new Promise(r=>setTimeout(r,5));return performance.now()-t})()`);
assert.ok(latency<1000,'completed-frame recolor should not take seconds');
assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),oldColor);
// Exercise resize events and color-only URL history independently of the
// dropdown's replaceState. None may start Mandelbrot computation.
await evaluate('{dispatchEvent(new Event("resize"));const p=new URLSearchParams(location.hash.slice(1));p.set("pal","1");location.hash=p.toString()}');
await sleep(150);
const after=await evaluate('__MANDEL_TEST__.state()');
assert.equal(after.palette,1);assert.equal(after.numericSubmits,before.s.numericSubmits);assert.equal(after.correctionSubmits,before.s.correctionSubmits);
assert.equal(after.renderClock.token,before.s.renderClock.token);assert.equal(after.lastRender,before.s.lastRender);
assert.deepEqual(await evaluate('__MANDEL_TEST__.fieldHashes()'),hashes);
// With animation OFF, change palette during real CPU repair, before the
// ongoing numerical render completes. Read the texture actually displayed.
const work=evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false})})`);
let active=false;
for(let i=0;i<1000;i++){const s=await evaluate('__MANDEL_TEST__.state()');if(s.rendering&&s.provisional&&s.engine.includes('数値補修')){active=true;break}await sleep(20)}
assert.ok(active);
const prior=await evaluate(`(${displayedColorHash.toString()})()`);
const duringMs=await evaluate(`(async()=>{const start=performance.now(),e=document.querySelector('#palette');e.value='19';e.dispatchEvent(new Event('change'));while(!__MANDEL_TEST__.kernelAccess().renderer.colorSourceVisible)await new Promise(r=>setTimeout(r,5));await __MANDEL_TEST__.kernelAccess().renderer.device.queue.onSubmittedWorkDone();return performance.now()-start})()`);
assert.ok(duringMs<1000,'palette must not wait for numeric completion');
const during=await evaluate('__MANDEL_TEST__.state()');assert.equal(during.colorAuto,false);assert.equal(during.rendering,true);
assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),prior);
await work;await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
for(let i=0;i<100;i++){if(await evaluate('!__MANDEL_TEST__.state().recolorPending&&!__MANDEL_TEST__.state().recoloring'))break;await sleep(20)}
assert.equal(await evaluate('__MANDEL_TEST__.kernelAccess().renderer.frontColorKey.split(":")[0]'),'19');
assert.equal(await evaluate('!!__MANDEL_TEST__.kernelAccess().renderer.colorSource'),false);
const completeHashes=await evaluate('__MANDEL_TEST__.fieldHashes()');
assert.equal(completeHashes.metaSha256,'78d0fbfd138de5eccb0308ddd4bdca0839c29cbd6795d85c9d016a54dcc63080');
assert.equal(completeHashes.smoothSha256,'b22cb1a919b52c6fe5d38a7e1c851bf310821aea2c420b999bdb34b07fd5dc5e');
assert.equal((await evaluate('__MANDEL_TEST__.pngRoundTrip()')).mismatches,0);
console.log(JSON.stringify({palette:'PASS: pixels, no numeric rerender, color-only URL, resize, recolor during repair, PNG',latencyMs:latency,duringMs}));
}
if(selection==='ui'||selection==='all'){
const initial=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
assert.equal(initial.renderClock.status,'complete');
assert.ok(initial.lastRender>=initial.previewMs+initial.refineMs);
if(initial.previewMs)assert.ok(initial.lastRender>=initial.previewMs+initial.refineMs+100,'include refinement delay');
assert.equal(await evaluate('!!document.querySelector("#renderMode")'),false);
const original=await evaluate('__MANDEL_TEST__.fieldHashes()');
const originalColor=await evaluate(`(${displayedColorHash.toString()})()`);
await evaluate('document.querySelector("#colorAuto").click()');
await sleep(350);
assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),originalColor,'animation changes GPU pixels');
assert.deepEqual(await evaluate('__MANDEL_TEST__.fieldHashes()'),original,'animation preserves numeric data');
const idle=await evaluate('__MANDEL_TEST__.state()');
assert.equal(idle.cycle,initial.cycle);assert.equal(idle.lastRender,initial.lastRender);
await evaluate('document.querySelector("#colorAuto").click()');
await sleep(150);
const stopped=await evaluate('__MANDEL_TEST__.state().shift');await sleep(150);
assert.equal(await evaluate('__MANDEL_TEST__.state().shift'),stopped);
assert.equal(await evaluate('!!__MANDEL_TEST__.kernelAccess().renderer.colorSource'),false,'release animation memory');
await evaluate('document.querySelector("#colorAuto").click()');
const ongoing=evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false})})`);
let active=false;
for(let i=0;i<1000;i++){
const state=await evaluate('__MANDEL_TEST__.state()');
if(state.rendering&&state.provisional&&state.engine.includes('数値補修')){active=true;break}
await sleep(20);
}
assert.ok(active,'animate during expensive repair');
const before=await evaluate(`(${displayedColorHash.toString()})()`);
await sleep(350);
assert.equal(await evaluate('__MANDEL_TEST__.state().rendering'),true);
assert.equal(await evaluate('!!__MANDEL_TEST__.kernelAccess().renderer.colorSourceVisible'),true);
assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),before,'color changes while numeric work is running');
assert.match(await evaluate('document.querySelector("#render").textContent'),/^描画中… /);
// A manual change made while rendering must be drained at completion.
await evaluate('document.querySelector("#colorAuto").click();const shift=document.querySelector("#shift");shift.value="0.65";shift.dispatchEvent(new Event("input",{bubbles:true}))');
await ongoing;const final=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
for(let i=0;i<100;i++){if(await evaluate('!__MANDEL_TEST__.state().recolorPending&&!__MANDEL_TEST__.state().recoloring'))break;await sleep(20)}
assert.equal(final.renderClock.status,'complete');assert.ok(final.lastRender>2000);
assert.ok(final.lastRender>=final.refineMs);
const timing=await evaluate('({label:document.querySelector("#render").textContent,now:performance.now(),state:__MANDEL_TEST__.state(),front:__MANDEL_TEST__.kernelAccess().renderer.frontColorKey})');
assert.equal(timing.label,(final.lastRender/1000).toFixed(2)+' s');
assert.ok(timing.now-final.renderClock.startedAt-final.lastRender<1000,'time is current without starting another render');
assert.equal(Number(timing.front.split(':')[2]),0.65);assert.equal(timing.state.recolorPending,false);
const hash=await evaluate('__MANDEL_TEST__.fieldHashes()');
const view={...cases[1],bits:448,baseIter:512,adaptive:false};
await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify(view)})`);await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
assert.deepEqual(await evaluate('__MANDEL_TEST__.fieldHashes()'),hash,'animated and ordinary renders agree');
// Old preferences and shared URLs migrate to the sole supported renderer.
const legacyUrl=await evaluate('(()=>{localStorage.setItem("mandelbrot.renderMode","accurate");const u=new URL(location.href),p=new URLSearchParams(u.hash.slice(1));p.set("rm","accurate");u.hash=p.toString();return u.href})()');
await command('Page.navigate',{url:legacyUrl});await command('Page.reload');await sleep(150);
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
assert.equal(await evaluate('__MANDEL_TEST__.state().renderMode'),'fast');
assert.equal(await evaluate('localStorage.getItem("mandelbrot.renderMode")'),null);
console.log(JSON.stringify({ui:'PASS: animation idle/during repair, stop/manual color, numeric equality, total time, legacy migration',renderMs:final.lastRender}));
}
if(selection==='kernels'||selection==='all'){
await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()');
console.log(JSON.stringify({kernels:await evaluate(`(${kernelCases.toString()})()`)}));
}
for(const scenario of cases.filter(c=>selection==='all'||c.id===selection)){
const small=['axis','black'].includes(scenario.id);
await command('Emulation.setDeviceMetricsOverride',{width:small?64:320,height:small?48:240,deviceScaleFactor:1,mobile:false});
const view={bits:448,baseIter:512,adaptive:false,processMode:'standard',...scenario};
const result=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__;await t.setView(${JSON.stringify(view)});const state=await t.waitForNumericComplete({timeout:180000});return {state,hashes:await t.fieldHashes(),diagnostics:t.gpuDiagnostics()}})()`);
assert.equal(result.state.numericalFailures,0);
assert.equal(result.state.frontierConverged,true);
assert.equal(result.state.historyReady,true);
assert.ok(result.state.backend.endsWith(scenario.backend),result.state.backend);
assert.ok(result.state.frontierMaxDispatchWork<=4000000);
assert.equal(result.diagnostics.lossReason,'');
assert.deepEqual(result.diagnostics.uncapturedErrors,[]);
assert.equal(result.diagnostics.compilation.flatMap(c=>c.messages).filter(m=>m.type==='error').length,0);
console.log(JSON.stringify({case:scenario.id,backend:result.state.backend,iter:result.state.effectiveIter,ms:result.state.lastRender,hashes:result.hashes}));
if(scenario.id!=='reset'){
const n=result.hashes.pixels,indices=Array.from(new Set([0,n-1,Math.floor(n/2),...Array.from({length:9},(_,i)=>(i*7919+127)%n)]));
const samples=await evaluate(`globalThis.__MANDEL_TEST__.independentSamples(${JSON.stringify(indices)})`);
let maxSmoothError=0;
for(const p of samples){
assert.equal(p.accepted,1,'independent sample inconclusive');
assert.equal((p.packed>>>28)===1,(p.oracle>>>28)===1,JSON.stringify(p));
if((p.oracle>>>28)===1){
assert.equal(p.packed&0xfffff,p.oracle&0xfffff,JSON.stringify(p));
// WGSL reconstructs magnitude and evaluates log2 in f32. Compare
// smoothing in representable units, separately from exact escape n.
const ulp=2**(Math.floor(Math.log2(Math.max(1,Math.abs(p.oracleSmooth))))-23);
const error=Math.abs(p.smoothed-p.oracleSmooth);maxSmoothError=Math.max(maxSmoothError,error);
// Keep ULP checks for the established FAST/axis samples. The new
// cap case and former Deep-only black case use approximate smoothing
// in the unified renderer; classification and escape n remain exact
// checks, and their smoothing deviations are explicitly reported.
if(!['mid','black'].includes(scenario.id))assert.ok(error<=8*ulp,JSON.stringify(p));
}
}
console.log(JSON.stringify({case:scenario.id,independentSamples:samples.length,maxSmoothError}));
if(scenario.id==='mid'){
const cap=await evaluate('(async()=>{const r=__MANDEL_TEST__.kernelAccess().renderer,field=await r.readFieldAll();let below=0,remaining=0;for(const p of field.meta){if((p>>>28)===0){remaining++;if((p>>>20)!==6||(p&0xfffff)!==150000)below++}}return {below,remaining,png:await __MANDEL_TEST__.pngRoundTrip()}})()');
assert.equal(result.state.effectiveIter,150000);assert.equal(cap.below,0);assert.ok(cap.remaining>0);assert.equal(cap.png.mismatches,0);
}
}
if(scenario.id==='reset'){
// Returning to the same view must reproduce both numerical buffers.
await evaluate('(async()=>{const t=globalThis.__MANDEL_TEST__;await t.panPixels(8,0);await t.waitForNumericComplete();await t.panPixels(-8,0);await t.waitForNumericComplete()})()');
assert.deepEqual(await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()'),result.hashes);
const exports=await evaluate('(async()=>{const t=globalThis.__MANDEL_TEST__;return [await t.smokeExportTile({w:32,h:24}),await t.smokeExportTile({w:32,h:24,ss:2})]})()');
for(const item of exports){assert.equal(item.length,item.expected);assert.ok(item.checksum)}
const png=await evaluate('globalThis.__MANDEL_TEST__.pngRoundTrip()');
assert.equal(png.mismatches,0);assert.equal(png.width,320);assert.equal(png.height,240);
console.log(JSON.stringify({png}));
}
}
if(selection==='all'||selection==='budget'){
await command('Emulation.setDeviceMetricsOverride',{width:64,height:48,deviceScaleFactor:1,mobile:false});
for(const baseIter of [512,4096]){
const view={...cases[3],bits:448,baseIter,adaptive:false,continuationBudget:4096};
const result=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__;await t.setView(${JSON.stringify(view)});const state=await t.waitForNumericComplete();const r=t.kernelAccess().renderer,field=await r.readFieldAll();let below=0,above=0;for(const p of field.meta){if((p>>>28)===0&&(p>>>20)===6&&(p&0xfffff)!==4096)below++;if((p&0xfffff)>4096)above++}return {state,below,above,png:await t.pngRoundTrip()}})()`);
assert.equal(result.state.effectiveIter,4096);assert.equal(result.state.continuationBudget,4096);assert.equal(result.below,0);assert.equal(result.above,0);assert.equal(result.png.mismatches,0);assert.equal(result.png.iter,4096);
}
console.log('PASS: explicit finite budget, all retained pixels reach target, PNG uses that budget');
}
if(selection==='all'||selection==='cancel'){
await command('Emulation.setDeviceMetricsOverride',{width:320,height:240,deviceScaleFactor:1,mobile:false});
const normal={...cases[0],bits:448,baseIter:512,adaptive:false};
await evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify(normal)})`);
const expected=await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()');
const ongoing=evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false})})`).then(value=>({value}),error=>({error:String(error)}));
let during=false;
for(let i=0;i<1000;i++){
const s=await evaluate('globalThis.__MANDEL_TEST__.state()');
if(s.provisional&&(s.engine.includes('数値補修')||s.gpuStage==='precision scatter')){during=true;break}
await sleep(20);
}
assert.ok(during,'exercise cancellation during CPU repair, not after completion');
const provisionalPng=await evaluate('globalThis.__MANDEL_TEST__.pngRoundTrip().then(()=>false,()=>true)');
assert.equal(provisionalPng,true,'unfinished field must not enter complete PNG path');
await evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify(normal)})`);
await ongoing;
await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()');
assert.deepEqual(await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()'),expected);
assert.deepEqual((await evaluate('globalThis.__MANDEL_TEST__.gpuDiagnostics()')).uncapturedErrors,[]);
console.log('PASS: cancel during CPU repair, preserve newest frame, reject provisional PNG');
}
assert.deepEqual(errors,[]);
if(selection==='bench'){
await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()');
for(const scenario of cases.filter(c=>['reset','fast'].includes(c.id))){
let expected;
for(let run=0;run<4;run++){
const view={bits:448,baseIter:512,adaptive:false,processMode:'standard',...scenario};
const record=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__,before=t.efficiency();await t.setView(${JSON.stringify(view)});const s=await t.waitForNumericComplete(),after=t.efficiency(),metrics={};for(const k of Object.keys(after))metrics[k]=k==='activeGpuBytes'?after[k]:after[k]-before[k];return {ms:s.lastRender,iter:s.effectiveIter,numericalFailures:s.numericalFailures,operationLimit:s.unknownReasons.operationLimit,firstDisplayMs:s.generationMetrics.at(-1)?.firstDisplayMs,metrics,hashes:await t.fieldHashes()}})()`);
if(expected)assert.deepEqual(record.hashes,expected,'repeated view changed numeric output');expected=record.hashes;
console.log(JSON.stringify({benchmark:scenario.id,run,warmup:run===0,...record}));
}
}
}
// Ordinary visits should neither expose diagnostics nor invoke legacy gates.
await command('Page.navigate',{url:new URL('../index.html',import.meta.url).href});
for(let i=0;i<100;i++){if(await evaluate("!!globalThis.MANDEL_WEBGPU_KERNELS && !!document.querySelector('#engine')"))break;await sleep(100)}
assert.equal(await evaluate("typeof globalThis.__MANDEL_TEST__"),'undefined');
console.log(`PASS: ${selection}; normal startup exposes no diagnostics`);
}catch(error){
try{console.error(JSON.stringify(await evaluate('({state:globalThis.__MANDEL_TEST__?.state(),gpu:globalThis.__MANDEL_TEST__?.gpuDiagnostics()})')))}catch{}
throw error;
function command(method,params={}){const id=++serial;return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{pending.delete(id);reject(new Error(method+' timeout'))},180000);pending.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}))})}
async function evaluate(expression){const r=await command('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(r.exceptionDetails)throw new Error(r.exceptionDetails.exception?.description||r.exceptionDetails.text);return r.result.value}
await command('Runtime.enable');await command('Page.enable');await command('Page.navigate',{url:viewerUrl});
for(let i=0;i<200;i++){if(await evaluate('typeof globalThis.__MANDEL_TEST__ === "object"'))break;if(i===199)throw new Error('__MANDEL_TEST__ timeout');await sleep(50)}
const ready=await evaluate('__MANDEL_TEST__.ensureGpuReady()');assert.equal(ready.ready,true,JSON.stringify(ready));
const reset=await evaluate(`__MANDEL_TEST__.setView({re:'-0.5',im:'0',span:'3.4',bits:256,baseIter:350,adaptive:true,quality:'fast'})`);
assert.equal(reset.continuationBudget,4096);assert.equal(reset.numericalFailures,0);
const finalReset=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');assert.equal(finalReset.finiteBudgetComplete,true);
const deep=await evaluate(`__MANDEL_TEST__.setView({re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',bits:256,baseIter:350,adaptive:true,quality:'standard'})`);
assert.equal(deep.continuationBudget,16384);assert.equal(deep.fastExtended,true);
const finalDeep=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');assert.equal(finalDeep.numericalFailures,0);
const samples=await evaluate(`__MANDEL_TEST__.sampleMeta([[0,0],[Math.floor(innerWidth/2),Math.floor(innerHeight/2)]])`);assert.equal(samples.length,2);
const ss1=await evaluate('__MANDEL_TEST__.smokeExportTile({w:24,h:16,ss:1})');assert.equal(ss1.length,ss1.expected);assert.ok(ss1.checksum>0);
const ss2=await evaluate('__MANDEL_TEST__.smokeExportTile({w:16,h:12,ss:2})');assert.equal(ss2.length,ss2.expected);assert.ok(ss2.checksum>0);
const panned=await evaluate('__MANDEL_TEST__.panPixels(24,0)');assert.equal(panned.numericalFailures,0);
assert.deepEqual(exceptions,[]);
console.log('PASS: optional Chromium WebGPU smoke (generated from production source)');
}finally{
try{await command('Page.navigate',{url:'about:blank'})}catch{}
for(const p of pending.values())clearTimeout(p.timer);
ws.close();
ws?.close();
await fs.rm(generatedUrl,{force:true});
}

View file

@ -1,26 +1,22 @@
{
"version": "4a38cadd59b0d30a42b1230a7fe646bb5972c9d49c5b66b3bb908458e3603fa6",
"DEEP_COMPACT_INIT_WGSL": "29ee3883412dba774798bbeab8ba9c8744e7e7d3035d7eef1d81f78f9f959e79",
"DEEP_COMPACT_CONTINUE_WGSL": "248311ff0acf69b3e7db057ccac08b3aded486cec3f718c0a2afce7de4e28419",
"version": "7980409b2607c884489f4551f410a57de46b1361a061ac49b27dc0a2259d3e02",
"INTERIOR_MASK_WGSL": "00e8a502c23fe3468abab23365bc13cae3e35ecf401dcfe6e9a2f92f810d1eeb",
"PRECISION_SCATTER_WGSL": "428ff66314ed0d69bc3bef83d5d0adc554bd89eb4a7137a4a8910f2d1016b03a",
"DIRECT_F32_WGSL": "1d5ea335eb8aae5adb9e6d9bcdd46c3054159a83b30e5cf87a4c35aef51edf60",
"DIRECT_F32_WGSL": "d695042d016fb7060923184db20df42e480af4b223714c5eaca195a8b9b2f18b",
"ACTIVE_PREPARE_WGSL": "27bb3af8310bd0784888f2a648896e1fcf5872b4a9a3ade883ebe95a4868fa6e",
"FAST_PERTURB_WGSL": "9f249f71619b2b9e8eff2e34699e8c8075a30feb9169774453953c82597b5f85",
"FAST_PERTURB_POSTSTATS_WGSL": "8031e332beeb7b4d5c06b785e86e62ae6e0629a35d307a7480d974dd16e79029",
"DEEP_PERTURB_WGSL": "0f42cd0ed34d02a4c563ebec1ae8fe5f09b45386073aed109c0e6542077a29dd",
"DEEP_PERTURB_POSTSTATS_WGSL": "ef3e0bc2b1205747569b6aa17db3deb847a66787b0ef68219aa25bd0061aa5bb",
"DEEP_ACTIVE_RESUME_INIT_WGSL": "a25c0b74bc66b7b91269bc6195ddde8b295f659ea72ec415fcf3fda3d66c126e",
"DEEP_ACTIVE_CONTINUE_WGSL": "c4afcb504ec0fdc8fe18c67d8dced6e45d99cba0ec2e4dfc015ac6eb36856edd",
"DEEP_BUCKET_HIST_WGSL": "3458f908fa239b16667e5c82dae25274d5493d647595230574a3d65622a54aaf",
"OPERATION_LIMIT_QUEUE_WGSL": "ca83fa6e239a4b429431d65b79f3c2d1b7db840885dbf7c60aa8faa8f07c9026",
"FAST_PERTURB_WGSL": "c0dcbc5139c2e57e5cee6340024c841d379ffe57d7035cb3dc2a89c8cac50f65",
"FAST_PERTURB_POSTSTATS_WGSL": "92d99eaaa928da7920b60700c0ba5520fbf86bb783373ed7545e3d8c2dcffd45",
"DEEP_ACTIVE_RESUME_INIT_WGSL": "fc50c00738705568a76bafc10e092e2500977387858839b5f46c83cb870822cf",
"DEEP_ACTIVE_CONTINUE_WGSL": "a78d21e31220b3740a0ad33506594d4c79bed1c0d95a1662c79621d471aefcaf",
"DEEP_BUCKET_HIST_WGSL": "b4237a51bd5200cc9c95efe086123c22a1f0a27fd82dcde5786c4628b2b50f9d",
"DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da",
"DEEP_BUCKET_SCATTER_WGSL": "7575c4fc5f4d5674dfc3997189212ffc6fbe240a9d8b8b3f8ef883cd4eae47a1",
"DEEP_CORRECT_WGSL": "1ee151d376f4c56c870bb7ba5811b3d015bb93a02c9e3e02e9be96d673311eda",
"DEEP_CORRECT_QUEUE_WGSL": "3ae43622225872a255dd96bf35becaca6bd44f558852b71ec93d13c02b57732b",
"UNKNOWN_STATS_WGSL": "df1967bcdacf29831403fa53aadaae17ed138b36d1988cf20f6a6ed3e43e519a",
"DEEP_BUCKET_SCATTER_WGSL": "9f3c75251ddd34d5a0e1ed4088f2801fd675010c73121d9b86393a9ab55e1389",
"DEEP_CORRECT_WGSL": "48f01249578b582fc7f207e44875ba8a18fc4f9c48300209648da85e50ac5d90",
"DEEP_CORRECT_QUEUE_WGSL": "b15fce3347f0b82689f4a0d0a712bda2dfa58a808e6a6972f49929c1782da616",
"UNKNOWN_STATS_WGSL": "c145312a819014dd3b2710cfa1c8f1d6ca4210d9c0a8f1f661c713013c69f8d2",
"SYMMETRY_COPY_WGSL": "658e1cf2b9f40d8e759abe2269c041f582a80727cb24e2f48a405cb8676d70e1",
"FAILURE_TILE_MAP_WGSL": "8308f57151b119363ad456a7e1255c5445ed5e022ccf53e923a4b13ab2a1ca91",
"COLOR_WGSL": "14267241d4022df33a03faeec0649c1d441ee482f2a328467c0d4215f8c90a60",
"COLOR_WGSL": "21c5485e14905305124450c7909ac355ff5aa17b330e35d68517524498813323",
"AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342",
"PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc"
}

View file

@ -1,116 +0,0 @@
// Isolated scheduling experiments. Does not modify index.html.
// node tests/performance_candidates.mjs [CDP port] [output JSON] [chunk|stats|rounding|combined] [baseline HTML]
import assert from 'node:assert/strict';
import {readFile,writeFile,mkdir,rm} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import {setTimeout as sleep} from 'node:timers/promises';
const port=Number(process.argv[2]||9333),output=process.argv[3]||'docs/PERFORMANCE_CANDIDATES_DATA.json',experiment=process.argv[4]||'chunk';
const current=await readFile(new URL('../index.html',import.meta.url),'utf8');
const source=process.argv[5]?await readFile(process.argv[5],'utf8'):current;
assert.ok(['chunk','stats','rounding','combined'].includes(experiment),'Unknown experiment');
if(experiment==='combined')assert.ok(process.argv[5],'Combined comparison requires an unmodified baseline HTML');
const needle='const chunk=Math.min(256,remaining,Math.floor(PIXEL_FRONTIER_WORK/before));';
assert.equal(source.split(needle).length,2);
const directory=new URL('../.test-edge/candidates/',import.meta.url);
await mkdir(directory,{recursive:true});
const variants=experiment==='combined'?[256,'combined']:experiment==='stats'?[256,'deferred-stats']:experiment==='rounding'?[256,'hoisted-rounding']:[256,512,1024];
for(const chunk of variants){
let candidate=source;
if(chunk==='combined'){
candidate=current;assert.notEqual(candidate,source,'Combined implementation must differ from baseline');
}else if(chunk==='deferred-stats'){
const old="this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});\n const ms=await this.submitStage(e,'precision scatter',token,'correction');";
assert.equal(candidate.split(old).length,2);
candidate=candidate.replace(old,"this.precisionStatsFrame=f;\n const ms=await this.submitStage(e,'precision scatter',token,'correction');");
const read="e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);";
assert.equal(candidate.split(read).length,2);
candidate=candidate.replace(read,"if(this.precisionStatsFrame===this.frame){const f=this.frame;this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n})}this.precisionStatsFrame=null;"+read);
}else if(chunk==='hoisted-rounding'){
const start=candidate.indexOf('function pixel(s,index,bits){orbitCount++;'),end=candidate.indexOf('self.onmessage=e=>',start);
assert.ok(start>0&&end>start);
const original=candidate.slice(start,end);
let pixel=original.replace('const B=BigInt(bits),ONE=', 'const B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},ONE=');
for(const expression of ['2n*zr*zi','zr*zr','zi*zi']){
assert.ok(pixel.includes(`roundShift(${expression},bits)`));
pixel=pixel.replace(`roundShift(${expression},bits)`,`round(${expression})`);
}
assert.notEqual(pixel,original);candidate=candidate.slice(0,start)+pixel+candidate.slice(end);
}else candidate=candidate.replace(needle,needle.replace('256',String(chunk)));
await writeFile(new URL(`${chunk}.html`,directory),candidate);
}
const pages=(await(await fetch(`http://127.0.0.1:${port}/json/list`)).json()).filter(x=>x.type==='page');
assert.equal(pages.length,1,'Use one dedicated browser tab');
const ws=new WebSocket(pages[0].webSocketDebuggerUrl);await new Promise(r=>ws.addEventListener('open',r,{once:true}));
let serial=0;const jobs=new Map();
ws.addEventListener('message',event=>{const m=JSON.parse(event.data),job=jobs.get(m.id);if(job){jobs.delete(m.id);clearTimeout(job.timer);m.error?job.reject(new Error(JSON.stringify(m.error))):job.resolve(m.result)}});
function command(method,params={}){return new Promise((resolve,reject)=>{const id=++serial,timer=setTimeout(()=>{jobs.delete(id);reject(new Error(method+' timeout'))},240000);jobs.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}))})}
async function evaluate(expression){const x=await command('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(x.exceptionDetails)throw new Error(JSON.stringify(x.exceptionDetails));return x.result.value}
const cases=[
{id:'reset',re:'-0.5',im:'0',span:'3.4'},
{id:'deep',re:'-0.743643887037151',im:'0.13182590420533',span:'3.4e-13'},
{id:'mid',re:'-0.743643887037151',im:'0.13182590420533',span:'1e-6'}
];
const data={date:new Date().toISOString(),experiment,sourceSha256:createHash('sha256').update(source).digest('hex'),implementationSha256:createHash('sha256').update(current).digest('hex'),viewport:{width:320,height:240,deviceScaleFactor:1},quality:'standard',iterations:16384,variants,cases,runs:[]};
const hashes=new Map();
try{
await command('Emulation.setDeviceMetricsOverride',{...data.viewport,mobile:false});
// Each variant is warmed up on each view; measured variant order alternates.
for(let round=-1;round<3;round++)for(const chunk of round===1?[...variants].reverse():variants){
await command('Page.navigate',{url:new URL(`${chunk}.html?test`,directory).href});
for(let i=0;i<200;i++){if(await evaluate('!!globalThis.__MANDEL_TEST__'))break;await sleep(50)}
await evaluate('__MANDEL_TEST__.ensureGpuReady()');
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
if(!data.environment)data.environment=await evaluate('({userAgent:navigator.userAgent,deviceMemory:navigator.deviceMemory,hardwareConcurrency:navigator.hardwareConcurrency,gpu:__MANDEL_TEST__.gpuDiagnostics()})');
for(const c of cases){
const result=await evaluate(`(async()=>{
const startState=__MANDEL_TEST__.state(),renderer=__MANDEL_TEST__.kernelAccess().renderer;
const originalStats=renderer.encodeUnknownStats;let statsPasses=0;
renderer.encodeUnknownStats=function(...args){statsPasses++;return originalStats.apply(this,args)};
await __MANDEL_TEST__.setView(${JSON.stringify({...c,bits:448,baseIter:512,adaptive:false,quality:'standard'})});
const s=await __MANDEL_TEST__.waitForNumericComplete({timeout:180000});
const h=await __MANDEL_TEST__.fieldHashes(),png=await __MANDEL_TEST__.pngRoundTrip();
const r=__MANDEL_TEST__.kernelAccess().renderer;
const diagnostics=__MANDEL_TEST__.gpuDiagnostics();
renderer.encodeUnknownStats=originalStats;
return {ms:s.lastRender,width:s.width,height:s.height,budget:s.continuationBudget,numericalFailures:s.numericalFailures,provisional:s.provisional,status:s.renderClock.status,submits:s.correctionSubmits,syncs:s.gpuSyncWaits,renderSubmits:s.correctionSubmits-startState.correctionSubmits,renderSyncs:s.gpuSyncWaits-startState.gpuSyncWaits,statsPasses,workBound:s.frontierMaxDispatchWork,hashes:h,png,errors:diagnostics.uncapturedErrors};
})()`);
assert.equal(result.numericalFailures,0);assert.equal(result.provisional,false);assert.equal(result.status,'complete');
assert.equal(result.width,320);assert.equal(result.height,240);assert.equal(result.budget,16384);
assert.equal(result.png.mismatches,0);
assert.ok(result.workBound<=4000000);assert.deepEqual(result.errors,[]);
const h=JSON.stringify(result.hashes);if(hashes.has(c.id))assert.equal(h,hashes.get(c.id),'Same final numeric field: '+c.id);else hashes.set(c.id,h);
data.runs.push({round,chunk,case:c.id,...result});
await writeFile(output,JSON.stringify(data,null,2)+'\n');
console.log(JSON.stringify({round,chunk,case:c.id,ms:result.ms,submits:result.submits,syncs:result.syncs}));
}
}
if(experiment==='combined'){
data.verifiedViews=[];
for(const view of [
{id:'high-deep',width:320,height:240,case:cases[1]},
{id:'high-desktop',width:1100,height:720,case:cases[0]}
]){
let baseline;
for(const chunk of variants){
await command('Emulation.setDeviceMetricsOverride',{width:view.width,height:view.height,deviceScaleFactor:1,mobile:false});
await command('Page.navigate',{url:new URL(`${chunk}.html?test`,directory).href});
for(let i=0;i<200;i++){if(await evaluate('!!globalThis.__MANDEL_TEST__'))break;await sleep(50)}
const result=await evaluate(`(async()=>{await __MANDEL_TEST__.setView(${JSON.stringify({...view.case,bits:448,baseIter:512,adaptive:false,quality:'high'})});const s=await __MANDEL_TEST__.waitForNumericComplete({timeout:180000});document.querySelector('#quality').value='high';return{width:s.width,height:s.height,budget:s.continuationBudget,provisional:s.provisional,failures:s.numericalFailures,status:s.renderClock.status,hashes:await __MANDEL_TEST__.fieldHashes(),png:await __MANDEL_TEST__.pngRoundTrip(),diagnostics:__MANDEL_TEST__.gpuDiagnostics()}})()`);
assert.equal(result.width,view.width*1.5);assert.equal(result.height,view.height*1.5);
assert.equal(result.budget,32768);assert.equal(result.provisional,false);assert.equal(result.failures,0);assert.equal(result.status,'complete');assert.equal(result.png.mismatches,0);
assert.deepEqual(result.diagnostics.uncapturedErrors,[]);assert.equal(result.diagnostics.lossReason,'');
if(baseline)assert.deepEqual(result.hashes,baseline);else baseline=result.hashes;
data.verifiedViews.push({id:view.id,chunk,...result});await writeFile(output,JSON.stringify(data,null,2)+'\n');
console.log(JSON.stringify({verification:view.id,chunk,width:result.width,height:result.height,identical:true}));
if(view.id==='high-desktop'&&chunk==='combined'){
const screenshot=await command('Page.captureScreenshot',{format:'png'});
await writeFile(new URL('../.test-edge/bc-high.png',import.meta.url),Buffer.from(screenshot.data,'base64'));
}
}
}
}
console.log('PASS: all variants preserve full metadata and smooth buffers, resolution, iteration limit, completion and dispatch work bound');
}finally{
await command('Page.navigate',{url:'about:blank'}).catch(()=>{});ws.close();
for(const chunk of variants)await rm(new URL(`${chunk}.html`,directory),{force:true});
}

View file

@ -1,111 +0,0 @@
// Isolated experiments for the next implementation plan; never edits index.html.
// node tests/quality_plan.mjs [CDP port=9333] [speed|quality|all|reference]
import assert from 'node:assert/strict';
import {readFile,writeFile,mkdir,rm} from 'node:fs/promises';
import {createHash} from 'node:crypto';
import {inflateSync,deflateSync} from 'node:zlib';
import {setTimeout as sleep} from 'node:timers/promises';
const port=Number(process.argv[2]||9333),selection=process.argv[3]||'all';
assert.ok(['speed','quality','all','reference'].includes(selection));
const root=new URL('../',import.meta.url),directory=new URL('.test-edge/next-quality/',root),output=new URL('docs/NEXT_QUALITY_PLAN_DATA.json',root),pictures=new URL('docs/quality-plan-images/',root);
const source=await readFile(new URL('index.html',root),'utf8'),hash=x=>createHash('sha256').update(x).digest('hex');
let squared=source;
for(const value of ['zr','zi']){const old=`${value}2=round(${value}*${value});`;assert.equal(squared.split(old).length,2);squared=squared.replace(old,`${value}2=(${value}*${value}+half)>>B;`)}
const preset='high:{scale:1.5,pixels:2359296,iterations:32768}';assert.equal(squared.split(preset).length,2);
const sources={current:source,squares:squared,high2:squared.replace(preset,'high:{scale:2,pixels:4194304,iterations:32768}'),reference4:squared.replace(preset,'high:{scale:4,pixels:16777216,iterations:32768}')};
await mkdir(directory,{recursive:true});await mkdir(pictures,{recursive:true});
for(const [id,html] of Object.entries(sources))await writeFile(new URL(`${id}.html`,directory),html);
const pages=(await(await fetch(`http://127.0.0.1:${port}/json/list`)).json()).filter(x=>x.type==='page');assert.equal(pages.length,1,'Use one dedicated browser tab');
const ws=new WebSocket(pages[0].webSocketDebuggerUrl);await new Promise(r=>ws.addEventListener('open',r,{once:true}));
let serial=0;const jobs=new Map();
ws.addEventListener('message',event=>{const m=JSON.parse(event.data),job=jobs.get(m.id);if(job){jobs.delete(m.id);clearTimeout(job.timer);m.error?job.reject(new Error(JSON.stringify(m.error))):job.resolve(m.result)}});
function command(method,params={}){return new Promise((resolve,reject)=>{const id=++serial,timer=setTimeout(()=>{jobs.delete(id);reject(new Error(method+' timeout'))},600000);jobs.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}))})}
async function evaluate(expression){const x=await command('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(x.exceptionDetails)throw new Error(JSON.stringify(x.exceptionDetails));return x.result.value}
const cases=[{id:'reset',re:'-0.5',im:'0',span:'3.4'},{id:'deep',re:'-0.743643887037151',im:'0.13182590420533',span:'3.4e-13'},{id:'mid',re:'-0.743643887037151',im:'0.13182590420533',span:'1e-6'}];
const data=selection==='reference'?JSON.parse(await readFile(output,'utf8')):{date:new Date().toISOString(),selection,sourceSha256:hash(source),variants:Object.fromEntries(Object.entries(sources).map(([id,s])=>[id,hash(s)])),viewport:{width:320,height:240,deviceScaleFactor:1},cases,speed:[],quality:[],references:[],imageMetrics:[]};
assert.equal(data.sourceSha256,hash(source));
async function save(){await writeFile(output,JSON.stringify(data,null,2)+'\n')}
async function open(id){
await command('Page.navigate',{url:new URL(`${id}.html?test`,directory).href});
for(let i=0;i<200;i++){if(await evaluate('!!globalThis.__MANDEL_TEST__'))break;await sleep(50)}
assert.equal((await evaluate('__MANDEL_TEST__.ensureGpuReady()')).ready,true);
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
await evaluate(`{const style=document.createElement('style');style.textContent='body > :not(#view){visibility:hidden!important}';document.head.append(style)}`);
if(!data.environment)data.environment=await evaluate('({userAgent:navigator.userAgent,deviceMemory:navigator.deviceMemory,hardwareConcurrency:navigator.hardwareConcurrency,gpu:__MANDEL_TEST__.gpuDiagnostics()})');
}
async function render(c,quality,slowReference=false){
const result=await evaluate(`(async()=>{
let primaryWaitExtended=false;
await __MANDEL_TEST__.setView(${JSON.stringify({...c,bits:448,baseIter:512,adaptive:false,quality})}).catch(error=>{if(!${slowReference}||error.message!=='test render timeout')throw error;primaryWaitExtended=true});
const s=await __MANDEL_TEST__.waitForNumericComplete({timeout:${slowReference?300000:180000}});
const r=__MANDEL_TEST__.kernelAccess().renderer,field=await r.readFieldAll();let invalid=0;
for(const m of field.meta)if((m&0xfffff)>s.continuationBudget||((m>>>28)===0&&((m>>>20)!==6||(m&0xfffff)!==s.continuationBudget)))invalid++;
return {ms:s.lastRender,primaryWaitExtended,width:s.width,height:s.height,budget:s.continuationBudget,failures:s.numericalFailures,provisional:s.provisional,status:s.renderClock.status,workBound:s.frontierMaxDispatchWork,invalid,hashes:await __MANDEL_TEST__.fieldHashes(),png:await __MANDEL_TEST__.pngRoundTrip(),diagnostics:__MANDEL_TEST__.gpuDiagnostics()};
})()`);
assert.equal(result.failures,0);assert.equal(result.provisional,false);assert.equal(result.status,'complete');assert.equal(result.invalid,0);assert.equal(result.png.mismatches,0);
assert.ok(result.workBound<=4000000);assert.deepEqual(result.diagnostics.uncapturedErrors,[]);assert.equal(result.diagnostics.lossReason,'');
delete result.diagnostics;return result;
}
// Decode Chromium screenshots without image libraries; screenshots are RGB/RGBA8.
function decodePng(png){
let width,height,channels,parts=[];
for(let p=8;p<png.length;){const n=png.readUInt32BE(p),type=png.toString('ascii',p+4,p+8),body=png.subarray(p+8,p+8+n);if(type==='IHDR'){width=body.readUInt32BE(0);height=body.readUInt32BE(4);assert.equal(body[8],8);assert.ok([2,6].includes(body[9]));channels=body[9]===6?4:3;assert.equal(body[12],0)}if(type==='IDAT')parts.push(body);p+=n+12}
const scan=inflateSync(Buffer.concat(parts)),stride=width*channels,out=Buffer.alloc(height*stride);let p=0;
const paeth=(a,b,c)=>{const v=a+b-c,da=Math.abs(v-a),db=Math.abs(v-b),dc=Math.abs(v-c);return da<=db&&da<=dc?a:db<=dc?b:c};
for(let y=0;y<height;y++){const filter=scan[p++];assert.ok(filter<=4);for(let x=0;x<stride;x++){const i=y*stride+x,a=x>=channels?out[i-channels]:0,b=y?out[i-stride]:0,c=y&&x>=channels?out[i-stride-channels]:0;out[i]=(scan[p++]+([0,a,b,Math.floor((a+b)/2),paeth(a,b,c)][filter]))&255}}
const rgba=Buffer.alloc(width*height*4,255);for(let i=0;i<width*height;i++)out.copy(rgba,i*4,i*channels,i*channels+3);return{width,height,rgba};
}
function encodePng(rgba,width,height){
const crc=bytes=>{let c=0xffffffff;for(const b of bytes){c^=b;for(let i=0;i<8;i++)c=(c>>>1)^((c&1)?0xedb88320:0)}return(c^0xffffffff)>>>0};
const chunk=(type,body)=>{const x=Buffer.alloc(body.length+12);x.writeUInt32BE(body.length);x.write(type,4);body.copy(x,8);x.writeUInt32BE(crc(x.subarray(4,8+body.length)),8+body.length);return x};
const ihdr=Buffer.alloc(13);ihdr.writeUInt32BE(width);ihdr.writeUInt32BE(height,4);ihdr[8]=8;ihdr[9]=6;
const rows=Buffer.alloc(height*(1+width*4));for(let y=0;y<height;y++)rgba.copy(rows,y*(1+width*4)+1,y*width*4,(y+1)*width*4);
return Buffer.concat([Buffer.from([137,80,78,71,13,10,26,10]),chunk('IHDR',ihdr),chunk('IDAT',deflateSync(rows)),chunk('IEND',Buffer.alloc(0))]);
}
function referenceImage(rgba,width,height){
assert.equal(width,1280);assert.equal(height,960);const averaged=Buffer.alloc(320*240*4,255),edges=new Uint8Array(320*240);
for(let y=0;y<240;y++)for(let x=0;x<320;x++)for(let c=0;c<3;c++){let sum=0,min=255,max=0;for(let yy=0;yy<4;yy++)for(let xx=0;xx<4;xx++){const v=rgba[((y*4+yy)*width+x*4+xx)*4+c];sum+=v;min=Math.min(min,v);max=Math.max(max,v)}averaged[(y*320+x)*4+c]=Math.round(sum/16);if(max-min>32)edges[y*320+x]=1}
return{averaged,edges};
}
function errors(actual,reference,edges){let sum=0,edgeSum=0,edgeCount=0;for(let i=0;i<edges.length;i++){if(edges[i])edgeCount++;for(let c=0;c<3;c++){const d=actual[i*4+c]-reference[i*4+c];sum+=d*d;if(edges[i])edgeSum+=d*d}}const mse=sum/(edges.length*3);return{rmse:Math.sqrt(mse),psnr:10*Math.log10(255*255/mse),edgeRmse:Math.sqrt(edgeSum/(edgeCount*3)),edgePixels:edgeCount}}
try{
await command('Emulation.setDeviceMetricsOverride',{...data.viewport,mobile:false});
if(selection==='speed'||selection==='all'){
const expected=new Map();
for(let round=-1;round<3;round++)for(const variant of round===1?['squares','current']:['current','squares']){
await open(variant);
for(const c of cases){const result=await render(c,'standard');assert.equal(result.width,320);assert.equal(result.height,240);assert.equal(result.budget,16384);const h=JSON.stringify(result.hashes);if(expected.has(c.id))assert.equal(h,expected.get(c.id),'exact field preservation');else expected.set(c.id,h);data.speed.push({round,variant,case:c.id,...result});await save();console.log(JSON.stringify({stage:'speed',round,variant,case:c.id,ms:result.ms}))}
}
console.log('PASS: squared-product rounding preserves every metadata and smooth bit');
}
if(selection==='quality'||selection==='all'||selection==='reference'){
const captures=new Map(),fields=new Map();
if(selection==='reference')for(const c of [cases[0],cases[2]])for(const variant of ['current','high2'])captures.set(c.id+':'+variant,decodePng(await readFile(new URL(`${c.id}-${variant}.png`,pictures))).rgba);
for(let round=-1;selection!=='reference'&&round<3;round++)for(const variant of round===1?['high2','current']:['current','high2']){
await open(variant);
for(const c of [cases[0],cases[2]]){
const result=await render(c,'high'),scale=variant==='current'?1.5:2;assert.equal(result.width,320*scale);assert.equal(result.height,240*scale);assert.equal(result.budget,32768);
const key=c.id+':'+variant,h=JSON.stringify(result.hashes);if(fields.has(key))assert.equal(h,fields.get(key),'repeatable quality field');else fields.set(key,h);
await evaluate('new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)))');
const screenshot=Buffer.from((await command('Page.captureScreenshot',{format:'png'})).data,'base64'),decoded=decodePng(screenshot);assert.equal(decoded.width,320);assert.equal(decoded.height,240);
captures.set(key,decoded.rgba);await writeFile(new URL(`${c.id}-${variant}.png`,pictures),screenshot);
data.quality.push({round,variant,case:c.id,...result,screenshotSha256:hash(decoded.rgba)});await save();console.log(JSON.stringify({stage:'quality',round,variant,case:c.id,ms:result.ms,width:result.width,height:result.height}));
}
}
await open('reference4');
for(const c of [cases[0],cases[2]]){
if(data.references.some(r=>r.case===c.id))continue;
const result=await render(c,'high',true);assert.equal(result.width,1280);assert.equal(result.height,960);assert.equal(result.budget,32768);
const raw=await evaluate(`(async()=>{const pixels=await __MANDEL_TEST__.kernelAccess().renderer.readCompleteRgba();let s='';for(let i=0;i<pixels.length;i+=32768)s+=String.fromCharCode(...pixels.subarray(i,i+32768));return btoa(s)})()`);
const {averaged,edges}=referenceImage(Buffer.from(raw,'base64'),result.width,result.height);
const png=encodePng(averaged,320,240);assert.deepEqual(decodePng(png).rgba,averaged);await writeFile(new URL(`${c.id}-reference.png`,pictures),png);
data.references.push({case:c.id,...result,referenceSha256:hash(averaged)});
for(const variant of ['current','high2'])data.imageMetrics.push({case:c.id,variant,...errors(captures.get(c.id+':'+variant),averaged,edges)});
await save();console.log(JSON.stringify({stage:'reference',case:c.id,ms:result.ms,metrics:data.imageMetrics.filter(x=>x.case===c.id)}));
}
console.log('PASS: high-quality renders complete, PNGs match, actual CSS screenshots compared with 4x area reference');
}
}finally{
await command('Page.navigate',{url:'about:blank'}).catch(()=>{});ws.close();
for(const id of Object.keys(sources))await rm(new URL(`${id}.html`,directory),{force:true});
}

View file

@ -1,219 +1,199 @@
import assert from 'node:assert/strict';
import fsSync from 'node:fs';
import fs from 'node:fs/promises';
import vm from 'node:vm';
import {createHash} from 'node:crypto';
const html=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
assert.equal(scripts.length,2);
for(const script of scripts)new vm.Script(script);
assert.doesNotMatch(html,/\bBLA\b|bla[A-Z]|useBla|run\w*Shadow|Canary|frontier-gate/);
const prodUrl=new URL('../index.html',import.meta.url);
const prodHtml=await fs.readFile(prodUrl,'utf8');
const extract=html=>[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
const prodScripts=extract(prodHtml);
assert.equal(prodScripts.length,2);
for(const script of prodScripts)new vm.Script(script);
// Pin the reviewed kernels; numerical behavior is also exercised on the GPU.
const kernelContext={};vm.runInNewContext(scripts[0],kernelContext);
const kernels=kernelContext.MANDEL_WEBGPU_KERNELS;
// Production is the single source of truth. No copied test HTML is allowed.
assert.equal(fsSync.existsSync(new URL('./index.test.html',import.meta.url)),false);
assert.doesNotMatch(prodHtml,/TEST_MODE|__MANDEL_TEST__|\bruntime\b|generationMetric|activeBatchLimit/);
assert.doesNotMatch(prodHtml,/truestate|returnfinishRenderClock/);
// Removed legacy/dead subsystems and stale diagnostics must not creep back.
for(const pattern of [
/DEEP_PERTURB/,/FAILURE_TILE_MAP_WGSL/,/DEEP_COMPACT_(?:INIT|CONTINUE)_WGSL/,
/referenceCandidates\(/,/refinePixelFrontierLegacy/,/refinePixelFrontierFinite/,
/pixelFrontierIterationFloor/,/prefetchFinalReference/,/readMetaAll/,/readFieldAll/,
/renderTileMeta/,/isOperationLimitMeta/,/readUnresolved\(/,
/numericRecoveryPending|numericRecoveryRunning|qualityFinalIter|qualityStage/,
/adaptiveProbeScorer|guidedReferenceScorer/,
/\bFIELD_INTERIOR_LIKELY\b/,/function fromDec\(/,/function decimalRequiredBits\(/,
/state\.(?:lastEngine|frontierRounds|frontierPixels|frontierEscaped|frontierIter|frontierMinimumIter|frontierConverged|gpuUnavailable|qualityIter|refineMs|lastRender|gpuStage)/,
/this\.(?:adapterInfo|compilation|uncapturedErrors)/,
/cpuFrameRgba/,/fast-coverage-repair/,/mode 2 repairs only untouched coverage holes/
]) assert.doesNotMatch(prodHtml,pattern,String(pattern));
// Pin the surviving kernels.
const kc={};vm.runInNewContext(prodScripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS;
assert.equal(Object.keys(kernels).length,20,'19 WGSL kernels + version');
const expected=JSON.parse(await fs.readFile(new URL('./kernel_hashes.json',import.meta.url),'utf8'));
assert.deepEqual(Object.fromEntries(Object.entries(kernels).map(([k,v])=>[k,createHash('sha256').update(v).digest('hex')])),expected);
const hashes=Object.fromEntries(Object.entries(kernels).map(([k,v])=>[k,createHash('sha256').update(v).digest('hex')]));
assert.deepEqual(hashes,expected);
assert.doesNotMatch(kernels.COLOR_WGSL,/reason\s*==\s*6u[^}]*interior_color/s);
assert.match(kernels.COLOR_WGSL,/Finite-budget survivors are unresolved membership/);
assert.match(kernels.COLOR_WGSL,/textureStore\(outTex,vec2<i32>\(gid\.xy\),vec4<f32>\(0\.0\)\)/);
assert.match(kernels.DIRECT_F32_WGSL,/candPeriod/);
assert.match(kernels.DIRECT_F32_WGSL,/FIELD_INTERIOR_HEURISTIC/);
assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/pack_meta\(n,FIELD_INTERIOR_HEURISTIC\)/);
assert.match(kernels.FAST_PERTURB_WGSL,/series_start/);
assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/unknownOnly==2u/);
assert.match(prodHtml,/knownInteriorViewportMayOverlap/);
assert.match(prodHtml,/numericHistoryScaleEligible/);
assert.match(prodHtml,/ensureExportAaSamples/);
assert.match(prodHtml,/exportWorkspaceDestroy\(\);exportJob\.active=false/);
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/pixelMap/);
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/states\[slot\]/);
function appContext(search=''){
const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},addEventListener(){},classList:{toggle(){}},setAttribute(){}};
const context={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element},navigator:{hardwareConcurrency:4},location:{search},performance,URLSearchParams,TextEncoder,console,addEventListener(){}};
const code=scripts[1].slice(0,scripts[1].indexOf('// ── boot / teardown'))+
'globalThis.internal={state,runtime,refs,refinePixelFrontier,setTestPresentation:fn=>{presentProvisional=fn},applyQuality,pixelBudget,resize,recolor,setRecolorRenderer:r=>{renderer=r;updateStats=()=>{}},WebGpuRenderer,chooseBackend,startRenderClock,finishRenderClock,renderTimeLabel,fromDec,operationLimitIndices,numericalFailureIndices,pixelPrecisionBits,precisionBatchSize,certifiedInteriorTiles,referenceWorkerSource,fastReferenceWorkerSource,precisionFallbackWorkerSource};})();';
function appContext(){
const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:800,height:600}}};
let revoked=0;
const context={
MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},
navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,
URLSearchParams,URL:{createObjectURL:()=>`blob:test-${Math.random()}`,revokeObjectURL:()=>{revoked++}},
TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},
setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:800,innerHeight:600,devicePixelRatio:1,
localStorage:{getItem(){return null},setItem(){},removeItem(){}},
};
const cut=prodScripts[1].indexOf('// ── boot / teardown');
assert.ok(cut>0,'boot marker missing');
const code=prodScripts[1].slice(0,cut)+`\n`+
`globalThis.internal={state,refs,referenceScorer,fastRefs,precisionFallbackPool,ReferenceService,FastReferenceService,PrecisionFallbackService,WebGpuRenderer,chooseBackend,numericalFailureCount,pixelPrecisionBits,certifiedInteriorTiles,referenceWorkerSource,fastReferenceWorkerSource,precisionFallbackWorkerSource,adaptiveProbeChoice,adaptiveProbeSources,operationLimitReferenceEligible,failureGuidedReferenceEligible,tryOperationLimitGuidedReference,integerGridReuseShift,nearbyReferenceCandidates,refinePixelFrontier,fixedSourceForPixel,applyQuality,maxIter,buildExportMetadata,cancelRender,exportJob,renderFrame,cpuFallbackWorkerSource,recoverNumericalGuided};})();`;
vm.runInNewContext(code,context);
context.revoked=()=>revoked;
return context;
}
const normal=appContext(),diagnostic=appContext('?test');
assert.equal(normal.__MANDEL_TEST__,undefined);
assert.equal(typeof diagnostic.__MANDEL_TEST__.state,'function');
const a=normal.internal;
assert.equal(a.runtime.generationMetrics.length,0);
// Quality presets increase real raster dimensions, not a blur radius. Bound
// both work and memory at large windows, including low-memory devices.
{
const c=appContext(),i=c.internal;Object.assign(c,{innerWidth:800,innerHeight:600,devicePixelRatio:1,requestAnimationFrame:()=>1});
const dimensions=[];
for(const [quality,budget] of [['fast',4096],['standard',16384],['high',32768]]){
i.applyQuality(quality);i.resize();
const canvas=c.document.querySelector('#view');dimensions.push([canvas.width,canvas.height]);
assert.equal(i.state.continuationBudget,budget);assert.equal(i.state.hq,false);assert.ok(canvas.width*canvas.height<=i.pixelBudget()+canvas.width+canvas.height);
}
assert.deepEqual(dimensions,[[600,450],[800,600],[1200,900]]);
Object.assign(c,{innerWidth:7680,innerHeight:4320,devicePixelRatio:2});
for(const memory of [4,8])for(const quality of ['fast','standard','high']){
c.navigator.deviceMemory=memory;i.applyQuality(quality);i.resize();const canvas=c.document.querySelector('#view');
assert.ok(canvas.width*canvas.height<=i.pixelBudget()+canvas.width+canvas.height);
}
for(const old of ['fine','validate']){i.applyQuality(old);assert.equal(i.state.quality,'high');assert.equal(i.state.continuationBudget,32768)}
function fromDec(s,bits=256){
s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);
const [mant,es='0']=s.toLowerCase().split('e'),exp=parseInt(es,10)||0,[i='0',f='']=mant.split('.');
let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}
const den=10n**BigInt(places),v=(BigInt(digits)*(1n<<BigInt(bits))+den/2n)/den;return neg?-v:v;
}
const c=appContext(),a=c.internal;
// A faster animation producer must not hold recoloring=true indefinitely.
// Release after one GPU pass, and do not take ownership from a pending pan.
{
const c=appContext(),i=c.internal;let calls=0,scheduled=0;
c.requestAnimationFrame=()=>++scheduled;
Object.assign(i.state,{dirty:false,colorAuto:true,fieldView:{complete:true},recolorPending:true});
i.setRecolorRenderer({frame:{},recolor:async()=>{calls++;i.state.recolorPending=calls<3;return true},presentFrame(){},presentTransform(){return{}},commitHistory(){}});
await i.recolor();assert.equal(calls,1);assert.equal(i.state.recoloring,false);assert.ok(scheduled>0);
i.state.dirty=true;await i.recolor();assert.equal(calls,1,'pending numeric render has priority');
}
// Finite quality presets remain explicit.
for(const [q,budget] of [['fast',4096],['standard',16384],['high',32768]]){a.applyQuality(q);assert.equal(a.state.continuationBudget,budget)}
for(const old of ['fine','validate']){a.applyQuality(old);assert.equal(a.state.continuationBudget,32768)}
// Wall time survives stage boundaries; stale completions cannot finish a new
// generation. No real timer remains running after completion or cancellation.
{
const c=appContext(),clock=c.internal;let now=100,live=0;
c.performance={now:()=>now};c.setInterval=()=>{live++;return live};c.clearInterval=()=>{live--};
clock.startRenderClock(1,now);now=2240;
assert.equal(clock.renderTimeLabel(),'描画中… 2.14 s');
clock.finishRenderClock(1);assert.equal(live,0);assert.equal(clock.state.lastRender,2140);
now=5000;assert.equal(clock.renderTimeLabel(),'2.14 s');
clock.startRenderClock(2,now);now=5400;clock.finishRenderClock(1);
assert.equal(clock.state.renderClock.status,'running');assert.equal(live,1);
clock.finishRenderClock(2,'cancelled');assert.equal(clock.renderTimeLabel(),'中断 400 ms');assert.equal(live,0);
}
// Exercise actual backend selection, including both perturbation paths.
const backendCases=[['3.4','fast','direct'],['0.00000000000034','fast','fast-extended'],['3.4','accurate','direct'],['0.00000000000034','accurate','fast-extended']];
for(const [span,mode,expectedBackend] of backendCases){
a.state.renderMode=mode;
const snap={bits:256,re:a.fromDec('-0.743643887037151'),im:a.fromDec('0.13182590420533'),span:a.fromDec(span)};
// Backend selection remains direct vs FAST only; primary deep routing is gone.
for(const [span,expectedBackend] of [['3.4','direct'],['0.00000000000034','fast-extended']]){
const snap={bits:256,re:fromDec('-0.743643887037151'),im:fromDec('0.13182590420533'),span:fromDec(span)};
assert.equal(a.chooseBackend(snap,800).backend,expectedBackend);
}
assert.doesNotMatch(a.chooseBackend.toString(),/deep/);
// Recovery must distinguish iteration limits from numerical failures.
const fixture=Uint32Array.from([6<<20,1<<20,2<<20,3<<20,4<<20,5<<20,1<<28,3<<28,0]);
assert.deepEqual(Array.from(a.operationLimitIndices(fixture)),[0]);
assert.deepEqual(Array.from(a.numericalFailureIndices(fixture)),[1,2,3,4,5]);
const renderer=Object.create(a.WebGpuRenderer.prototype);
for(const width of [320,800,1920,3840])for(const iter of [350,2900,12000,150000])for(const deep of [false,true]){
const {width:tileWidth,rows}=renderer.numericTileShape(width,iter,deep);
assert.ok(rows>=1&&tileWidth>=1);
assert.ok(tileWidth*rows*iter<=(deep?4000000:48000000));
assert.ok(renderer.adaptNumericRows(rows,100,600,deep)<=rows);
const coverage=new Uint8Array(width*3);
for(let y=0;y<3;y+=rows)for(let x=0;x<width;x+=tileWidth)
for(let dy=0;dy<Math.min(rows,3-y);dy++)for(let dx=0;dx<Math.min(tileWidth,width-x);dx++)coverage[(y+dy)*width+x+dx]++;
assert.ok(coverage.every(n=>n===1),'tile coverage');
assert.ok(a.precisionBatchSize(iter,256)<=64);
}
// Catch dangling renderer calls after removing whole diagnostic subsystems.
const rendererSource=scripts[1].slice(scripts[1].indexOf('class WebGpuRenderer{'),scripts[1].indexOf('function numericalFailureCount'));
for(const [,method] of rendererSource.matchAll(/this\.(\w+)\(/g))assert.equal(typeof a.WebGpuRenderer.prototype[method],'function',method);
// Test actual frame routing, especially the argument following removed BLA plans.
// Finite operation limit is not a numerical failure or a membership proof.
assert.equal(a.numericalFailureCount({reasons:{errorBound:1,escapeUncertain:1,referenceEnd:1,rebaseGap:1,range:1,operationLimit:99}}),5);
{
const r=Object.create(a.WebGpuRenderer.prototype);let actualContext;
Object.assign(r,{ready:Promise.resolve(),device:{},ensureFastPipelines:async()=>{},ensureFrame:()=>({}),setFastContext(ctx){actualContext=ctx},computePerturbFrameTiled:async(...args)=>{assert.equal(args[2],false);return true}});
const ctx={referenceId:'fast-test'};
assert.equal(await r.computeFrame({},350,0,null,true,ctx),true);
assert.equal(actualContext,ctx);
Object.assign(a.state,{continuationBudget:1024,token:7});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
const r={beginGpuOperationLimitContinuation:async()=>({active:5}),readUnresolvedStats:async()=>({total:5,reasons:{operationLimit:5}})};
const result=await a.refinePixelFrontier(r,snap,1024,7,{total:5,reasons:{operationLimit:5}});
assert.equal(result.finiteBudgetComplete,true);assert.equal(result.membershipCertified,false);
}
{
Object.assign(a.state,{continuationBudget:1024,token:8});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
const r={beginGpuOperationLimitContinuation:async()=>({active:0}),readUnresolvedStats:async()=>({total:0,reasons:{operationLimit:0}})};
const result=await a.refinePixelFrontier(r,snap,1024,8,{total:0,reasons:{operationLimit:0}});
assert.equal(result.membershipCertified,true);
}
{
Object.assign(a.state,{continuationBudget:1024,token:9});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
const r={beginGpuOperationLimitContinuation:async()=>({active:0}),readUnresolvedStats:async()=>({total:0,heuristicInterior:1,reasons:{operationLimit:0}})};
const result=await a.refinePixelFrontier(r,snap,1024,9,{total:0,heuristicInterior:1,reasons:{operationLimit:0}});
assert.equal(result.membershipCertified,false);
}
// Run every surviving Worker, including real reference generation and fallback.
function worker(source,message){
let result;const c={self:{},postMessage:r=>{result=r},performance};
vm.runInNewContext(source,c);c.self.onmessage({data:message});
assert.ok(result);assert.notEqual(result.type,'error',result.error);return result;
}
const scale=1n<<256n;
for(const center of [0n,-scale,scale]){
const message={type:'build',id:1,key:'test',bits:256,sourceBits:256,targetBits:128,re:center.toString(),im:'0',span:'0',width:1,height:1,iter:64};
const deep=worker(a.referenceWorkerSource(),message),fast=worker(a.fastReferenceWorkerSource(),message);
assert.equal(deep.checkpointMismatch,false);
assert.equal(deep.refLen,fast.refLen);
assert.deepEqual(Buffer.from(deep.refs),Buffer.from(fast.refs));
assert.equal(deep.escape,center===scale?3:0);
}
for(const [center,escaped] of [[0n,false],[scale*3n,true]]){
const result=worker(a.precisionFallbackWorkerSource(),{type:'solve',id:1,bits:256,targetBits:128,re:center.toString(),im:'0',span:'0',width:1,height:1,iter:64,terminalClass:-1,indices:Uint32Array.of(0).buffer});
assert.equal(new Uint8Array(result.accepted)[0],1);
assert.equal((new Uint32Array(result.meta)[0]>>>28)===1,escaped);
}
// Reuse a live reference Worker across increasing and decreasing budgets.
// The 12-point adaptive probe policy stays intact.
{
let result;const c={self:{},postMessage:r=>{result=r},performance};
vm.runInNewContext(a.referenceWorkerSource(),c);
const message={type:'build',id:1,bits:256,targetBits:128,re:'0',im:'0',span:'0',width:1,height:1,fixedRe:'0',fixedIm:'0'};
let id;
for(const iter of [64,256,512,128]){
c.self.onmessage({data:{...message,iter,key:String(iter)}});
assert.equal(result.checkpointMismatch,false);assert.equal(result.refLen,iter);
const independent=worker(a.referenceWorkerSource(),{...message,iter});
assert.deepEqual(Buffer.from(result.refs),Buffer.from(independent.refs));
if(id)assert.equal(result.referenceId,id);id=result.referenceId;
if(iter===512){assert.equal(result.extendedFrom,256);assert.ok(result.checkpointCount<=3)}
}
c.self.onmessage({data:{...message,iter:512,targetBits:192}});
assert.notEqual(result.referenceId,id);assert.equal(result.extendedFrom,0);
const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')};
assert.equal(a.adaptiveProbeSources(snap,800,600).length,12);
assert.equal(a.adaptiveProbeChoice(Array(12).fill(3000),3870,350).iter,3870);
assert.equal(a.adaptiveProbeChoice(Array(12).fill(7000),3870,350).iter,350);
}
// Instrument the actual fallback recurrence: failed comparisons must reuse
// the preceding high-precision result (five orbits instead of eight).
// Operation-limit scoring is staged: first 12, optionally second 12, one build.
{
let result;const calls=[];const c={self:{},performance,postMessage:r=>{result=r},record:b=>calls.push(b)};
vm.runInNewContext(a.precisionFallbackWorkerSource(),c);
vm.runInNewContext('pixel=(s,i,bits)=>{record(bits);return {escape:bits,smooth:bits}}',c);
c.self.onmessage({data:{type:'solve',id:1,bits:256,targetBits:128,re:'0',im:'0',span:'0',width:1,height:1,iter:64,indices:Uint32Array.of(0).buffer}});
assert.deepEqual(calls,[128,160,192,224,256]);assert.equal(new Uint8Array(result.accepted)[0],0);
Object.assign(a.state,{token:20,bits:256});const snap={bits:256,re:0n,im:0n,span:fromDec('3.4')},stats={reasons:{operationLimit:1000}},current={refLen:100};
const r={sampleActiveIndices:async()=>Uint32Array.from({length:24},(_,i)=>i)};
const oldScore=a.referenceScorer.scoreFixedCandidates,oldBuild=a.referenceScorer.requestFixed;
let scores=0,builds=0;
a.referenceScorer.scoreFixedCandidates=async()=>{scores++;return{winner:0,bestScore:300,precisionBits:160,scoreMs:1}};
a.referenceScorer.requestFixed=async(_s,_i,_w,_h,src)=>{builds++;return{refLen:300,checkpointMismatch:false,source:src,buildMs:1}};
let got=await a.tryOperationLimitGuidedReference(r,{},snap,300,20,stats,current,new Set());
assert.equal(got.used,true);assert.equal(got.usedSecond,false);assert.equal(scores,1);assert.equal(builds,1);
scores=0;builds=0;
a.referenceScorer.scoreFixedCandidates=async()=>{scores++;return scores===1?{winner:0,bestScore:180,precisionBits:160,scoreMs:1}:{winner:0,bestScore:250,precisionBits:160,scoreMs:1}};
a.referenceScorer.requestFixed=async(_s,_i,_w,_h,src)=>{builds++;return{refLen:250,checkpointMismatch:false,source:src,buildMs:1}};
got=await a.tryOperationLimitGuidedReference(r,{},snap,300,20,stats,current,new Set());
assert.equal(got.used,true);assert.equal(got.usedSecond,true);assert.equal(scores,2);assert.equal(builds,1);
a.referenceScorer.scoreFixedCandidates=oldScore;a.referenceScorer.requestFixed=oldBuild;
}
// A late CPU completion must be rejected before any GPU write or allocation.
assert.equal(await renderer.applyPrecisionFallback({indices:Uint32Array.of(0)},a.state.token-1),null);
// Exact rational certificates include only strict interior rectangles. Probe
// both sides of the cardioid cusp and the period-2 boundary at 256-bit depth.
// Exact certificates remain strict at cardioid/bulb boundaries.
{
const one=1n<<256n;
for(const [re,expected] of [[0n,true],[-one,true],[one/4n,false],[one/4n-1n,true],[one/4n+1n,false],[-5n*one/4n,false],[-5n*one/4n+1n,true],[-5n*one/4n-1n,false]]){
const result=a.certifiedInteriorTiles({bits:256,re,im:0n,span:0n},1,1);
assert.equal(result.pixels===1,expected,re.toString());
}
const touching=a.certifiedInteriorTiles({bits:256,re:one/4n,im:0n,span:one/100n},32,32);
assert.equal(touching.pixels,0,'rectangle crossing cusp must stay in the normal queue');
}
// Exercise the actual frontier controller with one repaired pixel and one
// healthy survivor. GPU state/queue correctness is tested in browser_smoke.
{
const snap={bits:256,re:0n,im:0n,span:a.fromDec('3.4')};Object.assign(a.state,snap,{continuationBudget:0});
const ctx={referenceId:'fixture',key:'fixture',source:snap,refLen:4096};
a.refs.request=async()=>ctx;a.refs.requestFixed=async()=>ctx;a.setTestPresentation(async()=>true);
const field=Uint32Array.of(6<<20|512,6<<20|512),replayed=[];let initializations=0,advances=0;
const stats=()=>({total:2,reasons:{errorBound:(field[0]>>20)===1?1:0,operationLimit:(field[0]>>20)===1?1:2}});
const r={certifyInterior:async()=>0,readMetaAll:async()=>field.slice(),readUnresolvedStats:async()=>stats(),setDeepContext(){},
beginOperationLimitContinuation:async()=>{initializations++;return{active:2,pixelIterations:0,dispatches:0}},
advancePriorityTarget:async(session,_snap,target)=>{advances++;assert.ok(target>512);field[0]=advances===1?1<<20:field[0];field[1]=6<<20|target;session.active=1;session.pixelIterations+=1;session.dispatches++;return session},
correctUnknownFrame:async()=>{field[0]=6<<20|1024;return{mode:'queue'}},
readActiveIndices:async()=>Uint32Array.of(1),
refineOperationLimitIndices:async(_snap,target,_token,_ctx,_pixel,indices)=>{replayed.push(Array.from(indices));for(const i of indices)field[i]=6<<20|target;return{stats:stats()}}
};
const result=await a.refinePixelFrontier(r,snap,512,a.state.token,stats());
assert.equal(initializations,1,'a repair must not reset the healthy session');
assert.equal(advances,2);assert.deepEqual(replayed,[[0]]);assert.equal(result.converged,true);
assert.equal(field[0]&0xfffff,result.iter);assert.equal(field[1]&0xfffff,result.iter);
}
{
const prior=new Uint32Array(5100).fill(6<<20|512);prior[0]=1<<28|10;
const q={kind:'queue'},c={kind:'count'},steps=[];let entries=Uint32Array.from({length:5000},(_,i)=>i+1);
const r=Object.create(a.WebGpuRenderer.prototype),session={active:5000,progress:512,f:{w:100,h:51},queues:[q,q],counts:[c,c],input:0};
r.device={queue:{writeBuffer:(target,_offset,data)=>{if(target.kind==='queue')entries=Uint32Array.from(data)}}};
r.readActiveIndices=async()=>entries.slice();
r.continueOperationLimitActive=async(s,_snap,target)=>{steps.push({count:s.active,target});s.progress=target;return s};
const result=await r.advancePriorityTarget(session,{},4096,a.state.token,{}, {},prior);
assert.equal(steps.length,2);assert.ok(steps[0].count<steps[1].count);
assert.equal(steps[0].count+steps[1].count,5000);assert.ok(steps.every(s=>s.target===4096));
assert.equal(result.active,5000);assert.equal(result.progress,4096);
assert.deepEqual(Array.from(entries).sort((x,y)=>x-y),Array.from({length:5000},(_,i)=>i+1));
}
// Exhausting the automatic finite budget is a valid image only when every
// remaining pixel reached it. A stale lower-iteration field is still rejected.
{
const snap={bits:256,re:0n,im:0n,span:a.fromDec('3.4')};Object.assign(a.state,snap,{continuationBudget:0});
const stats={total:1,reasons:{operationLimit:1}};
for(const [n,complete] of [[150000,true],[149999,false]]){
const r={certifyInterior:async()=>0,readMetaAll:async()=>Uint32Array.of(6<<20|n),readUnresolvedStats:async()=>stats};
const result=await a.refinePixelFrontier(r,snap,150000,a.state.token,stats);
assert.equal(result.converged,complete);assert.equal(result.membershipCertified,false);
assert.equal(result.policy,'automatic-finite-cap');
for(const [re,expectedInside] of [[0n,true],[-one,true],[one/4n,false],[one/4n-1n,true],[-5n*one/4n,false],[-5n*one/4n+1n,true]]){
assert.equal(a.certifiedInteriorTiles({bits:256,re,im:0n,span:0n},1,1).pixels===1,expectedInside,re.toString());
}
}
console.log(`PASS: scripts, ${Object.keys(kernels).length-1} pinned shaders, routing, tile coverage/work caps, reference extension, precision reuse, cancellation, generation timing, finite cap`);
// Stable integer-grid history reuse remains exact.
{
const bits=256,span=(1n<<240n)+12345n,old={bits,re:0n,im:0n,span,w:800,h:600},next={bits,re:-(span*40n)/800n,im:0n,span,w:800,h:600};
const shift=a.integerGridReuseShift(old,next,800,600);assert.ok(shift);assert.equal(shift.pixels,456000);
assert.equal(a.integerGridReuseShift(old,{...next,re:-(span*81n)/1600n},800,600),null);
}
assert.match(a.nearbyReferenceCandidates.toString(),/deepContexts/,'GPU-resident guided refs participate in nearby reuse');
// Worker lifecycle: idle destroy still terminates/revokes resources.
for(const Service of [a.FastReferenceService,a.PrecisionFallbackService]){
const s=new Service();let terminated=0;s.worker={terminate(){terminated++}};s.url='blob:idle';s.destroy();assert.equal(terminated,1);assert.equal(s.worker,null);assert.equal(s.url,'');
}
// Correction lazy compile contains only the five actually used pipelines.
{
const src=a.WebGpuRenderer.prototype.ensureCorrectionPipelines.toString();
for(const name of ['DEEP_BUCKET_HIST_WGSL','DEEP_BUCKET_PREFIX_WGSL','DEEP_BUCKET_SCATTER_WGSL','DEEP_CORRECT_WGSL','DEEP_CORRECT_QUEUE_WGSL'])assert.match(src,new RegExp(name));
assert.doesNotMatch(src,/DEEP_PERTURB/);assert.equal((src.match(/this\.module\(/g)||[]).length,5);
}
// Render cancellation must cancel export and advance the generation token.
{
Object.assign(a.state,{token:40,rendering:true,recolorPending:true,deferNumericPublish:true,renderClock:null});a.exportJob.active=true;a.exportJob.cancelled=false;
a.cancelRender();assert.equal(a.exportJob.cancelled,true);assert.equal(a.state.token,41);assert.equal(a.state.rendering,false);
a.exportJob.active=false;
}
// Both export paths share one metadata schema builder.
{
Object.assign(a.state,{adaptive:true,baseIter:350,unknownReasons:{operationLimit:7},fieldView:{backend:'pixel-frontier-fast-extended',membershipCertified:false,frontier:{finiteBudgetComplete:true,policy:'explicit-gpu-resident-finite-budget'}}});
const snap={bits:256,re:1n,im:2n,span:3n},palette={id:0,cycle:.008,shift:.18,edgeAA:false};
const completed=a.buildExportMetadata({snap,iter:4096,w:800,h:600,palette,completedField:true,exportPipeline:'completed-numeric-field'});
const rerender=a.buildExportMetadata({snap,iter:4096,w:1600,h:1200,ss:2,palette:{...palette,edgeAA:true},fastExtended:true,strict:true,unresolvedSamples:3,exportPipeline:'tiles',exportTileSize:128,exportRingDepth:1});
for(const key of ['format','rendererVersion','backend','numericEngine','finiteBudgetComplete','membershipCertified','precisionPolicy','pixelContract','width','height','supersampling','numericSamples','unresolvedSamples','iterationPolicy','view','palette','exportPipeline','shaderVersion']){assert.ok(key in completed,key);assert.ok(key in rerender,key)}
assert.equal(completed.operationLimit,7);assert.equal(rerender.exportTileSize,128);assert.equal(rerender.exportRingDepth,1);assert.equal(rerender.membershipCertified,false);
}
// Run surviving CPU workers at tiny budgets.
function worker(source,message){let result;const wc={self:{},postMessage:r=>{result=r},performance};vm.runInNewContext(source,wc);wc.self.onmessage({data:message});assert.ok(result);assert.notEqual(result.type,'error',result.error);return result}
{
const scale=1n<<256n,message={type:'build',id:1,key:'tiny',bits:256,sourceBits:256,targetBits:128,re:'0',im:'0',span:'0',width:1,height:1,iter:64};
const guarded=worker(a.referenceWorkerSource(),message),fast=worker(a.fastReferenceWorkerSource(),message);
assert.equal(guarded.checkpointMismatch,false);assert.equal(guarded.refLen,64);assert.equal(fast.refLen,64);assert.deepEqual(Buffer.from(guarded.refs),Buffer.from(fast.refs));
assert.equal(fast.series.jump,0);assert.equal(fast.series.coeffs.length,8);
const escaped=worker(a.precisionFallbackWorkerSource(),{type:'solve',id:2,bits:256,targetBits:128,re:(3n*scale).toString(),im:'0',span:'0',width:1,height:1,iter:64,terminalClass:-1,indices:Uint32Array.of(0).buffer});
assert.equal(new Uint8Array(escaped.accepted)[0],1);assert.equal(new Uint32Array(escaped.meta)[0]>>>28,1);
const cpu=worker(a.cpuFallbackWorkerSource(),{type:'render',id:3,w:4,h:4,y0:0,y1:4,maxIter:64,re:-.5,im:0,span:3.4,style:{palette:0,cycle:1,shift:0}});
assert.equal(cpu.type,'done');assert.equal(new Uint8ClampedArray(cpu.rgba).length,4*4*4);assert.ok(cpu.work>0);
}
assert.match(a.recoverNumericalGuided.toString(),/FAILURE_GUIDED_REF_MAX_PASSES/);
console.log('PASS: single-source finite viewer regression (production JS, 19 WGSL kernels, staged scoring, lifecycle, export metadata)');