sss
This commit is contained in:
parent
0c61354a5a
commit
ec61f13f4c
21 changed files with 69565 additions and 198 deletions
26013
docs/NEXT_ACTIVE_WORK_DATA.json
Normal file
26013
docs/NEXT_ACTIVE_WORK_DATA.json
Normal file
File diff suppressed because it is too large
Load diff
178
docs/NEXT_QUALITY_PLAN.md
Normal file
178
docs/NEXT_QUALITY_PLAN.md
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
# 追加の軽量化・高品質化の実装計画
|
||||
|
||||
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倍解像度からの参考画像 |
|
||||
| --- | --- | --- |
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|
||||
参考画像の完成時間は初期表示約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を確認した。
|
||||
1280
docs/NEXT_QUALITY_PLAN_DATA.json
Normal file
1280
docs/NEXT_QUALITY_PLAN_DATA.json
Normal file
File diff suppressed because it is too large
Load diff
37029
docs/NEXT_SHARD_DATA.json
Normal file
37029
docs/NEXT_SHARD_DATA.json
Normal file
File diff suppressed because it is too large
Load diff
1370
docs/PERFORMANCE_BC_DATA.json
Normal file
1370
docs/PERFORMANCE_BC_DATA.json
Normal file
File diff suppressed because it is too large
Load diff
134
docs/PERFORMANCE_CANDIDATES.md
Normal file
134
docs/PERFORMANCE_CANDIDATES.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# 品質を保持する追加軽量化の実測
|
||||
|
||||
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と専用プロファイルは終了・削除済み。
|
||||
1252
docs/PERFORMANCE_CANDIDATES_DATA.json
Normal file
1252
docs/PERFORMANCE_CANDIDATES_DATA.json
Normal file
File diff suppressed because it is too large
Load diff
879
docs/PERFORMANCE_ROUNDING_DATA.json
Normal file
879
docs/PERFORMANCE_ROUNDING_DATA.json
Normal file
|
|
@ -0,0 +1,879 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
879
docs/PERFORMANCE_STATS_DATA.json
Normal file
879
docs/PERFORMANCE_STATS_DATA.json
Normal file
|
|
@ -0,0 +1,879 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,10 @@ BLAと比較専用の監査処理は削除済み。[整理内容と検証範囲]
|
|||
|
||||
色相・時間表示の修正と描画方式統合については[ビューワー修正](VIEWER_FIXES.md)を参照。
|
||||
|
||||
BLAを使わず現行品質を保持する追加候補の比較と、補修集計・丸め定数再利用の実装結果は[追加軽量化の実測](PERFORMANCE_CANDIDATES.md)を参照。
|
||||
|
||||
2026-09-08の追加調査は[軽量化・高品質化の実装計画](NEXT_QUALITY_PLAN.md)を参照。二乗の丸め、継続計算の処理単位、高画質2倍解像度を実測し、比較画像も掲載した。
|
||||
|
||||
## 検証
|
||||
|
||||
Node.js 22以降で実行する。追加パッケージは不要。
|
||||
|
|
|
|||
BIN
docs/quality-plan-images/mid-current.png
Normal file
BIN
docs/quality-plan-images/mid-current.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/quality-plan-images/mid-high2.png
Normal file
BIN
docs/quality-plan-images/mid-high2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/quality-plan-images/mid-reference.png
Normal file
BIN
docs/quality-plan-images/mid-reference.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/quality-plan-images/reset-current.png
Normal file
BIN
docs/quality-plan-images/reset-current.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/quality-plan-images/reset-high2.png
Normal file
BIN
docs/quality-plan-images/reset-high2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
docs/quality-plan-images/reset-reference.png
Normal file
BIN
docs/quality-plan-images/reset-reference.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
435
index.html
435
index.html
|
|
@ -1,44 +1,46 @@
|
|||
<!doctype html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta name="mandelbrot-bundle" content="single-file-v24.4.0">
|
||||
<meta name="mandelbrot-bundle" content="single-file-v24.4.1">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||
<meta name="theme-color" content="#232a38">
|
||||
<title>Mandelbrot Deep Zoom v24.4.0</title>
|
||||
<title>Mandelbrot Deep Zoom v24.4.1</title>
|
||||
<style>
|
||||
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
|
||||
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#000;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
|
||||
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#000;image-rendering:auto;touch-action:none}
|
||||
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
|
||||
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
|
||||
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand-return{display:block;margin-top:3px;color:var(--muted);font-size:10px;font-weight:650;letter-spacing:0;text-decoration:none}.brand-return:hover{text-decoration:underline;color:var(--text)}.brand-return:focus-visible{outline:2px solid #fff;outline-offset:2px;border-radius:3px}
|
||||
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
|
||||
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));max-height:calc(100dvh - 20px);overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
|
||||
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.18);background:#000;color:#fff;min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}select option{background:#000;color:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
|
||||
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
|
||||
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
|
||||
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
|
||||
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
|
||||
.render-info{margin-top:8px;padding:8px 9px;border:1px solid rgba(255,255,255,.09);border-radius:12px;background:rgba(255,255,255,.045);font:10.5px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;color:#d9dfed;overflow:hidden}.info-line{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-main{display:flex;justify-content:space-between;gap:10px}.info-main #badge{min-width:0;overflow:hidden;text-overflow:ellipsis}.info-main #render{flex:none;color:var(--muted)}.info-key{color:var(--muted)}
|
||||
.bottom{display:flex;justify-content:flex-end}.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
|
||||
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
|
||||
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
@media(max-width:700px){.stats{display:none}.brand-return{font-size:9.5px}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.compact-status{display:block}button,select{min-height:44px}}
|
||||
@media(max-width:700px){.brand-return{font-size:9.5px}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}button,select{min-height:44px}}
|
||||
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
|
||||
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
|
||||
@media(prefers-reduced-transparency:reduce){.brand,.panel,.toast,#uiToggle{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
|
||||
<div class="top"><div class="brand"><span>MANDELBROT DEEP ZOOM</span><a class="brand-return" href="https://2012r2.nishi.boats/~333/">333の部屋に戻る</a></div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord">…</span></div><div class="row"><span class="muted">倍率</span> <span id="zoom">…</span> <span class="muted">表示幅</span> <span id="span">…</span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render">…</span></div></div></div>
|
||||
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
|
||||
<div class="top"><div class="brand"><span>MANDELBROT DEEP ZOOM</span><a class="brand-return" href="https://2012r2.nishi.boats/~333/">333の部屋に戻る</a></div></div>
|
||||
<div id="controls" class="panel">
|
||||
<div class="toolbar"><button id="zin" aria-label="中心を拡大">+</button><button id="zout" aria-label="中心を縮小">−</button><button id="reset">リセット</button><button id="png">出力</button></div>
|
||||
<div class="group">
|
||||
<div class="line"><label for="quality">画質</label><select id="quality"><option value="fast">高速</option><option value="standard" selected>標準</option><option value="high">高画質</option></select><output></output></div>
|
||||
<div class="line"><label for="palette">彩色</label><select id="palette" aria-label="彩色プリセット"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option><option value="3">インフェルノ</option><option value="5">氷晶</option><option value="7">オーロラ</option><option value="10">黄金</option><option value="13">電光</option><option value="15">蒼橙</option><option value="19">ピンクと黒</option></select><output></output></div>
|
||||
<div class="line"><label for="quality">画質</label><select id="quality"><option value="fast">高速</option><option value="standard" selected>標準</option><option value="high">高画質</option></select></div>
|
||||
<div class="line"><label for="palette">彩色</label><select id="palette" aria-label="彩色プリセット"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option><option value="3">インフェルノ</option><option value="5">氷晶</option><option value="7">オーロラ</option><option value="10">黄金</option><option value="13">電光</option><option value="15">蒼橙</option><option value="19">ピンクと黒</option></select></div>
|
||||
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0" max="1000" step="1" value="532" aria-label="色周期(対数スケール)"><output id="cycleO" for="cycle">0.0080</output></div>
|
||||
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
|
||||
<div class="mini-actions"><button id="colorAuto" type="button" aria-pressed="false" title="色周期は固定し、色相位置だけを動かします">色相変化</button></div>
|
||||
<div class="render-info" aria-label="描画情報">
|
||||
<div class="info-line info-main"><span id="badge">起動中</span><span id="render">…</span></div>
|
||||
<div class="info-line"><span class="info-key">中心</span> <span id="coord">…</span></div>
|
||||
<div class="info-line"><span class="info-key">倍率</span> <span id="zoom">…</span> · <span class="info-key">幅</span> <span id="span">…</span> · <span id="engine">起動中…</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
|
||||
<div class="group bottom"><button id="share">URL共有</button></div>
|
||||
</div>
|
||||
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI−</button>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
|
|
@ -122,6 +124,7 @@ fn analytic(cr:f32,ci:f32)->bool{
|
|||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(((fieldMeta[out]>>28u)&3u)==FIELD_INTERIOR_PROVEN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX;
|
||||
let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
let scale=p.span/f32(p.fullW);
|
||||
|
|
@ -150,6 +153,29 @@ const ACTIVE_PREPARE_WGSL=String.raw`
|
|||
@compute @workgroup_size(1) fn main(){let n=atomicLoad(&activeCount[0]);indirect[0]=(n+63u)/64u;indirect[1]=1u;indirect[2]=1u;}
|
||||
`;
|
||||
|
||||
const OPERATION_LIMIT_QUEUE_WGSL=COMMON+String.raw`
|
||||
struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32};
|
||||
struct DeepState{d:vec2<f32>,w:vec2<f32>,scaleExp:i32,n:u32,m:u32,errScaled:f32};
|
||||
@group(0) @binding(0) var<uniform> p:Params;
|
||||
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
|
||||
@group(0) @binding(2) var<storage,read_write> states:array<DeepState>;
|
||||
@group(0) @binding(3) var<storage,read_write> queue:array<u32>;
|
||||
@group(0) @binding(4) var<storage,read_write> count:array<atomic<u32>>;
|
||||
@group(0) @binding(5) var<storage,read_write> activeMark:array<atomic<u32>>;
|
||||
@compute @workgroup_size(256)
|
||||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
let out=gid.x;if(out>=p.fullW*p.fullH){return;}
|
||||
let packed=fieldMeta[out];
|
||||
if(((packed>>28u)&3u)!=FIELD_UNKNOWN||((packed&REASON_MASK)>>REASON_SHIFT)!=REASON_OPERATION_LIMIT){return;}
|
||||
let claim=atomicCompareExchangeWeak(&activeMark[out],0u,1u);if(!claim.exchanged){return;}
|
||||
let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;
|
||||
let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW);
|
||||
let d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);
|
||||
states[out]=DeepState(d,vec2<f32>(0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));
|
||||
let q=atomicAdd(&count[0],1u);queue[q]=out;
|
||||
}
|
||||
`;
|
||||
|
||||
const FAST_PERTURB_WGSL=COMMON+String.raw`
|
||||
struct Params{
|
||||
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
|
||||
|
|
@ -202,6 +228,7 @@ fn render_fast(out:u32,gx:f32,gy:f32){
|
|||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(((fieldMeta[out]>>28u)&3u)==FIELD_INTERIOR_PROVEN){return;}
|
||||
if(p.unknownOnly!=0u){
|
||||
let prior=fieldMeta[out];let cls=(prior>>28u)&3u;
|
||||
if(cls!=FIELD_UNKNOWN){return;}
|
||||
|
|
@ -313,6 +340,7 @@ fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
|
|||
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
||||
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
|
||||
let out=p.outputBase+gid.y*p.outputStride+gid.x;
|
||||
if(((fieldMeta[out]>>28u)&3u)==FIELD_INTERIOR_PROVEN){return;}
|
||||
if(p.unknownOnly!=0u && ((fieldMeta[out]>>28u)&3u)!=FIELD_UNKNOWN){return;}
|
||||
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
|
||||
render_pixel(out,gx,gy,p.strict!=0u);
|
||||
|
|
@ -323,16 +351,16 @@ fn main(@builtin(global_invocation_id) gid:vec3<u32>){
|
|||
const DEEP_ACTIVE_RESUME_INIT_WGSL=COMMON+String.raw`
|
||||
struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32};
|
||||
struct DeepState{d:vec2<f32>,w:vec2<f32>,scaleExp:i32,n:u32,m:u32,errScaled:f32};
|
||||
@group(0) @binding(0) var<uniform> p:Params;@group(0) @binding(1) var<storage,read_write> states:array<DeepState>;@group(0) @binding(2) var<storage,read> queue:array<u32>;
|
||||
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){let qi=gid.x;if(qi>=p.unknownOnly){return;}let out=queue[qi];if(out>=p.fullW*p.fullH){return;}let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW);let d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);states[out]=DeepState(d,vec2<f32>(0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));}
|
||||
@group(0) @binding(0) var<uniform> p:Params;@group(0) @binding(1) var<storage,read_write> states:array<DeepState>;@group(0) @binding(2) var<storage,read> queue:array<u32>;@group(0) @binding(3) var<storage,read_write> activeMark:array<atomic<u32>>;
|
||||
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){let qi=gid.x;if(qi>=p.unknownOnly){return;}let out=queue[qi];if(out>=p.fullW*p.fullH){return;}let px=out%p.fullW;let py=out/p.fullW;let gx=f32(px)+p.sampleX;let gy=f32(py)+p.sampleY;let dx=(gx-p.refPixelX)/f32(p.fullW);let dy=(p.refPixelY-gy)/f32(p.fullW);let d=vec2<f32>(p.spanMantHi*dx,p.spanMantHi*dy);states[out]=DeepState(d,vec2<f32>(0.0),p.spanExp,0u,0u,1.0*F32_U*maxabs(d));atomicStore(&activeMark[out],1u);}
|
||||
`;
|
||||
|
||||
const DEEP_ACTIVE_CONTINUE_WGSL=COMMON+String.raw`
|
||||
struct Params{tileW:u32,tileH:u32,fullW:u32,fullH:u32,tileX:u32,tileY:u32,maxIter:u32,refLen:u32,strict:u32,unknownOnly:u32,outputStride:u32,outputBase:u32,spanMantHi:f32,spanExp:i32,sampleX:f32,sampleY:f32,refPixelX:f32,refPixelY:f32,spanMantLo:f32,invFullWHi:f32,invFullWLo:f32,targetIter:u32,chunkSize:u32,_numeric2:u32};
|
||||
struct RefPoint{hi:vec2<f32>,lo:vec2<f32>};struct DeepState{d:vec2<f32>,w:vec2<f32>,scaleExp:i32,n:u32,m:u32,errScaled:f32};
|
||||
@group(0) @binding(0) var<uniform> p:Params;@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;@group(0) @binding(2) var<storage,read_write> states:array<DeepState>;@group(0) @binding(3) var<storage,read> queueIn:array<u32>;@group(0) @binding(4) var<storage,read_write> countIn:array<atomic<u32>>;@group(0) @binding(5) var<storage,read_write> queueOut:array<u32>;@group(0) @binding(6) var<storage,read_write> countOut:array<atomic<u32>>;@group(0) @binding(7) var<storage,read_write> fieldMeta:array<u32>;@group(0) @binding(8) var<storage,read_write> fieldSmooth:array<f32>;
|
||||
fn fail(out:u32,n:u32,reason:u32){fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;}
|
||||
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){if(gid.x>=atomicLoad(&countIn[0])){return;}let local=queueIn[gid.x];let out=p.outputBase+(local/p.tileW)*p.outputStride+(local%p.tileW);if(((fieldMeta[out]>>28u)&3u)==FIELD_INTERIOR_PROVEN){return;}var s=states[local];let stop=min(p.targetIter,s.n+max(1u,p.chunkSize));loop{if(s.n>=stop){break;}if(s.m>p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}let rp=refs[s.m];let delta=scaled_to_f32(s.w,s.scaleExp);let z=rp.hi+(rp.lo+delta);let mag=dot(z,z);if(mag>4.0){let errAbs=safe_abs_error(s.errScaled,s.scaleExp,z,delta);if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(s.n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(s.n,mag);return;}fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;}if(s.m>=p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}let refAbs=maxabs(rp.hi)+maxabs(rp.lo);let wAbs=maxabs(s.w);let dAbs=maxabs(s.d);let deltaAbs=maxabs(delta);let sq=scaled_to_f32(cmul(s.w,s.w),s.scaleExp);let sqAbs=maxabs(sq);let gain=2.0*refAbs+2.0*deltaAbs;let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30);s.errScaled=gain*s.errScaled+roundErr;let linear=2.0*(cmul(rp.hi,s.w)+cmul(rp.lo,s.w));s.w=linear+sq+s.d;s.m++;s.n++;if(maxabs(s.w)>=1.0e30||maxabs(s.d)>=1.0e30){fail(out,s.n,REASON_RANGE);return;}let mm=max(maxabs(s.w),maxabs(s.d));if(mm>65536.0){s.w*=0.0000152587890625;s.d*=0.0000152587890625;s.errScaled*=0.0000152587890625;s.scaleExp+=16;}else if(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp){s.w*=65536.0;s.d*=65536.0;s.errScaled*=65536.0;s.scaleExp-=16;}if(s.scaleExp>126||s.errScaled!=s.errScaled||s.errScaled>1.0e35){fail(out,s.n,REASON_RANGE);return;}}
|
||||
@group(0) @binding(0) var<uniform> p:Params;@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;@group(0) @binding(2) var<storage,read_write> states:array<DeepState>;@group(0) @binding(3) var<storage,read> queueIn:array<u32>;@group(0) @binding(4) var<storage,read_write> countIn:array<atomic<u32>>;@group(0) @binding(5) var<storage,read_write> queueOut:array<u32>;@group(0) @binding(6) var<storage,read_write> countOut:array<atomic<u32>>;@group(0) @binding(7) var<storage,read_write> fieldMeta:array<u32>;@group(0) @binding(8) var<storage,read_write> fieldSmooth:array<f32>;@group(0) @binding(9) var<storage,read_write> activeMark:array<atomic<u32>>;
|
||||
fn fail(out:u32,n:u32,reason:u32){fieldMeta[out]=pack_unknown(n,reason);fieldSmooth[out]=0.0;atomicStore(&activeMark[out],0u);}
|
||||
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) gid:vec3<u32>){if(gid.x>=atomicLoad(&countIn[0])){return;}let local=queueIn[gid.x];let out=p.outputBase+(local/p.tileW)*p.outputStride+(local%p.tileW);if(((fieldMeta[out]>>28u)&3u)==FIELD_INTERIOR_PROVEN){atomicStore(&activeMark[out],0u);return;}var s=states[local];let stop=min(p.targetIter,s.n+max(1u,p.chunkSize));loop{if(s.n>=stop){break;}if(s.m>p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}let rp=refs[s.m];let delta=scaled_to_f32(s.w,s.scaleExp);let z=rp.hi+(rp.lo+delta);let mag=dot(z,z);if(mag>4.0){let errAbs=safe_abs_error(s.errScaled,s.scaleExp,z,delta);if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(s.n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(s.n,mag);atomicStore(&activeMark[out],0u);return;}fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;}if(s.m>=p.refLen){fail(out,s.n,REASON_REFERENCE_END);return;}let refAbs=maxabs(rp.hi)+maxabs(rp.lo);let wAbs=maxabs(s.w);let dAbs=maxabs(s.d);let deltaAbs=maxabs(delta);let sq=scaled_to_f32(cmul(s.w,s.w),s.scaleExp);let sqAbs=maxabs(sq);let gain=2.0*refAbs+2.0*deltaAbs;let roundErr=1.0*F32_U*(2.0*refAbs*wAbs+sqAbs+dAbs+1.0e-30);s.errScaled=gain*s.errScaled+roundErr;let linear=2.0*(cmul(rp.hi,s.w)+cmul(rp.lo,s.w));s.w=linear+sq+s.d;s.m++;s.n++;if(maxabs(s.w)>=1.0e30||maxabs(s.d)>=1.0e30){fail(out,s.n,REASON_RANGE);return;}let mm=max(maxabs(s.w),maxabs(s.d));if(mm>65536.0){s.w*=0.0000152587890625;s.d*=0.0000152587890625;s.errScaled*=0.0000152587890625;s.scaleExp+=16;}else if(mm>0.0&&mm<0.0000152587890625&&s.scaleExp>p.spanExp){s.w*=65536.0;s.d*=65536.0;s.errScaled*=65536.0;s.scaleExp-=16;}if(s.scaleExp>126||s.errScaled!=s.errScaled||s.errScaled>1.0e35){fail(out,s.n,REASON_RANGE);return;}}
|
||||
// Evaluate z_target before adopting the finite-budget result. A valid
|
||||
// nonescaping state stays in the queue for the next target.
|
||||
if(s.n>=p.targetIter){
|
||||
|
|
@ -342,13 +370,13 @@ if(s.n>=p.targetIter){
|
|||
let errAbs=safe_abs_error(s.errScaled,s.scaleExp,zEnd,deltaEnd);
|
||||
if(mag!=mag||errAbs!=errAbs){fail(out,s.n,REASON_RANGE);return;}
|
||||
if(mag>4.0){
|
||||
if(length(zEnd)-errAbs>2.0){fieldMeta[out]=pack_meta(s.n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(s.n,mag);return;}
|
||||
if(length(zEnd)-errAbs>2.0){fieldMeta[out]=pack_meta(s.n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(s.n,mag);atomicStore(&activeMark[out],0u);return;}
|
||||
fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;
|
||||
}
|
||||
if(length(zEnd)+errAbs>2.0){fail(out,s.n,REASON_ESCAPE_UNCERTAIN);return;}
|
||||
let limit=select(1.0e-3,1.0e-4,p.strict!=0u);
|
||||
if(errAbs>limit){fail(out,s.n,REASON_ERROR_BOUND);return;}
|
||||
fail(out,s.n,REASON_OPERATION_LIMIT);
|
||||
fieldMeta[out]=pack_unknown(s.n,REASON_OPERATION_LIMIT);fieldSmooth[out]=0.0;
|
||||
}
|
||||
states[local]=s;let q=atomicAdd(&countOut[0],1u);queueOut[q]=local;}
|
||||
`;
|
||||
|
|
@ -989,7 +1017,7 @@ fn main(@builtin(local_invocation_id) lid:vec3<u32>,@builtin(workgroup_id) wid:v
|
|||
`;
|
||||
|
||||
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
|
||||
version:'24.4.0-bounded-continuation',DEEP_COMPACT_INIT_WGSL,DEEP_COMPACT_CONTINUE_WGSL,INTERIOR_MASK_WGSL,PRECISION_SCATTER_WGSL,DIRECT_F32_WGSL,ACTIVE_PREPARE_WGSL,FAST_PERTURB_WGSL,FAST_PERTURB_POSTSTATS_WGSL,DEEP_PERTURB_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,DEEP_ACTIVE_RESUME_INIT_WGSL,DEEP_ACTIVE_CONTINUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,UNKNOWN_STATS_WGSL,SYMMETRY_COPY_WGSL,FAILURE_TILE_MAP_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
|
||||
version:'24.5.0-gpu-resident-frontier',DEEP_COMPACT_INIT_WGSL,DEEP_COMPACT_CONTINUE_WGSL,INTERIOR_MASK_WGSL,PRECISION_SCATTER_WGSL,DIRECT_F32_WGSL,ACTIVE_PREPARE_WGSL,OPERATION_LIMIT_QUEUE_WGSL,FAST_PERTURB_WGSL,FAST_PERTURB_POSTSTATS_WGSL,DEEP_PERTURB_WGSL,DEEP_PERTURB_POSTSTATS_WGSL,DEEP_ACTIVE_RESUME_INIT_WGSL,DEEP_ACTIVE_CONTINUE_WGSL,DEEP_BUCKET_HIST_WGSL,DEEP_BUCKET_PREFIX_WGSL,DEEP_BUCKET_SCATTER_WGSL,DEEP_CORRECT_WGSL,DEEP_CORRECT_QUEUE_WGSL,UNKNOWN_STATS_WGSL,SYMMETRY_COPY_WGSL,FAILURE_TILE_MAP_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
|
||||
});
|
||||
})();
|
||||
|
||||
|
|
@ -997,26 +1025,23 @@ globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
|
|||
<script data-bundle="app">
|
||||
(()=>{'use strict';
|
||||
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
|
||||
const $=s=>document.querySelector(s),canvas=$('#view');
|
||||
const $=id=>document.getElementById(id),canvas=$('view');
|
||||
const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
|
||||
const CYCLE_MIN=.001,CYCLE_MAX=.05,CYCLE_SLIDER_MAX=1000,PALETTE_MAX=19,VALID_PALETTE_IDS=new Set([0,1,2,3,5,7,10,13,15,19]);
|
||||
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3,ITER_MASK=0x000fffff,REASON_SHIFT=20;
|
||||
const UNKNOWN_REASON_NAMES=['none','error-bound','escape-uncertain','reference-end','rebase-gap','range','operation-limit','ds-sensitivity'];
|
||||
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,ITER_MASK=0x000fffff,REASON_SHIFT=20;
|
||||
const NUMERIC_PARAM_BYTES=96,UNRESOLVED_BYTES=32,SPARSE_QUEUE_STATS_BYTES=32,SPARSE_INDIRECT_BYTES=16,DEEP_BUCKET_STATE_BYTES=96;
|
||||
const PIXEL_FRONTIER_PRODUCTION=true,PIXEL_FRONTIER_WORK=4000000,PIXEL_FRONTIER_MAX_ITER=150000;
|
||||
const PIXEL_FRONTIER_ENABLED=PIXEL_FRONTIER_PRODUCTION;
|
||||
const QUALITY_PRESETS=Object.freeze({
|
||||
const PIXEL_FRONTIER_WORK=4000000,ACTIVE_CONTINUATION_WORK=8000000,PIXEL_FRONTIER_MAX_ITER=150000;
|
||||
const QUALITY_PRESETS={
|
||||
fast:{scale:.75,pixels:524288,iterations:4096},
|
||||
standard:{scale:1,pixels:1048576,iterations:16384},
|
||||
high:{scale:1.5,pixels:2359296,iterations:32768}
|
||||
});
|
||||
high:{scale:2,pixels:4194304,iterations:32768}
|
||||
};
|
||||
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;state.hq=false}
|
||||
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,continuationBudget:16384,hq:false,quality:'standard',renderMode:'fast',palette:0,cycle:.008,shift:.18,colorAuto:false,colorRevision:0,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,renderClock:null,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,unknownReasons:null,correctionPasses:0,correctedPixels:0,multiRefPasses:0,multiRefRecovered:0,frontierRounds:0,frontierPixels:0,frontierEscaped:0,frontierIter:0,frontierMinimumIter:0,frontierConverged:false,backendDecision:null,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false,qualityStage:'idle',qualityIter:0,qualityFinalIter:0,previewMs:0,refineMs:0,refinePending:false,temporalFill:false,numericRecoveryPending:false,numericRecoveryRunning:false,gpuStage:'',gpuTileRows:0};
|
||||
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,lastRender:0,renderClock:null,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,unresolved:0,unknownReasons:null,multiRefPasses:0,multiRefRecovered:0,frontierRounds:0,frontierPixels:0,frontierEscaped:0,frontierIter:0,frontierMinimumIter:0,frontierConverged:false,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),uiHidden:false,qualityStage:'idle',qualityIter:0,qualityFinalIter:0,previewMs:0,refineMs:0,refinePending:false,temporalFill:false,numericRecoveryPending:false,numericRecoveryRunning:false,gpuStage:''};
|
||||
let renderer=null,rendererInitPromise=null,raf=0,settleTimer=0,refineTimer=0,lastWrittenHash='',navigationHash='';
|
||||
const viewHistory=[];let viewHistoryIndex=-1;
|
||||
const TEST_MODE=new URLSearchParams(location.search).has('test');
|
||||
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0,correctionPasses:0,multiRefBuilds:0,numericSubmits:0,correctionSubmits:0,gpuSyncWaits:0,stageCounts:{},stageMs:{},generationMetrics:[]};
|
||||
const runtime={numericSubmits:0,correctionSubmits:0,gpuSyncWaits:0,stageCounts:{},stageMs:{},generationMetrics:[]};
|
||||
function generationMetric(token=state.token){for(let i=runtime.generationMetrics.length-1;i>=0;i--)if(runtime.generationMetrics[i].token===token)return runtime.generationMetrics[i];return null}
|
||||
|
||||
// ── exact fixed-point view state ─────────────────────────────────────────
|
||||
|
|
@ -1031,17 +1056,10 @@ function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=
|
|||
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
|
||||
function log2Fixed(v){return log2FixedAt(v,state.bits)}
|
||||
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
|
||||
function f32ToFixed(x,bits){
|
||||
x=Math.fround(x);if(!Number.isFinite(x))throw new Error('non-finite f32 split component');if(x===0)return 0n;
|
||||
const ab=new ArrayBuffer(4),dv=new DataView(ab);dv.setFloat32(0,x,false);const u=dv.getUint32(0,false),neg=(u>>>31)!==0,eb=(u>>>23)&255,frac=u&0x7fffff,m=BigInt(eb===0?frac:(0x800000|frac)),e=(eb===0?-149:eb-127-23),sh=bits+e;
|
||||
let q=sh>=0?m<<BigInt(sh):roundDivSigned(m,1n<<BigInt(-sh));return neg?-q:q;
|
||||
}
|
||||
function splitFixedF32(v,bits,count=3){const out=[];let r=v;for(let i=0;i<count;i++){const x=Math.fround(fixedNum(r,bits));out.push(x);if(x===0)continue;r-=f32ToFixed(x,bits)}return out}
|
||||
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
|
||||
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
|
||||
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
|
||||
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
|
||||
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
|
||||
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
|
||||
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
|
||||
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
|
||||
|
|
@ -1051,12 +1069,7 @@ function directPixelRatio(snap,w){const cr=Math.abs(fixedNum(snap.re,snap.bits))
|
|||
function fastNeedsExtended(snap,w){return !(directPixelRatio(snap,w)>=4)}
|
||||
function chooseBackend(snap=snapshot(),w=canvas.width){const fastExtended=fastNeedsExtended(snap,w);return{backend:fastExtended?'fast-extended':'direct',deep:false,fastExtended,probe:false,reason:fastExtended?'auto-fast-extended':'auto-direct'}}
|
||||
function pixelPrecisionBits(snap=snapshot(),w=canvas.width,guard=32){const pixelLog2=log2FixedAt(snap.span,snap.bits)-Math.log2(Math.max(1,w)),need=Math.ceil(Math.max(0,-pixelLog2))+Math.max(16,guard|0);return Math.max(96,Math.min(snap.bits+64,need))}
|
||||
function deepNeeded(){return false}
|
||||
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive,continuationBudget:state.continuationBudget,quality:state.quality,renderMode:state.renderMode}}
|
||||
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive,v.continuationBudget||0,v.quality||'standard'].join(':')}
|
||||
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
|
||||
function numericViewKey(){return[state.bits,state.re,state.im,state.span,state.baseIter,state.adaptive,state.continuationBudget,state.quality].join(':')}
|
||||
function restoreView(v){if(!v)return;const before=numericViewKey();Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:VALID_PALETTE_IDS.has(v.palette)?v.palette:0,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive,continuationBudget:v.continuationBudget||0,renderMode:'fast'});applyQuality(v.quality||'standard');ensurePrecision();resize();syncControls();saveHash(false);if(before===numericViewKey())requestRecolor();else markDirty()}
|
||||
|
||||
// ── iteration / quality policy ───────────────────────────────────────────
|
||||
const ADAPTIVE_ITER_QUANTUM=64;
|
||||
|
|
@ -1088,10 +1101,10 @@ function reprojectionSafe(t,maxZoom=1.30,maxShift=.18){const s=Math.abs(Number(t
|
|||
function cycleToSlider(c){const x=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(c)||CYCLE_MIN));return Math.round(CYCLE_SLIDER_MAX*Math.log(x/CYCLE_MIN)/Math.log(CYCLE_MAX/CYCLE_MIN))}
|
||||
function sliderToCycle(v){const t=Math.max(0,Math.min(CYCLE_SLIDER_MAX,Number(v)||0))/CYCLE_SLIDER_MAX;return CYCLE_MIN*Math.pow(CYCLE_MAX/CYCLE_MIN,t)}
|
||||
function fract01(x){return x-Math.floor(x)}
|
||||
function colorStyleSnapshot(){return{palette:state.palette,cycle:state.cycle,shift:state.shift,hq:state.hq}}
|
||||
function colorStyleKey(style=colorStyleSnapshot()){return[style.palette,style.cycle,style.shift,style.hq?1:0].join(':')}
|
||||
function colorStyleSnapshot(){return{palette:state.palette,cycle:state.cycle,shift:state.shift}}
|
||||
function colorStyleKey(style=colorStyleSnapshot()){return style.palette+':'+style.cycle+':'+style.shift}
|
||||
function pixelBudget(){const preset=QUALITY_PRESETS[state.quality],low=Number(navigator.deviceMemory||8)<=4,limit=renderer?.device?.limits?.maxStorageBufferBindingSize||134217728;return Math.max(4096,Math.min(Math.floor(preset.pixels*(low?.5:1)),Math.floor(limit/40)))}
|
||||
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native*QUALITY_PRESETS[state.quality].scale,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
|
||||
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native*QUALITY_PRESETS[state.quality].scale,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
|
||||
|
||||
// ── high precision reference worker ─────────────────────────────────────
|
||||
function referenceWorkerSource(){return String.raw`
|
||||
|
|
@ -1108,10 +1121,10 @@ function orbit(bits,re,im,iter){
|
|||
let r=old||{rr:new Float64Array(iter+1),ri:new Float64Array(iter+1),zr:0n,zi:0n,zr2:0n,zi2:0n,n:0,escape:0,terminalEscape:0,verifiedTo:-1};
|
||||
const previousLen=r.n;
|
||||
if(r.rr.length<=iter){const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);rr.set(r.rr);ri.set(r.ri);r.rr=rr;r.ri=ri;}
|
||||
const bail=16n*(1n<<BigInt(bits));
|
||||
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},bail=16n*(1n<<B);
|
||||
while(r.n<iter&&!r.terminalEscape){
|
||||
r.zi=roundShift(2n*r.zr*r.zi,bits)+im;r.zr=r.zr2-r.zi2+re;
|
||||
r.zr2=roundShift(r.zr*r.zr,bits);r.zi2=roundShift(r.zi*r.zi,bits);r.n++;
|
||||
r.zi=round(2n*r.zr*r.zi)+im;r.zr=r.zr2-r.zi2+re;
|
||||
r.zr2=(r.zr*r.zr+half)>>B;r.zi2=(r.zi*r.zi+half)>>B;r.n++;
|
||||
r.rr[r.n]=fixedNum(r.zr,bits);r.ri[r.n]=fixedNum(r.zi,bits);
|
||||
if(r.zr2+r.zi2>bail)r.terminalEscape=r.n;
|
||||
}
|
||||
|
|
@ -1120,19 +1133,17 @@ function orbit(bits,re,im,iter){
|
|||
while(orbitCache.size>4)orbitCache.delete(orbitCache.keys().next().value);
|
||||
return r;
|
||||
}
|
||||
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<iter;n++){const zri=roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2>BAIL)return n+1}return 0}
|
||||
function orbitEscape(bits,re,im,iter){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},BAIL=16n*(1n<<B);let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<iter;n++){zi=round(2n*zr*zi)+im;zr=zr2-zi2+re;zr2=(zr*zr+half)>>B;zi2=(zi*zi+half)>>B;if(zr2+zi2>BAIL)return n+1}return 0}
|
||||
function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const out=[{re,im}],den=8n,bh=BigInt(h),bw=BigInt(w);for(let gy=-3;gy<=3;gy++)for(let gx=-3;gx<=3;gx++){if(!gx&&!gy)continue;out.push({re:re+span*BigInt(gx)/den,im:im+span*bh*BigInt(gy)/(den*bw)})}return out}
|
||||
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.score){best={...c,escape,score}}if(escape===0)break}return best}
|
||||
function samePacked(a,b){const ah=Math.fround(a),bh=Math.fround(b);return Object.is(ah,bh)&&Object.is(Math.fround(a-ah),Math.fround(b-bh))}
|
||||
function verify(baseBits,re,im,ref,refLen){
|
||||
const high=orbit(baseBits+32,re<<32n,im<<32n,refLen);
|
||||
let mismatch=high.refLen<refLen,checked=0;
|
||||
// Checkpoints are independent of the requested orbit length; every new
|
||||
// endpoint is also compared, including an escaping endpoint.
|
||||
for(let n=Math.max(0,ref.verifiedTo+1);n<=refLen&&!mismatch;n++){
|
||||
if(n!==refLen&&n%256!==0&&(n&(n-1))!==0)continue;
|
||||
checked++;if(!samePacked(high.rr[n],ref.rr[n])||!samePacked(high.ri[n],ref.ri[n]))mismatch=true;
|
||||
}
|
||||
const bits=baseBits+32,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},BAIL=16n*(1n<<B),hre=re<<32n,him=im<<32n,from=Math.max(0,ref.verifiedTo+1);
|
||||
let zr=0n,zi=0n,zr2=0n,zi2=0n,n=0,escape=0,mismatch=false,checked=0;
|
||||
const check=()=>{if(n<from||n!==refLen&&n%256!==0&&(n&(n-1))!==0)return;checked++;if(!samePacked(fixedNum(zr,bits),ref.rr[n])||!samePacked(fixedNum(zi,bits),ref.ri[n]))mismatch=true};
|
||||
check();
|
||||
while(n<refLen&&!escape&&!mismatch){zi=round(2n*zr*zi)+him;zr=zr2-zi2+hre;zr2=(zr*zr+half)>>B;zi2=(zi*zi+half)>>B;n++;check();if(zr2+zi2>BAIL)escape=n}
|
||||
if(n<refLen)mismatch=true;
|
||||
if(!mismatch)ref.verifiedTo=Math.max(ref.verifiedTo,refLen);
|
||||
return{mismatch,checked};
|
||||
}
|
||||
|
|
@ -1141,8 +1152,8 @@ self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=perfor
|
|||
`}
|
||||
class ReferenceService{
|
||||
constructor(maxCacheBytes=8*1024*1024){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=new Map();this.failed=false;this.maxCache=8;this.maxCacheBytes=maxCacheBytes}
|
||||
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;if(d.type!=='error'&&d.checkpointMismatch&&p.attempt<3){p.attempt++;p.message.targetBits+=32;this.worker.postMessage(p.message);return}this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,precisionBits:d.precisionBits,referenceId:d.referenceId,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};runtime.referenceBuilds++;this.cache.set(d.key,d);while(this.cache.size>1&&(this.cache.size>this.maxCache||Array.from(this.cache.values()).reduce((n,c)=>n+c.refs.byteLength,0)>this.maxCacheBytes))this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
|
||||
request(snap,iter,width=1,height=1,fresh=false){const targetBits=pixelPrecisionBits(snap,width,40),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'pixel-guarded-perturb-v24.3'].join(':');if(fresh)this.cache.delete(key);const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail}),message={type:'build',id,key,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter};this.pending.set(id,{resolve,reject,key,promise,message,attempt:0,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage(message);return promise}
|
||||
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;if(d.type!=='error'&&d.checkpointMismatch&&p.attempt<3){p.attempt++;p.message.targetBits+=32;this.worker.postMessage(p.message);return}this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{d.source={...p.source,precisionBits:d.precisionBits,referenceId:d.referenceId,re:d.referenceRe!=null?BigInt(d.referenceRe):p.source.re,im:d.referenceIm!=null?BigInt(d.referenceIm):p.source.im};this.cache.set(d.key,d);while(this.cache.size>1&&(this.cache.size>this.maxCache||Array.from(this.cache.values()).reduce((n,c)=>n+c.refs.byteLength,0)>this.maxCacheBytes))this.cache.delete(this.cache.keys().next().value);p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
|
||||
request(snap,iter,width=1,height=1){const targetBits=pixelPrecisionBits(snap,width,40),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,'pixel-guarded-perturb-v24.3'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail}),message={type:'build',id,key,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter};this.pending.set(id,{resolve,reject,key,promise,message,attempt:0,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage(message);return promise}
|
||||
requestFixed(snap,iter,width,height,fixedSource){if(!fixedSource)return this.request(snap,iter,width,height);const targetBits=Math.max(pixelPrecisionBits(snap,width,40),fixedSource.precisionBits||0),key=[snap.bits,snap.re,snap.im,snap.span,iter,width,height,targetBits,fixedSource.re,fixedSource.im,'pixel-fixed-reference-v24.3'].join(':');const hit=this.cache.get(key);if(hit)return Promise.resolve(hit);for(const p of this.pending.values())if(p.key===key)return p.promise;if(this.pending.size)this.cancelPending('superseded fixed reference request');if(!this.ensure())return Promise.reject(new Error('Reference Worker unavailable'));const id=++this.serial;let resolve,reject;const promise=new Promise((ok,fail)=>{resolve=ok;reject=fail}),message={type:'build',id,key,bits:snap.bits,targetBits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString(),width,height,iter,fixedRe:fixedSource.re.toString(),fixedIm:fixedSource.im.toString()};this.pending.set(id,{resolve,reject,key,promise,message,attempt:0,source:{bits:snap.bits,re:snap.re,im:snap.im,viewRe:snap.re,viewIm:snap.im,span:snap.span,iter}});this.worker.postMessage(message);return promise}
|
||||
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
|
||||
destroy(){this.cancelPending('destroyed');this.cache.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
|
||||
|
|
@ -1157,8 +1168,8 @@ function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
|||
function roundShift(v,b){if(b<=0)return v;const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
|
||||
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
|
||||
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<<BigInt(d):roundShift(v,-d)}
|
||||
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zri=roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
|
||||
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<iter;n++){const zri=roundShift(2n*zr*zi,bits);zi=zri+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);if(zr2+zi2>BAIL)return n+1}return 0}
|
||||
function orbit(bits,re,im,iter){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},BAIL=16n*(1n<<B);let zr=0n,zi=0n,zr2=0n,zi2=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);zi=round(2n*zr*zi)+im;zr=zr2-zi2+re;zr2=(zr*zr+half)>>B;zi2=(zi*zi+half)>>B;if(zr2+zi2>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
|
||||
function orbitEscape(bits,re,im,iter){const B=BigInt(bits),half=1n<<(B-1n),round=v=>{const neg=v<0n,a=neg?-v:v,q=(a+half)>>B;return neg?-q:q},BAIL=16n*(1n<<B);let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<iter;n++){zi=round(2n*zr*zi)+im;zr=zr2-zi2+re;zr2=(zr*zr+half)>>B;zi2=(zi*zi+half)>>B;if(zr2+zi2>BAIL)return n+1}return 0}
|
||||
function referenceCandidates(re,im,span,w,h){if(span<=0n||w<=0||h<=0)return[{re,im}];const out=[{re,im}],den=8n,bh=BigInt(h),bw=BigInt(w);for(let gy=-3;gy<=3;gy++)for(let gx=-3;gx<=3;gx++){if(!gx&&!gy)continue;out.push({re:re+span*BigInt(gx)/den,im:im+span*bh*BigInt(gy)/(den*bw)})}return out}
|
||||
function chooseReference(bits,re,im,span,w,h,iter,centerEscape=0){let best={re,im,escape:centerEscape,score:centerEscape||iter};if(centerEscape===0)return best;const list=referenceCandidates(re,im,span,w,h);for(let k=1;k<list.length;k++){const c=list[k],escape=orbitEscape(bits,c.re,c.im,iter),score=escape||iter;if(score>best.score){best={...c,escape,score}}if(escape===0)break}return best}
|
||||
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
|
||||
|
|
@ -1183,7 +1194,8 @@ function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
|
|||
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
|
||||
function requant(v,fromBits,toBits){const d=toBits-fromBits;return d>=0?v<<BigInt(d):roundShift(v,-d)}
|
||||
function smooth(n,mag,bits){const m=Math.max(4.0000005,fixedNum(mag,bits)),u=Math.log2(m);return Math.fround(n+1-Math.log2(Math.max(1e-20,.5*u)))}
|
||||
function pixel(s,index,bits){orbitCount++;const B=BigInt(bits),ONE=1n<<B,w=BigInt(s.width),x=BigInt(index%s.width),y=BigInt(Math.floor(index/s.width)),den=2n*w,re=requant(BigInt(s.re),s.bits,bits),im=requant(BigInt(s.im),s.bits,bits),span=requant(BigInt(s.span),s.bits,bits),cr=re+roundDiv(span*(2n*x+1n-w),den),ci=im+roundDiv(span*(BigInt(s.height)-2n*y-1n),den),bail=4n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<s.iter;n++){zi=roundShift(2n*zr*zi,bits)+ci;zr=zr2-zi2+cr;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);const mag=zr2+zi2;if(mag>bail)return{escape:n+1,smooth:smooth(n+1,mag,bits)}}return{escape:0,smooth:0}}
|
||||
// The rounding width is constant for this orbit, including negative ties.
|
||||
function pixel(s,index,bits){orbitCount++;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=1n<<B,w=BigInt(s.width),x=BigInt(index%s.width),y=BigInt(Math.floor(index/s.width)),den=2n*w,re=requant(BigInt(s.re),s.bits,bits),im=requant(BigInt(s.im),s.bits,bits),span=requant(BigInt(s.span),s.bits,bits),cr=re+roundDiv(span*(2n*x+1n-w),den),ci=im+roundDiv(span*(BigInt(s.height)-2n*y-1n),den),bail=4n*ONE;let zr=0n,zi=0n,zr2=0n,zi2=0n;for(let n=0;n<s.iter;n++){zi=round(2n*zr*zi)+ci;zr=zr2-zi2+cr;zr2=(zr*zr+half)>>B;zi2=(zi*zi+half)>>B;const mag=zr2+zi2;if(mag>bail)return{escape:n+1,smooth:smooth(n+1,mag,bits)}}return{escape:0,smooth:0}}
|
||||
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='solve')return;orbitCount=0;try{const indices=new Uint32Array(d.indices),meta=new Uint32Array(indices.length),smoothOut=new Float32Array(indices.length),accepted=new Uint8Array(indices.length),s={bits:d.bits,re:d.re,im:d.im,span:d.span,width:d.width,height:d.height,iter:d.iter},baseBits=d.targetBits;for(let i=0;i<indices.length;i++){let a=pixel(s,indices[i],baseBits);for(let attempt=0;attempt<4&&!accepted[i];attempt++){const bits=baseBits+attempt*32,b=pixel(s,indices[i],bits+32);if(a.escape&&a.escape===b.escape&&Object.is(a.smooth,b.smooth)){meta[i]=(a.escape&ITER_MASK)|(FIELD_ESCAPED<<28);smoothOut[i]=a.smooth;accepted[i]=1}else if(!a.escape&&!b.escape){meta[i]=d.terminalClass>=0?((d.iter&ITER_MASK)|((d.terminalClass&3)<<28)):((d.iter&ITER_MASK)|(6<<REASON_SHIFT));accepted[i]=1}a=b}}postMessage({type:'solved',id:d.id,orbitCount,meta:meta.buffer,smooth:smoothOut.buffer,accepted:accepted.buffer},[meta.buffer,smoothOut.buffer,accepted.buffer])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
|
||||
`}
|
||||
class PrecisionFallbackService{
|
||||
|
|
@ -1211,7 +1223,7 @@ class WebGpuRenderer{
|
|||
this.frame=null;this.deepCtx=null;this.fastCtx=null;this.exportWs=[];this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.activeGpuStage='';this.lastCompletedGpuStage='';this.historyView=null;this.historyColorKey='';this.frontColorKey='';this.historyReady=false;this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});this.reprojectSampler=device.createSampler({magFilter:'nearest',minFilter:'nearest'});
|
||||
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
|
||||
this.ready=this.initPipelines();
|
||||
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;const where=this.activeGpuStage||this.lastCompletedGpuStage||'unknown-stage';state.gpuStage=where;state.gpuError='device lost @ '+where+': '+this.lossReason;if(renderer===this){renderer=null;state.gpuInitFailed=true;markDirty(false);setTimeout(()=>{state.gpuInitFailed=false;initRenderer().then(()=>markDirty(false))},1200)}});
|
||||
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';const where=this.activeGpuStage||this.lastCompletedGpuStage||'unknown-stage';state.gpuStage=where;state.gpuError='device lost @ '+where+': '+this.lossReason;if(renderer===this){renderer=null;state.gpuInitFailed=true;markDirty(false);setTimeout(()=>{state.gpuInitFailed=false;initRenderer().then(()=>markDirty(false))},1200)}});
|
||||
}
|
||||
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'premultiplied'})}
|
||||
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>'line '+x.lineNum+':'+x.linePos+' '+x.message).join('\n'))}return m}
|
||||
|
|
@ -1234,7 +1246,7 @@ class WebGpuRenderer{
|
|||
async ensureDeepPipelines(){if(this.deep&&this.deepPostStats)return;if(this.deepPipelinePromise)return this.deepPipelinePromise;this.deepPipelinePromise=(async()=>{const [base,screen]=await Promise.all([this.module('deep',G.DEEP_PERTURB_WGSL),this.module('deep-post-stats-production',G.DEEP_PERTURB_POSTSTATS_WGSL)]);[this.deep,this.deepPostStats]=await Promise.all([this.makeCompute(base),this.makeCompute(screen)])})();try{return await this.deepPipelinePromise}finally{this.deepPipelinePromise=null}}
|
||||
async ensureCorrectionPipelines(){await this.ensureDeepPipelines();if(this.correct&&this.correctQueued&&this.deepBucketHist&&this.deepBucketPrefix&&this.deepBucketScatter&&this.failureTileMap)return;if(this.correctionPipelinePromise)return this.correctionPipelinePromise;this.correctionPipelinePromise=(async()=>{const modules=await Promise.all([this.module('deep-bucket-histogram-production',G.DEEP_BUCKET_HIST_WGSL),this.module('deep-bucket-prefix-production',G.DEEP_BUCKET_PREFIX_WGSL),this.module('deep-bucket-scatter-production',G.DEEP_BUCKET_SCATTER_WGSL),this.module('deep-correction',G.DEEP_CORRECT_WGSL),this.module('deep-correction-queued-production',G.DEEP_CORRECT_QUEUE_WGSL),this.module('failure-tile-map',G.FAILURE_TILE_MAP_WGSL)]);[this.deepBucketHist,this.deepBucketPrefix,this.deepBucketScatter,this.correct,this.correctQueued,this.failureTileMap]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.correctionPipelinePromise}finally{this.correctionPipelinePromise=null}}
|
||||
async ensureAaPipeline(){if(this.aaResolve)return;if(this.aaPipelinePromise)return this.aaPipelinePromise;this.aaPipelinePromise=(async()=>{this.aaResolve=await this.makeCompute(await this.module('aa-resolve',G.AA_RESOLVE_WGSL))})();try{return await this.aaPipelinePromise}finally{this.aaPipelinePromise=null}}
|
||||
async ensureDeepActivePipelines(){if(this.activePrepare&&this.deepActiveResumeInit&&this.deepActiveContinue)return;if(this.deepActivePipelinePromise)return this.deepActivePipelinePromise;this.deepActivePipelinePromise=(async()=>{const modules=await Promise.all([this.module('active-prepare-production',G.ACTIVE_PREPARE_WGSL),this.module('deep-active-resume-init-production',G.DEEP_ACTIVE_RESUME_INIT_WGSL),this.module('deep-active-continue-production',G.DEEP_ACTIVE_CONTINUE_WGSL)]);[this.activePrepare,this.deepActiveResumeInit,this.deepActiveContinue]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.deepActivePipelinePromise}finally{this.deepActivePipelinePromise=null}}
|
||||
async ensureDeepActivePipelines(){if(this.activePrepare&&this.operationLimitQueue&&this.deepActiveResumeInit&&this.deepActiveContinue)return;if(this.deepActivePipelinePromise)return this.deepActivePipelinePromise;this.deepActivePipelinePromise=(async()=>{const modules=await Promise.all([this.module('active-prepare-production',G.ACTIVE_PREPARE_WGSL),this.module('operation-limit-queue-production',G.OPERATION_LIMIT_QUEUE_WGSL),this.module('deep-active-resume-init-production',G.DEEP_ACTIVE_RESUME_INIT_WGSL),this.module('deep-active-continue-production',G.DEEP_ACTIVE_CONTINUE_WGSL)]);[this.activePrepare,this.operationLimitQueue,this.deepActiveResumeInit,this.deepActiveContinue]=await Promise.all(modules.map(m=>this.makeCompute(m)))})();try{return await this.deepActivePipelinePromise}finally{this.deepActivePipelinePromise=null}}
|
||||
async ensureCompactActivePipelines(){
|
||||
if(this.deepCompactInit&&this.deepCompactContinue)return;
|
||||
this.deepCompactInit=await this.makeCompute(await this.module('deep-compact-init',G.DEEP_COMPACT_INIT_WGSL));
|
||||
|
|
@ -1245,15 +1257,15 @@ class WebGpuRenderer{
|
|||
cachedTextureView(texture){if(!this.textureViews)this.textureViews=new WeakMap();let view=this.textureViews.get(texture);if(!view){view=texture.createView();this.textureViews.set(texture,view)}return view}
|
||||
colorTarget(f=this.frame){if(!f)throw new Error('color target requested without a frame');if(f.back===f.history){const old=f.history;f.back=f.spare;f.spare=old}return f.back}
|
||||
cachedBindGroup(pipeline,entries){if(!this.bindGroupCache)this.bindGroupCache=new Map();const key=this.bindResourceId(pipeline)+'|'+entries.map(e=>e.binding+':'+this.bindResourceId(e.resource)+':'+(e.resource?.offset||0)+':'+(e.resource?.size||0)).join('|');let bg=this.bindGroupCache.get(key);if(!bg){bg=this.device.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries});this.bindGroupCache.set(key,bg)}return bg}
|
||||
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 k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead','failureMap','failureParams','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}
|
||||
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 k of ['meta','smooth','unresolved','deepQueueStats','deepBucketState','deepQueue','deepIndirect','deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead','deepActiveMark','failureMap','failureParams','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}
|
||||
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'),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,failureMap:null,failureMapBytes:0,readbackRings:{},failureParams:buf(d,32,B.UNIFORM|B.COPY_DST,'failure-map-params'),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;
|
||||
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),deepQueueStats:null,deepBucketState:null,deepQueue:null,deepIndirect:null,deepActiveCapacity:0,deepActiveState:null,deepActiveQueueA:null,deepActiveQueueB:null,deepActiveCountA:null,deepActiveCountB:null,deepActiveIndirect:null,deepActiveRead:null,deepActiveMark:null,failureMap:null,failureMapBytes:0,readbackRings:{},failureParams:buf(d,32,B.UNIFORM|B.COPY_DST,'failure-map-params'),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){if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+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}
|
||||
ensureDeepQueueWorkspace(f=this.frame){if(!f)return null;this.ensureDeepBucketState(f);if(f.deepQueue&&f.deepQueueStats&&f.deepIndirect)return f;const d=this.device,B=GPUBufferUsage;f.deepQueueStats=buf(d,SPARSE_QUEUE_STATS_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue-stats');f.deepQueue=buf(d,f.n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-unknown-queue');f.deepIndirect=buf(d,SPARSE_INDIRECT_BYTES,B.STORAGE|B.INDIRECT|B.COPY_SRC|B.COPY_DST,'deep-correction-indirect');return f}
|
||||
ensureDeepActiveWorkspace(capacity,f=this.frame,stride=32){if(!f)return null;if(f.deepActiveCapacity>=capacity&&capacity>=f.deepActiveCapacity/4&&f.deepActiveState&&f.deepActiveStride===stride)return f;this.clearBindGroupCache();for(const k of ['deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead'])destroy(f[k]);const d=this.device,B=GPUBufferUsage;f.deepActiveCapacity=capacity;f.deepActiveStride=stride;f.deepActiveState=buf(d,capacity*stride,B.STORAGE|B.COPY_DST,'deep-active-state');f.deepActiveQueueA=buf(d,capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-queue-a');f.deepActiveQueueB=buf(d,capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-queue-b');f.deepActiveCountA=buf(d,4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-count-a');f.deepActiveCountB=buf(d,4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-count-b');f.deepActiveIndirect=buf(d,12,B.STORAGE|B.INDIRECT|B.COPY_DST,'deep-active-indirect');f.deepActiveRead=buf(d,4,B.COPY_DST|B.MAP_READ,'deep-active-count-readback');return f}
|
||||
ensureDeepActiveWorkspace(capacity,f=this.frame,stride=32){if(!f)return null;if(f.deepActiveCapacity>=capacity&&capacity>=f.deepActiveCapacity/4&&f.deepActiveState&&f.deepActiveMark&&f.deepActiveStride===stride)return f;this.clearBindGroupCache();for(const k of ['deepActiveState','deepActiveQueueA','deepActiveQueueB','deepActiveCountA','deepActiveCountB','deepActiveIndirect','deepActiveRead','deepActiveMark'])destroy(f[k]);const d=this.device,B=GPUBufferUsage;f.deepActiveCapacity=capacity;f.deepActiveStride=stride;f.deepActiveState=buf(d,capacity*stride,B.STORAGE|B.COPY_DST,'deep-active-state');f.deepActiveQueueA=buf(d,capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-queue-a');f.deepActiveQueueB=buf(d,capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-queue-b');f.deepActiveCountA=buf(d,4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-count-a');f.deepActiveCountB=buf(d,4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'deep-active-count-b');f.deepActiveIndirect=buf(d,12,B.STORAGE|B.INDIRECT|B.COPY_DST,'deep-active-indirect');f.deepActiveRead=buf(d,4,B.COPY_DST|B.MAP_READ,'deep-active-count-readback');f.deepActiveMark=buf(d,f.n*4,B.STORAGE|B.COPY_DST,'deep-active-membership');return f}
|
||||
ensureFailureMap(bytes,f=this.frame){if(!f)return null;if(f.failureMap&&f.failureMapBytes>=bytes)return f;destroy(f.failureMap);const d=this.device,B=GPUBufferUsage;f.failureMap=buf(d,bytes,B.STORAGE|B.COPY_SRC|B.COPY_DST,'numerical-failure-tile-map');f.failureMapBytes=bytes;return f}
|
||||
exportTileSize(){const l=this.device.limits||{},memory=Number(navigator.deviceMemory||8),storage=Number(l.maxStorageBufferBindingSize||0),maxBuffer=Number(l.maxBufferSize||0);return memory<=4||(storage&&storage<2*1024*1024)||(maxBuffer&&maxBuffer<8*1024*1024)?256:512}
|
||||
exportRingDepth(tile=this.exportTileSize()){const memory=Number(navigator.deviceMemory||8),limit=Number(this.device.limits?.maxBufferSize||0);return memory<=4||(limit&&limit<32*1024*1024)||tile<512?2:3}
|
||||
|
|
@ -1289,13 +1301,10 @@ class WebGpuRenderer{
|
|||
setFastContext(ctx){if(this.fastCtx&&this.fastCtx.key===ctx.key)return;this.destroyFastContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'fast-reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.fastCtx={...ctx,refsB}}
|
||||
destroyFastContext(){if(this.fastCtx)destroy(this.fastCtx.refsB);this.fastCtx=null;this.clearBindGroupCache()}
|
||||
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,outputStride=w,outputBase=0){return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true);d.setUint32(52,outputStride,true);d.setUint32(56,outputBase,true)})}
|
||||
directActiveParams(w,h,iter,snap,targetIter=iter,chunkSize=128){const data=this.directParams(w,h,w,h,0,0,iter,snap);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(64,targetIter,true);d.setUint32(68,chunkSize,true)})}
|
||||
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0,queueOffset=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true);d.setUint32(92,queueOffset,true)})}
|
||||
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0,refPixelX=fullW*.5,refPixelY=fullH*.5,unknownOnly=0,outputStride=w,outputBase=0,queueOffset=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx?.refLen||0,strict,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true);d.setUint32(92,queueOffset,true)})}
|
||||
deepActiveParams(w,h,iter,snap,refPixel,targetIter=iter,chunkSize=128,strict=0,selectionCount=0){const data=this.deepParams(w,h,w,h,0,0,iter,snap,.5,.5,strict,refPixel.x,refPixel.y,selectionCount,w,0);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(84,targetIter,true);d.setUint32(88,chunkSize,true)})}
|
||||
deepActiveTileParams(w,h,fullW,fullH,tileX,tileY,iter,snap,refPixel,targetIter=iter,chunkSize=128,strict=0,outputStride=fullW,outputBase=0){const data=this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,.5,.5,strict,refPixel.x,refPixel.y,0,outputStride,outputBase);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{new Uint8Array(d.buffer).set(new Uint8Array(data));d.setUint32(84,targetIter,true);d.setUint32(88,chunkSize,true)})}
|
||||
certifiedInteriorParams(count,ctx,snap,width,refPixel){const se=spanMantExp(snap),hi=Math.fround(se.mant);return writeU32F32(48,d=>{d.setUint32(0,count,true);d.setUint32(4,ctx.refLen,true);d.setInt32(8,se.exp,true);d.setFloat32(16,hi,true);d.setFloat32(20,Math.fround(se.mant-hi),true);d.setFloat32(24,width,true);d.setFloat32(28,Math.fround(refPixel.x),true);d.setFloat32(32,Math.fround(refPixel.y),true)})}
|
||||
fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,refPixelX=fullW*.5,refPixelY=fullH*.5,outputStride=w,outputBase=0,unknownOnly=0){const se=spanMantExp(snap),mantHi=Math.fround(se.mant),invW=1/Math.max(1,fullW),invWHi=Math.fround(invW);return writeU32F32(NUMERIC_PARAM_BYTES,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.fastCtx.refLen,0,unknownOnly,outputStride,outputBase].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,mantHi,true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true);d.setFloat32(64,Math.fround(refPixelX),true);d.setFloat32(68,Math.fround(refPixelY),true);d.setFloat32(72,Math.fround(se.mant-mantHi),true);d.setFloat32(76,invWHi,true);d.setFloat32(80,Math.fround(invW-invWHi),true)})}
|
||||
colorParamsData(w,h,_iter=state.fieldView?.iter??maxIter(),style=colorStyleSnapshot()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,style.palette,true);d.setUint32(12,style.hq?1:0,true);d.setFloat32(16,style.cycle,true);d.setFloat32(20,style.shift,true)})}
|
||||
colorParamsData(w,h,_iter=state.fieldView?.iter??maxIter(),style=colorStyleSnapshot()){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,style.palette,true);d.setUint32(12,0,true);d.setFloat32(16,style.cycle,true);d.setFloat32(20,style.shift,true)})}
|
||||
encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h,pipeline=this.deep}){const bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDeepPostStatsNumeric(encoder,{pbuf,meta,smooth,w,h}){const pipeline=this.deepPostStats,bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}}]),pass=encoder.beginComputePass();pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h}){const bg=this.cachedBindGroup(this.direct,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end()}
|
||||
|
|
@ -1304,7 +1313,6 @@ class WebGpuRenderer{
|
|||
encodeUnknownStats(encoder,{meta,unresolved,n,params=this.frame?.statsParams}){const d=this.device;if(!params)throw new Error('unknown stats params are missing');d.queue.writeBuffer(params,0,new Uint32Array([n,0,0,0]));encoder.clearBuffer(unresolved,0,28);const bg=this.cachedBindGroup(this.unknownStats,[{binding:0,resource:{buffer:params}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass({label:'unknown-post-stats'});pass.setPipeline(this.unknownStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(n/256));pass.end()}
|
||||
encodeSymmetryCopy(encoder,{meta,smooth,w,h,params=this.frame?.symmetryParams}){if(h<2)return;const d=this.device;if(!params)throw new Error('symmetry params are missing');d.queue.writeBuffer(params,0,new Uint32Array([w,h,0,0]));const bg=this.cachedBindGroup(this.symmetryCopy,[{binding:0,resource:{buffer:params}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]),pass=encoder.beginComputePass({label:'real-axis-symmetry-copy'});pass.setPipeline(this.symmetryCopy);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(Math.floor(h/2)/8));pass.end()}
|
||||
encodeDeepBucketHistogram(encoder,{pbuf,meta,bucketState,w,h}){const bg=this.cachedBindGroup(this.deepBucketHist,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHist);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
|
||||
encodeDeepBucketHistogramStats(encoder,{pbuf,meta,bucketState,unresolved,w,h}){const bg=this.cachedBindGroup(this.deepBucketHistStats,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:unresolved}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketHistStats);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
|
||||
encodeDeepBucketPrefix(encoder,{bucketState,queueStats,indirect}){const bg=this.cachedBindGroup(this.deepBucketPrefix,[{binding:0,resource:{buffer:bucketState}},{binding:1,resource:{buffer:queueStats}},{binding:2,resource:{buffer:indirect}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketPrefix);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end()}
|
||||
encodeDeepBucketScatter(encoder,{pbuf,meta,bucketState,queueStats,queue,w,h}){const bg=this.cachedBindGroup(this.deepBucketScatter,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:bucketState}},{binding:3,resource:{buffer:queueStats}},{binding:4,resource:{buffer:queue}}]),pass=encoder.beginComputePass();pass.setPipeline(this.deepBucketScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/64),h);pass.end()}
|
||||
encodeQueuedDeepCorrection(encoder,{pbuf,meta,smooth,unresolved,queueStats,queue,indirect}){const bg=this.cachedBindGroup(this.correctQueued,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}},{binding:5,resource:{buffer:queueStats}},{binding:6,resource:{buffer:queue}}]),pass=encoder.beginComputePass();pass.setPipeline(this.correctQueued);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end()}
|
||||
|
|
@ -1345,20 +1353,20 @@ class WebGpuRenderer{
|
|||
}
|
||||
submitStageDeferred(encoder,stage,counter='numeric'){this.activeGpuStage=stage;state.gpuStage=stage;this.device.queue.submit([encoder.finish()]);if(counter==='numeric')runtime.numericSubmits++;else if(counter==='correction')runtime.correctionSubmits++;return stage}
|
||||
async waitStageBurst(stage,token,t0,count=1){await this.device.queue.onSubmittedWorkDone();runtime.gpuSyncWaits++;const ms=(performance.now()-t0)/Math.max(1,count);this.lastCompletedGpuStage=stage;this.activeGpuStage='';return token===undefined||token===state.token?ms:null}
|
||||
encodeDeepActiveResumeInit(e,{pbuf,stateB,queue,count,pipeline=this.deepActiveResumeInit}){const d=this.device,bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:stateB}},{binding:2,resource:{buffer:queue}}]),pass=e.beginComputePass({label:'deep-active-resume-init-production'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(count/64));pass.end()}
|
||||
encodeDeepActiveResumeInit(e,{pbuf,stateB,queue,count,mark=this.frame?.deepActiveMark,pipeline=this.deepActiveResumeInit}){const d=this.device,bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:stateB}},{binding:2,resource:{buffer:queue}},{binding:3,resource:{buffer:mark}}]),pass=e.beginComputePass({label:'deep-active-resume-init-production'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(count/64));pass.end()}
|
||||
async readDeepActiveCount(buffer){if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+4;await buffer.mapAsync(GPUMapMode.READ,0,4);const n=new Uint32Array(buffer.getMappedRange(0,4))[0]||0;buffer.unmap();return n}
|
||||
async certifyInterior(snap,token){
|
||||
async certifyInterior(snap,token,clear=false,proof=null){
|
||||
if(token!==state.token)return 0;
|
||||
const f=this.frame,{mask,pixels}=certifiedInteriorTiles(snap,f.w,f.h);if(!pixels)return 0;
|
||||
const f=this.ensureFrame(canvas.width,canvas.height),result=proof||certifiedInteriorTiles(snap,f.w,f.h),{mask,pixels}=result;if(!pixels)return 0;
|
||||
if(!this.interiorMask)this.interiorMask=await this.makeCompute(await this.module('exact-interior-mask',G.INTERIOR_MASK_WGSL));
|
||||
if(token!==state.token)return 0;
|
||||
const d=this.device,input=buf(d,mask.byteLength,GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST,'interior-mask');
|
||||
try{
|
||||
d.queue.writeBuffer(input,0,mask);const e=d.createCommandEncoder();
|
||||
d.queue.writeBuffer(input,0,mask);const e=d.createCommandEncoder({label:'exact-interior-before-primary'});if(clear){e.clearBuffer(f.meta);e.clearBuffer(f.smooth)}
|
||||
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 p=e.beginComputePass();p.setPipeline(this.interiorMask);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();
|
||||
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
|
||||
return await this.submitStage(e,'exact interior tiles',token,'correction')===null?0:pixels;
|
||||
return await this.submitStage(e,'exact interior before primary',token,'correction')===null?0:pixels;
|
||||
}finally{destroy(input)}
|
||||
}
|
||||
async readActiveIndices(session,slots=false){
|
||||
|
|
@ -1366,10 +1374,17 @@ class WebGpuRenderer{
|
|||
const d=this.device,bytes=session.active*4;if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+bytes;const r=buf(d,bytes,GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ,'active-indices');
|
||||
try{const e=d.createCommandEncoder();e.copyBufferToBuffer(session.queues[session.input],0,r,0,bytes);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return session.compact&&!slots?Uint32Array.from(out,i=>session.mapping[i]):out;}finally{destroy(r)}
|
||||
}
|
||||
async beginGpuOperationLimitContinuation(snap,token,refPixel={x:canvas.width*.5,y:canvas.height*.5},strict=false){
|
||||
await this.ready;await this.ensureDeepActivePipelines();if(token!==state.token)return null;const f=this.ensureFrame(canvas.width,canvas.height),limit=Math.min(Number(this.device.limits?.maxStorageBufferBindingSize||this.adapterLimits.maxStorageBufferBindingSize||Infinity),Number(this.device.limits?.maxBufferSize||this.adapterLimits.maxBufferSize||Infinity)),stateBytes=f.n*32;if(stateBytes>limit)throw new Error('GPU-resident active state exceeds adapter buffer limit: '+stateBytes+' > '+limit);this.ensureDeepActiveWorkspace(f.n,f,32);const d=this.device,queues=[f.deepActiveQueueA,f.deepActiveQueueB],counts=[f.deepActiveCountA,f.deepActiveCountB];d.queue.writeBuffer(f.numericParams,0,this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER+1,snap,refPixel,0,1,strict?1:0,0));const e=d.createCommandEncoder({label:'gpu-resident-frontier-init'});e.clearBuffer(f.deepActiveMark);e.clearBuffer(counts[0]);e.clearBuffer(counts[1]);this.encodeOperationLimitQueue(e,{pbuf:f.numericParams,meta:f.meta,stateB:f.deepActiveState,queue:queues[0],count:counts[0],mark:f.deepActiveMark,n:f.n});e.copyBufferToBuffer(counts[0],0,f.deepActiveRead,0,4);this.submitStageDeferred(e,'GPU-resident frontier init','correction');const count=await this.readDeepActiveCount(f.deepActiveRead);runtime.gpuSyncWaits++;if(token!==state.token)return null;if(TEST_MODE)runtime.activeInitialized=(runtime.activeInitialized||0)+count;return{f,queues,counts,compact:false,mapping:null,pipeline:this.deepActiveContinue,input:0,active:count,initial:count,progress:0,maxDispatchWork:0,dispatches:0,pixelIterations:0,strict:!!strict,batchMsPerPass:8,referenceId:null};
|
||||
}
|
||||
async reinitializeActiveSession(session,snap,token,refPixel){if(!session?.active||token!==state.token)return session;const {f}=session,d=this.device;d.queue.writeBuffer(f.numericParams,0,this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER+1,snap,refPixel,0,1,session.strict?1:0,session.active));const e=d.createCommandEncoder({label:'active-reference-reinit'});this.encodeDeepActiveResumeInit(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queue:session.queues[session.input],count:session.active,mark:f.deepActiveMark,pipeline:this.deepActiveResumeInit});const ms=await this.submitStage(e,'active reference reinit '+session.active,token,'correction');if(ms===null)return null;session.progress=0;return session}
|
||||
async appendRecoveredOperationLimits(session,snap,token,refPixel){if(!session||token!==state.token)return null;const {f}=session,d=this.device,before=session.active;d.queue.writeBuffer(f.numericParams,0,this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER+1,snap,refPixel,0,1,session.strict?1:0,0));const e=d.createCommandEncoder({label:'active-resume-recovered'});this.encodeOperationLimitQueue(e,{pbuf:f.numericParams,meta:f.meta,stateB:f.deepActiveState,queue:session.queues[session.input],count:session.counts[session.input],mark:f.deepActiveMark,n:f.n});e.copyBufferToBuffer(session.counts[session.input],0,f.deepActiveRead,0,4);this.submitStageDeferred(e,'active resume recovered','correction');session.active=await this.readDeepActiveCount(f.deepActiveRead);runtime.gpuSyncWaits++;if(token!==state.token)return null;const added=Math.max(0,session.active-before);if(added){session.progress=0;if(TEST_MODE)runtime.activeRecovered=(runtime.activeRecovered||0)+added}return added}
|
||||
async sampleActiveIndices(session,maxSamples=12){if(!session?.active)return new Uint32Array();const count=Math.min(session.active,Math.max(1,maxSamples|0)),d=this.device,read=buf(d,count*4,GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ,'active-samples'),e=d.createCommandEncoder({label:'active-samples'});for(let i=0;i<count;i++){const slot=count===1?0:Math.round(i*(session.active-1)/(count-1));e.copyBufferToBuffer(session.queues[session.input],slot*4,read,i*4,4)}d.queue.submit([e.finish()]);if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+count*4;await read.mapAsync(GPUMapMode.READ);runtime.gpuSyncWaits++;const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();destroy(read);return out}
|
||||
async beginOperationLimitContinuation(snap,token,ctx,refPixel,indices,strict=false){
|
||||
await this.ready;await this.ensureDeepActivePipelines();if(!ctx)throw new Error('active continuation reference context is missing');const f=this.ensureFrame(canvas.width,canvas.height),limit=Math.min(Number(this.device.limits?.maxStorageBufferBindingSize||this.adapterLimits.maxStorageBufferBindingSize||Infinity),Number(this.device.limits?.maxBufferSize||this.adapterLimits.maxBufferSize||Infinity)),compact=indices.length*52<f.n*32,stride=compact?40:32,capacity=compact?indices.length:f.n,stateBytes=capacity*stride;if(stateBytes>limit)throw new Error('active continuation state exceeds adapter buffer limit: '+stateBytes+' > '+limit);if(indices.length>f.n)throw new Error('active continuation candidate overflow');this.setDeepContext(ctx);if(compact)await this.ensureCompactActivePipelines();if(token!==state.token)return null;this.ensureDeepActiveWorkspace(capacity,f,stride);const d=this.device,queues=[f.deepActiveQueueA,f.deepActiveQueueB],counts=[f.deepActiveCountA,f.deepActiveCountB],count=indices.length;d.queue.writeBuffer(queues[0],0,indices);d.queue.writeBuffer(counts[0],0,new Uint32Array([count]));d.queue.writeBuffer(counts[1],0,new Uint32Array([0]));d.queue.writeBuffer(f.numericParams,0,this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER+1,snap,refPixel,0,1,strict?1:0,count));const e=d.createCommandEncoder({label:'all-pixel-active-init'});this.encodeDeepActiveResumeInit(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queue:queues[0],count,pipeline:compact?this.deepCompactInit:this.deepActiveResumeInit});const ms=await this.submitStage(e,'all-pixel active init '+count,token,'correction');if(ms===null)return null;if(TEST_MODE)runtime.activeInitialized=(runtime.activeInitialized||0)+count;return{f,queues,counts,compact,mapping:compact?indices:null,pipeline:compact?this.deepCompactContinue:this.deepActiveContinue,input:0,active:count,initial:count,progress:0,maxDispatchWork:0,dispatches:0,pixelIterations:0,strict:!!strict};
|
||||
}
|
||||
encodeDeepActiveChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input,passes,refsB=this.deepCtx.refsB,pipeline=this.deepActiveContinue}){const d=this.device;for(let k=0;k<passes;k++){const output=1-input;e.clearBuffer(counts[output]);let bg=d.createBindGroup({layout:this.activePrepare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:counts[input]}},{binding:1,resource:{buffer:indirect}}]}),pass=e.beginComputePass({label:'deep-active-indirect-prepare'});pass.setPipeline(this.activePrepare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end();bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:refsB}},{binding:2,resource:{buffer:stateB}},{binding:3,resource:{buffer:queues[input]}},{binding:4,resource:{buffer:counts[input]}},{binding:5,resource:{buffer:queues[output]}},{binding:6,resource:{buffer:counts[output]}},{binding:7,resource:{buffer:meta}},{binding:8,resource:{buffer:smooth}}]});pass=e.beginComputePass({label:'deep-active-chunk'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end();input=output}return input}
|
||||
encodeOperationLimitQueue(e,{pbuf,meta,stateB,queue,count,mark,n}){const pipeline=this.operationLimitQueue,bg=this.cachedBindGroup(pipeline,[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:stateB}},{binding:3,resource:{buffer:queue}},{binding:4,resource:{buffer:count}},{binding:5,resource:{buffer:mark}}]),pass=e.beginComputePass({label:'operation-limit-queue'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(n/256));pass.end()}
|
||||
encodeDeepActiveChunks(e,{pbuf,stateB,queues,counts,indirect,meta,smooth,input,passes,refsB=this.deepCtx.refsB,pipeline=this.deepActiveContinue,mark=this.frame?.deepActiveMark}){const d=this.device;for(let k=0;k<passes;k++){const output=1-input;e.clearBuffer(counts[output]);let bg=d.createBindGroup({layout:this.activePrepare.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:counts[input]}},{binding:1,resource:{buffer:indirect}}]}),pass=e.beginComputePass({label:'deep-active-indirect-prepare'});pass.setPipeline(this.activePrepare);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(1);pass.end();bg=d.createBindGroup({layout:pipeline.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:refsB}},{binding:2,resource:{buffer:stateB}},{binding:3,resource:{buffer:queues[input]}},{binding:4,resource:{buffer:counts[input]}},{binding:5,resource:{buffer:queues[output]}},{binding:6,resource:{buffer:counts[output]}},{binding:7,resource:{buffer:meta}},{binding:8,resource:{buffer:smooth}},{binding:9,resource:{buffer:mark}}]});pass=e.beginComputePass({label:'deep-active-chunk'});pass.setPipeline(pipeline);pass.setBindGroup(0,bg);pass.dispatchWorkgroupsIndirect(indirect,0);pass.end();input=output}return input}
|
||||
async advancePriorityTarget(session,snap,target,token,ctx,refPixel,previousMeta){
|
||||
// Small queues are faster as a single batch. For large expensive rounds,
|
||||
// show the existing boundary first, then give every remaining pixel its
|
||||
|
|
@ -1402,9 +1417,9 @@ class WebGpuRenderer{
|
|||
while(session.active>0&&session.progress<target){
|
||||
if(token!==state.token)return null;
|
||||
const before=session.active,remaining=target-session.progress;
|
||||
const chunk=Math.min(256,remaining,Math.floor(PIXEL_FRONTIER_WORK/before));
|
||||
const chunk=Math.min(256,remaining,Math.floor(ACTIVE_CONTINUATION_WORK/before));
|
||||
if(chunk<1)throw new Error('active workspace exceeds per-dispatch work capacity');
|
||||
const passes=Math.min(session.batchPasses||1,Math.ceil(remaining/chunk),4);
|
||||
const budgetMs=this.numericInFlightBudgetMs(),seedMs=Math.max(.5,session.batchMsPerPass||session.batchPasses&&budgetMs/session.batchPasses||8),passCap=TEST_MODE&&this.activeBatchLimit?Math.max(1,this.activeBatchLimit|0):12,passes=Math.min(Math.ceil(remaining/chunk),passCap,Math.max(1,Math.floor(budgetMs/seedMs)));
|
||||
const params=this.deepActiveParams(f.w,f.h,PIXEL_FRONTIER_MAX_ITER,snap,refPixel,target,chunk,session.strict?1:0);new DataView(params).setUint32(28,ctx.refLen,true);d.queue.writeBuffer(f.numericParams,0,params);
|
||||
const e=d.createCommandEncoder({label:'active-batch'}),started=performance.now();
|
||||
session.input=this.encodeDeepActiveChunks(e,{pbuf:f.numericParams,stateB:f.deepActiveState,queues,counts,indirect:f.deepActiveIndirect,meta:f.meta,smooth:f.smooth,input:session.input,passes,refsB,pipeline:session.pipeline});
|
||||
|
|
@ -1418,9 +1433,8 @@ class WebGpuRenderer{
|
|||
session.progress=Math.min(target,session.progress+chunk*passes);
|
||||
session.dispatches+=passes;session.pixelIterations+=work*passes;if(TEST_MODE){runtime.activeBatches=(runtime.activeBatches||0)+1;runtime.activeWorkBound=(runtime.activeWorkBound||0)+work*passes;}
|
||||
session.maxDispatchWork=Math.max(session.maxDispatchWork,work);
|
||||
runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,work);
|
||||
session.batchPasses=elapsed<10?Math.min(4,passes+1):elapsed>32?1:passes;
|
||||
if(TEST_MODE&&this.activeBatchLimit)session.batchPasses=Math.min(session.batchPasses,this.activeBatchLimit);
|
||||
runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,work);runtime.activeContinuationMaxDispatchWork=Math.max(runtime.activeContinuationMaxDispatchWork||0,work);
|
||||
const msPerPass=elapsed/Math.max(1,passes);session.batchMsPerPass=.65*seedMs+.35*Math.max(.25,msPerPass);session.batchPasses=passes;
|
||||
this.lastCompletedGpuStage='active batch';this.activeGpuStage='';
|
||||
}
|
||||
if(token!==state.token)return null;
|
||||
|
|
@ -1429,7 +1443,7 @@ class WebGpuRenderer{
|
|||
this.submitStageDeferred(e,'active post-stats','correction');
|
||||
return session;
|
||||
}
|
||||
async computePerturbFrameTiled(snap,iter,deep,token,forceStrict,referencePixel,fastExtended,colorStyle,colorKey){
|
||||
async computePerturbFrameTiled(snap,iter,deep,token,forceStrict,referencePixel,fastExtended,colorStyle,colorKey,preserveInterior=false){
|
||||
const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;
|
||||
d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h,previous=state.fieldView,currentReferenceKey=(deep?this.deepCtx:this.fastCtx)?.key||'',reusePreview=!!previous&&sameViewSnapshot(previous,snap)&&previous.w===f.w&&previous.h===f.h&&previous.iter<iter&&previous.referenceKey===currentReferenceKey&&previous.deep===deep&&previous.fastExtended===fastExtended;
|
||||
|
|
@ -1447,12 +1461,12 @@ class WebGpuRenderer{
|
|||
// If any strip or pixel is accidentally left untouched, the coverage
|
||||
// repair below can detect and recompute it instead of publishing stale
|
||||
// metadata or a rectangular transparent/black hole.
|
||||
if(x===0&&y===0&&!reusePreview){e.clearBuffer(f.meta);e.clearBuffer(f.smooth);}
|
||||
if(x===0&&y===0&&!reusePreview&&!preserveInterior){e.clearBuffer(f.meta);e.clearBuffer(f.smooth);}
|
||||
if(deep)this.encodeDeepPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});
|
||||
else this.encodeFastPostStatsNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});
|
||||
lastStage=(deep?'deep':'fast')+' primary rows '+y+'..'+(y+th-1);
|
||||
this.submitStageDeferred(e,lastStage,'numeric');
|
||||
burstCount++;state.gpuTileRows=th;
|
||||
burstCount++;
|
||||
if(burstCount>=burstLimit||x+tw>=f.w){
|
||||
const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;
|
||||
rows=this.adaptNumericRows(rows,ms,computeH-y,deep);predictedStripMs=.65*predictedStripMs+.35*ms;burstLimit=this.numericBurstLimit(predictedStripMs);burstCount=0;burstStart=performance.now();
|
||||
|
|
@ -1478,26 +1492,25 @@ class WebGpuRenderer{
|
|||
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,'fast coverage + color',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;
|
||||
}
|
||||
runtime.gpuFrames++;return true;
|
||||
return true;
|
||||
}
|
||||
async computeFrame(snap,iter,token,referencePixel=null,fastExtended=false,fastContext=null){
|
||||
async computeFrame(snap,iter,token,referencePixel=null,fastExtended=false,fastContext=null,preserveInterior=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);
|
||||
if(fastExtended){this.setFastContext(fastContext);return this.computePerturbFrameTiled(snap,iter,false,token,false,referencePixel,true,colorStyle,colorKey)}
|
||||
// Direct arithmetic is unchanged; large frames use bounded tiles.
|
||||
if(fastExtended){this.setFastContext(fastContext);return this.computePerturbFrameTiled(snap,iter,false,token,false,referencePixel,true,colorStyle,colorKey,preserveInterior)}
|
||||
d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
const mirror=canUseRealAxisSymmetry(snap),computeH=mirror?Math.ceil(f.h/2):f.h,tiled=f.w*computeH*iter>48000000;
|
||||
if(tiled){const shape=this.numericTileShape(f.w,iter,false);for(let y=0;y<computeH;y+=shape.rows)for(let x=0;x<f.w;x+=shape.width){
|
||||
if(tiled){const shape=this.numericTileShape(f.w,iter,false);let burstCount=0,burstStart=performance.now(),predictedTileMs=8,burstLimit=this.numericBurstLimit(predictedTileMs),lastStage='';for(let y=0;y<computeH;y+=shape.rows)for(let x=0;x<f.w;x+=shape.width){
|
||||
if(token!==state.token)return false;const tw=Math.min(shape.width,f.w-x),th=Math.min(shape.rows,computeH-y);
|
||||
d.queue.writeBuffer(f.numericParams,0,this.directParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,0,f.w,y*f.w+x));
|
||||
const e=d.createCommandEncoder();this.encodeDirectNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});
|
||||
if(await this.submitStage(e,'direct bounded tile',token,'numeric')===null)return false;
|
||||
}}else d.queue.writeBuffer(f.numericParams,0,this.directParams(f.w,computeH,f.w,f.h,0,0,iter,snap));
|
||||
const e=d.createCommandEncoder({label:'direct-bounded-tile'});if(x===0&&y===0&&!preserveInterior){e.clearBuffer(f.meta);e.clearBuffer(f.smooth)}this.encodeDirectNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:tw,h:th});lastStage='direct bounded tile';this.submitStageDeferred(e,lastStage,'numeric');burstCount++;
|
||||
if(burstCount>=burstLimit){const ms=await this.waitStageBurst(lastStage,token,burstStart,burstCount);if(ms===null)return false;predictedTileMs=.65*predictedTileMs+.35*ms;burstLimit=this.numericBurstLimit(predictedTileMs);burstCount=0;burstStart=performance.now()}
|
||||
}if(burstCount&&await this.waitStageBurst(lastStage,token,burstStart,burstCount)===null)return false}else d.queue.writeBuffer(f.numericParams,0,this.directParams(f.w,computeH,f.w,f.h,0,0,iter,snap));
|
||||
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h,iter,colorStyle));const encoder=d.createCommandEncoder({label:'mandelbrot-direct-frame'});
|
||||
if(!tiled)this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:computeH});
|
||||
if(!tiled){if(!preserveInterior){encoder.clearBuffer(f.meta);encoder.clearBuffer(f.smooth)}this.encodeDirectNumeric(encoder,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,w:f.w,h:computeH})}
|
||||
if(mirror)this.encodeSymmetryCopy(encoder,{meta:f.meta,smooth:f.smooth,w:f.w,h:f.h});
|
||||
this.encodeUnknownStats(encoder,{meta:f.meta,unresolved:f.unresolved,n:f.n});
|
||||
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=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
|
||||
const ms=await this.submitStage(encoder,'direct primary',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;runtime.gpuFrames++;return true;
|
||||
const ms=await this.submitStage(encoder,'direct primary',token,'numeric');if(ms===null)return false;[f.front,f.back]=[f.back,f.front];this.frontColorKey=colorKey;return true;
|
||||
}
|
||||
releaseColorSource(){
|
||||
const source=this.colorSource;this.colorSource=null;this.colorSourceVisible=false;
|
||||
|
|
@ -1525,16 +1538,15 @@ class WebGpuRenderer{
|
|||
const d=this.device;d.queue.writeBuffer(source.params,0,this.colorParamsData(f.w,f.h));
|
||||
const bg=this.cachedBindGroup(this.color,[{binding:0,resource:{buffer:source.params}},{binding:1,resource:{buffer:source.meta}},{binding:2,resource:{buffer:source.smooth}},{binding:3,resource:this.cachedTextureView(source.texture)}]);
|
||||
const e=d.createCommandEncoder({label:'animate-published-color'}),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()]);
|
||||
this.colorSourceVisible=true;runtime.gpuRecolors++;this.presentFrame();
|
||||
this.colorSourceVisible=true;this.presentFrame();
|
||||
// Bound optional color work even when the GPU is slower than animation.
|
||||
const work=d.queue.onSubmittedWorkDone();this.publishedColorWork=work;
|
||||
const done=()=>{if(this.publishedColorWork===work)this.publishedColorWork=null};work.then(done,done);
|
||||
return true;
|
||||
}
|
||||
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;runtime.gpuRecolors++;return true}
|
||||
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;const metric=generationMetric();if(metric&&!metric.historyCommitMs)metric.historyCommitMs=performance.now()-metric.startedAt;return true}
|
||||
invalidateHistory(){this.historyView=null;this.historyColorKey='';this.historyReady=false}
|
||||
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){
|
||||
|
|
@ -1555,7 +1567,7 @@ class WebGpuRenderer{
|
|||
}
|
||||
async readMetaAll(){if(!this.frame)return new Uint32Array();const f=this.frame,d=this.device,B=GPUBufferUsage,r=buf(d,f.n*4,B.COPY_DST|B.MAP_READ,'field-meta-readback'),e=d.createCommandEncoder({label:'field-meta-readback'});e.copyBufferToBuffer(f.meta,0,r,0,f.n*4);if(TEST_MODE)runtime.readbackBytes=(runtime.readbackBytes||0)+f.n*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 readFieldAll(){if(!this.frame)return{meta:new Uint32Array(),smoothBits:new Uint32Array()};const f=this.frame,d=this.device,B=GPUBufferUsage,bytes=f.n*4,r=buf(d,bytes*2,B.COPY_DST|B.MAP_READ,'field-meta-smooth-readback'),e=d.createCommandEncoder({label:'field-meta-smooth-readback'});e.copyBufferToBuffer(f.meta,0,r,0,bytes);e.copyBufferToBuffer(f.smooth,0,r,bytes,bytes);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const raw=r.getMappedRange(),meta=new Uint32Array(raw.slice(0,bytes)),smoothBits=new Uint32Array(raw.slice(bytes,bytes*2));r.unmap();destroy(r);return{meta,smoothBits}}
|
||||
async readNumericalFailureTiles(size){await this.ready;await this.ensureCorrectionPipelines();if(!this.frame)return[];const f=this.frame,d=this.device,B=GPUBufferUsage,cols=Math.ceil(f.w/size),rows=Math.ceil(f.h/size),stride=6,bytes=cols*rows*stride*4;this.ensureFailureMap(bytes,f);d.queue.writeBuffer(f.failureParams,0,new Uint32Array([f.w,f.h,size,cols,rows,0,0,0]));const read=buf(d,bytes,B.COPY_DST|B.MAP_READ,'failure-map-readback'),e=d.createCommandEncoder({label:'failure-tile-map'}),bg=d.createBindGroup({layout:this.failureTileMap.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.failureParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.failureMap}}]}),pass=e.beginComputePass({label:'failure-tile-map'});pass.setPipeline(this.failureTileMap);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(cols,rows);pass.end();e.copyBufferToBuffer(f.failureMap,0,read,0,bytes);d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();await read.mapAsync(GPUMapMode.READ);const raw=new Uint32Array(read.getMappedRange().slice(0));read.unmap();destroy(read);runtime.failureMapReadBytes=(runtime.failureMapReadBytes||0)+bytes;const tiles=[];for(let ty=0;ty<rows;ty++)for(let tx=0;tx<cols;tx++){const o=(ty*cols+tx)*stride,reasons={errorBound:raw[o]||0,escapeUncertain:raw[o+1]||0,referenceEnd:raw[o+2]||0,rebaseGap:raw[o+3]||0,range:raw[o+4]||0},numeric=reasons.errorBound+reasons.escapeUncertain+reasons.referenceEnd+reasons.rebaseGap+reasons.range;if(!numeric)continue;const x=tx*size,y=ty*size,w=Math.min(size,f.w-x),h=Math.min(size,f.h-y),representative=raw[o+5],cx=representative<f.n?representative%f.w+.5:x+w*.5,cy=representative<f.n?Math.floor(representative/f.w)+.5:y+h*.5;tiles.push({x,y,w,h,numeric,sumX:cx*numeric,sumY:cy*numeric,minX:x,minY:y,maxX:x+w-1,maxY:y+h-1,representative,reasons})}return tiles.sort((a,b)=>b.numeric-a.numeric)}
|
||||
async readNumericalFailureTiles(size){await this.ready;await this.ensureCorrectionPipelines();if(!this.frame)return[];const f=this.frame,d=this.device,B=GPUBufferUsage,cols=Math.ceil(f.w/size),rows=Math.ceil(f.h/size),stride=6,bytes=cols*rows*stride*4;this.ensureFailureMap(bytes,f);d.queue.writeBuffer(f.failureParams,0,new Uint32Array([f.w,f.h,size,cols,rows,0,0,0]));const read=buf(d,bytes,B.COPY_DST|B.MAP_READ,'failure-map-readback'),e=d.createCommandEncoder({label:'failure-tile-map'}),bg=d.createBindGroup({layout:this.failureTileMap.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.failureParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.failureMap}}]}),pass=e.beginComputePass({label:'failure-tile-map'});pass.setPipeline(this.failureTileMap);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(cols,rows);pass.end();e.copyBufferToBuffer(f.failureMap,0,read,0,bytes);d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();await read.mapAsync(GPUMapMode.READ);const raw=new Uint32Array(read.getMappedRange().slice(0));read.unmap();destroy(read);const tiles=[];for(let ty=0;ty<rows;ty++)for(let tx=0;tx<cols;tx++){const o=(ty*cols+tx)*stride,reasons={errorBound:raw[o]||0,escapeUncertain:raw[o+1]||0,referenceEnd:raw[o+2]||0,rebaseGap:raw[o+3]||0,range:raw[o+4]||0},numeric=reasons.errorBound+reasons.escapeUncertain+reasons.referenceEnd+reasons.rebaseGap+reasons.range;if(!numeric)continue;const x=tx*size,y=ty*size,w=Math.min(size,f.w-x),h=Math.min(size,f.h-y),representative=raw[o+5],cx=representative<f.n?representative%f.w+.5:x+w*.5,cy=representative<f.n?Math.floor(representative/f.w)+.5:y+h*.5;tiles.push({x,y,w,h,numeric,sumX:cx*numeric,sumY:cy*numeric,minX:x,minY:y,maxX:x+w-1,maxY:y+h-1,representative,reasons})}return tiles.sort((a,b)=>b.numeric-a.numeric)}
|
||||
async applyPrecisionFallback(result,token){
|
||||
if(token!==state.token||!this.frame||!result)return null;
|
||||
if(!this.precisionScatter)this.precisionScatter=await this.makeCompute(await this.module('precision-scatter',G.PRECISION_SCATTER_WGSL));
|
||||
|
|
@ -1571,18 +1583,22 @@ class WebGpuRenderer{
|
|||
const e=d.createCommandEncoder({label:'precision-results-scatter'});
|
||||
const bg=d.createBindGroup({layout:this.precisionScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:input}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]});
|
||||
const p=e.beginComputePass();p.setPipeline(this.precisionScatter);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(indices.length/64));p.end();
|
||||
this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});
|
||||
// Aggregate once when statistics are consumed, after all queued scatters.
|
||||
// Keep the flag on the frame so resize/cancellation cannot transfer it.
|
||||
f.precisionStatsDirty=true;
|
||||
const ms=await this.submitStage(e,'precision scatter',token,'correction');
|
||||
return ms===null?null:{applied,ms};
|
||||
}finally{destroy(input)}
|
||||
}
|
||||
|
||||
async recoverUnknownTile(snap,iter,token,deepContext,tile,referencePixel=null){
|
||||
await this.ready;await this.ensureCorrectionPipelines();const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!deepContext)throw new Error('tile deep reference context is missing');this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
const refX=referencePixel?.x??(tile.x+tile.w*.5),refY=referencePixel?.y??(tile.y+tile.h*.5);let rows=this.initialNumericRows(tile.w,iter,true);
|
||||
for(let sy=0;sy<tile.h;){if(token!==state.token)return null;const th=Math.min(rows,tile.h-sy),gy=tile.y+sy,base=gy*f.w+tile.x;d.queue.writeBuffer(f.numericParams,0,this.deepParams(tile.w,th,f.w,f.h,tile.x,gy,iter,snap,.5,.5,1,refX,refY,2,f.w,base));const e=d.createCommandEncoder({label:'tile-multireference-recovery-'+gy});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:tile.w,h:th});const ms=await this.submitStage(e,'multi-ref correction rows '+gy+'..'+(gy+th-1),token,'correction');if(ms===null)return null;sy+=th;rows=this.adaptNumericRows(rows,ms,tile.h-sy,true)}const statsEncoder=d.createCommandEncoder({label:'tile-recovery-post-stats'});this.encodeUnknownStats(statsEncoder,{meta:f.meta,unresolved:f.unresolved,n:f.n});const statsMs=await this.submitStage(statsEncoder,'tile recovery post-stats',token,'correction');if(statsMs===null)return null;runtime.correctionPasses++;runtime.multiRefBuilds++;return this.readUnresolvedStats()
|
||||
async readUnresolvedStats(){
|
||||
const f=this.frame;if(!f)return{total:0,corrected:0,reasons:{}};
|
||||
const d=this.device,r=this.frameReadback('unknown-stats',UNRESOLVED_BYTES),e=d.createCommandEncoder();
|
||||
if(f.precisionStatsDirty){this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});f.precisionStatsDirty=false}
|
||||
e.copyBufferToBuffer(f.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);
|
||||
await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();
|
||||
return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}};
|
||||
}
|
||||
async readUnresolvedStats(){if(!this.frame)return{total:0,corrected:0,reasons:{}};const d=this.device,r=this.frameReadback('unknown-stats',UNRESOLVED_BYTES),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,UNRESOLVED_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return{total:a[0]||0,corrected:a[7]||0,reasons:{errorBound:a[1]||0,escapeUncertain:a[2]||0,referenceEnd:a[3]||0,rebaseGap:a[4]||0,range:a[5]||0,operationLimit:a[6]||0}}}
|
||||
async readUnresolved(){return(await this.readUnresolvedStats()).total}
|
||||
async readDeepQueueStats(){if(!this.frame?.deepQueueStats)return{selected:0,overflow:0,enqueued:0,dispatchCount:0,invalidIndex:0,staleEntry:0,processed:0};const d=this.device,r=this.frameReadback('deep-queue-stats',SPARSE_QUEUE_STATS_BYTES),e=d.createCommandEncoder({label:'deep-queue-stats-readback'});e.copyBufferToBuffer(this.frame.deepQueueStats,0,r,0,SPARSE_QUEUE_STATS_BYTES);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const a=new Uint32Array(r.getMappedRange().slice(0));r.unmap();return{selected:a[0]||0,overflow:a[1]||0,enqueued:a[2]||0,dispatchCount:a[3]||0,invalidIndex:a[4]||0,staleEntry:a[5]||0,processed:a[6]||0}}
|
||||
async correctUnknownFrame(snap,iter,token,referencePixel=null,numericCount=0,tile=null){
|
||||
|
|
@ -1598,16 +1614,16 @@ class WebGpuRenderer{
|
|||
const built=await this.readDeepQueueStats();if(token!==state.token)return false;
|
||||
const buildValid=built.selected===built.enqueued&&built.overflow===0&&built.invalidIndex===0;if(!buildValid){console.warn('Sparse correction build invariant failed; using bounded full scan',built);return this.correctUnknownFrameScan(snap,iter,token,referencePixel)}
|
||||
const maxBatch=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,iter)));let offset=0;
|
||||
while(offset<built.selected){if(token!==state.token)return false;const count=Math.min(maxBatch,built.selected-offset),params=this.deepParams(area.w,area.h,f.w,f.h,area.x,area.y,iter,snap,.5,.5,0,refX,refY,2,f.w,area.y*f.w+area.x,offset);d.queue.writeBuffer(f.numericParams,0,params);d.queue.writeBuffer(f.deepQueueStats,12,new Uint32Array([count]));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array([Math.ceil(count/64),1,1]));e=d.createCommandEncoder({label:'deep-sparse-correction-'+offset});this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});const ms=await this.submitStage(e,'queued DS correction '+(offset+1)+'..'+(offset+count)+'/'+built.selected,token,'correction');if(ms===null)return false;totalMs+=ms;runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,count*Math.max(1,iter));offset+=count}
|
||||
while(offset<built.selected){if(token!==state.token)return false;const count=Math.min(maxBatch,built.selected-offset),params=this.deepParams(area.w,area.h,f.w,f.h,area.x,area.y,iter,snap,.5,.5,0,refX,refY,2,f.w,area.y*f.w+area.x,offset);d.queue.writeBuffer(f.numericParams,0,params);d.queue.writeBuffer(f.deepQueueStats,12,new Uint32Array([count]));d.queue.writeBuffer(f.deepIndirect,0,new Uint32Array([Math.ceil(count/64),1,1]));e=d.createCommandEncoder({label:'deep-sparse-correction-'+offset});this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});const ms=await this.submitStage(e,'queued DS correction '+(offset+1)+'..'+(offset+count)+'/'+built.selected,token,'correction');if(ms===null)return false;totalMs+=ms;const dispatchWork=count*Math.max(1,iter);runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,dispatchWork);runtime.correctionMaxDispatchWork=Math.max(runtime.correctionMaxDispatchWork||0,dispatchWork);offset+=count}
|
||||
d.queue.writeBuffer(f.deepQueueStats,12,new Uint32Array([built.selected]));e=d.createCommandEncoder({label:'deep-sparse-correction-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const statsMs=await this.submitStage(e,'queued DS post-stats',token,'correction');if(statsMs===null)return false;totalMs+=statsMs;
|
||||
const q=await this.readDeepQueueStats();if(token!==state.token)return false;
|
||||
const valid=q.selected===q.enqueued&&q.enqueued===q.dispatchCount&&q.dispatchCount===q.processed&&q.overflow===0&&q.invalidIndex===0&&q.staleEntry===0;
|
||||
runtime.correctionPasses++;runtime.lastSparseQueue={...q,valid,ms:totalMs,batches:Math.ceil(built.selected/maxBatch)};
|
||||
|
||||
if(valid)return{mode:'queue',queue:q};
|
||||
console.warn('Sparse correction invariant failed; using bounded full scan',q);
|
||||
return this.correctUnknownFrameScan(snap,iter,token,referencePixel)
|
||||
}
|
||||
async correctUnknownFrameScan(snap,iter,token,referencePixel=null){const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,maxPixels=Math.max(1,Math.floor(4000000/Math.max(1,iter))),tileW=Math.min(f.w,maxPixels),rows=Math.max(1,Math.min(128,Math.floor(maxPixels/tileW)));for(let y=0;y<f.h;y+=rows){const th=Math.min(rows,f.h-y);for(let x=0;x<f.w;x+=tileW){if(token!==state.token)return false;const tw=Math.min(tileW,f.w-x),base=y*f.w+x;d.queue.writeBuffer(f.numericParams,0,this.deepParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,0,refX,refY,2,f.w,base));const e=d.createCommandEncoder({label:'deep-correction-fallback-'+x+'-'+y});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:tw,h:th});const ms=await this.submitStage(e,'DS fallback tile '+x+','+y+' '+tw+'x'+th,token,'correction');if(ms===null)return false}}const e=d.createCommandEncoder({label:'deep-correction-fallback-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms=await this.submitStage(e,'DS fallback post-stats',token,'correction');if(ms===null)return false;runtime.correctionPasses++;return{mode:'scan'}}
|
||||
async correctUnknownFrameScan(snap,iter,token,referencePixel=null){const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5,maxPixels=Math.max(1,Math.floor(4000000/Math.max(1,iter))),tileW=Math.min(f.w,maxPixels),rows=Math.max(1,Math.min(128,Math.floor(maxPixels/tileW)));for(let y=0;y<f.h;y+=rows){const th=Math.min(rows,f.h-y);for(let x=0;x<f.w;x+=tileW){if(token!==state.token)return false;const tw=Math.min(tileW,f.w-x),base=y*f.w+x;d.queue.writeBuffer(f.numericParams,0,this.deepParams(tw,th,f.w,f.h,x,y,iter,snap,.5,.5,0,refX,refY,2,f.w,base));const e=d.createCommandEncoder({label:'deep-correction-fallback-'+x+'-'+y});this.encodeCorrectionNumeric(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,w:tw,h:th});const ms=await this.submitStage(e,'DS fallback tile '+x+','+y+' '+tw+'x'+th,token,'correction');if(ms===null)return false}}const e=d.createCommandEncoder({label:'deep-correction-fallback-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms=await this.submitStage(e,'DS fallback post-stats',token,'correction');if(ms===null)return false;return{mode:'scan'}}
|
||||
async refineOperationLimitIndices(snap,iter,token,deepContext,referencePixel,indices){
|
||||
await this.ready;await this.ensureCorrectionPipelines();if(!deepContext)throw new Error('frontier reference context is missing');
|
||||
const f=this.ensureDeepQueueWorkspace(this.ensureFrame(canvas.width,canvas.height)),d=this.device;this.setDeepContext(deepContext);
|
||||
|
|
@ -1618,10 +1634,8 @@ class WebGpuRenderer{
|
|||
const e=d.createCommandEncoder({label:'pixel-frontier-'+iter+'-'+start});e.clearBuffer(f.unresolved);this.encodeQueuedDeepCorrection(e,{pbuf:f.numericParams,meta:f.meta,smooth:f.smooth,unresolved:f.unresolved,queueStats:f.deepQueueStats,queue:f.deepQueue,indirect:f.deepIndirect});const ms=await this.submitStage(e,'pixel frontier '+(start+1)+'..'+(start+count)+'/'+indices.length+' @ '+iter,token,'correction');if(ms===null)return null;
|
||||
const q=await this.readDeepQueueStats();if(token!==state.token)return null;const valid=q.selected===count&&q.enqueued===count&&q.dispatchCount===count&&q.processed===count&&q.overflow===0&&q.invalidIndex===0&&q.staleEntry===0;if(!valid)throw new Error('pixel frontier queue invariant failed: '+JSON.stringify(q));processed+=count;maxDispatchWork=Math.max(maxDispatchWork,count*iter);
|
||||
}
|
||||
const e=d.createCommandEncoder({label:'pixel-frontier-post-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms=await this.submitStage(e,'pixel frontier post-stats',token,'correction');if(ms===null)return null;runtime.frontierDispatches=(runtime.frontierDispatches||0)+Math.ceil(indices.length/Math.max(1,chunkSize));runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,maxDispatchWork);return{stats:await this.readUnresolvedStats(),processed,maxDispatchWork,chunkSize};
|
||||
const e=d.createCommandEncoder({label:'pixel-frontier-post-stats'});this.encodeUnknownStats(e,{meta:f.meta,unresolved:f.unresolved,n:f.n});const ms=await this.submitStage(e,'pixel frontier post-stats',token,'correction');if(ms===null)return null;runtime.frontierMaxDispatchWork=Math.max(runtime.frontierMaxDispatchWork||0,maxDispatchWork);runtime.correctionMaxDispatchWork=Math.max(runtime.correctionMaxDispatchWork||0,maxDispatchWork);return{stats:await this.readUnresolvedStats(),processed,maxDispatchWork,chunkSize};
|
||||
}
|
||||
async extendUnknownFrame(snap,iter,token,deepContext,referencePixel=null){await this.ready;await this.ensureCorrectionPipelines();if(token!==state.token)return null;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(!deepContext)throw new Error('extended deep reference context is missing');this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));const refX=referencePixel?.x??f.w*.5,refY=referencePixel?.y??f.h*.5;d.queue.writeBuffer(f.numericParams,0,this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,0,refX,refY,1,f.w,0));const e=d.createCommandEncoder({label:'targeted-deep-unknown-recovery'});this.encodeCorrectionNumeric(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});d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;runtime.correctionPasses++;return true}
|
||||
|
||||
async renderTileMeta({snap,iter,deep,deepContext,fastContext=null,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false,correctUnknown=false}){
|
||||
await this.ready;const fastExtended=!deep&&!!fastContext;if(deep){await this.ensureDeepPipelines();if(correctUnknown)await this.ensureCorrectionPipelines();this.setDeepContext(deepContext)}else if(fastExtended){await this.ensureFastPipelines();this.setFastContext(fastContext)}const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,UNRESOLVED_BYTES,B.STORAGE|B.COPY_SRC|B.COPY_DST),pbuf=buf(d,NUMERIC_PARAM_BYTES,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(UNRESOLVED_BYTES/4));
|
||||
if(deep){const ref=referencePixelForSource(deepContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0,ref.x,ref.y));this.encodeDeepNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h});if(correctUnknown){encoder.clearBuffer(unresolved);this.encodeCorrectionNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}}else if(fastExtended){const ref=referencePixelForSource(fastContext?.source,snap,fullW,fullH);d.queue.writeBuffer(pbuf,0,this.fastParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,ref.x,ref.y));this.encodeFastNumeric(encoder,{pbuf,meta,smooth,unresolved,w,h})}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));this.encodeDirectNumeric(encoder,{pbuf,meta,smooth,w,h})}
|
||||
|
|
@ -1677,7 +1691,6 @@ function numericalFailureCount(stats){const r=stats&&stats.reasons||{};return (r
|
|||
function tileRecoveryConfig(){const deep=zoomExp()>=32;return{size:deep?160:224,maxRefs:12,minFailed:deep?1:12}}
|
||||
function isOperationLimitMeta(m){return ((m>>>28)&3)===FIELD_UNKNOWN&&((m>>>REASON_SHIFT)&255)===6}
|
||||
function operationLimitIndices(meta){const out=[];for(let i=0;i<meta.length;i++)if(isOperationLimitMeta(meta[i]))out.push(i);return Uint32Array.from(out)}
|
||||
function numericalFailureIndices(meta){const out=[];for(let i=0;i<meta.length;i++){if(((meta[i]>>>28)&3)!==FIELD_UNKNOWN)continue;const reason=(meta[i]>>>REASON_SHIFT)&255;if(reason>=1&&reason<=5)out.push(i)}return Uint32Array.from(out)}
|
||||
function operationLimitFrontier(meta,w,h){const out=[];for(let y=0;y<h;y++){const row=y*w;for(let x=0;x<w;x++){const i=row+x;if(!isOperationLimitMeta(meta[i]))continue;let edge=false;for(let oy=-1;oy<=1&&!edge;oy++){const yy=y+oy;if(yy<0||yy>=h)continue;for(let ox=-1;ox<=1;ox++){if(ox===0&&oy===0)continue;const xx=x+ox;if(xx<0||xx>=w)continue;if(((meta[yy*w+xx]>>>28)&3)===FIELD_ESCAPED){edge=true;break}}}if(edge)out.push(i)}}return Uint32Array.from(out)}
|
||||
function pixelEscapeAdvance(before,after,w,h){let escaped=0,outsideEnvelope=0;for(let i=0;i<after.length;i++){if(((after[i]>>>28)&3)!==FIELD_ESCAPED||((before[i]>>>28)&3)===FIELD_ESCAPED)continue;escaped++;const x=i%w,y=Math.floor(i/w);let adjacent=false;for(let oy=-1;oy<=1&&!adjacent;oy++){const yy=y+oy;if(yy<0||yy>=h)continue;for(let ox=-1;ox<=1;ox++){if(!ox&&!oy)continue;const xx=x+ox;if(xx>=0&&xx<w&&((before[yy*w+xx]>>>28)&3)===FIELD_ESCAPED){adjacent=true;break}}}if(!adjacent)outsideEnvelope++}return{escaped,outsideEnvelope,pixelStable:outsideEnvelope===0}}
|
||||
// Integer interval tests over exact rational pixel coordinates. No f32
|
||||
|
|
@ -1767,16 +1780,41 @@ async function recoverResidualPrecision(r,snap,iter,token,initialStats,terminalC
|
|||
}
|
||||
}
|
||||
const stats=await r.readUnresolvedStats();if(token!==state.token)return null;
|
||||
state.precisionFallbackAttempted=(state.precisionFallbackAttempted||0)+attempted;
|
||||
state.precisionFallbackApplied=(state.precisionFallbackApplied||0)+appliedCount;
|
||||
return{stats,attempted,applied:appliedCount};
|
||||
}finally{
|
||||
for(const worker of active.keys())precisionFallbackPool[worker].cancel('cancelled precision batch');
|
||||
}
|
||||
}
|
||||
|
||||
async function refinePixelFrontier(r,snap,initialIter,token,initialStats){
|
||||
if(!PIXEL_FRONTIER_ENABLED)return{stats:initialStats,iter:initialIter,rounds:0,processed:0,escaped:0,converged:false,applied:false};
|
||||
async function continuationReferenceForActive(r,session,snap,target,token,preferredSource=null){
|
||||
let ctx=preferredSource?await refs.requestFixed(snap,target,canvas.width,canvas.height,preferredSource):await refs.request(snap,target,canvas.width,canvas.height);if(token!==state.token)return null;if(ctx.checkpointMismatch)throw new Error('active reference guard checkpoint mismatch');if(ctx.refLen>=target)return{ctx,source:ctx.source};
|
||||
const samples=await r.sampleActiveIndices(session,8);if(token!==state.token)return null;let best=ctx,bestSource=ctx.source;
|
||||
for(const index of samples){const source=fixedSourceForPixel(index,snap,canvas.width,canvas.height),local=await refs.requestFixed(snap,target,canvas.width,canvas.height,source);if(token!==state.token)return null;if(local.checkpointMismatch)throw new Error('local active reference guard checkpoint mismatch');if(local.refLen>best.refLen){best=local;bestSource=local.source}if(local.refLen>=target)return{ctx:local,source:local.source}}
|
||||
return{ctx:best,source:bestSource};
|
||||
}
|
||||
async function refinePixelFrontierFinite(r,snap,initialIter,token,initialStats,certified=0){
|
||||
const maxIter=Math.max(initialIter,state.continuationBudget||initialIter),minimumIter=maxIter;state.frontierMinimumIter=minimumIter;
|
||||
let session=await r.beginGpuOperationLimitContinuation(snap,token);if(!session||token!==state.token)return null;let iter=initialIter,stats=initialStats,rounds=0,processed=0,retired=0,ctx=null,fixedSource=null,converged=session.active===0||iter>=maxIter;
|
||||
state.frontierPixels=session.active;state.frontierConverged=converged;
|
||||
while(!converged&&iter<maxIter){
|
||||
if(token!==state.token)return null;let target=Math.min(maxIter,Math.max(iter+1,iter*2));state.lastEngine='WebGPU · GPU常駐 active-list '+session.active+' px · '+target+' 反復';state.frontierPixels=session.active;updateStats();
|
||||
const selected=await continuationReferenceForActive(r,session,snap,target,token,fixedSource);if(!selected||token!==state.token)return null;ctx=selected.ctx;if(ctx.refLen<=iter)throw new Error('active領域を前進できるreferenceを選択できません');if(ctx.refLen<target)target=Math.min(target,ctx.refLen);const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height),refId=ctx.referenceId||ctx.key;
|
||||
r.setDeepContext(ctx);if(session.referenceId!==refId){session=await r.reinitializeActiveSession(session,snap,token,refPixel);if(!session||token!==state.token)return null;session.referenceId=refId;fixedSource=ctx.source}
|
||||
const before=session.active,workBefore=session.pixelIterations;const pass=await r.continueOperationLimitActive(session,snap,target,token,ctx,refPixel);if(!pass||token!==state.token)return null;processed+=session.pixelIterations-workBefore;retired+=Math.max(0,before-session.active);
|
||||
stats=await r.readUnresolvedStats();if(token!==state.token)return null;let failed=numericalFailureCount(stats);
|
||||
if(failed>0){r.setDeepContext(ctx);const corrected=await r.correctUnknownFrame(snap,target,token,refPixel);if(!corrected||token!==state.token)return null;stats=await r.readUnresolvedStats();if(token!==state.token)return null}
|
||||
if(numericalFailureCount(stats)>512){const beforeLocal=numericalFailureCount(stats),mr=await recoverNumericalUnknownTiles(r,snap,target,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;if(numericalFailureCount(stats)>=beforeLocal)state.multiRefRecovered=0;r.setDeepContext(ctx)}
|
||||
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,target,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats;r.setDeepContext(ctx)}
|
||||
const added=await r.appendRecoveredOperationLimits(session,snap,token,refPixel);if(added===null||token!==state.token)return null;stats=await r.readUnresolvedStats();if(token!==state.token)return null;
|
||||
rounds++;iter=target;state.frontierRounds=rounds;state.frontierEscaped=retired;state.frontierIter=iter;state.frontierPixels=session.active;
|
||||
if(!await presentProvisional(r,snap,iter,token))return null;converged=session.active===0||iter>=maxIter;
|
||||
}
|
||||
stats=await r.readUnresolvedStats();if(token!==state.token)return null;if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats}
|
||||
state.frontierConverged=converged;state.frontierIter=iter;state.unresolved=stats.total;state.unknownReasons=stats.reasons;return{stats,iter,minimumIter,rounds,processed,escaped:retired,converged,policy:'explicit-gpu-resident-finite-budget',membershipCertified:true,applied:rounds>0||certified>0};
|
||||
}
|
||||
async function refinePixelFrontier(r,snap,initialIter,token,initialStats,certified=0){if(state.continuationBudget)return refinePixelFrontierFinite(r,snap,initialIter,token,initialStats,certified);if(!TEST_MODE)throw new Error('production quality requires a finite continuation budget');return refinePixelFrontierLegacy(r,snap,initialIter,token,initialStats)}
|
||||
|
||||
async function refinePixelFrontierLegacy(r,snap,initialIter,token,initialStats){
|
||||
const certified=await r.certifyInterior(snap,token);if(token!==state.token)return null;
|
||||
const explicitBudget=state.continuationBudget||0,maxIter=Math.max(initialIter,explicitBudget||PIXEL_FRONTIER_MAX_ITER),minimumIter=pixelFrontierIterationFloor(snap,canvas.width,initialIter),initialMeta=await r.readMetaAll();if(token!==state.token)return null;state.frontierMinimumIter=minimumIter;let previousMeta=initialMeta,candidates=operationLimitIndices(initialMeta),iter=initialIter,stats=initialStats,rounds=0,processed=0,escaped=0,stableRounds=0,converged=candidates.length===0||iter>=maxIter&&candidates.every(i=>(initialMeta[i]&ITER_MASK)>=maxIter),session=null,fixedSource=null,deferred=new Uint32Array();
|
||||
while(!converged&&iter<maxIter){
|
||||
|
|
@ -1785,9 +1823,9 @@ async function refinePixelFrontier(r,snap,initialIter,token,initialStats){
|
|||
// origin is unchanged and reaches the new target. If it does not, rebuild
|
||||
// the candidate list from the already-published metadata and restart once
|
||||
// with a newly selected long reference; no pixel is silently discarded.
|
||||
if(ctx.refLen<target){ctx=await refs.request(snap,target,canvas.width,canvas.height,true);if(token!==state.token)return null;const meta=await r.readMetaAll();if(token!==state.token)return null;candidates=operationLimitIndices(meta);session=null;fixedSource=null;deferred=new Uint32Array();if(!candidates.length){converged=true;break}}
|
||||
if(ctx.refLen<target){const meta=await r.readMetaAll();if(token!==state.token)return null;candidates=operationLimitIndices(meta);session=null;deferred=new Uint32Array();if(!candidates.length){converged=true;break}let best=ctx;const sampleCount=Math.min(8,candidates.length);for(let k=0;k<sampleCount;k++){const index=candidates[Math.round(k*(candidates.length-1)/Math.max(1,sampleCount-1))],source=fixedSourceForPixel(index,snap,canvas.width,canvas.height),local=await refs.requestFixed(snap,target,canvas.width,canvas.height,source);if(token!==state.token)return null;if(local.checkpointMismatch)throw new Error('automatic local reference guard checkpoint mismatch');if(local.refLen>best.refLen)best=local;if(best.refLen>=target)break}ctx=best;fixedSource=ctx.source}
|
||||
const refPixel=referencePixelForSource(ctx.source,snap,canvas.width,canvas.height);if(session&&session.referenceId!==(ctx.referenceId||ctx.key)){session=null;deferred=new Uint32Array();candidates=operationLimitIndices(previousMeta)}if(!session){session=await r.beginOperationLimitContinuation(snap,token,ctx,refPixel,candidates,false);if(!session||token!==state.token)return null;fixedSource=ctx.source;session.referenceId=ctx.referenceId||ctx.key}
|
||||
const workBefore=session.pixelIterations,dispatchBefore=session.dispatches,pass=await r.advancePriorityTarget(session,snap,target,token,ctx,refPixel,previousMeta);if(!pass||token!==state.token)return null;processed+=pass.pixelIterations-workBefore;runtime.frontierDispatches=(runtime.frontierDispatches||0)+(pass.dispatches-dispatchBefore);if(deferred.length){const repaired=await r.refineOperationLimitIndices(snap,target,token,ctx,refPixel,deferred);if(!repaired||token!==state.token)return null;}stats=await r.readUnresolvedStats();if(token!==state.token)return null;let failed=numericalFailureCount(stats);if(failed>0){r.setDeepContext(ctx);const corrected=await r.correctUnknownFrame(snap,target,token,refPixel);if(!corrected||token!==state.token)return null;stats=await r.readUnresolvedStats();if(token!==state.token)return null}if(numericalFailureCount(stats)>512){const beforeLocal=numericalFailureCount(stats),mr=await recoverNumericalUnknownTiles(r,snap,target,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;if(numericalFailureCount(stats)>=beforeLocal)state.multiRefRecovered=0}if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,target,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats}const after=await r.readMetaAll();if(token!==state.token)return null;const advance=pixelEscapeAdvance(previousMeta,after,canvas.width,canvas.height);rounds++;escaped+=advance.escaped;iter=target;stableRounds=advance.pixelStable?stableRounds+1:0;previousMeta=after;state.frontierRounds=rounds;state.frontierEscaped=escaped;state.frontierIter=iter;
|
||||
const workBefore=session.pixelIterations,dispatchBefore=session.dispatches,pass=await r.advancePriorityTarget(session,snap,target,token,ctx,refPixel,previousMeta);if(!pass||token!==state.token)return null;processed+=pass.pixelIterations-workBefore;if(deferred.length){const repaired=await r.refineOperationLimitIndices(snap,target,token,ctx,refPixel,deferred);if(!repaired||token!==state.token)return null;}stats=await r.readUnresolvedStats();if(token!==state.token)return null;let failed=numericalFailureCount(stats);if(failed>0){r.setDeepContext(ctx);const corrected=await r.correctUnknownFrame(snap,target,token,refPixel);if(!corrected||token!==state.token)return null;stats=await r.readUnresolvedStats();if(token!==state.token)return null}if(numericalFailureCount(stats)>512){const beforeLocal=numericalFailureCount(stats),mr=await recoverNumericalUnknownTiles(r,snap,target,token,stats);if(!mr||token!==state.token)return null;stats=mr.stats;if(numericalFailureCount(stats)>=beforeLocal)state.multiRefRecovered=0}if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,target,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats}const after=await r.readMetaAll();if(token!==state.token)return null;const advance=pixelEscapeAdvance(previousMeta,after,canvas.width,canvas.height);rounds++;escaped+=advance.escaped;iter=target;stableRounds=advance.pixelStable?stableRounds+1:0;previousMeta=after;state.frontierRounds=rounds;state.frontierEscaped=escaped;state.frontierIter=iter;
|
||||
if(!await presentProvisional(r,snap,iter,token))return null;
|
||||
// A quiet pair of doubling rounds is only meaningful after every remaining
|
||||
// operation-limit pixel has actually reached the budget derived from one
|
||||
|
|
@ -1804,9 +1842,10 @@ async function refinePixelFrontier(r,snap,initialIter,token,initialStats){
|
|||
}
|
||||
stats=await r.readUnresolvedStats();if(token!==state.token)return null;
|
||||
if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision(r,snap,iter,token,stats);if(!pf||token!==state.token)return null;stats=pf.stats}
|
||||
state.frontierConverged=converged;state.frontierIter=iter;state.unresolved=stats.total;state.unknownReasons=stats.reasons;runtime.frontierFrames=(runtime.frontierFrames||0)+1;runtime.frontierPixels=(runtime.frontierPixels||0)+processed;runtime.frontierEscaped=(runtime.frontierEscaped||0)+escaped;return{stats,iter,minimumIter,rounds,processed,escaped,converged,policy:explicitBudget?'explicit-finite-budget':iter>=maxIter?'automatic-finite-cap':'finite-budget-with-stability-check',membershipCertified:false,applied:escaped>0||certified>0};
|
||||
state.frontierConverged=converged;state.frontierIter=iter;state.unresolved=stats.total;state.unknownReasons=stats.reasons;return{stats,iter,minimumIter,rounds,processed,escaped,converged,policy:explicitBudget?'explicit-finite-budget':iter>=maxIter?'automatic-finite-cap':'finite-budget-with-stability-check',membershipCertified:false,applied:escaped>0||certified>0};
|
||||
}
|
||||
|
||||
function fixedSourceForPixel(index,snap,w,h){const x=BigInt(index%w),y=BigInt(Math.floor(index/w)),bw=BigInt(w),den=2n*bw;return{bits:snap.bits,re:snap.re+roundDivSigned(snap.span*(2n*x+1n-bw),den),im:snap.im+roundDivSigned(snap.span*(BigInt(h)-2n*y-1n),den),viewRe:snap.re,viewIm:snap.im,span:snap.span}}
|
||||
function referencePixelForSource(src,snap,w,h){if(!src)return{x:w*.5,y:h*.5};const b=Math.max(snap.bits,src.bits),span=align(snap.span,snap.bits,b),dr=align(src.re,src.bits,b)-align(snap.re,snap.bits,b),di=align(src.im,src.bits,b)-align(snap.im,snap.bits,b);return{x:w*.5+fixedRatio(dr,span)*w,y:h*.5-fixedRatio(di,span)*w}}
|
||||
function referenceOriginMatchesView(src,snap){return !!src&&!!snap&&src.bits===snap.bits&&(src.viewRe??src.re)===snap.re&&(src.viewIm??src.im)===snap.im&&src.span===snap.span}
|
||||
function reusableReferencePixel(ctx,snap,iter,w,h,requiredBits){const src=ctx&&ctx.source;if(!ctx||!src||!referenceOriginMatchesView(src,snap)||ctx.refLen<iter||(ctx.precisionBits||0)<requiredBits||ctx.checkpointMismatch)return null;const pixel=referencePixelForSource(src,snap,w,h),margin=.125*Math.max(w,h);if(!Number.isFinite(pixel.x)||!Number.isFinite(pixel.y)||pixel.x < -margin||pixel.x>w+margin||pixel.y < -margin||pixel.y>h+margin)return null;return{ctx,pixel}}
|
||||
|
|
@ -1825,14 +1864,14 @@ function renderTimeLabel(now=performance.now()){
|
|||
function startRenderClock(token,startedAt){
|
||||
if(renderClockTimer)clearInterval(renderClockTimer);
|
||||
state.lastRender=0;state.renderClock={token,startedAt,status:'running',elapsed:0};
|
||||
renderClockTimer=setInterval(()=>{$('#render').textContent=renderTimeLabel()},250);
|
||||
renderClockTimer=setInterval(()=>{$('render').textContent=renderTimeLabel()},250);
|
||||
}
|
||||
function finishRenderClock(token,status='complete'){
|
||||
const c=state.renderClock;if(!c||c.token!==token||c.status!=='running')return;
|
||||
c.elapsed=performance.now()-c.startedAt;c.status=status;
|
||||
if(status==='complete'){state.lastRender=c.elapsed;if(!state.colorAuto&&!state.recolorPending)renderer?.releaseColorSource()}
|
||||
clearInterval(renderClockTimer);renderClockTimer=0;
|
||||
$('#render').textContent=renderTimeLabel();
|
||||
$('render').textContent=renderTimeLabel();
|
||||
}
|
||||
function cancelRender(){finishRenderClock(state.token,'cancelled');if(exportJob.active)exportJob.cancelled=true;const metric=generationMetric();if(metric&&metric.status==='running'){metric.status='cancelled';metric.cancelMs=performance.now()-metric.startedAt}state.token++;state.rendering=false;state.recolorPending=false;state.refinePending=false;state.numericRecoveryPending=false;state.numericRecoveryRunning=false;if(refineTimer){clearTimeout(refineTimer);refineTimer=0}refs.cancelPending('render cancelled');for(const service of tileReferencePool)service.cancelPending('render cancelled');fastRefs.cancelPending('render cancelled');precisionFallback.cancel('render cancelled');for(const service of precisionFallbackPool)service.cancel('render cancelled')}
|
||||
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
|
||||
|
|
@ -1849,14 +1888,23 @@ async function presentProvisional(r,snap,iter,token,force=false){
|
|||
}
|
||||
async function renderQualityStage(r,snap,iter,token,isFinal=false){
|
||||
let decision=chooseBackend(snap,canvas.width,isFinal),deep=decision.deep,fastExtended=decision.fastExtended;
|
||||
state.backendDecision=decision;let ctx=null,fastCtx=null,referencePixel=null,referenceReused=false;
|
||||
const proof=certifiedInteriorTiles(snap,canvas.width,canvas.height);let certified=0,preserveInterior=false;
|
||||
if(proof.pixels){state.lastEngine='WebGPU · exact interiorをprimary前に証明';updateStats();certified=await r.certifyInterior(snap,token,true,proof);if(token!==state.token)return null;preserveInterior=certified>0}
|
||||
if(certified===canvas.width*canvas.height){
|
||||
decision={backend:'exact-interior',deep:false,fastExtended:false,probe:false,reason:'exact-full-interior'};deep=false;fastExtended=false;
|
||||
const stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;
|
||||
const frontierResult=isFinal?{stats,iter,minimumIter:iter,rounds:0,processed:0,escaped:0,converged:true,policy:'exact-interior-proof',membershipCertified:true,applied:true}:null;
|
||||
state.frontierConverged=!!isFinal;state.frontierIter=iter;state.frameView=snap;state.fieldView={...snap,iter,baseIter:iter,frontier:frontierResult,w:canvas.width,h:canvas.height,deep:false,fastExtended:false,backend:'exact-interior',referenceKey:'',referenceReused:false,complete:isFinal,progressive:true};
|
||||
r.captureColorSource(snap,state.colorAuto||state.recolorPending||!isFinal);r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});return{decision,referenceReused:false,referencePixel:null,certified};
|
||||
}
|
||||
let ctx=null,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{state.lastEngine='WebGPU · '+(isFinal?'最終':'preview')+' 高速拡張 reference準備';updateStats();fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height)}
|
||||
}
|
||||
state.lastEngine='WebGPU · '+(isFinal?'最終品質':'即時preview')+' '+(deep?'高精度補修':fastExtended?'高速拡張':'高速');updateStats();
|
||||
let ok=await r.computeFrame(snap,iter,token,referencePixel,fastExtended,fastCtx);if(!ok||token!==state.token)return null;
|
||||
let ok=await r.computeFrame(snap,iter,token,referencePixel,fastExtended,fastCtx,preserveInterior);if(!ok||token!==state.token)return null;
|
||||
if(isFinal&&!await presentProvisional(r,snap,iter,token,true))return null;
|
||||
if(isFinal&&fastExtended){
|
||||
// A FAST frame is not publishable while any numeric UNKNOWN remains.
|
||||
|
|
@ -1872,12 +1920,12 @@ async function renderQualityStage(r,snap,iter,token,isFinal=false){
|
|||
const stats=await r.readUnresolvedStats();if(token!==state.token)return null;state.unresolved=stats.total;state.unknownReasons=stats.reasons;
|
||||
}
|
||||
let effectiveIter=iter,frontierResult=null;
|
||||
if(isFinal&&PIXEL_FRONTIER_ENABLED){frontierResult=await refinePixelFrontier(r,snap,iter,token,{total:state.unresolved,reasons:{...(state.unknownReasons||{})}});if(!frontierResult||token!==state.token)return null;effectiveIter=frontierResult.iter;if(!frontierResult.converged)throw new Error('全画素反復が安全上限 '+PIXEL_FRONTIER_MAX_ITER+' までに収束しなかったためframeを公開しません');if(frontierResult.applied){const painted=await r.recolor(token,effectiveIter);if(!painted||token!==state.token)return null}}
|
||||
if(isFinal){frontierResult=await refinePixelFrontier(r,snap,iter,token,{total:state.unresolved,reasons:{...(state.unknownReasons||{})}},certified);if(!frontierResult||token!==state.token)return null;effectiveIter=frontierResult.iter;if(!frontierResult.converged)throw new Error('全画素反復が安全上限 '+PIXEL_FRONTIER_MAX_ITER+' までに収束しなかったためframeを公開しません');if(frontierResult.applied){const painted=await r.recolor(token,effectiveIter);if(!painted||token!==state.token)return null}}
|
||||
if(isFinal){const remaining=numericalFailureCount({reasons:state.unknownReasons||{}}),unprocessed=Math.max(0,state.unresolved-remaining-(state.unknownReasons?.operationLimit||0));if(unprocessed)throw new Error('未計算画素が '+unprocessed+' px 残っています');if(remaining>0)throw new Error('画素精度補修後も数値未確定が '+remaining+' px 残ったためframeを公開しません')}
|
||||
state.frameView=snap;state.fieldView={...snap,iter:effectiveIter,baseIter:iter,frontier:frontierResult,w:canvas.width,h:canvas.height,deep,fastExtended,backend:frontierResult?.applied?'pixel-frontier-'+decision.backend:decision.backend,referenceKey:(deep?ctx:fastExtended?fastCtx:null)?.key||'',referenceReused,complete:isFinal,progressive:true};
|
||||
r.captureColorSource(snap);
|
||||
r.captureColorSource(snap,state.colorAuto||state.recolorPending||!isFinal);
|
||||
r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});
|
||||
return{decision,referenceReused,referencePixel};
|
||||
return{decision,referenceReused,referencePixel,certified};
|
||||
}
|
||||
function numericFrameComplete(){return state.fieldView?.complete!==false&&numericalFailureCount({reasons:state.unknownReasons||{}})===0&&!state.numericRecoveryPending&&!state.numericRecoveryRunning}
|
||||
function scheduleQualityRefinement(snap,token,stages,index){
|
||||
|
|
@ -1885,7 +1933,7 @@ function scheduleQualityRefinement(snap,token,stages,index){
|
|||
state.refinePending=true;if(refineTimer)clearTimeout(refineTimer);
|
||||
refineTimer=setTimeout(()=>{refineTimer=0;runQualityRefinement(snap,token,stages,index)},refinementDelay(index));
|
||||
}
|
||||
function prefetchFinalReference(snap,finalIter,token,decision){if(token!==state.token||!decision||finalIter<=state.qualityIter)return;let work=null;if(decision.deep)work=refs.request(snap,finalIter,canvas.width,canvas.height);else if(decision.fastExtended)work=fastRefs.request(snap,finalIter,canvas.width,canvas.height);if(work){runtime.referencePrefetches=(runtime.referencePrefetches||0)+1;work.catch(e=>{if(!/cancelled|superseded/i.test(String(e&&e.message||e)))console.warn('final reference prefetch skipped:',e)})}}
|
||||
function prefetchFinalReference(snap,finalIter,token,decision){if(token!==state.token||!decision||finalIter<=state.qualityIter)return;let work=null;if(decision.deep)work=refs.request(snap,finalIter,canvas.width,canvas.height);else if(decision.fastExtended)work=fastRefs.request(snap,finalIter,canvas.width,canvas.height);if(work){work.catch(e=>{if(!/cancelled|superseded/i.test(String(e&&e.message||e)))console.warn('final reference prefetch skipped:',e)})}}
|
||||
async function runQualityRefinement(snap,token,stages,index){
|
||||
if(token!==state.token||state.pointerActive||state.wheelActive||state.dirty||!sameViewSnapshot(snap,snapshot())){state.refinePending=false;return}
|
||||
if(state.recoloring){refineTimer=setTimeout(()=>{refineTimer=0;runQualityRefinement(snap,token,stages,index)},16);return}
|
||||
|
|
@ -1906,7 +1954,7 @@ async function renderFrame(){
|
|||
if(renderer&&state.fieldView?.complete)renderer.captureColorSource(state.frameView,true);
|
||||
startRenderClock(token,t0);
|
||||
const generation=TEST_MODE?{token,startedAt:t0,viewKey:[snap.bits,snap.re,snap.im,snap.span].join(':'),width:canvas.width,height:canvas.height,previewIter:iter,finalIter,numericSubmitsStart:runtime.numericSubmits,correctionSubmitsStart:runtime.correctionSubmits,gpuSyncWaitsStart:runtime.gpuSyncWaits,status:'running'}:null;if(generation){runtime.generationMetrics.push(generation);if(runtime.generationMetrics.length>16)runtime.generationMetrics.shift();}
|
||||
state.rendering=true;state.dirty=false;state.drawState=isSingleFinal&&holdStable?'REPROJECTED':'COVERING';state.unresolved=0;state.unknownReasons=null;state.correctionPasses=0;state.correctedPixels=0;state.multiRefPasses=0;state.multiRefRecovered=0;state.frontierRounds=0;state.frontierPixels=0;state.frontierEscaped=0;state.frontierIter=iter;state.frontierMinimumIter=PIXEL_FRONTIER_ENABLED?pixelFrontierIterationFloor(snap,canvas.width,iter):iter;state.frontierConverged=false;state.qualityStage=isSingleFinal?'final':'preview';state.qualityIter=iter;state.qualityFinalIter=finalIter;state.previewMs=0;state.refineMs=0;state.refinePending=false;runtime.renderStarts++;runtime.stageCounts={};runtime.stageMs={};runtime.frontierMaxDispatchWork=0;updateStats();
|
||||
state.rendering=true;state.dirty=false;state.drawState=isSingleFinal&&holdStable?'REPROJECTED':'COVERING';state.unresolved=0;state.unknownReasons=null;state.multiRefPasses=0;state.multiRefRecovered=0;state.frontierRounds=0;state.frontierPixels=0;state.frontierEscaped=0;state.frontierIter=iter;state.frontierMinimumIter=pixelFrontierIterationFloor(snap,canvas.width,iter);state.frontierConverged=false;state.qualityStage=isSingleFinal?'final':'preview';state.qualityIter=iter;state.qualityFinalIter=finalIter;state.previewMs=0;state.refineMs=0;state.refinePending=false;runtime.stageCounts={};runtime.stageMs={};runtime.frontierMaxDispatchWork=0;runtime.activeContinuationMaxDispatchWork=0;runtime.correctionMaxDispatchWork=0;updateStats();
|
||||
try{
|
||||
const r=renderer||await initRenderer();if(token!==state.token)return;
|
||||
if(!r){finishRenderClock(token,'error');state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU必須 · fallback無効';updateStats();return}
|
||||
|
|
@ -1971,54 +2019,52 @@ function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&st
|
|||
|
||||
// ── interaction / view history ──────────────────────────────────────────
|
||||
function viewRect(){return canvas.getBoundingClientRect()}
|
||||
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
|
||||
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
|
||||
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
|
||||
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
|
||||
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash()}
|
||||
const pts=new Map();let lx=0,ly=0,pinch=0;
|
||||
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
|
||||
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
|
||||
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
|
||||
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
|
||||
canvas.addEventListener('wheel',e=>{e.preventDefault();if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;saveHash();markDirty()},110)},{passive:false});
|
||||
canvas.addEventListener('pointerdown',e=>{try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
|
||||
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
|
||||
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;saveHash();markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
|
||||
|
||||
// ── URL / controls ───────────────────────────────────────────────────────
|
||||
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');p.set('q',state.quality);p.set('rm',state.renderMode);const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
|
||||
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal')){const pal=Math.max(0,Math.min(PALETTE_MAX,Number(p.get('pal'))|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;}if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';applyQuality(p.has('q')?p.get('q'):p.has('cb')&&Number(p.get('cb'))>0?(Number(p.get('cb'))<=4096?'fast':Number(p.get('cb'))<=16384?'standard':'high'):state.quality);state.renderMode='fast';ensurePrecision();return true}catch{return false}}
|
||||
function syncHistoryButtons(){}
|
||||
function syncControls(){$('#quality').value=state.quality;$('#palette').value=String(state.palette);$('#cycle').value=String(cycleToSlider(state.cycle));$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()}
|
||||
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
|
||||
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
|
||||
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
|
||||
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
|
||||
function saveHash(){const h='#v='+VERSION+'&b='+state.bits+'&re='+state.re+'&im='+state.im+'&sp='+state.span+'&pal='+state.palette+'&cy='+state.cycle+'&sh='+state.shift+'&it='+state.baseIter+'&ad='+(state.adaptive?1:0)+'&q='+state.quality;lastWrittenHash=h;try{history.replaceState(null,'',h)}catch{location.hash=h}}
|
||||
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal')){const pal=Math.max(0,Math.min(PALETTE_MAX,Number(p.get('pal'))|0));state.palette=VALID_PALETTE_IDS.has(pal)?pal:0;}if(p.has('cy'))state.cycle=Math.max(CYCLE_MIN,Math.min(CYCLE_MAX,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';applyQuality(p.has('q')?p.get('q'):p.has('cb')&&Number(p.get('cb'))>0?(Number(p.get('cb'))<=4096?'fast':Number(p.get('cb'))<=16384?'standard':'high'):state.quality);ensurePrecision();return true}catch{return false}}
|
||||
function syncControls(){$('quality').value=state.quality;$('palette').value=String(state.palette);$('cycle').value=String(cycleToSlider(state.cycle));$('cycleO').textContent=state.cycle.toFixed(4);$('shift').value=String(state.shift);$('shiftO').textContent=state.shift.toFixed(2);syncColorAutoButton()}
|
||||
function toast(s){const e=$('toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
|
||||
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('uiToggle').textContent=state.uiHidden?'UI+':'UI−';$('uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
|
||||
$('uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
|
||||
$('zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);saveHash();markDirty()};$('zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);saveHash();markDirty()};$('reset').onclick=()=>{reset();syncControls()};
|
||||
async function copyShareUrl(){
|
||||
saveHash(false);const url=location.href;
|
||||
saveHash();const url=location.href;
|
||||
try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(url);toast('共有URLをコピーしました');return}}
|
||||
catch{}
|
||||
try{const ta=document.createElement('textarea');ta.value=url;ta.setAttribute('readonly','');ta.style.position='fixed';ta.style.opacity='0';document.body.appendChild(ta);ta.select();const ok=document.execCommand&&document.execCommand('copy');ta.remove();if(ok){toast('共有URLをコピーしました');return}}catch{}
|
||||
try{if(navigator.share){await navigator.share({title:'MANDELBROT DEEP ZOOM',url});toast('共有しました');return}}catch(e){if(e?.name==='AbortError')return}
|
||||
try{prompt('共有URLをコピーしてください',url)}catch{}toast('共有URLを表示しました')
|
||||
}
|
||||
$('#share').onclick=copyShareUrl;
|
||||
$('share').onclick=copyShareUrl;
|
||||
let colorAutoRaf=0,colorAutoLast=0,colorAutoPaint=0;
|
||||
function syncColorAutoButton(){const b=$('#colorAuto');if(!b)return;b.textContent=state.colorAuto?'色相変化停止':'色相変化';b.classList.toggle('on',state.colorAuto);b.setAttribute('aria-pressed',state.colorAuto?'true':'false')}
|
||||
function syncColorAutoButton(){const b=$('colorAuto');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&&!state.numericRecoveryRunning)renderer?.captureColorSource();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(false)});
|
||||
function applyCycleControl(e){state.cycle=sliderToCycle(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);requestRecolor()}
|
||||
function applyShiftControl(e){if(state.colorAuto)stopColorAuto();state.shift=Math.max(0,Math.min(1,Number(e.target.value)||0));$('#shiftO').textContent=state.shift.toFixed(2);requestRecolor()}
|
||||
$('#cycle').addEventListener('input',applyCycleControl);$('#cycle').addEventListener('change',()=>saveHash(false));
|
||||
$('#shift').addEventListener('input',applyShiftControl);$('#shift').addEventListener('change',()=>saveHash(false));
|
||||
$('#quality').onchange=e=>{cancelRender();applyQuality(e.target.value);resize();saveHash(false);recordView();markDirty(false);try{localStorage.setItem('mandelbrot.quality',state.quality)}catch{}};
|
||||
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&&!state.numericRecoveryRunning)renderer?.captureColorSource();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()}
|
||||
$('cycle').addEventListener('input',applyCycleControl);$('cycle').addEventListener('change',()=>saveHash());
|
||||
$('shift').addEventListener('input',applyShiftControl);$('shift').addEventListener('change',()=>saveHash());
|
||||
$('quality').onchange=e=>{cancelRender();applyQuality(e.target.value);resize();saveHash();markDirty(false);try{localStorage.setItem('mandelbrot.quality',state.quality)}catch{}};
|
||||
|
||||
addEventListener('resize',()=>{const w=canvas.width,h=canvas.height;resize();if(w!==canvas.width||h!==canvas.height)markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
|
||||
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);const before=numericViewKey();if(loadHash()){syncControls();recordView();if(before===numericViewKey())requestRecolor();else{resize();markDirty()}}});
|
||||
addEventListener('resize',()=>{const w=canvas.width,h=canvas.height;resize();if(w!==canvas.width||h!==canvas.height)markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('uiToggle').click();else if(e.key==='r'||e.key==='R')$('reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();saveHash();markDirty()}});
|
||||
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);const before=numericViewKey();if(loadHash()){syncControls();if(before===numericViewKey())requestRecolor();else{resize();markDirty()}}});
|
||||
|
||||
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
|
||||
const exportJob={active:false,cancelled:false};
|
||||
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
|
||||
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
|
||||
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
|
||||
let CRC_TABLE;
|
||||
function crc32Parts(parts){const t=CRC_TABLE||=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();let c=0xffffffff;for(const part of parts)for(const b of part)c=t[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
|
||||
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
|
||||
class StreamingPng{
|
||||
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
|
||||
|
|
@ -2026,59 +2072,58 @@ class StreamingPng{
|
|||
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
|
||||
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
|
||||
}
|
||||
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
|
||||
function exportDimensions(){const scale=Number($('exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
|
||||
async function yieldExportUi(lastYield,force=false){if(!force&&performance.now()-lastYield<24)return lastYield;if(globalThis.scheduler?.yield)await scheduler.yield();else await new Promise(resolve=>setTimeout(resolve,0));return performance.now()}
|
||||
async function runExport(){
|
||||
if(exportJob.active)return;
|
||||
const requestedToken=state.token;
|
||||
while(state.rendering||state.dirty||state.refinePending){
|
||||
$('#exportStatus').textContent='現在の描画が完了するのを待っています…';
|
||||
$('exportStatus').textContent='現在の描画が完了するのを待っています…';
|
||||
await new Promise(resolve=>setTimeout(resolve,50));
|
||||
if(state.token!==requestedToken){$('#exportStatus').textContent='表示位置が変わったため出力を中止しました。';return;}
|
||||
if(state.token!==requestedToken){$('exportStatus').textContent='表示位置が変わったため出力を中止しました。';return;}
|
||||
}
|
||||
if(state.fieldView?.complete===false){$('#exportStatus').textContent='未完了の描画です。計算完了後に出力してください。';return;}
|
||||
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
|
||||
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=state.fieldView?.iter??maxIter(),deep=deepNeeded(snap,w,h),fastExtended=!deep&&fastNeedsExtended(snap,w),strict=$('#exportPrecision').value==='strict';
|
||||
if(state.fieldView?.complete===false){$('exportStatus').textContent='未完了の描画です。計算完了後に出力してください。';return;}
|
||||
const r=renderer||await initRenderer();if(!r){$('exportStatus').textContent='WebGPUが必要です。';return}
|
||||
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('exportAA').value)||1)),snap=snapshot(),iter=state.fieldView?.iter??maxIter(),fastExtended=fastNeedsExtended(snap,w),strict=$('exportPrecision').value==='strict';
|
||||
if(w===canvas.width&&h===canvas.height&&ss===1){
|
||||
exportJob.active=true;exportJob.cancelled=false;$('#exportStart').disabled=true;
|
||||
exportJob.active=true;exportJob.cancelled=false;$('exportStart').disabled=true;
|
||||
const png=new StreamingPng(w,h),generation=state.token;
|
||||
try{
|
||||
const paletteParts=r.frontColorKey.split(':'),palette={id:Number(paletteParts[0]),cycle:Number(paletteParts[1]),shift:Number(paletteParts[2]),edgeAA:paletteParts[3]==='1'},rgba=await r.readCompleteRgba(generation),rows=new Uint8Array((w*4+1)*h);
|
||||
const paletteParts=r.frontColorKey.split(':'),palette={id:Number(paletteParts[0]),cycle:Number(paletteParts[1]),shift:Number(paletteParts[2]),edgeAA:false},rgba=await r.readCompleteRgba(generation),rows=new Uint8Array((w*4+1)*h);
|
||||
for(let y=0;y<h;y++)rows.set(rgba.subarray(y*w*4,(y+1)*w*4),y*(w*4+1)+1);
|
||||
await png.rows(rows);const blob=await png.finish();
|
||||
if(exportJob.cancelled||generation!==state.token)throw new Error('cancelled');
|
||||
const base='mandelbrot-'+Date.now(),meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,width:w,height:h,supersampling:1,iterationPolicy:{base:state.baseIter,effective:iter,policy:state.fieldView.frontier?.policy||'finite-budget'},membershipCertified:false,numericalFailures:0,operationLimit:state.unknownReasons?.operationLimit||0,view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette,exportPipeline:'completed-numeric-field',shaderVersion:G.version};
|
||||
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;$('#exportStatus').textContent='計算済みの画像と座標メタデータを保存しました。';
|
||||
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e.message||e)}
|
||||
finally{exportJob.active=false;$('#exportStart').disabled=false}
|
||||
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');$('exportStatus').textContent='計算済みの画像と座標メタデータを保存しました。';
|
||||
}catch(e){await png.abort(e);$('exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e.message||e)}
|
||||
finally{exportJob.active=false;$('exportStart').disabled=false}
|
||||
return;
|
||||
}
|
||||
let ctx=null,fastCtx=null;
|
||||
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}else if(fastExtended){$('#exportStatus').textContent='高速拡張参照を準備中…';fastCtx=await fastRefs.request(snap,iter,w,h)}
|
||||
const tile=r.exportTileSize(),ringDepth=deep||iter>4096?1:r.exportRingDepth(tile),totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
|
||||
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
|
||||
let fastCtx=null;if(fastExtended){$('exportStatus').textContent='高速拡張参照を準備中…';fastCtx=await fastRefs.request(snap,iter,w,h)}
|
||||
const tile=r.exportTileSize(),ringDepth=iter>4096?1:r.exportRingDepth(tile),totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
|
||||
exportJob.active=true;exportJob.cancelled=false;$('exportProgress').hidden=false;$('exportProgress').value=0;$('exportStart').disabled=true;
|
||||
let done=0,unresolvedSamples=0,lastYield=performance.now();
|
||||
try{
|
||||
for(let y=0;y<h;y+=tile){
|
||||
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
|
||||
for(let x=0;x<w;){
|
||||
if(exportJob.cancelled)throw new Error('cancelled');lastYield=await yieldExportUi(lastYield);if(exportJob.cancelled)throw new Error('cancelled');
|
||||
const batch=[];for(let slot=0;slot<ringDepth&&x<w;slot++,x+=tile){const tileX=x,tw=Math.min(tile,w-tileX),args={snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX,tileY:y,w:tw,h:th,forceStrict:strict,workspaceSlot:slot,workspaceSize:tile};batch.push({tileX,tw,promise:ss===1?r.renderTileRGBA({...args,sampleX:.5,sampleY:.5,edgeAA:false}):r.renderTileRGBA2x(args)})}
|
||||
const batch=[];for(let slot=0;slot<ringDepth&&x<w;slot++,x+=tile){const tileX=x,tw=Math.min(tile,w-tileX),args={snap,iter,deep:false,fastContext:fastCtx,fullW:w,fullH:h,tileX,tileY:y,w:tw,h:th,forceStrict:strict,workspaceSlot:slot,workspaceSize:tile};batch.push({tileX,tw,promise:ss===1?r.renderTileRGBA({...args,sampleX:.5,sampleY:.5,edgeAA:false}):r.renderTileRGBA2x(args)})}
|
||||
const settled=await Promise.allSettled(batch.map(item=>item.promise)),failed=settled.find(item=>item.status==='rejected');if(failed)throw failed.reason;const results=settled.map(item=>item.value);
|
||||
for(let i=0;i<batch.length;i++){const {tileX,tw}=batch[i],result=results[i],data=result.rgba;unresolvedSamples+=result.unresolved||0;for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+tileX*4);done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'')}
|
||||
for(let i=0;i<batch.length;i++){const {tileX,tw}=batch[i],result=results[i],data=result.rgba;unresolvedSamples+=result.unresolved||0;for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+tileX*4);done++;$('exportProgress').value=done/totalTiles;$('exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'')}
|
||||
}
|
||||
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);lastYield=await yieldExportUi(lastYield);
|
||||
}
|
||||
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
|
||||
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':fastExtended?'reduced-reference + fast-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + '+ringDepth+'-slot staging ring + streaming-png':'gpu-tile + '+ringDepth+'-slot staging ring + streaming-png',exportTileSize:tile,exportRingDepth:ringDepth,iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch}:null,shaderVersion:G.version};
|
||||
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
|
||||
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'(sidecar参照)':'PNGと座標メタデータを保存しました。';
|
||||
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
|
||||
finally{exportJob.active=false;$('#exportStart').disabled=false}
|
||||
if(exportJob.cancelled)throw new Error('cancelled');$('exportStatus').textContent='PNGストリームを確定中…';
|
||||
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:fastExtended?'reduced-reference + fast-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + '+ringDepth+'-slot staging ring + streaming-png':'gpu-tile + '+ringDepth+'-slot staging ring + streaming-png',exportTileSize:tile,exportRingDepth:ringDepth,iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},shaderVersion:G.version};
|
||||
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}
|
||||
}
|
||||
$('#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');
|
||||
$('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');
|
||||
|
||||
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8)),decision=state.backendDecision||chooseBackend();$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?('WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'WebGPU 復旧中':'起動中');$('#render').textContent=renderTimeLabel();let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'数値補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.fieldView?.complete===false)status+=' · 暫定表示';if(state.unresolved){const nf=numericalFailureCount({reasons:state.unknownReasons||{}}),op=state.unknownReasons?.operationLimit||0;status+=' · 未確定 '+state.unresolved+(nf||op?' (数値 '+nf+' / 上限 '+op+')':'');}if(state.correctionPasses)status+=' · DS補正';if(state.multiRefPasses)status+=' · 局所ref '+state.multiRefPasses+' / 回収 '+state.multiRefRecovered;if(state.qualityStage==='preview')status+=' · preview '+state.qualityIter+'/'+state.qualityFinalIter;if(state.refinePending)status+=' · 最終品質待機';if(state.numericRecoveryPending)status+=' · 数値穴補修待機';if(state.numericRecoveryRunning)status+=' · 数値穴補修中';if(state.qualityStage==='refining')status+=' · 精細化 '+state.qualityIter+'/'+state.qualityFinalIter;if(state.qualityStage==='final')status+=' · 最終 '+(state.fieldView?.iter??state.qualityIter);if(state.fieldView?.frontier?.policy==='automatic-finite-cap')status+=' · 反復上限で完了';if(state.temporalFill)status+=' · 前フレーム保持';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('#badge').textContent=status;$('#compactStatus').textContent=status}
|
||||
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8));$('coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'−':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('span').textContent=fmtSpan();$('engine').textContent=renderer?('WebGPU 高速'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'WebGPU 復旧中':'起動中');$('render').textContent=renderTimeLabel();let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'前フレーム再利用':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINING'?'数値補修中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'描画完了':'準備中';if(state.fieldView?.complete===false)status+=' · 暫定表示';if(state.unresolved){const nf=numericalFailureCount({reasons:state.unknownReasons||{}}),op=state.unknownReasons?.operationLimit||0;status+=' · 未確定 '+state.unresolved+(nf||op?' (数値 '+nf+' / 上限 '+op+')':'');}if(state.multiRefPasses)status+=' · 局所ref '+state.multiRefPasses+' / 回収 '+state.multiRefRecovered;if(state.qualityStage==='preview')status+=' · preview '+state.qualityIter+'/'+state.qualityFinalIter;if(state.refinePending)status+=' · 最終品質待機';if(state.numericRecoveryPending)status+=' · 数値穴補修待機';if(state.numericRecoveryRunning)status+=' · 数値穴補修中';if(state.qualityStage==='refining')status+=' · 精細化 '+state.qualityIter+'/'+state.qualityFinalIter;if(state.qualityStage==='final')status+=' · 最終 '+(state.fieldView?.iter??state.qualityIter);if(state.fieldView?.frontier?.policy==='automatic-finite-cap')status+=' · 反復上限で完了';if(state.temporalFill)status+=' · 前フレーム保持';if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge}$('badge').textContent=status}
|
||||
|
||||
if(TEST_MODE){
|
||||
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))}}
|
||||
|
|
@ -2107,26 +2152,26 @@ globalThis.__MANDEL_TEST__={
|
|||
return Array.from(solved.indices,(index,j)=>({index,packed:field.meta[index],smoothed:smoothed[index],oracle:solved.meta[j],oracleSmooth:solved.smooth[j],accepted:solved.accepted[j]}));
|
||||
},
|
||||
async ensureGpuReady(){const r=renderer||await initRenderer();return{ready:!!r,navigatorGpu:!!navigator.gpu,renderer:!!renderer,gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError}},
|
||||
async setView({re,im,span,bits,baseIter=350,adaptive=false,continuationBudget=0,processMode='standard',renderMode='fast',quality=null,fixed=false}){const ready=await this.ensureGpuReady();if(!ready.ready)throw new Error('WebGPU initialization failed: '+JSON.stringify(ready));cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fixed?BigInt(re):fromDec(re);state.im=fixed?BigInt(im):fromDec(im);state.span=fixed?BigInt(span):fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(continuationBudget)));state.quality='standard';if(quality)applyQuality(quality);state.renderMode='fast';state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');if(!renderer?.frame||!state.fieldView)throw new Error('test render completed without a GPU frame: '+JSON.stringify({gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError,drawState:state.drawState,engine:state.lastEngine}));return{width:canvas.width,height:canvas.height,state:this.state()}},
|
||||
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();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fixed?BigInt(re):fromDec(re);state.im=fixed?BigInt(im):fromDec(im);state.span=fixed?BigInt(span):fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.continuationBudget=Math.max(0,Math.min(150000,Math.floor(continuationBudget)));state.quality='standard';if(quality)applyQuality(quality);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');if(!renderer?.frame||!state.fieldView)throw new Error('test render completed without a GPU frame: '+JSON.stringify({gpuInitFailed:state.gpuInitFailed,gpuUnavailable:state.gpuUnavailable,gpuError:state.gpuError,drawState:state.drawState,engine:state.lastEngine}));return{width:canvas.width,height:canvas.height,state:this.state()}},
|
||||
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
|
||||
async waitForPrimary({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&!!state.fieldView,'primary',timeout);return this.state()},
|
||||
async waitForFinal({timeout=120000}={}){await waitForTestState(()=>!state.dirty&&!state.rendering&&!state.refinePending&&state.qualityStage==='final'&&state.fieldView?.complete===true&&state.renderClock?.status==='complete','final',timeout);return this.state()},
|
||||
async waitForNumericComplete({timeout=120000}={}){await this.waitForFinal({timeout});await waitForTestState(()=>!state.numericRecoveryPending&&!state.numericRecoveryRunning,'numeric recovery',timeout);const result=this.state();if(result.numericalFailures)throw new Error('numeric recovery quiesced with '+result.numericalFailures+' unresolved failures');return result},
|
||||
async fieldHashes(){if(!renderer?.frame)throw new Error('WebGPU field unavailable');const field=await renderer.readFieldAll();return{pixels:field.meta.length,metaSha256:await sha256View(field.meta),smoothSha256:await sha256View(field.smoothBits)}},
|
||||
gpuDiagnostics:()=>renderer?{adapterInfo:{...renderer.adapterInfo},adapterLimits:{...renderer.adapterLimits},compilation:renderer.compilation.map(x=>({...x})),uncapturedErrors:[...renderer.uncapturedErrors],lossReason:renderer.lossReason||'',activeGpuStage:renderer.activeGpuStage,lastCompletedGpuStage:renderer.lastCompletedGpuStage}:null,
|
||||
stabilityPolicy:()=>({adaptiveIterationQuantum:ADAPTIVE_ITER_QUANTUM,pixelFrontierProduction:PIXEL_FRONTIER_PRODUCTION,pixelFrontierEnabled:PIXEL_FRONTIER_ENABLED,pixelFrontierWorkLimit:PIXEL_FRONTIER_WORK,pixelFrontierMaxIter:PIXEL_FRONTIER_MAX_ITER,pixelFrontierMinimumIter:pixelFrontierIterationFloor(snapshot(),canvas.width,maxIter()),stableHistoryReady:!!renderer?.historyReady,deepCorrectionDispatchWork:PIXEL_FRONTIER_WORK}),
|
||||
state:()=>{const snap=snapshot(),se=spanMantExp(snap),reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),spanExp:se.exp,directPixelRatio:directPixelRatio(snap,canvas.width),width:canvas.width,height:canvas.height,iter:maxIter(),effectiveIter:state.fieldView?.iter??maxIter(),continuationBudget:state.continuationBudget,quality:state.quality,renderMode:state.renderMode,palette:state.palette,cycle:state.cycle,shift:state.shift,colorRevision:state.colorRevision,colorAuto:state.colorAuto,rendering:state.rendering,recoloring:state.recoloring,recolorPending:state.recolorPending,lastRender:state.lastRender,renderClock:state.renderClock?{...state.renderClock}:null,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,provisional:state.fieldView?.complete===false,qualityStage:state.qualityStage,qualityIter:state.qualityIter,qualityFinalIter:state.qualityFinalIter,previewMs:state.previewMs,refineMs:state.refineMs,refinePending:state.refinePending,temporalFill:state.temporalFill,numericRecoveryPending:state.numericRecoveryPending,numericRecoveryRunning:state.numericRecoveryRunning,historyReady:!!renderer?.historyReady,multiRefPasses:state.multiRefPasses,multiRefRecovered:state.multiRefRecovered,frontierRounds:state.frontierRounds,frontierPixels:state.frontierPixels,frontierEscaped:state.frontierEscaped,frontierIter:state.frontierIter,frontierMinimumIter:state.frontierMinimumIter,frontierConverged:state.frontierConverged,frontierMaxDispatchWork:runtime.frontierMaxDispatchWork||0,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version,gpuStage:state.gpuStage,gpuTileRows:state.gpuTileRows,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,stageCounts:{...runtime.stageCounts},stageMs:{...runtime.stageMs},generationMetrics:runtime.generationMetrics.slice(-16).map(x=>({...x}))}},
|
||||
stabilityPolicy:()=>({adaptiveIterationQuantum:ADAPTIVE_ITER_QUANTUM,pixelFrontierProduction:true,pixelFrontierEnabled:true,pixelFrontierWorkLimit:PIXEL_FRONTIER_WORK,activeContinuationWorkLimit:ACTIVE_CONTINUATION_WORK,pixelFrontierMaxIter:PIXEL_FRONTIER_MAX_ITER,pixelFrontierMinimumIter:pixelFrontierIterationFloor(snapshot(),canvas.width,maxIter()),stableHistoryReady:!!renderer?.historyReady,deepCorrectionDispatchWork:PIXEL_FRONTIER_WORK}),
|
||||
state:()=>{const snap=snapshot(),se=spanMantExp(snap),reasons=state.unknownReasons||{};return{bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),spanExp:se.exp,directPixelRatio:directPixelRatio(snap,canvas.width),width:canvas.width,height:canvas.height,iter:maxIter(),effectiveIter:state.fieldView?.iter??maxIter(),continuationBudget:state.continuationBudget,quality:state.quality,palette:state.palette,cycle:state.cycle,shift:state.shift,colorRevision:state.colorRevision,colorAuto:state.colorAuto,rendering:state.rendering,recoloring:state.recoloring,recolorPending:state.recolorPending,lastRender:state.lastRender,renderClock:state.renderClock?{...state.renderClock}:null,engine:state.lastEngine,backend:state.fieldView?.backend||null,fastExtended:!!state.fieldView?.fastExtended,referenceReused:!!state.fieldView?.referenceReused,provisional:state.fieldView?.complete===false,qualityStage:state.qualityStage,qualityIter:state.qualityIter,qualityFinalIter:state.qualityFinalIter,previewMs:state.previewMs,refineMs:state.refineMs,refinePending:state.refinePending,temporalFill:state.temporalFill,numericRecoveryPending:state.numericRecoveryPending,numericRecoveryRunning:state.numericRecoveryRunning,historyReady:!!renderer?.historyReady,multiRefPasses:state.multiRefPasses,multiRefRecovered:state.multiRefRecovered,frontierRounds:state.frontierRounds,frontierPixels:state.frontierPixels,frontierEscaped:state.frontierEscaped,frontierIter:state.frontierIter,frontierMinimumIter:state.frontierMinimumIter,frontierConverged:state.frontierConverged,frontierMaxDispatchWork:runtime.frontierMaxDispatchWork||0,activeContinuationMaxDispatchWork:runtime.activeContinuationMaxDispatchWork||0,correctionMaxDispatchWork:runtime.correctionMaxDispatchWork||0,numericalFailures:numericalFailureCount({reasons}),unknownReasons:state.unknownReasons,kernelVersion:G.version,gpuStage:state.gpuStage,numericSubmits:runtime.numericSubmits,correctionSubmits:runtime.correctionSubmits,gpuSyncWaits:runtime.gpuSyncWaits,stageCounts:{...runtime.stageCounts},stageMs:{...runtime.stageMs},generationMetrics:runtime.generationMetrics.slice(-16).map(x=>({...x}))}},
|
||||
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 compareCurrentToFrontierBaseline({strict=true}={}){if(!renderer?.frame)throw new Error('WebGPU frame unavailable');const snap=snapshot(),iter=state.fieldView?.iter??maxIter(),w=canvas.width,h=canvas.height,view=state.fieldView||{},deep=true,exactRequired=!!view.deep,ctx=await refs.request(snap,iter,w,h);if(ctx.checkpointMismatch)throw new Error('frontier baseline reference guard checkpoint mismatch');const rows=Math.max(1,Math.floor(PIXEL_FRONTIER_WORK/Math.max(1,w*iter))),baseline=new Uint32Array(w*h);let maxDispatchWork=0;for(let y=0;y<h;y+=rows){const th=Math.min(rows,h-y),part=await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:y,w,h:th,forceStrict:!!strict,correctUnknown:true});baseline.set(part,y*w);maxDispatchWork=Math.max(maxDispatchWork,w*th*iter)}const current=await renderer.readMetaAll(),disputes=[];let mismatch=0,classMismatch=0,escapeIterationMismatch=0,first=-1,escaped=0,baselineEscaped=0;for(let i=0;i<current.length;i++){const a=current[i]>>>0,b=baseline[i]>>>0,ac=(a>>>28)&3,bc=(b>>>28)&3;if(ac===FIELD_ESCAPED)escaped++;if(bc===FIELD_ESCAPED)baselineEscaped++;const sameClass=(ac===FIELD_ESCAPED)===(bc===FIELD_ESCAPED),sameIter=ac!==FIELD_ESCAPED||(a&ITER_MASK)===(b&ITER_MASK);if(!sameClass)classMismatch++;else if(!sameIter)escapeIterationMismatch++;if(!sameClass||(exactRequired&&!sameIter)){mismatch++;disputes.push(i);if(first<0)first=i}}let oracleChecked=0,oracleInconclusive=0,oracleCurrentMismatch=0,oracleBaselineMismatch=0;if(disputes.length){const solved=await precisionFallback.request(snap,iter,w,h,disputes,-1);for(let j=0;j<solved.indices.length;j++){if(!solved.accepted[j]){oracleInconclusive++;continue}oracleChecked++;const i=solved.indices[j],o=solved.meta[j]>>>0,a=current[i]>>>0,b=baseline[i]>>>0,oc=(o>>>28)&3,ac=(a>>>28)&3,bc=(b>>>28)&3,currentSame=(oc===FIELD_ESCAPED)===(ac===FIELD_ESCAPED)&&(!exactRequired||oc!==FIELD_ESCAPED||(o&ITER_MASK)===(a&ITER_MASK)),baselineSame=(oc===FIELD_ESCAPED)===(bc===FIELD_ESCAPED)&&(!exactRequired||oc!==FIELD_ESCAPED||(o&ITER_MASK)===(b&ITER_MASK));if(!currentSame)oracleCurrentMismatch++;if(!baselineSame)oracleBaselineMismatch++}}const oracleGate=oracleCurrentMismatch===0&&oracleInconclusive===0;return{pixels:current.length,mismatch,classMismatch,escapeIterationMismatch,exactRequired,first,escaped,baselineEscaped,iter,baseline:'deep-gold',oracleChecked,oracleInconclusive,oracleCurrentMismatch,oracleBaselineMismatch,oracleGate,maxDispatchWork,workLimit:PIXEL_FRONTIER_WORK,gatePassed:oracleGate&&maxDispatchWork<=PIXEL_FRONTIER_WORK}},
|
||||
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=forceDeep===null?deepNeeded(snap,w):!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
|
||||
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w),fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
|
||||
async probeMeta({w,h,strict=true,forceDeep=null,correctUnknown=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=!!forceDeep,fastExtended=!deep&&fastNeedsExtended(snap,w);let ctx=null,fastCtx=null;if(deep)ctx=await refs.request(snap,iter,w,h);else if(fastExtended)fastCtx=await fastRefs.request(snap,iter,w,h);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fastContext:fastCtx,fullW:w,fullH:h,w,h,forceStrict:strict,correctUnknown:deep&&correctUnknown}))},
|
||||
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),fastExtended=fastNeedsExtended(snap,w),fastContext=fastExtended?await fastRefs.request(snap,iter,w,h):null,args={snap,iter,deep:false,fastContext,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict},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,deep:false,strict,ss,unresolved:result.unresolved||0}}
|
||||
};
|
||||
|
||||
} // TEST_MODE
|
||||
|
||||
// ── boot / teardown ──────────────────────────────────────────────────────
|
||||
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{stopColorAuto(false);cancelRender();refs.destroy();fastRefs.destroy();precisionFallback.destroy();for(const service of precisionFallbackPool)service.destroy();if(renderer)renderer.destroy()},{once:true});
|
||||
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';applyQuality(localStorage.getItem('mandelbrot.quality')||localStorage.getItem('mandelbrot.processMode'));localStorage.removeItem('mandelbrot.processMode');localStorage.removeItem('mandelbrot.renderMode')}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();if(!state.rendering&&!state.frameView)markDirty(false)});schedule();
|
||||
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';applyQuality(localStorage.getItem('mandelbrot.quality')||localStorage.getItem('mandelbrot.processMode'));localStorage.removeItem('mandelbrot.processMode')}catch{}applyUi();resize();if(!loadHash())reset();syncControls();updateStats();initRenderer().then(()=>{resize();if(!state.rendering&&!state.frameView)markDirty(false)});schedule();
|
||||
|
||||
})();
|
||||
|
||||
|
|
|
|||
43
tests/active_work_plan.mjs
Normal file
43
tests/active_work_plan.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// 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})}
|
||||
|
|
@ -114,6 +114,37 @@ async function kernelCases(){
|
|||
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()}
|
||||
}
|
||||
|
|
@ -202,6 +233,9 @@ try{
|
|||
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,'ピンクと黒');
|
||||
|
|
@ -213,7 +247,7 @@ try{
|
|||
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 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);
|
||||
|
|
@ -289,7 +323,7 @@ try{
|
|||
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();u.href');
|
||||
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');
|
||||
|
|
|
|||
116
tests/performance_candidates.mjs
Normal file
116
tests/performance_candidates.mjs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// 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});
|
||||
}
|
||||
111
tests/quality_plan.mjs
Normal file
111
tests/quality_plan.mjs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
// 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});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue