eee
This commit is contained in:
parent
0be5ce3ce3
commit
178cac581c
28 changed files with 1695 additions and 2746 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Optional local Edge profile used for GPU smoke tests.
|
||||
.test-edge/
|
||||
28
docs/CLEANUP.md
Normal file
28
docs/CLEANUP.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# BLA・監査整理(2026-09-05)
|
||||
|
||||
BLAの計算経路、テーブル構築Worker、キャッシュ、GPU probe、shadow比較、canary適用、遅延コンパイル、タイマー、状態表示、切替APIを除去した。
|
||||
|
||||
比較専用のDirect/Deep active-list実験と内部点判定実験、未使用の全画面Deep候補、重複した統計シェーダーも削除した。描画完了後に比較計算を開始する処理や、URLから自動監査を起動する処理は残していない。
|
||||
|
||||
既存の変更に含まれていたfrontierの本番有効化は維持した。通常のDirect、FAST perturbation、Accurate DS、Deep perturbation、未脱出画素の継続計算、数値UNKNOWNの補修、世代キャンセル、GPU処理量制限、完成画像の履歴保持は維持している。Strictは数値判定の設定として利用できる。
|
||||
|
||||
詳細な世代別計測と検証APIは `?test` を指定した場合だけ有効になる。描画時間に応じて処理を分割するための計測と、GPUエラーの報告は通常時も維持する。
|
||||
|
||||
旧版別のハッシュ、保存済みベンチマーク、複数の昇格ゲート、重複したPython/JavaScript静的監査は廃止した。検証は `tests/regression.mjs` と `tests/browser_smoke.mjs` にまとめ、現行シェーダーのハッシュだけを保持する。過去の計画・結果はGit履歴から参照できる。
|
||||
|
||||
## 回帰テスト
|
||||
|
||||
- アプリとシェーダー生成のJavaScript構文。
|
||||
- 維持した23本のGPUシェーダーが整理前と完全一致。
|
||||
- Direct/FAST拡張/Accurate DS/Deepの選択と、引数削除後のフレーム振り分け。
|
||||
- 数値エラーと反復上限画素の分類、既存のGPU strip処理量制限。
|
||||
- 3種類のWorkerによる参照軌道生成・数値補修。
|
||||
- 通常起動で検証APIが公開されず、テスト起動で利用できること。
|
||||
|
||||
## 実GPU確認
|
||||
|
||||
独立した一時プロファイルのEdgeをheadlessモード、320×240で実行。初期表示、深部FAST、Accurate DS、Deepで数値UNKNOWNゼロ、GPUエラーなし、描画完了を確認した。初期表示ではパン往復のmeta/smoothハッシュ一致、通常/2倍サンプリングの出力タイルも確認した。同じSeahorse座標のFASTとDeepでも最終meta/smoothハッシュが一致した。
|
||||
|
||||
深部FAST(Seahorse、span 3.4e-13、初期512反復)は継続計算で32768反復まで進み、最終描画に約36.7秒かかった。比較前後の同一条件ベンチマークではないため、速度改善率は主張しない。BLAとは別の継続計算・数値補修の負荷は残っている。
|
||||
|
||||
2026-09-06追記:上の「数値UNKNOWNゼロ」は当時のカウンター値であり、全画素の終端誤差検査や厳密な分類の証明ではない。その後のコード精査で、継続経路の目標終端検査と処理量上限に不足を確認した。[改善案](PERFORMANCE_PLAN.md)と[実装設計](PERFORMANCE_IMPLEMENTATION.md)に修正方針・テストの不足範囲を記載した。過去の測定値自体は変更していない。
|
||||
69
docs/IMPLEMENTATION_RESULTS.md
Normal file
69
docs/IMPLEMENTATION_RESULTS.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# 軽量化の実装と確認結果
|
||||
|
||||
更新日:2026-09-06。対象:[index.html](../index.html)。[改善案](PERFORMANCE_PLAN.md) と [実装設計](PERFORMANCE_IMPLEMENTATION.md) に基づく変更。
|
||||
|
||||
この文書は軽量化実装時点の記録。以後の色相・時間表示の修正と正確モードの削除は[ビューワー修正](VIEWER_FIXES.md)を参照。
|
||||
|
||||
## 実装した処理
|
||||
|
||||
| 計画 | 変更 | 確認方法 |
|
||||
| --- | --- | --- |
|
||||
| A:判定と上限 | 目標の最後の更新後に脱出・誤差・参照範囲を確認する。正常な未脱出は実到達反復数を記録して継続キューへ残す。Direct DSも未脱出をreason 6へ統一した | 実GPUの目標境界・誤差超過・参照末尾テスト |
|
||||
| A:分割 | DSの適応バッチを毎回400万pixel-iterationsへ制限。幅1行でも上限を超える場合は横に分割する。PNGの重いタイルも分割する | 幅320~3840、350~150000反復の上限・全画素被覆テスト |
|
||||
| B:同期 | 継続を1~4 passずつ適応投入し、最後の件数コピーをmapする。直前の全queue待ちは省く | 0・1・63・64・65件、奇数/偶数pass、次目標への再投入 |
|
||||
| B:CPU補修 | 比較済みの高精度結果を次の比較へ使う。最大8軌道を5軌道にする。1~2 Workerへ画素数・反復数・精度に応じて小分けに投入し、完成した結果をまとめてGPUへ渡す | 実Workerの軌道呼び出し数と採用条件を確認 |
|
||||
| C:状態保持 | 数値補修で正常画素を再初期化しない。metaだけ回復した画素を別に保持し、それらだけ次目標で再計算する。Deepの初期計算も同じ継続stateを作る | キューの保持・コンパクトslot・描画回帰テスト |
|
||||
| C:参照 | 同一の原点・実精度の軌道をWorkerで末尾延長する。基準軌道と高精度検証軌道は独立したBigInt終端を持つ。GPU側も容量内では末尾だけ転送する | 延長結果と独立再生成のbyte一致、精度変更時のID分離 |
|
||||
| D:失敗転送 | 既存のGPU histogram/prefix/scatterをタイル補修へ使う。CPU補修へ渡す際も失敗indexのみ読み戻す | タイル内のindex集合をGPU出力と厳密比較 |
|
||||
| D:結果反映 | CPU結果をGPU scatterで反映する。書き込み前に描画世代を確認し、GPU側でも現在のmetaが数値失敗であることを確認する | 古い世代の拒否、確定画素・反復待ち画素の保持 |
|
||||
| E:内点 | 主カージオイドと周期2円に矩形全体が入ることをBigIntの整数区間で確認し、該当タイルを継続対象から除く | 256 bitの境界内外・境界をまたぐ矩形、実軸のGPU描画 |
|
||||
| E:優先順位 | 大きく重い継続ラウンドでは既存境界を先に進め、残りも同じ目標まで必ず進める | 実コントローラーのテストで両グループの目標到達・index全件保持を確認 |
|
||||
| E:反復予算 | 「反復上限」で画素精度から独立した有限予算を指定できる。自動設定では従来の停止方針を維持する。指定値はURLと表示履歴へ保存する | 初期反復512/4096から指定4096への到達、未脱出全画素のmeta、PNGの予算一致 |
|
||||
| E:表示とメモリ | 現在の画素格子の暫定結果を公開する。完成履歴へは入れない。疎な候補はコンパクトslot、密な候補は32 byteのstateを使う | 暫定PNG拒否、キャンセル、slotと画素indexの一致 |
|
||||
|
||||
## 数値・品質の扱い
|
||||
|
||||
数値故障、有限反復での未脱出、証明できた内点を区別する。誤差guardを緩めたり、未確定画素を周囲の色で埋めて成功件数へ算入したりしない。
|
||||
|
||||
既定の「自動」では、従来の最低反復予算と安定ラウンド判定を維持した。「反復上限」を指定すると、その有限予算まで必要な全画素を処理して終了する。優先順位は表示順を変えるもので、低優先度の画素を省くものではない。`pixelBits × 256` や安定2回を内点証明とは扱わず、結果には有限予算の方針を記録する。一般周期成分の包含証明や新しい近似加速器は、元の計画どおり研究候補として残している。
|
||||
|
||||
参照が実際に脱出した場合や採用精度が変わった場合は、同じstateを無条件には使えない。その場合の再初期化と、少数の補修による不要な全体再初期化は別に扱う。
|
||||
|
||||
初期描画用の参照と補修用の参照を分離し、同一のテスト入力では繰り返し描画・FAST/Deepのmetaとsmoothのbyte一致を確認した。独立BigIntサンプルでは脱出分類・脱出反復数を厳密比較し、f32の再構成とlog2を含むsmoothは8 ULP以内を確認する。このサンプル検査を全画素の数学的証明とはしない。
|
||||
|
||||
主参照のCPUキャッシュは8 MiB、局所参照は2 Worker×2 MiBを基本上限とし、上限より大きな単独参照は1本だけ保持する。GPU参照は最大3種類。activeの密なstateは32 byte/画素、疎なslotは40 byte/候補とindex対応表を持ち、後者が実際に小さくなる場合だけ使う。密なframeでは既存の画素数上限とdeviceのbinding上限を使い、タイルごとに生存stateを捨てる方式は採らない。
|
||||
|
||||
通常の同一解像度PNGは補修済みのframeをコピーする。別解像度・複数sample出力は独立したタイル計算であり、残った未確定sample数を従来どおり出力メタデータへ記録する。任意の出力解像度・座標の数値失敗0は保証していない。
|
||||
|
||||
## 検証
|
||||
|
||||
検証用Edgeは専用プロファイル、1インスタンス・1タブで直列実行する。常時監査は追加せず、検証APIと詳細計測は `?test` のみ。
|
||||
|
||||
```sh
|
||||
node tests/regression.mjs
|
||||
node tests/browser_smoke.mjs 9333 all
|
||||
```
|
||||
|
||||
CPUテスト:構文、シェーダー固定値、ルーティング、分割の被覆と上限、Workerの参照延長・精度再利用、内点区間、古い世代の拒否。
|
||||
|
||||
GPUテスト:終端判定、queueの全index、コンパクトstate、scatterの対象保護、6つの描画座標、独立BigIntサンプル、パン往復、補修中のキャンセル、PNGの符号化と復号。
|
||||
|
||||
最終版で全回帰テストが通過した。640×480の実画面で「反復上限」を4096へ変更し、URLへの保存と再読み込み後の復元も確認した。検証終了後、専用Edgeとそのプロファイルを終了・削除し、残存Edgeプロセスがないことを確認した。
|
||||
|
||||
## 測定結果
|
||||
|
||||
Intel Gen-9の実GPU、Edge、320×240、標準負荷、自動の反復上限。Seahorseは `re=-0.743643887037151, im=0.13182590420533, span=3.4e-13`、初期512反復。各ケースを1回ウォームアップした後、同じタブで3回測定した。
|
||||
|
||||
| ケース | 3回の完成時間 (ms) | 中央値 (ms) | 暫定表示までの中央値 (ms) | 最終反復 | 数値失敗 | 未脱出 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 初期表示 | 211.2 / 214.4 / 205.0 | 211.2 | 9.8 | 4096 | 0 | 1174 |
|
||||
| Seahorse FAST | 12002.7 / 11637.2 / 11577.7 | 11637.2 | 55.9 | 32768 | 0 | 1 |
|
||||
| Seahorse Deep | 11828.2 / 11164.0 / 11153.8 | 11164.0 | 84.7 | 32768 | 0 | 1 |
|
||||
|
||||
Seahorseでは最終的に残った1画素も32768反復へ到達しており、数値故障とは別の未脱出である。3回とも数値bufferのhashが一致した。FAST/Deep間でも一致した。
|
||||
|
||||
FASTの継続バッチは64~65回、主な数値readbackは約4.00 MB、CPU補修は3618軌道。Deepは64~65回、約4.31 MB、3618軌道。仕事量の集計は入口件数×chunkの上界であり、早期脱出を差し引いた実反復数ではない。初期化157185画素には参照原点を変更した際の再初期化を含む。最終active workspaceは疎な1候補用48 byteだが、これは描画中の最大使用量やframe全体のメモリではない。
|
||||
|
||||
[前回記録](CLEANUP.md)のFAST約36.7秒・Deep約41.3秒に対し、今回の同じ解像度・座標の中央値は約11.6秒・11.2秒だった。ただし今回は終端判定と補修も修正しており、旧版との数学的処理条件を完全に固定した速度比較ではない。全座標で同じ倍率の高速化を約束する値ではなく、p95も算出していない。
|
||||
|
||||
回帰用の実軸と既知の黒領域は64×48で確認した。独立サンプルはSeahorse FAST/Deep・実軸・黒領域の各12画素。PNGは320×240の全画素を復号比較し、不一致0だった。処理中の操作で古いCPU補修が新しいframeを上書きせず、暫定frameが完成PNGへ入らないことも確認した。
|
||||
|
|
@ -1,579 +0,0 @@
|
|||
# マンデルブロ集合ビューアー最適化計画
|
||||
|
||||
対象baseline: `index.html` v24.2.55 (`24.2.55-no-black-hole-async-recovery`)
|
||||
|
||||
実装版: `index.html` v24.3.0 (`24.3.0-optimized-unknown-pipeline`)
|
||||
|
||||
作成日: 2026-08-25
|
||||
|
||||
## 本番昇格状況(2026-08-25)
|
||||
|
||||
`PIXEL_FRONTIER_PRODUCTION=true` とし、pixel-convergent frontier を既定経路へ昇格した。ブラウザー操作機能を取得できず実機WebGPUゲートは実行できなかったが、保存済みコミット `83b874c` を復旧点として、ユーザー判断により未検証リスクを受容して本番化した。active-list engine は Accurate の初期全面描画には使わず、初期描画は bounded strip の既存 Deep kernel、active-list は operation-limit 画素の継続計算だけに限定する。
|
||||
|
||||
| 条件 | 状態 | 根拠 / 残作業 |
|
||||
| --- | --- | --- |
|
||||
| JavaScript 構文、worker 構文、kernel hash | 合格 | `tests/optimization_regression.mjs` |
|
||||
| 1 dispatch の上限 4,000,000 pixel-iterations | 静的合格 | 初期 Deep、sparse DS、frontier の各契約を回帰テストで固定 |
|
||||
| Accurate 初期描画が active-list production path を使わない | 静的合格 | `computeFrame()` は bounded `computePerturbFrameTiled()` を使用 |
|
||||
| UNKNOWN を色で隠さず対象画素を計算する | 静的合格 | operation-limit 全 index を continuation queue に投入。色置換や倍率しきい値は不使用 |
|
||||
| 早すぎる安定判定の防止 | 候補実装済み | 画素幅から連続的に求めた最低反復数へ到達する前は、2 round 安定でも終了しない |
|
||||
| exact escape iteration / class mismatch = 0 | 本番後確認 | Browser接続が利用可能になった時点で corpus と高精度 oracle を実行する |
|
||||
| final numeric UNKNOWN = 0 | 本番後確認 | reset、seahorse、real-axis、報告座標で確認する |
|
||||
| WGSL error / uncaptured error / device loss = 0 | 本番後確認 | Windows D3D12 実機で確認する |
|
||||
| p50 / p95 性能基準 | 本番後確認 | 同一セッション A/B を採取し、特に 10^14 付近の初回表示を確認する |
|
||||
| 通常操作で黒領域が出現・消失しない | 本番後確認 | 同一近傍の pan / zoom / round-trip hash を確認する |
|
||||
|
||||
`pixelFrontierIterationFloor()` は zoom 倍率で描画方式を切り替えない。`span / canvasWidth` が表す1画素のスケールから最低反復予算を連続的に算出する。これは「少し拡大しただけで未計算領域を黒として確定する」問題を抑えるための下限であり、数学的な集合所属証明ではない。今回はBrowser接続を利用できないため実機ゲートを昇格前条件から本番後確認へ移した。
|
||||
|
||||
本番判定は `tests/production_gate.mjs` に固定した。候補段階では `node tests/production_gate.mjs <artifact> preflight`、本番フラグを有効化した後は同じコーパスを再実行して `node tests/production_gate.mjs <artifact> production` を通す。artifact は同一adapterで採取したbaseline/candidateの対応、異なる2 adapter以上、全12 correctness case、全6 performance caseを各10計測run、3 interaction caseを含まなければ合格しない。10^5と10^8はFAST/Accurateの両方、10^14は800×600でFAST p50 1000 ms・p95 1500 ms未満、Accurate p50 1500 ms・p95 2000 ms未満を追加の絶対条件とする。
|
||||
|
||||
## 追加修正: 10^14付近の速度とWindows GPU停止
|
||||
|
||||
実機ログで、Accurate Deepの長い単一dispatchがWindowsのGPU監視時間を超え、`DXGI_ERROR_DEVICE_HUNG`を発生させていたことを確認した。`powerPreference`の警告は原因ではない。
|
||||
|
||||
- Accurate DeepのActive-list本番化は、正常版との分類経路差になるため撤回する。正確モードはv24.2.55と同じDeep perturbation式へ戻し、1 stripを最大4,000,000 pixel-iterationsへ縮小することでTDRを回避する。
|
||||
- Deep/Direct-DS補修はより重い演算なので、1 dispatch相当を最大4,000,000 pixel-iterationsへ制限する。疎Deep補修がこの上限を超える場合も補修を省略せず、2次元タイルのbounded scanですべての数値UNKNOWNを処理する。
|
||||
- state/二重queueは最大64 MiBのband workspaceとして再利用し、adapterのbuffer/binding上限を超えない。band間で出力座標を保持するためqueue indexはband-local、meta/smooth indexはframe-globalとする。
|
||||
- device loss直後のadapter再作成を1.2秒遅延し、D3D12復旧前の連続した`Device failed at creation`を避ける。
|
||||
- FASTの最終フレームから無条件Deep補修を外す。数値UNKNOWNが実際に残った場合だけ局所reference補修と画素精度BigInt補修を同期実行し、数値UNKNOWNが0になるまで新しいframeを公開しない。通常FASTはDeepを待たず、未計算frameも表示しない。
|
||||
- 履歴のない深部FASTでは、短いreferenceのpreviewがfinalへ再利用できず二重計算になっていたため、最終品質を1回だけ生成する。既存のstable historyがある操作では従来どおり再投影を即時previewとして使う。
|
||||
- 倍率による描画方式の固定閾値は設けない。必要精度を `span / canvasWidth`(複素平面上の1画素幅)から算出し、参照軌道は画素精度+40 guard bit、任意精度補修は同じ精度と+32 bitの二重計算が一致した場合だけ採用する。画素より細かい座標情報は計算しない。数値UNKNOWNは件数上限で打ち切らず、bounded chunkですべて補修してから完成扱いにする。色で未計算を隠す処理や10^100切替は使用しない。
|
||||
|
||||
### 根本解決: pixel-convergent frontier
|
||||
|
||||
黒領域の膨張には、座標精度不足とは別に「反復上限まで脱出しなかった点を、そのまま集合内部と同じ黒で表示する」問題がある。倍率ごとの固定反復数や固定切替では解決しないため、最終形は次の画素収束方式とする。
|
||||
|
||||
1. 初回passは現在のadaptive反復数まで全画素を計算する。
|
||||
2. `operation-limit` 画素のうち、8近傍に `escaped` がある画素だけをGPU上でfrontier queueへcompactする。黒領域の奥を再計算しない。
|
||||
3. frontier画素は保存したperturbation stateからbounded chunkで継続し、新たに脱出した画素だけを更新する。その近傍を次のfrontierへ追加する。
|
||||
4. analytic cardioid/bulbまたは検証済み周期性で内部と証明できた画素はfrontierから除外する。単に反復上限へ達しただけの画素は「証明済み内部」へ昇格しない。
|
||||
5. 変化した境界のHausdorff距離が1画素未満になり、数値UNKNOWNが0になった時だけframeを完成・history commitする。これにより画素以下の境界情報は追跡しない。
|
||||
6. 1 dispatchは既存のpixel-iteration予算内に分割し、操作が入ればgeneration tokenで即時中止する。同一viewの反復延長ではreference/state/queueを再利用し、iteration 0からやり直さない。
|
||||
|
||||
現在の実装では、この前提となる画素幅基準precision、全残存数値UNKNOWNのbounded補修、未完成frameのpublish禁止までを本番経路へ入れた。frontier continuationは既存のshadow active-listを、実機zero-tolerance corpusとWindows TDR試験に通した後で本番へ昇格する。これが反復上限由来の過大な黒を、全画面高反復の速度低下なしに除く本命経路である。
|
||||
- adaptive反復数の64単位上方丸め、BLA既定OFF、同一viewだけのreference再利用は維持する。したがって操作順による黒領域の揺れを抑える決定性は維持し、反復数・誤差guard・分類式は削減しない。
|
||||
|
||||
## 実装状況(2026-08-25)
|
||||
|
||||
この計画のうち、数値品質を維持したまま静的・モデル検証できる項目をv24.3.0へ実装した。
|
||||
|
||||
- generationごとにprimary/final/numeric-quiescent/history-commit、submit/sync差分、UNKNOWN数を最大64件記録し、テストhookへprimary/final/numeric-complete待機とmeta/smooth SHA-256取得を追加する。
|
||||
- Accurate seedを最大24反復で終了し、未採用画素をreason 1〜5のUNKNOWNとしてDSへ渡す。
|
||||
- DSと高精度・高速参照workerで更新後平方を次反復へ引き継ぐ。
|
||||
- Direct、Accurate、FAST、Deep、BLAで共通のUNKNOWN post-statsを使い、理由0、1〜5、6を同じ規則で集計する。
|
||||
- Deep補正をbucket、prefix、scatter、indirect dispatchの疎queueへ接続し、queue不変条件が崩れた時だけbounded full scanへ戻す。
|
||||
- Accurate seed後はUNKNOWN indexだけをcompactし、時間予算に合わせたDirect-DS queue sliceで処理する。selected/enqueued/dispatch/processedまたは範囲不変条件が崩れた場合だけ従来の全画面guard走査へ戻す。
|
||||
- failure mapをGPU上で空間タイル集計し、全画面metadata readbackを局所補修ループから除去する。局所referenceは3 workerで並列準備する。
|
||||
- failure密度によるFAST→Deep直接昇格、同一viewの検証済みreference再利用、final reference prefetch、BLA table cacheを追加する。近傍viewへのreference流用は黒領域の操作順依存を生じ得るため撤回した。
|
||||
- previewの確定済みescape・解析的内点を同一referenceのfinalで再利用し、UNKNOWNだけを再計算する。
|
||||
- GPU先行投入を24 ms(直近入力)/40 ms(idle)の予測時間予算で制限する。
|
||||
- 初期pipelineをDirect、color、present、UNKNOWN stats、実軸対称copyへ縮小し、Accurate、FAST拡張、Deep、補正、AAを遅延コンパイルする。
|
||||
- 最後に残ったreason 1〜5だけをview精度+128 bitと+192 bitで再計算し、両者のescape反復とf32 smoothが一致した結果だけを採用する。
|
||||
- 中心虚部が厳密に0の通常画面では、共役対称性により上半分だけを計算して下半分へmetadata/smoothを複製する。
|
||||
- BLAはoperation削減率だけでなく、直近baseline、table build、probeを含む推定net時間が5%以上改善する場合だけ有効化する。
|
||||
- BLAの不安全分岐は0反復目restartではなく最後の正確checkpointからFAST recurrenceを再開する。
|
||||
- exportはadapter limitに応じた256/512 px tileと2〜3個のstaging workspaceを使い、GPU/readbackを重ねる。PNGのrow順は維持し、UI yieldは24 ms経過時だけ行う。
|
||||
- UNKNOWN/queue statsはframe単位の3-buffer readback ringへ移し、安定buffer/textureのbind groupをcacheする。
|
||||
- stable historyは全画面copyを廃止し、3枚のcolor textureをfront/back/history/spareとして役割回転する。
|
||||
- Direct/Deepのactive-list continuation、indirect compaction、FAST/Deepの証明付き内点判定候補をshadow-onlyで追加した。Deep continuationも品質回帰の切り分けを優先して本番採用を撤回した。内点候補は外向き丸めのf32区間演算を使い、候補indexだけを画素精度+40/+72 bit oracleで確認する。
|
||||
- 操作ごとに黒領域が揺れる問題への安定化として、adaptive反復数を品質を下げない64反復単位の上方丸めにし、近傍view間の参照軌道流用を廃止した。FASTで数値UNKNOWNが発生した場合は、同一viewの局所referenceと画素精度BigInt補修を完了してから表示し、未完成frameをpresent/commitしない。
|
||||
|
||||
`tests/optimization_regression.mjs` は、2本のscript構文、36 string/kernel hash、旧新BigInt軌道2,000例、旧新DS軌道600例、Direct active chunk分割1,000例、任意精度worker構文を検査する。この環境ではブラウザ接続とWebGPU adapterを利用できなかったため、実GPU WGSL compile、画面hash、KPI実測は未実施である。
|
||||
|
||||
`tests/OPTIMIZATION_BENCHMARK.json` のBigInt worker modelでは、240点、700反復、192〜320 bit、7 runの中央値が201.5509 msから115.3558 msになり、42.77%短縮した。この値は参照軌道の平方再利用だけを測るもので、ブラウザや実GPUのend-to-end改善率ではない。
|
||||
|
||||
BLA replayコードは保持しているが、実測時間で数値backendが揺れることを防ぐためproduction既定値をOFFに戻し、明示的なテストopt-inだけを許可する。export ring、readback/bind cache、history role rotationはproduction経路へ接続済みである。Active-listと証明付き内点判定は`productionEligible:false`のshadow-onlyへ戻し、本番Deepは正常版の式を小さなstripで実行する。
|
||||
|
||||
### 要件別の実装証拠
|
||||
|
||||
| 計画項目 | 現在の実装 | 検証証拠 |
|
||||
| --- | --- | --- |
|
||||
| P0-1 seed打切り | `DIRECT_ACCURATE_SEED_WGSL` が24反復でreason 2へ移行 | kernel hash、source invariant |
|
||||
| P0-2 平方再利用 | Direct DS、Deep/Fast reference worker、独立verifyで更新後平方を保持 | BigInt 2,000例、DS 600例の旧新全state一致 |
|
||||
| P0-3 計測 | generation別primary/final/numeric-quiescent/history、submit/sync、UNKNOWN理由を記録 | `waitForPrimary/Final/NumericComplete`、`fieldHashes`、`gpuDiagnostics` |
|
||||
| P1-1 疎queue | Deep bucket/prefix/scatter/indirectとAccurate Direct-DS compact queue | selected/enqueued/dispatch/processed不変条件、異常時full scan |
|
||||
| P1-2 補修routing | failure密度によるFAST→Deep、GPU reason tile map、局所reference | source invariant、reason別post-stats |
|
||||
| P1-3 実体cache | reference prefix/checkpoint、BLA table、同一pending requestを再利用 | key/coalescing/cancel source invariant |
|
||||
| P1-4 preview再利用 | 同一view/referenceのknown pixelを保持しUNKNOWNだけfinal再計算 | `unknownOnly` とreference key guard |
|
||||
| P1-5 時間予算 | 24/40 ms予測budget、adaptive strip/burst、queued DS slice | scheduler source invariant |
|
||||
| P2-1 failure map | GPU tile集計、3 workerのlocal reference並列準備 | recovery経路に全画面meta readbackなし |
|
||||
| P2-2 BLA replay | 最後の正確anchorからbaseline recurrenceを再開。production既定OFF、明示opt-inのみ | 7 fallback分岐、zero restartなし、kernel hash |
|
||||
| P2-3 固定費 | lazy pipeline、3-buffer stats readback、bind cache、history role rotation | 1,000回のtexture role model、source invariant |
|
||||
| P2-4 export | 256/512 tile、2〜3 staging slot、ordered `allSettled`、24 ms yield | export構造回帰、SS1/SS2 smoke hook |
|
||||
| P2-5 任意精度 | reason 1〜5を画素精度+40/+72 bitで評価して一致時のみpatch。全残存画素をbounded chunkで処理 | worker構文、受理条件source invariant |
|
||||
| P3-1 active-list | Direct/Deep state、二重queue、indirect compaction、preview→final resume | Direct chunk 1,000例、GPU zero-tolerance shadow hook |
|
||||
| P3-2 証明付きskip | 厳密実軸対称copy、外向きf32区間によるcardioid/bulb候補 | 画素精度+40/+72 bit oracle shadow、`productionEligible:false` |
|
||||
|
||||
`tests/optimization_webgpu_corpus.mjs` は上表の実GPU証拠を収集するdriverである。7 correctness case、4 performance scenario、SS1/SS2 exportに加え、近接2 viewと同一view再訪からなるinteraction stability sequenceを定義する。WGSL error、uncaptured error、device loss、coverage UNKNOWN、最終numeric UNKNOWN、history commit、field hash、Direct/Deep active mismatch、内点oracle mismatchをzero-toleranceで検査し、同一view再訪時のmeta/smooth hash一致も必須とする。numeric recoveryが完了しないcaseも即失敗とする。
|
||||
|
||||
## 結論
|
||||
|
||||
品質を落とさずに改善できる可能性が高い順序は、次のとおりである。これはprofile前の期待順位であり、Milestone 0のbaseline実測で入れ替える。
|
||||
|
||||
1. Accurate の f32 seed を24反復で終了させる。現在は「24未満の脱出だけを採用」するにもかかわらず、未採用ピクセルを `maxIter` または脱出まで計算し、その後DSで0反復目から再計算している。
|
||||
2. Accurate DSとBigInt参照軌道の反復内で重複計算している更新後の平方を次反復へ引き継ぐ。
|
||||
3. すでに存在する Deep UNKNOWN のbucket/queue/indirect補正を本番経路へ接続し、全画面DS補正走査をやめる。
|
||||
4. previewで確定した脱出・解析的内点と、同一prefixの参照軌道をfinalへ引き継ぎ、同じピクセルを0反復目から二度計算しない。
|
||||
5. 参照軌道とBLA tableを実際の参照点でキャッシュする。同一viewの反復数増加では再利用または延長できるが、近接pan/zoomへの流用は同一view再訪hash gateを通過するまで行わない。
|
||||
6. 数値未確定の位置特定を全画面GPU→CPU readbackからGPU側tile集計へ移し、reason別にDS、軌道延長、局所referenceを選ぶ。
|
||||
7. GPUへ先行投入する仕事を「strip本数」ではなく予測GPU時間で制限し、キャンセル済みビューの無駄計算を抑える。
|
||||
8. 長期的には、一定反復ごとのactive-pixel compactionへ移行し、preview継続、分岐発散削減、細粒度キャンセルを同時に実現する。
|
||||
|
||||
最初の3項目は、最終的に実行する数値式を変えず、明白な重複または空振り計算だけを減らせる。特に1項目は小さな変更で高い効果が見込まれるため最初に検証するが、実効果は解析的内点、24反復未満の脱出、DS対象ピクセルの比率に依存する。
|
||||
|
||||
この文書は実装監査に基づく提案であり、ここに記載した性能効果はまだ実GPUで測定した結果ではない。既存の386.9 ms削減値もworker modelであり、canvasを含む実端末フレーム時間ではない。
|
||||
|
||||
## 目標と非目標
|
||||
|
||||
目標は次の3点である。
|
||||
|
||||
- 最終フレームに残る数値未確定ピクセルを最小化する。
|
||||
- 入力から新規画像、最終品質、数値補修完了までの時間と、PNG生成時間を短縮する。
|
||||
- v24.2.55の解像度、`maxIter`、サンプリング位置、色、数値安全弁を維持し、既知ピクセルをUNKNOWNへ退行させない。
|
||||
|
||||
速度のためにDPR、反復数、AA sample数、誤差判定の安全余裕を下げることは対象外とする。また、本ビューアーのexport metadataは現在も `membershipCertified:false` であるため、本計画の「品質不変」は数学的な集合所属証明ではなく、現行baselineとの同等性と高精度oracleに対する非退行を意味する。
|
||||
|
||||
## 用語と計測単位
|
||||
|
||||
コード上には「計算放棄」という正式な状態名がない。曖昧な集計を避けるため、本計画では次のように分ける。
|
||||
|
||||
| 区分 | metadata reason | 本計画での扱い |
|
||||
| --- | ---: | --- |
|
||||
| 未書込・coverage hole | 0 | バグ検知指標。最終値は必ず0 |
|
||||
| 数値未確定 | 1〜5 | 「計算放棄ピクセル」の主指標 |
|
||||
| 反復上限到達 | 6 | 数値故障ではない。失敗率へ加算しない |
|
||||
| キャンセル済みビューの計算 | metadata外 | GPUへ投入済みだがpublishされない無駄仕事として別集計 |
|
||||
| 重複計算 | metadata外 | preview→final、seed→DS、primary→recoveryで同じピクセルを再反復した仕事 |
|
||||
|
||||
reason 1〜5は `error-bound`、`escape-uncertain`、`reference-end`、`rebase-gap`、`range` であり、現行の `numericalFailureCount()` と同じ定義である(`index.html:2023-2025`)。reason 6は `maxIter` まで脱出しなかったピクセルで、COLOR passでは不透明な黒になる。数値未確定とは区別する。
|
||||
|
||||
ピクセル数だけでは、1反復で失敗したピクセルと12000反復後に捨てたピクセルを区別できない。主な性能指標には必ず「pixel-iterationsまたは実際のkernel steps」も併記する。
|
||||
|
||||
## 現行パイプライン
|
||||
|
||||
```text
|
||||
入力
|
||||
├─ 操作中: stable historyを再投影
|
||||
└─ 操作確定
|
||||
├─ backend選択
|
||||
│ ├─ FAST direct f32 → COLOR → PRESENT
|
||||
│ ├─ FAST perturbation / optional BLA
|
||||
│ │ └─ coverage repair + COLOR → stats readback
|
||||
│ │ └─ background recovery予約 → 一次PRESENT
|
||||
│ │ └─ 60 ms後にlocal refs
|
||||
│ │ └─ 残存時full Deep + DS + local refs → recolor
|
||||
│ ├─ Accurate direct: f32 seed → guarded DS → COLOR → PRESENT
|
||||
│ └─ Accurate Deep: perturbation → stats readback
|
||||
│ └─ 条件成立時full-screen guarded DS → local refs
|
||||
│ └─ COLOR → PRESENT
|
||||
└─ intended contract: 数値未確定が0の時だけstable historyへcommit
|
||||
```
|
||||
|
||||
品質面では、未完成frontをhistoryへ昇格させず、通常のpan/zoomでは透明な数値未確定部分を直前のstable frameで補うことが意図された契約である。ただし現行Accurate hybridには未確定を0件と仮定する例外があり、後述の共通post-statsで修正する。最適化後は全backendでこの契約を満たす。
|
||||
|
||||
## 実装監査で確認した主要ボトルネック
|
||||
|
||||
### 1. Accurate seedが「安価なseed」になっていない
|
||||
|
||||
`DIRECT_ACCURATE_SEED_WGSL` は脱出反復が24未満のピクセルだけを採用するが、loop自体は `maxIter` まで継続する(`index.html:191-200`)。その後、UNKNOWNピクセルはDSで反復0から再計算される(`index.html:1944-1964`)。
|
||||
|
||||
したがって、解析的に証明できない内点は f32で `maxIter` 回、さらにDSで `maxIter` 回計算される。遅く脱出する境界ピクセルも、f32の脱出まで計算した後にDSで再計算される。既存fuzz契約は「f32 escape iteration < 24」のみを採用するため、seedは24反復に達した時点でUNKNOWNとして終了しても最終DS結果は変わらない(`tests/ACCURATE_SEED_FUZZ.json`)。
|
||||
|
||||
### 2. 疎なDS補正用GPU queueが存在するが未使用
|
||||
|
||||
8 bucketのhistogram、prefix、scatter、indirect dispatch、およびqueued DS kernelはすでに実装されている(`index.html:416-800`)。pipelineとencoder helperも起動時に作成される(`index.html:1820-1827`, `1841-1842`, `1863-1867`)。
|
||||
|
||||
一方、本番の `correctUnknownFrame()` は全画面をrow stripでdispatchし、各strip後に同期する(`index.html:1994-2000`)。shader内で既知ピクセルはearly returnするが、全ピクセル分のinvocation、buffer read、分岐とsubmit/waitは残る。「sparse correction」という名前の判定も、実際には全画面走査を呼んでいる。
|
||||
|
||||
### 3. previewとfinalが同じ仕事を繰り返す
|
||||
|
||||
historyがないFAST viewではpreviewとfinalの2段階を使う(`index.html:1445-1453`)。しかしfinalではmeta/smoothをclearし、全ピクセルを0反復目から再計算する(`index.html:1897-1939`)。参照軌道cache keyにも `iter` が入るため、previewとfinalで軌道も再構築される(`index.html:1490`, `1513`)。
|
||||
|
||||
previewで既に脱出したピクセルと解析的に証明された内点は、`maxIter` を増やしても結果が変わらない。同じ参照軌道prefixと同じ数値式を使える場合、これらをfinalで再計算する必要はない。
|
||||
|
||||
### 4. 局所補修が全画面readbackと直列処理を繰り返す
|
||||
|
||||
`recoverNumericalUnknownTiles()` は各roundでmeta全体をGPUからCPUへ読み戻し、main threadで全画素を走査してtileを作る。最後にも再読込する(`index.html:1986`, `2031-2034`)。その後は、1 tileごとにreference build、GPU correction、stats readbackを順番に待つ。
|
||||
|
||||
また「広域failureならfull Deepが安い」とする `shouldPromoteFastToDeep()` が定義されているが、呼ばれていない(`index.html:2026-2028`)。実際のFAST background recoveryはfailure密度にかかわらずlocal recoveryから開始し、残ればfull Deepで画面全体を上書きする(`index.html:2050-2073`)。広域failureでは最初のlocal処理が無駄になる。
|
||||
|
||||
### 5. 参照軌道とBLA tableの再利用範囲が狭い
|
||||
|
||||
Deep/FAST referenceの再利用は同じ `iter` と完全に同じ `span` を要求する(`index.html:2089-2090`)。しかし参照軌道そのものは参照点 `c_ref` に依存し、表示spanには依存しない。参照点が新viewの有効範囲にあり、精度と長さが足りれば、既存の誤差guardを保ったままzoom後にも利用できる。
|
||||
|
||||
BLA tableも参照軌道から作るが、build keyはview座標・寸法込みであり、近接viewでも再構築される(`index.html:1598`, `1674-1679`, `2110-2114`)。さらに `BlaShadowService.cancelPending()` はPromiseを破棄するだけでworkerをterminateしない。同期的な旧viewのtable build中は、新viewの要求がworker queueで待たされる(`index.html:1594-1600`)。
|
||||
|
||||
### 6. キャンセルはpublishを止めるが、投入済みGPU仕事は止めない
|
||||
|
||||
token checkにより古い結果のpublishは防いでいるが、WebGPU queueへsubmit済みのcommand bufferはキャンセルできない。現行はDeep 2本、FAST 4本、BLA 8本をまとめて投入してから待つ(`index.html:1897-1925`)。目標strip時間はDeep 18 ms、FAST 22 msであるため、BLAでは理論上かなり長い旧view仕事がqueueに残り得る。
|
||||
|
||||
`cancelRender()` はJS状態とworker要求を止めるが、GPU queueをpreemptしない(`index.html:2094`)。新viewのcommandは旧viewの仕事の後ろへ並ぶため、無駄計算と入力後latencyの両方に影響する。
|
||||
|
||||
### 7. 初期化、bind group、history copy、exportに固定費がある
|
||||
|
||||
- 初期化では初期FAST表示に不要なAccurate、Deep、queue、AA pipelineまで一括compileする(`index.html:1811-1834`)。
|
||||
- stripごとに同じresource構成のbind groupを作り直す(`index.html:1857-1868`)。
|
||||
- stable history更新はfront texture全体をcopyする(`index.html:1982`)。
|
||||
- exportは単一readback workspaceでtileを完全直列処理し、各row band後に必ず `requestAnimationFrame` を待つ(`index.html:2276-2289`)。
|
||||
|
||||
これらは数値品質に触れずに削減できる固定費である。
|
||||
|
||||
### 8. backend間でUNKNOWN集計が一致していない
|
||||
|
||||
Directはreason 6をmetadataへ書くがstatsを集計しない。より重要なのは、Accurate hybridのDS kernelが `REASON_RANGE` を出し得る一方、orchestrationは完了後に `state.unresolved=0` と無条件設定する点である(`index.html:248`, `2131-2133`)。この場合、数値未確定を見逃してincomplete frameをhistoryへcommitする可能性がある。
|
||||
|
||||
全backendの最終color前に共通post-statsを通し、reason 0、reason 1〜5、reason 6をmetadata実体から集計する。これは最適化効果を測る前提であると同時に、品質契約の穴を塞ぐ変更でもある。
|
||||
|
||||
## 期待優先順位付き最適化案
|
||||
|
||||
| 期待順位 | 案 | 数値未確定 | 生成時間への期待 | キャンセル無駄 | 品質リスク |
|
||||
| ---: | --- | --- | --- | --- | --- |
|
||||
| P0 | Accurate seedを24反復で終了 | 同じ | 大(view依存) | 削減 | 低 |
|
||||
| P0 | DS/BigIntの更新後平方を再利用 | 同じ | 中〜大 | 削減 | 低 |
|
||||
| P0 | 計測契約とbaselineを固定 | 判定可能にする | 判定可能にする | 判定可能にする | 低 |
|
||||
| P1 | UNKNOWN統計をpost-pass化 | 同じ | interior-heavyで削減 | 削減 | 低 |
|
||||
| P1 | 既存の疎queueでDS補正 | 同じか減少 | 大 | 削減 | 低〜中 |
|
||||
| P1 | failure密度・reason別routing | 減少 | 大 | 削減 | 低 |
|
||||
| P1 | reference/BLA cache・延長・同一view再利用 | 減少 | 大 | 削減 | 低〜中 |
|
||||
| P1 | preview確定結果をfinalで再利用 | 同じ | 大 | 削減 | 低〜中 |
|
||||
| P1 | in-flight GPU時間を動的制限 | 同じ | 操作時に改善 | 大きく削減 | 低 |
|
||||
| P2 | GPU failure mapと並列local refs | 減少 | 大 | 削減 | 中 |
|
||||
| P2 | BLA checkpoint replayとnet-time gate | 同じ | BLA viewで改善 | 削減 | 中 |
|
||||
| P2 | pipeline/export/resource固定費削減 | 同じ | cold load/export改善 | 小幅削減 | 低 |
|
||||
| P2 | 残存UNKNOWNだけ任意精度fallback | 最終値を0へ | 少数時のみ有利 | 同じ | 中 |
|
||||
| P3 | active-pixel continuation/compaction | 同じか減少 | hard viewで最大 | 大きく削減 | 高 |
|
||||
| P3 | 証明付き内点判定・対称性 | 同じ | 対象viewで改善 | 削減 | 中〜高 |
|
||||
|
||||
### P0-1. Accurate seedを `lateThreshold` で打ち切る
|
||||
|
||||
`DIRECT_ACCURATE_SEED_WGSL` の各pixelは次のどちらかだけを行う。
|
||||
|
||||
- cardioid/period-2 bulbを解析的に証明できれば確定する。
|
||||
- 最大24回だけf32反復し、24未満で安全に脱出した場合だけ確定する。それ以外は直ちにUNKNOWNとする。
|
||||
|
||||
DS passは現在と同じくUNKNOWNを0反復目から計算する。このため最終meta/smoothは現行と同じで、seed側の24回を超える仕事だけが消える。打切りmetadataは後段queueが選べるreason 1〜5(例えば `REASON_ESCAPE_UNCERTAIN`)にし、reason 6やzero sentinelとは区別する。まず単独feature flagで実装し、既存60,000 fuzz、full-frame meta/smooth hash、strip-order testを通す。
|
||||
|
||||
### P0-2. DSとBigInt軌道で更新後の平方を再利用する
|
||||
|
||||
`DIRECT_DS_CORRECT_WGSL` は、各反復の更新前に `zr²`、`zi²`、`zr*zi` を計算し、更新直後の脱出判定で新しい `zr²` と `zi²` を再計算する。次のloop先頭では同じ新しい平方をもう一度計算する(`index.html:240-246`)。更新後の2平方をstateとして次反復へ渡せば、DS乗算は概ね5回/反復から3回/反復になり、個々の `ds_mul` の入力と演算順は変わらない。
|
||||
|
||||
Deep/Fast reference workerの `orbit()`、`orbitEscape()`、`verify()` にも、座標更新用と直後のmag用で同じBigInt平方を二度作る箇所がある(`index.html:1479-1483`, `1503-1506`)。丸め済みの更新後平方を次反復へ持ち越す。各変更は旧/新の軌道配列、脱出反復、meta/smoothをbit比較する。
|
||||
|
||||
### P0-3. 先に計測を直す
|
||||
|
||||
現行の `setView()` は `dirty || rendering` が終わるまでしか待たず、`refinePending` とbackground numeric recoveryを待たない(`index.html:2303`)。`lastRender` だけを比較すると、preview完了と最終補修完了を取り違える。
|
||||
|
||||
テストhookへ次を追加する。
|
||||
|
||||
- `waitForPrimary()`、`waitForFinal()`、`waitForNumericComplete()`。
|
||||
- generationごとのphase時刻: reference、BLA build/probe、primary、coverage+color、stats readback、DS、local refs、full Deep、recolor、history commit。
|
||||
- reason 0、reason 1〜5の内訳、reason 6をprimary/DS/local/finalの各時点で保存。
|
||||
- 全backendを共通post-statsへ通し、Accurate hybridで `unresolved=0` を仮定しない。
|
||||
- kernel steps: direct、perturbation、DS、BLA、replay、およびキャンセルされたgenerationのsteps。
|
||||
- submit、sync wait、readback回数・bytes・時間、reference/BLA cache hit。
|
||||
- adapter/browser/実ピクセル数/effective DPR/実効 `maxIter`。
|
||||
|
||||
`timestamp-query` が利用可能なadapterではGPU timestampを使い、利用できない場合はwall clockと明記する。累積runtime counterではなく、各generationの差分を記録する。
|
||||
|
||||
### P1-1. Deep/Accurate補正を既存の疎queueへ接続する
|
||||
|
||||
推奨フローは次のとおりである。
|
||||
|
||||
1. primary Deepはpixelごとのglobal atomicを避けるpost-stats variantを使う。
|
||||
2. histogram passでreason 1〜5だけを8個の開始反復bucketへ集計する。
|
||||
3. prefixとscatterでUNKNOWN index queueを作る。
|
||||
4. `dispatchWorkgroupsIndirect` でqueued `correct_pixel()` だけを実行する。
|
||||
5. queue overflow、invalid index、stale entryが1件でもあれば現行full-scanへfallbackする。
|
||||
|
||||
histogram/prefix/scatter基盤はAccurate f32 seed後のDSにも使う。ただし既存queued kernelはDeep referenceを読むperturbation版 `correct_pixel()` であり、絶対座標を使う `DIRECT_DS_CORRECT_WGSL` へそのまま接続してはならない。Direct-DSの数値式をそのまま持つ別のqueued kernelを作る。seedで確定したピクセルはqueueに入らず、DS invocation自体を発生させない。bucket順により、同じ程度の反復で終わるlaneが隣接し、SIMD divergenceも減る。
|
||||
|
||||
queue構築は全画面を軽く走査するため、UNKNOWNが非常に多い場合はfull-scanの方が速い可能性がある。adapter別の実測から密度crossoverを決め、queue/full-scanを選ぶ。各backendで元kernelと同じper-pixel arithmeticとoutput semanticsを保ち、出力差を許容しない。
|
||||
|
||||
同時に、primary hot loopからpixel単位のglobal atomic統計を外す。現在はFAST/Deep/BLAのUNKNOWN 1 pixelにつき最大2回、同じcounterへatomic addする(`index.html:268-270`, `339-347`, `1335-1339`)。特にreason 6が多いinterior-heavy viewでは、長い反復の最後に競合が集中する。Deepにはatomicを除いた `DEEP_PERTURB_POSTSTATS_WGSL` とworkgroup集約histogramが既にあるため、まずこれを接続し、FAST/BLAにも同型のpost-pass集計を用意する。meta/smoothは変更しない。
|
||||
|
||||
### P1-2. failure密度とreasonで補修経路を選ぶ
|
||||
|
||||
一次statsを得た直後に次のroutingを行う。
|
||||
|
||||
- failureが `max(60000, 8% of pixels)` 以上なら、既存 `shouldPromoteFastToDeep()` を使ってlocal-firstを省き、full Deepへ直行する。
|
||||
- `error-bound` / `escape-uncertain` はまずqueued DSを使う。
|
||||
- `reference-end` は「cache prefixが要求iterより短い」場合と「reference自身が脱出して `refLen=escape` になった」場合を分ける。前者だけ同一軌道を延長し、後者は原則として非脱出またはより長く安定する新referenceを選ぶ。脱出後の軌道を無条件に延長しない。
|
||||
- `range` / `rebase-gap` は失敗領域に近いlocal referenceを優先する。
|
||||
- reason 0は数値補修へ混ぜず、coverage契約だけで処理する。
|
||||
|
||||
各経路で `recovered / attempted` とmsを記録し、近接viewでは前回の成功経路をpriorとして使う。ただし結果が残る場合は現在と同じDeep/DS/local fallbackを最後まで保持する。
|
||||
|
||||
### P1-3. 参照軌道とBLA tableを実体ベースで再利用する
|
||||
|
||||
reference cache keyを「要求view」から次の実体へ変更する。
|
||||
|
||||
- 実際に選択された `referenceRe/referenceIm`。
|
||||
- precision bits。
|
||||
- orbitの最長計算済み長さ。
|
||||
- kernel format/version。
|
||||
|
||||
短い `iter` 要求にはprefixを返し、長い要求にはworker内に保持したBigInt状態から延長する。preview→finalで同じ軌道を作り直さない。
|
||||
|
||||
参照点選択でも二重計算を避ける。現在は `orbitEscape()` で候補を評価した後、選ばれた非中心候補を `orbit()` でもう一度0から計算する。候補評価時の最良軌道を保持すれば、選択軌道の再走査を省ける。
|
||||
|
||||
一方、Deepの独立 `verify()` は通常modeでも外さない。新規orbitは従来どおり検証し、検証済みimmutable orbitをcacheから再利用する時だけ検証結果も再利用する。第一pass中のhashだけへ置き換える案は独立guardの弱体化になるため、別実装の同等以上の検証器を用意できるまで採用しない。
|
||||
|
||||
再利用判定では `newSpan === oldSpan` を必須にせず、参照点が新view内または安全margin内にあること、精度が十分なこと、orbit長が足りることを確認する。Deepの誤差bound、reference-end、checkpoint検証は残す。guardが不利と判断した場合だけ新しい参照点を作る。
|
||||
|
||||
BLA tableは `reference buffer/hash + refLen + safety + table format version` でcacheする。viewごとに必要なのはtable再構築ではなく、pixel分布に対する採用probeだけである。旧view buildのキャンセルは、ReferenceServiceと同様のworker terminate/recreate、またはchunk化した協調キャンセルで実際のCPU仕事も止める。
|
||||
|
||||
### P1-4. previewの確定結果をfinalへ引き継ぐ
|
||||
|
||||
最初の段階ではorbit stateを保存せず、次だけを再利用する。
|
||||
|
||||
- `FIELD_ESCAPED`: finalの `maxIter` を増やしても最初の脱出反復とsmooth値は変わらない。
|
||||
- `FIELD_INTERIOR_PROVEN`: classとsmoothは変わらないが、packed iteration部はpreviewの `maxIter` になっている。final前の軽量patch passで `pack_meta(finalIter, FIELD_INTERIOR_PROVEN)` へ更新してから再利用する。
|
||||
- previewのreason 6: finalで再計算が必要。
|
||||
- 数値未確定reason 1〜5: finalまたは補修で再計算が必要。
|
||||
|
||||
Directはfinal kernelへ `unknownOnly` を加えるだけで実現できる。perturbationでは、previewとfinalが同じ最終長referenceのprefix、同じ参照点、同じpixel latticeを使う場合だけ既知pixelを再利用する。条件が1つでも違えば現行full recomputeへ戻す。
|
||||
|
||||
次段階では、未脱出pixelの `z/w/d/scaleExp/reference index/error` をcheckpointとして保存し、preview末尾からfinalを再開する。ただし浮動小数点演算順を変える可能性があるため、これはP3のactive-list方式と同じ厳しい品質gateを通す。
|
||||
|
||||
### P1-5. GPU先行投入量を時間予算で制限する
|
||||
|
||||
現在の固定 `burstLimit` を、直近stripのGPU時間EMAから求める。
|
||||
|
||||
```text
|
||||
burstLimit = clamp(1, maxBurst, floor(maxInFlightMs / predictedStripMs))
|
||||
```
|
||||
|
||||
初期案として、直近に入力があった時は `maxInFlightMs=24`、十分idleなら40程度から測定を始める。値は品質ではなくsubmit/waitとキャンセルlatencyのtrade-offなので、adapter別に自動調整する。
|
||||
|
||||
Directの全画面1 dispatchと高反復exportも同じ時間上限でstrip化する。perturbationだけを細分化しても、Directが長時間queueを占有すれば新viewは待たされる。
|
||||
|
||||
追加で、top-to-bottom固定順ではなく、focus座標と画面中心に近いmacro-tileから処理する。キャンセルされた場合でも、ユーザーが見ている領域に使える仕事の割合が増える。部分表示を行う場合はgeneration stamp付きwork textureを使い、旧generationを絶対にpublishしない。
|
||||
|
||||
### P2-1. failure mapをGPUで作り、local refsを並列化する
|
||||
|
||||
全meta readbackの代わりに、GPUで固定macro-tileごとの次の小さなsummaryを作る。
|
||||
|
||||
- reason別件数。
|
||||
- `sumX/sumY`、min/max座標。
|
||||
- UNKNOWN index rangeまたはcompact queue offset。
|
||||
|
||||
CPUへ戻すのは数KBのsummaryだけにする。必要なlocal referenceは2〜4 workerのpoolで並列構築し、完成したreferenceごとに1 tileずつ待たず、複数queueをまとめてGPUへsubmitする。補正対象も長方形全体ではなく、そのreferenceへ割り当てたUNKNOWN indexだけにする。
|
||||
|
||||
近接pan/zoomでは前frameのfailure mapを再投影してcoarse probeの優先候補に使える。ただし最終判定は新frameのguard結果で行う。
|
||||
|
||||
### P2-2. BLA fallbackを0反復目restartからcheckpoint replayへ変える
|
||||
|
||||
production candidateのBLAは近似chainが不安全になるとbaselineを0反復目から再実行する(`index.html:1320-1323`)。一方、検証用shadow kernelには最後のexact anchorからreplayする実装がある(`index.html:1130-1232`)。このcheckpoint/replayをproductionへ移すと、fallback pixelでBLA以前の仕事を捨てずに済む。
|
||||
|
||||
BLA採用条件も、現在の `fallback <= 12.5%` と `operation reduction >= 50%` だけでなく、table build、upload、probe、replayを含む予測net msで決める。cache miss時の固定費が節約時間を上回るviewではbaseline FASTを選ぶ。
|
||||
|
||||
品質gateではclassとescape iterationだけでなくsmooth値と最終RGBAも比較する。approximationが不確かなpixelは必ず既存FAST recurrenceへreplay/fallbackし、BLA由来の新しいUNKNOWNを許さない。
|
||||
|
||||
### P2-3. 初期化・resource・historyの固定費を減らす
|
||||
|
||||
- cold startではPRESENT、Direct、COLORだけを先にcompileし、FAST extended、Accurate、Deep、queue、AA、診断pipelineは必要時またはidle時にwarm upする。
|
||||
- pipeline/frame/contextごとにbind groupとtexture viewをcacheする。
|
||||
- 小さなstats readback bufferを毎回create/destroyせずringで再利用する。
|
||||
- front/back/historyを3 textureのrole rotationにし、history commit時の全texture copyをなくす。
|
||||
|
||||
これらはshader結果を変えないため、bit-identical testを要求する。
|
||||
|
||||
### P2-4. exportをGPU・readback・PNG圧縮のpipelineにする
|
||||
|
||||
現在の単一workspaceを2〜3個のringへ変え、tile N+1のGPU計算、tile Nのmap/copy、前bandのCompressionStream書込を重ねる。PNGのrow順は維持する。
|
||||
|
||||
各row band後の無条件 `requestAnimationFrame` は、経過時間に応じたyieldへ変える。操作応答性を守る必要がある時だけyieldし、headless/非操作exportでは不要な1 frame待ちを入れない。tile sizeも512固定ではなく、adapter memory limitと目標in-flight時間から選ぶ。
|
||||
|
||||
2x AAは4 sampleを維持する。サンプル削減は、pixel footprint全体が内点であることを証明できる場合以外は行わない。
|
||||
|
||||
### P2-5. 最後に残ったUNKNOWNだけを任意精度で解決する
|
||||
|
||||
local referenceにはmode別の `maxRefs` 上限があり、予算を使い切るとreason 1〜5が残り得る(`index.html:2029-2034`)。final Accurate/strict exportでは、残存indexだけをCPU workerの固定小数点または疎なGPU limb演算で `maxIter` まで直接評価し、escapedまたは各backendの現行maxIter metadataへpatchする。Deep/Fastはreason 6、Accurate Direct DSは `FIELD_INTERIOR_LIKELY` という違いを保持する。これにより、画面やtile全体を再計算せずに最終numeric UNKNOWNを0へ近づけられる。
|
||||
|
||||
これは集合所属の無限反復証明ではない。`maxIter` まで未脱出なら、起点backendの現行分類・metadata policyを変えない。対象が多い場合は先に別reference/full Deepへ戻し、疎な最終手段としてだけ使う。
|
||||
|
||||
### P3-1. active-pixel continuationへ移行する
|
||||
|
||||
最終的な高反復view向けには、1 pixelを1 dispatchで `maxIter` まで回す方式を次へ置き換える。
|
||||
|
||||
1. 64〜256反復のchunkだけ実行する。
|
||||
2. 脱出・証明済み内点を除き、active pixel indexと継続stateをcompactする。
|
||||
3. 次chunkはactive queueだけを実行する。
|
||||
4. preview時点のqueue/stateをfinalへそのまま継続する。
|
||||
|
||||
これにより、早期脱出pixelを後続chunkから除外し、workgroup divergenceを減らし、chunk境界でキャンセルできる。Directでは `z`、Deepでは `w/d/scaleExp/reference index/error` が必要になる。二重queueの論理使用量はactive数に比例するが、通常はworst caseとして全 `P` pixel分のbuffer確保が必要であり、メモリ量もKPIと採用条件に含める。
|
||||
|
||||
最大の注意点は再開前後で演算順、rescale位置、誤差伝播を変えないことである。baselineとbit-identicalにならない場合は高精度oracleに対する誤差非増加を証明し、既存pathをfallbackとして残す。
|
||||
|
||||
### P3-2. 証明できる場合だけ計算を省く
|
||||
|
||||
- Deep/FASTでも `c_ref + dc` をdouble-singleまたは区間で再構成し、main cardioid/period-2 bulbの片側安全判定を追加する。
|
||||
- center imaginary=0かつsample latticeが厳密に鏡映対称なviewでは、実軸対称性を使って片側のmeta/smoothをcopyする。
|
||||
- 周期検出やtile一括fillは、有限精度のepsilon一致だけで内点扱いしない。区間または収縮証明が成立した時だけ確定する。
|
||||
|
||||
素朴なperiodicity checking、境界だけを調べるrectangle fill、色差だけによるadaptive AAは小さい構造を消す可能性があるため採用しない。
|
||||
|
||||
## 品質契約
|
||||
|
||||
最適化を2種類に分ける。
|
||||
|
||||
### 構造だけを変える最適化
|
||||
|
||||
seedの早期終了、平方再利用、queue化、post-stats、bind group再利用、export pipelineなど、最終数値式と採用経路を変えない案は次を必須とする。
|
||||
|
||||
- `fieldMeta`、`fieldSmooth`、最終RGBAがbaselineとbit-identical。
|
||||
- known→UNKNOWNが0。
|
||||
- final reason 0が0。
|
||||
- 数値未確定を含むframeのhistory commitが0。
|
||||
- stale generationのpublishが0。
|
||||
|
||||
### 数値アルゴリズムを変える最適化
|
||||
|
||||
BLA、continuation、証明付き内点判定に加え、選択referenceや補修経路が変わるcache/routingは次を必須とする。同じreference実体をそのまま再利用するだけなら構造変更としてbit比較する。
|
||||
|
||||
- baselineで既知のpixelをUNKNOWNへ戻さない。
|
||||
- baselineとcandidateの双方が既知なら、classはbaselineおよび高精度oracleと一致する。
|
||||
- baseline UNKNOWNをcandidateがescapedまたはreason 6へ解決する差分は改善として許可し、raw classification mismatchへ加算しない。
|
||||
- exact同値を主張する経路ではfirst escape iteration mismatchが0。
|
||||
- BLA derived UNKNOWNとinternal failureが0。
|
||||
- smooth/RGBAのoracle誤差がbaseline以下。単なる画像平均ではなく最大誤差と差分pixel数を保存する。
|
||||
- final numeric UNKNOWNは各caseでbaseline以下。baselineが0ならcandidateも0。
|
||||
- 異常時は同一viewでbaselineへ自動fallbackする。
|
||||
|
||||
既存Accurate hybrid記録にはreset viewで2/1200のescape iteration mismatchがあるため、少なくともこれを悪化させず、目標は0とする。SwiftShaderの時間値はcorrectness用の相対参考であり、実GPU性能gateには使わない。
|
||||
|
||||
## KPI
|
||||
|
||||
画素数を `P = width * height`、reason別件数を `U_r` とする。
|
||||
|
||||
- `coverage_hole_ppm = 1e6 * U_0 / P`。hard targetはfinalで0。
|
||||
- `numeric_unknown_ppm = 1e6 * sum(U_1..U_5) / P`。
|
||||
- `operation_limit_ppm = 1e6 * U_6 / P`。情報指標であり失敗率ではない。
|
||||
- `recovery_rate = (U_primary - U_final) / max(1, U_primary)`。
|
||||
- `recovery_cost_ms_per_1k = (T_recovery_quiescent - T_primary) / recovered_kpixels`。
|
||||
- `cancelled_work_ratio = cancelled_kernel_steps / all_kernel_steps`。
|
||||
- `duplicate_work_ratio = repeated_kernel_steps / all_kernel_steps`。
|
||||
|
||||
latencyは最低でも次の5点を分ける。
|
||||
|
||||
- `T_feedback`: inputから再投影presentまで。
|
||||
- `T_primary`: 同一viewの最初の新規frameまで。
|
||||
- `T_final`: finalIter frameのpresentまで。
|
||||
- `T_recovery_quiescent`: background補修が成功、予算終了、またはfallback完了で停止するまで。数値未確定が残る場合も記録する。
|
||||
- `T_history_commit`: 数値未確定0のstable frameがhistoryへcommitされるまで。未確定が残って到達しないrunは、打切り時刻でcensored dataとして扱う。
|
||||
|
||||
p50、p95、maxを保存し、reference build、BLA build/probe、primary、DS、local refs、readback、color、export compressionの内訳も残す。
|
||||
|
||||
## ベンチマーク行列
|
||||
|
||||
### correctness corpus
|
||||
|
||||
- reset: center `(-0.5, 0)`, span `3.4`。
|
||||
- seahorse-low: `(-0.743643887037151, 0.13182590420533)`, span `0.1`。
|
||||
- cusp-low: `(0.25, 0)`, span `0.85`。
|
||||
- classic Seahorse deep: span `3.4e-13`, 2900 iterations。
|
||||
- BLAが有利なviewと、BLA probeがOFFになるSeahorse/Misiurewicz hard view。
|
||||
- `directPixelRatio` の4直前/直後、32直前/直後。
|
||||
- reason 1〜5をそれぞれ誘発する固定seed。
|
||||
- 実軸対称、interior-heavy、boundary-heavy、reference-end-heavyの各view。
|
||||
|
||||
### performance corpus
|
||||
|
||||
- 320×220 / 750 iter: 既存Accurate比較の継続用。
|
||||
- 654×690 / 2900 iter: 既存BLA prep scenarioとの接続用。
|
||||
- 1920×1080、および対象端末で可能なら4K。
|
||||
- 900 / 1500 / 3000 / 6000 / 12000 iter。
|
||||
- cold load、warm same view、same-span pan、small zoom、distant jump。
|
||||
- FAST/Accurate、BLA forced on/off、連続pan/zoomのcancel storm。
|
||||
- export SS1/SS2、balanced/strict。
|
||||
|
||||
`processMode` はpixel budgetとiteration係数を同時に変えるため、異なるmodeのraw msを直接比較しない。必ず実ピクセル数、DPR、iterを記録する。
|
||||
|
||||
実機は低電力integrated、現行integrated、discrete GPUを最低構成とし、SwiftShaderはcorrectnessだけに使う。各caseはwarmup後にbaseline/candidateを同一sessionで交互に10〜20回実行し、medianとp95を比較する。
|
||||
|
||||
## 合格基準
|
||||
|
||||
### hard quality/safety gate
|
||||
|
||||
- 全caseでfinal reason 0 = 0。
|
||||
- known→UNKNOWN、stale publish、不完全history commit = 0。
|
||||
- baseline既知pixelのknown→UNKNOWN = 0。双方既知pixelのclass mismatch = 0。
|
||||
- baseline UNKNOWNの解決は許可し、その結果が高精度oracleと矛盾しない。
|
||||
- exact経路のescape iteration mismatch = 0。
|
||||
- final numeric UNKNOWNは全caseでbaseline以下。strict/validate golden corpusの目標は0。
|
||||
- reference checkpoint mismatch、strip/full mismatch、WGSL error、uncaptured error、device loss = 0。
|
||||
- operation-limitは別集計し、数値未確定削減として数えない。
|
||||
|
||||
### performance gateの初期値
|
||||
|
||||
以下は実測後に調整する導入基準であり、達成値ではない。
|
||||
|
||||
- 対象hard viewの `T_primary` と `T_recovery_quiescent` のp50を20%以上、p95を15%以上短縮。`T_history_commit` 到達runも別集計で非退行とする。
|
||||
- 非対象viewでp50 +5%、p95 +10%を超える退行を出さない。
|
||||
- failure corpusのprimary numeric UNKNOWN中央値を50%以上、finalを90%以上削減し、各caseでは非増加。
|
||||
- BLA enabled viewはbuild+probe込みでbaselineより10%以上速い。BLA rejected/offの追加費用は5%または8 ms以下。
|
||||
- 1 burstのp95はFAST 25 ms以下、Deep 22 ms以下、max 50 ms以下を初期目標とする。
|
||||
- readback bytes、readback回数、correction sync waitsはbaseline以下。
|
||||
|
||||
## 実装ロードマップ
|
||||
|
||||
### Milestone 0: 観測とbaseline固定
|
||||
|
||||
1. generation単位のmetricsと3種類のUNKNOWN集計を追加する。
|
||||
2. `waitForFinal()` と `waitForNumericComplete()` をテストhookへ追加する。
|
||||
3. meta/smooth/RGBAのgolden artifactと操作traceを作る。
|
||||
4. 実GPUでcold/warm/cancel/export baselineを採る。
|
||||
|
||||
### Milestone 1: 小変更で重複仕事を除く
|
||||
|
||||
1. Accurate seedを24反復で終了する。
|
||||
2. Accurate DSとreference workerで更新後平方を再利用する。
|
||||
3. 全backendのUNKNOWNを共通post-statsで集計し、Deep/FAST/BLA primaryのpixel単位atomicを外す。
|
||||
4. `shouldPromoteFastToDeep()` を実経路へ接続する。
|
||||
5. reference/BLA cache keyを実体ベースへ変更し、旧BLA buildを実際にキャンセルする。
|
||||
6. stats readback bufferとbind groupを再利用する。
|
||||
|
||||
各変更を単独flagでA/Bし、bit-identicalと性能gateを確認する。
|
||||
|
||||
### Milestone 2: 疎queueとpreview再利用
|
||||
|
||||
1. Deep補正を既存bucket/indirect queueへ接続する。
|
||||
2. 同じcompaction基盤と新しいDirect-DS queued kernelをAccurate seed後へ接続する。
|
||||
3. previewのknown pixelと最終長reference prefixをfinalへ引き継ぐ。
|
||||
4. in-flight時間budgetを導入する。
|
||||
|
||||
queue異常時は現行full-scan、reuse条件不成立時は現行full recomputeへ戻す。
|
||||
|
||||
### Milestone 3: recoveryとBLA/exportのpipeline化
|
||||
|
||||
1. GPU failure summaryとUNKNOWN index queueを実装する。
|
||||
2. local reference worker poolとbatched correctionを実装する。
|
||||
3. production BLAへcheckpoint replayとnet-time gateを移す。
|
||||
4. export staging ringとtime-based yieldを実装する。
|
||||
5. strict/export向けの疎な任意精度最終fallbackを実装する。
|
||||
|
||||
### Milestone 4: active-list engine
|
||||
|
||||
chunk continuation、compaction、証明付き内点判定、厳密対称性をshadow-onlyで導入する。golden full-frame比較、複数adapter canary、kill switchを通過したbackendだけを標準へ昇格する。
|
||||
|
||||
## 展開とrollback
|
||||
|
||||
- 新経路はすべてfeature flag既定OFFから開始する。
|
||||
- 数値kernel/BLA変更は最初にshadow-onlyでbaselineと同時計算する。
|
||||
- adapter/browser bucket単位で10%→25%→50%→100%へ段階展開する。
|
||||
- hard quality gateはzero toleranceとし、1件でも違反したbucketは即baselineへ戻す。
|
||||
- default化後も旧path、kill switch、kernel hashを最低1 release残す。
|
||||
- mismatchまたは新しいUNKNOWN reasonを起こしたviewは座標、span、iter、adapter情報と共にgolden corpusへ追加する。
|
||||
|
||||
## 採用しない近道
|
||||
|
||||
次は生成時間を短く見せられるが、品質不変の条件に反するため採用しない。
|
||||
|
||||
- adaptive `maxIter`、DPR、export解像度、AA sample数を下げる。
|
||||
- error boundやBLA safetyを緩め、失敗を内点または脱出扱いする。
|
||||
- reason 6を「計算放棄」と数え、黒のまま補修済みと見なす。
|
||||
- 再投影画像の色が近いという理由だけで、新viewの数値metaを再利用する。
|
||||
- 未証明のperiodicity、rectangle border fill、色差ベースAAで内部sampleを省く。
|
||||
- BLA/近似経路からbaseline fallbackとgeneration tokenを外す。
|
||||
|
||||
この順序なら、まず明白な二重計算と未使用の疎処理を取り除き、その後に参照選択とGPU schedulingを改善できる。画質を守る責務は各最適化のfallbackと自動比較に持たせ、速度と数値未確定率のどちらか一方だけを改善したように見せない。
|
||||
194
docs/PERFORMANCE_IMPLEMENTATION.md
Normal file
194
docs/PERFORMANCE_IMPLEMENTATION.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# 軽量化の実装設計と精査結果
|
||||
|
||||
更新日:2026-09-06。[改善案](PERFORMANCE_PLAN.md)の続き。以下は承認前の設計と精査記録。採用した実装、検証結果、残る数値上の制約は [実装結果](IMPLEMENTATION_RESULTS.md) を参照。
|
||||
|
||||
## 1. 精査で確定した修正
|
||||
|
||||
| 論点 | 現在のコードで確認した内容 | 案への反映 |
|
||||
| --- | --- | --- |
|
||||
| 参照の強制更新 | `ReferenceService.request(..., fresh=false)` があり、trueで該当cacheを削除する。実コードを使った小さなCPU確認でも再投入された | 「引数がなく無効」を撤回。同じ入力から同じ短い参照を再選択し得る問題として扱う |
|
||||
| 局所参照の位置 | `FAILURE_TILE_MAP_WGSL` が失敗indexの最小値を求め、`readNumericalFailureTiles()` が代表点へ変換する | 既に失敗画素を使う。重心選択と誤記せず、代替候補・回収実績を改善対象とする |
|
||||
| reason 4・7 | 4は定義・集計、7は名前がある。現行シェーダーにそれらを失敗として書く経路は確認できない | 現在の発生原因や改善効果に算入しない |
|
||||
| 継続の終端 | `DEEP_ACTIVE_CONTINUE_WGSL` の終端分岐はtarget>=150001が必要だが、通常の目標は最大150000 | 目標到達の判定とstateの寿命を分ける |
|
||||
| 最後の1反復 | 継続loopは更新後のnがstopに達すると、次回の脱出判定より先に終了する | 結果採用時に最終stateの脱出・誤差を確認する |
|
||||
| DS queueの上限 | `computeAccurateDirectHybrid()` が初期batchを時間に応じて拡大し、元の仕事量上限へ戻していない | 適応後のclampと横分割を前提にしてからまとめ投入する |
|
||||
| 現行の計測値 | `session.pixelIterations` は入口active数×chunkの加算 | 実反復数と呼ばず、仕事量上界として比較する |
|
||||
| 現行の「正確」 | Direct DS/Deep DS補修はDSで計算するが、全丸め誤差を区間伝播する証明ではない | 数値採用・有限回未脱出・内点証明を分ける |
|
||||
|
||||
照合対象は現行 [index.html](../index.html)。精査時のSHA-256は `045a5de85b816808ea8f54c5a390196b7436b3c7802267aadcd7bc1baa7c6792`。この値は照合した版を識別する記録で、今後の編集を禁止するゲートではない。
|
||||
|
||||
## 2. 計算状態の契約
|
||||
|
||||
既存のmetaを表示用の最小情報として残し、継続可能なstate・参照の識別・仕事の所属を分ける。reasonやclassを増やすこと自体を目的にしない。
|
||||
|
||||
| 論理状態 | 必須情報 | 次の処理 |
|
||||
| --- | --- | --- |
|
||||
| 未着手 | 画素index、描画世代 | 初期計算 |
|
||||
| 継続可能 | 実到達n、参照内のm、差分、指数、誤差、参照ID | 同じ数値方式で次の有界chunk |
|
||||
| 数値補修待ち | 失敗理由、画素index、最後に有効だったstateの有無 | 理由に合う参照/精度で補修 |
|
||||
| 目標まで未脱出 | 採用済みのn、誤差確認の状態、継続stateの有無 | 次の反復目標を待つ。集合内の証明とはしない |
|
||||
| 脱出を採用 | 脱出反復数、smooth、採用条件 | 数値計算から除外し、配色変更だけに応答 |
|
||||
| 内点を確認 | 確認した方式と対象画素 | 継続から除外 |
|
||||
|
||||
各画素が同時に複数の仕事へ所属しないことを基本にする。画素単位の所有者は `継続/補修/保留/数値処理終了` のどれか1つ。集計はこの排他的な所属を数え、active件数とmetaのUNKNOWN総数をそのまま足し合わせない。
|
||||
|
||||
`n` は処理した反復数、`m` は参照軌道内の位置であり、片方から他方を推定しない。画素ごとに到達nが違う可能性があるため、sessionの単一progressを全画素の到達証明として使わない。
|
||||
|
||||
### 目標到達時の処理
|
||||
|
||||
1. 最後の更新後のstateから `z_n` を再構成する。参照範囲外なら補修へ回し、末尾indexを丸めて代用しない。
|
||||
2. 脱出条件を満たすか、境界を誤差込みで判定できるかを確認する。確定できない境界は補修へ回す。
|
||||
3. 未脱出なら現在の目標に対する誤差条件を確認し、metaへ実到達nを記録する。
|
||||
4. 採用できた未脱出画素はstateを保存し、次の目標に使える保留対象として保持する。脱出・内点は除外する。
|
||||
5. 誤差条件を満たさないstateを「正常な継続state」として再利用しない。前chunk等の有効stateがある場合だけそこから補修できるものとする。
|
||||
|
||||
P0では既存の誤差閾値を無条件に緩めず、未実行だった採用処理を到達可能にする。DSやCPU補修にも採用条件の限界を残す。数学的保証を強める場合は、別の数値方式変更として評価する。
|
||||
|
||||
## 3. GPUスケジューラーの具体案
|
||||
|
||||
同じ描画世代・参照ID・数値方式・誤差方針の画素を1グループとして処理する。最初は既存の2本のactive queueを使い、複数の常駐エンジンを増やさない。
|
||||
|
||||
### まとめ投入の上限
|
||||
|
||||
```text
|
||||
upper = このまとまりの入口のactive件数
|
||||
workCap = 継続カーネル1回の上限(初期案:4,000,000)
|
||||
chunk = min(256, floor(workCap / upper), この目標までの残り反復)
|
||||
batchPasses = 1~4(初期は1、短い完了時間を確認して増加)
|
||||
|
||||
upper == 0 または残り反復 == 0 → 反復を投入せず目標の完了処理へ
|
||||
floor(workCap / upper) == 0 → 対象を分割する。chunkを1へ丸めて上限を超えない
|
||||
batchPasses × upper × chunk → まとまり全体の仕事量上限でも制限
|
||||
```
|
||||
|
||||
この上界が有効なのは、まとまりの途中で対象を追加せず、各画素を高々1回だけ次queueへ戻す場合である。補修済み画素の再参加・別グループの統合はまとまりの境界で行う。nの違う画素の残り反復はカーネル内でも切り詰める。
|
||||
|
||||
```text
|
||||
世代を確認
|
||||
同じ参照のbufferを確保
|
||||
2~4回以下の「件数→indirect引数→有界継続」をencode
|
||||
最後の件数・失敗要約だけをstagingへcopy
|
||||
submit
|
||||
staging.mapAsyncの成立を待つ
|
||||
世代を再確認して要約を採用、unmap
|
||||
表示更新・入力処理へ制御を返す
|
||||
残件があれば次のまとまりへ
|
||||
```
|
||||
|
||||
CPU時間による初期調整目標は、1まとまりの完了を数ms~十数ms程度に収めること。ただしmapを含む経過時間は純GPU時間ではない。過去の実測が長いときは回数か仕事量を減らし、数値失敗を捨てて短くしない。初回に4回を固定投入したり、毎回時間計測用の追加同期を挟んだりしない。
|
||||
|
||||
専用参照・uniform・queue・stagingは、そのまとまりが使い終わるまで破棄/上書きしない。`mapAsync` が成立したstagingは `unmap` までGPUへ再投入しない。単一stagingを直列再利用する実装から始め、重ね合わせが必要になった場合だけ少数ringへ広げる。
|
||||
|
||||
GPUへ既に投入した処理を世代tokenだけで取り消すことはできない。入力時は追加投入を止め、旧世代の結果を新しい画素バッファへ書き込まない。参照の破棄は、それを使う処理の完了後に行う。
|
||||
|
||||
### 他の重い経路にも適用する上限
|
||||
|
||||
`computeAccurateDirectHybrid()` のDS queueは、初期値と適応後の値の両方を `maxPixels=floor(workCap/iter)` で制限する。workgroupが64でも仕事件数を最低64にする必要はなく、端数laneをguardすれば1~63画素も処理できる。
|
||||
|
||||
全幅1行でも予算を超えるDeep stripは、既存の `correctUnknownFrameScan()` と同様にx方向も分割する。処理量は論理画素数×反復上限で数え、丸められたworkgroup数だけを根拠にしない。デバイスのbuffer・binding・dispatchサイズ上限は、この仕事量上限とは別に満たす。
|
||||
|
||||
## 4. 補修と参照を再利用する手順
|
||||
|
||||
### 正常画素を巻き戻さない
|
||||
|
||||
失敗したindexを別queueへ移す際、残る画素のstateとqueueを保持する。補修から戻る情報は次の2種類を区別する。
|
||||
|
||||
- **meta・smoothのみ**:脱出なら計算終了。未脱出ならstate未提供として保留し、この画素だけを必要に応じて再計算する。現行のDS/CPU補修は主にこちら。
|
||||
- **有効な継続state付き**:同じ参照・数値方式のグループへ戻せる。異なる局所参照なら、その参照に属する別グループへ入れる。
|
||||
|
||||
局所参照で回復したstateを、元の全体参照のstateとして扱わない。全体の `setDeepContext()` を変更して別グループの処理と競合させず、encode時に使う参照bufferを明示する。原点が変わる場合の差分変換は、この最初の実装には含めない。
|
||||
|
||||
### 参照軌道の末尾延長
|
||||
|
||||
参照IDは原点・実際の計算精度・数値方式で識別し、反復目標だけの増加では同じIDを維持する。精度の自動再試行でbit数が増えた場合は、要求精度ではなく採用した精度に基づいて別IDにする。
|
||||
|
||||
Workerは参照本体の `zr, zi, zr², zi², n` と、検査用の高精度軌道の独立した終端stateを保存する。精度Pの丸め済み終端を単にP+32へ拡張して検査の初期値にしない。両者の検査済みprefixを維持し、それぞれのstateから末尾を進める。
|
||||
|
||||
参照が既に脱出した場合は単純延長を続けず、別候補か局所参照へ進む。同じ候補を同じ条件で何度も選び直さない。prefixの検査点配置は長さに依存しない規則へ揃えるか、新目標で必要になる過去の検査点も確認する。GPU側も容量が足りる範囲では末尾だけを転送し、容量拡張時は旧参照の利用完了を待つ。
|
||||
|
||||
### CPU補修の小分け化
|
||||
|
||||
最初の候補設定は同時1~2 Worker、1ジョブ最大16~64画素に加え `件数×反復数` による上限を設ける。これは固定の最適値ではない。精度bit数・実行時間も見てジョブを縮小し、少数でも長い画素がUIの次の仕事を妨げないようにする。
|
||||
|
||||
同じ精度の結果再利用は最初に導入できる。Workerへ渡した入力の世代・座標・解像度・反復予算・精度が同じ場合だけ再利用し、前回の高精度結果を次の比較へ渡す。
|
||||
|
||||
結果は完了したジョブから受け取るが、GPUへの反映は1か所で直列化する。writeBuffer/scatterの**前**に世代と対象metaを確認し、同時進行するGPU補修が確定させた画素を古いCPU結果で上書きしない。現在の `applyPrecisionFallback()` 内ではtoken確認がwriteBuffer後にあるため、呼び出し側の確認だけに依存する非同期化を避ける。
|
||||
|
||||
回収できなかった画素は、小さな保留リストと次の手段を保持する。時間予算を使い切ったことを `numeric-complete` として報告しない。
|
||||
|
||||
## 5. 未脱出の反復予算と表示
|
||||
|
||||
最初のP0~P2では、変更前に採用されていた反復方針を可能な範囲で固定して比較する。ただしP0で新しい失敗や目標境界の脱出を正しく認識した場合、その差を性能回帰と誤認しない。
|
||||
|
||||
P3の候補は、同じ大域目標の中で「境界周辺・孤立成分・長時間残件」を別優先度にする方式から始める。全候補が目標に到達したかはGPUで `未到達件数` を集約する。低優先度の残件にも割当を設け、速く終わる画素ばかりを処理し続けない。これだけでは総反復数は減らないが、表示の改善順と処理継続性を制御できる。
|
||||
|
||||
総反復数を減らす変更は、検証できた内点の除外か、明示した有限予算への停止として別に扱う。内点判定を増やす場合は適用領域を限定し、軌道の近接や少数回の無変化だけで内点へ昇格しない。
|
||||
|
||||
表示・履歴・出力の意味は次のように統一する。
|
||||
|
||||
| 表示段階 | 条件 | 履歴・PNG |
|
||||
| --- | --- | --- |
|
||||
| 現在視点の暫定表示 | 計算済みタイルを順次表示。未処理領域があることを状態として保持 | 完成履歴へ保存しない |
|
||||
| 数値補修が完了した予算画像 | 被覆漏れ0、数値補修待ち0、残る未脱出は到達nを記録 | 既定の反復方針を満たすまでは暫定。明示的な暫定出力を設けるなら予算を付記 |
|
||||
| 現在の品質方針で完了 | 必要な全候補が目標と数値条件を満たし、品質方針の停止条件を満たす | 完成履歴へ保存できる。集合内の完全証明とは別 |
|
||||
|
||||
同一画素格子のタイル表示を先に検討する。低解像度の全体previewを追加する場合、半画素中心のずれを確認し、単に同じ画像範囲という理由で最終画素の計算へ流用しない。暫定表示で初回待ちが改善しても、数値補修完了の時間を別途示す。
|
||||
|
||||
## 6. 実装単位と確認ケース
|
||||
|
||||
| 単位 | 対象 | 採用するための確認 |
|
||||
| --- | --- | --- |
|
||||
| A:判定と上限 | 継続の目標終端、meta更新、DS/Deepの分割 | 最後の反復での脱出、誤差超過、未脱出の再投入、全分割の被覆を確認 |
|
||||
| B:同期とBigInt重複 | 小さなまとめ投入、精度ペア再利用 | Aと同一入力・予算で分類、脱出反復、smooth、到達nが一致。同期や軌道数が減る |
|
||||
| C:stateと参照の寿命 | 補修queue分離、参照末尾延長 | 少数の補修で正常stateが0に戻らず、参照変更・精度変更を混同しない |
|
||||
| D:補修転送 | タイル別GPU queue、scatter反映 | 件数だけでなくindexの被覆を確認。重複・古い世代・確定画素への上書きがない |
|
||||
| E:予算と表示 | 内点判定、優先度、暫定表示 | 未到達を完了扱いしない。操作と出力で品質方針が一致する |
|
||||
|
||||
必要な確認は既存の2つのテストファイルへ足す。常時監査や別の昇格システムは作らない。
|
||||
|
||||
| ケース | 確認する差分 | 現行テストの不足 |
|
||||
| --- | --- | --- |
|
||||
| 目標最後の1回で脱出する単一画素 | 更新後の脱出反復が目標内に記録される。例:c=1なら半径2を越えるのは3反復目 | Workerの参照は半径4で生成されるため、既存のWorker確認だけでGPUの目標境界は証明できない |
|
||||
| 目標到達時の誤差超過・未脱出・参照末尾 | 誤差検査が走る。正常未脱出だけが継続可能。参照外を読まない | スモークはmetaの反復到達・この分岐を直接検査しない |
|
||||
| 0/1/63/64/65件、奇数回/偶数回のまとめ投入 | queue交換、端数lane、空queue、同じ画素の重複処理がない | 現行テストは複数pass化のqueue内容を確認しない |
|
||||
| 広いcanvas・高反復・適応batch拡大 | `logicalCount×iterationBound` がカーネル別上限内、横分割で全画素を1回ずつ処理 | 現行のstrip検査は最小1行の上限超過を許容する |
|
||||
| 大半が正常で1画素だけ補修、参照の原点/精度変更 | 正常な進捗保持、補修画素だけの再参加、参照IDの分離 | ハッシュだけでは無駄な再計算を検出できない |
|
||||
| CPU補修中のパン・再ズーム・戻る操作 | 古い世代がwriteBuffer前に破棄される。最新画面の結果だけが残る | 現行のパン往復は各回の完了を待つため、処理中の競合を検査しない |
|
||||
| 同じ深部座標のFAST/Deepと独立した少数画素 | 共通バグを含む一致と、独立な高精度確認を区別 | 比較関数は主に不一致画素だけをCPU確認する |
|
||||
| 暫定表示中・完了後のPNG | 配列長だけでなく、同じ座標・予算の数値結果を出力する | 現行スモークは小さなRGBAタイルの長さ・checksumで、PNG全体の検証ではない |
|
||||
|
||||
正確性用の小ケースを先に実行し、性能ケースは初期表示とSeahorseのFAST/Deepを基本にする。主カージオイド/周期2円の境界変更時は `re=-0.75, im=0, span=0.02` の実軸付近を追加する。既知の黒領域問題は次の入力を固定小解像度で使用する。
|
||||
|
||||
```json
|
||||
{
|
||||
"re": "-29466147000382485924219538765424656674342940730553342389512209954887705938978413587",
|
||||
"im": "5179018789925933872641942859702187326563010594882200888329237785122225251258352",
|
||||
"span": "250637324516887125119843167990565159991251418774166207381437546036409",
|
||||
"bits": 273,
|
||||
"baseIter": 350,
|
||||
"adaptive": true,
|
||||
"processMode": "standard",
|
||||
"renderMode": "accurate",
|
||||
"fixed": true
|
||||
}
|
||||
```
|
||||
|
||||
出典はGit履歴の `tests/optimization_webgpu_corpus.mjs` にある `reported-black-regression`。`fixed:true` の整数座標なので10進実数として読み替えない。今回はこのケースをGPU実行していない。
|
||||
|
||||
## 7. 性能評価と中止条件
|
||||
|
||||
同じadapter、canvas画素数、座標、初期反復、最終品質方針で比較する。固定予算での実行効率と、予算を変える方式は別の結果にする。ブラウザー起動直後のコンパイル込み時間と、同じpipelineを使う描画時間も分ける。
|
||||
|
||||
ウォームアップ1回+計測3回を最初の上限とし、中央値と全3件の値を保存する。ばらつきが大きく結論が出ない場合は「未判定」とし、同一条件が整ってから必要なケースだけ再測定する。多数の測定を自動で走らせ続けない。
|
||||
|
||||
追加する計測は、理由別失敗数、未到達数、GPU往復回数、readback byte数、仕事量上界、再初期化した画素数、BigInt軌道数の小さな集計に絞る。性能測定中に全画面のoracle比較を走らせない。独立した画素確認は正確性用の別実行にする。
|
||||
|
||||
数値補修完了時間が悪化する変更は、初回表示だけの改善と区別する。仕事量が減ってもメモリ増加や同期で時間が増える場合は、その段階の変更を採用しない。数値判定を緩めて性能値を合わせない。
|
||||
|
||||
GPUエラー・device loss・画素欠落・古い世代の書込みがあれば、そのケースを中止して原因を確認する。重いケースのtimeout後に、新しいEdgeを追加起動して続けない。検証用Edgeは1インスタンス・1タブ・直列実行とし、起動したプロファイルを識別して終了まで管理する。
|
||||
|
||||
## 8. 計画作成時点の成果と未実施事項
|
||||
|
||||
今回実施したのは、現行コードとの照合、旧案の誤記修正、参照キャッシュの小さな実コード確認、仕事量の数値例の再計算、実装設計の追記である。終端分岐・queue・精度の確認方法を具体化したが、新しい数値方式やスケジューラーを実装・GPU検証したわけではない。
|
||||
|
||||
参照サービスのfresh処理、代表点選択、予約reason、処理量上限、現行テストの限界は本文へ反映済み。期待効果は仮説として残し、改善率や任意座標での未確定ゼロを検証済みとは記載していない。新たなEdgeは起動していない。
|
||||
205
docs/PERFORMANCE_PLAN.md
Normal file
205
docs/PERFORMANCE_PLAN.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# 未確定画素を減らしながら軽量化する案
|
||||
|
||||
作成日:2026-09-05。精査・追記:2026-09-06。以下はBLA除去後のコードを調査した時点の計画。承認後の変更と測定結果は [実装結果](IMPLEMENTATION_RESULTS.md) を参照。
|
||||
|
||||
実装時の状態遷移・処理量上限・手順・確認ケースは [実装設計](PERFORMANCE_IMPLEMENTATION.md) にまとめた。
|
||||
|
||||
## 推奨方針
|
||||
|
||||
まず **計算済みの状態を使い続けること、GPUとの往復を減らすこと、失敗した画素だけを適切な精度で補修すること** に集中する。反復回数や誤差判定を一律に削って軽くする方針は採らない。
|
||||
|
||||
現状はBLAを除去しても、未脱出画素の全件継続、補修後のやり直し、細かいGPU完了待ち、各画素のBigInt再計算が残っている。ここを直す方が、別の近似加速器を追加するより先である。
|
||||
|
||||
最初の実装単位は次の3つを推奨する。
|
||||
|
||||
1. 未脱出画素の到達反復数・誤差を正しく記録し、未確定の分類を揃える。
|
||||
2. 継続計算のGPU投入を小さなまとまりにし、まとまりごとの読戻しにする。
|
||||
3. 補修が必要な画素だけを別キューへ移し、正常な画素の状態と参照軌道を保持する。
|
||||
|
||||
P0の判定修正は正しい結果へ直す変更であり、従来と結果が変わる可能性がある。P0後の結果を基準として、P1では最終反復数の方針を維持し、同じ計算結果を少ない再計算・同期で得る。その後に、内点の早期確定と画素ごとの反復予算を導入する。
|
||||
|
||||
## 1. 「計算不能」を分けて扱う
|
||||
|
||||
ここでいうセルは画素を指す。現在の `FIELD_UNKNOWN` は複数の状態を含むため、総数だけを最小化すると誤判定を増やしやすい。
|
||||
|
||||
| 状態 | 現行の表現 | 目指す扱い |
|
||||
| --- | --- | --- |
|
||||
| まだ計算していない | reason 0 | 最終画像で0。タイルの被覆漏れを防ぐ |
|
||||
| 精度・参照軌道の都合で結果を採用できない | reason 1~5 | 最優先で減らす。局所参照・精度昇格・高精度補修へ回す |
|
||||
| 現在の反復数まで脱出していない | reason 6、または `FIELD_INTERIOR_LIKELY` | 数値故障とは分ける。到達反復数を記録し、継続または内点判定へ回す |
|
||||
| DS感度を表す予約名 | reason 7(名前のみ。現行シェーダーに出力経路なし) | 現在の失敗原因とは数えない。将来出力するなら補修対象・集計まで同時に定義する |
|
||||
| 内点と確認できた | `FIELD_INTERIOR_PROVEN` | 継続キューから安全に除く |
|
||||
|
||||
**有限回の未脱出だけで集合内と証明したことにはならない。** また、異なる2精度で結果が一致したことは有用な確認だが、一般の境界画素について数学的な証明そのものではない。Direct DSとDeep DS補修も、全丸め誤差の区間を伝播して脱出を証明する実装ではない。全座標で「厳密分類・未確定0・短時間」を保証する目標は置かない。
|
||||
|
||||
reason 4の `rebase-gap` も、定義と集計は残るが現行シェーダーに失敗を書き込む経路は見当たらない。rangeやreference-endの実測件数と、予約理由を区別する。現在の統計配列の要素7は `corrected` 用なので、reason 7をそこへ足す変更はできない。
|
||||
|
||||
目標は、同じ表示精度・反復方針での数値失敗を可能な限り0へ近づけ、残った未脱出画素には正直な状態と継続手段を持たせること。UNKNOWNを黒や周囲の色へ置き換えるだけでは改善と数えない。
|
||||
|
||||
## 2. 現状から分かった負荷
|
||||
|
||||
根拠は [index.html](../index.html) のコードと [前回の確認結果](CLEANUP.md)。今回、追加のEdge起動や速度測定は行っていない。2026-09-06の精査では、参照サービスの実コードを使ったキャッシュ更新確認と、小さなCPU計算で数値例を確認した。以下のボトルネック順位はコードからの推定で、GPU時間・同期時間・CPU時間の内訳は未計測である。
|
||||
|
||||
| 箇所 | 確認できた処理 | 問題・軽量化の方向 |
|
||||
| --- | --- | --- |
|
||||
| `refinePixelFrontier()`、`operationLimitIndices()` | reason 6の全画素を候補にする。8近傍だけを選ぶ `operationLimitFrontier()` は本経路から呼ばれていない | 名前に反して境界だけの計算ではない。内点判定と優先順位が必要 |
|
||||
| `pixelFrontierIterationFloor()` | 画素幅のビット数×256を最低反復予算とする | 空間精度と必要脱出反復数を強く結び付けたヒューリスティック。深部では大きな追加計算になる |
|
||||
| `DEEP_ACTIVE_RESUME_INIT_WGSL` | `w=0, n=0, m=0` から開始する | 初期描画で計算した反復を、継続経路で再び計算する |
|
||||
| `refinePixelFrontier()` の `restart` | 数値補修があると、次の周回で残存候補全体のsessionを作り直すことがある | 少数の失敗で正常な画素まで再計算する |
|
||||
| `continueOperationLimitActive()` | 原則1 dispatchごとにsubmit、全queue完了待ち、4 byte件数のmapを行う | GPUとCPUの細かい往復。GPUに次の仕事が届くまで空く可能性がある |
|
||||
| `ReferenceService.requestFixed()`、参照Worker | 反復目標が変わりキャッシュにない場合、参照を再生成して検証する | 同一原点・同一精度なら末尾延長で済ませられる余地がある |
|
||||
| `recoverNumericalUnknownTiles()` | 局所参照のバッチごとに全画面metaを読戻し、CPUで所属画素を抽出 | 失敗が疎でも全画面転送・走査が発生する |
|
||||
| `precisionFallbackWorkerSource()` | 最大4試行で2精度ずつ、各画素を反復0から計算する | 高精度計算が重複する。同じ精度の計算結果を再利用できる |
|
||||
| `recoverResidualPrecision()` | 残件をWorker数で割り、実質全残件を一度に投入して `Promise.all` で待つ | 小さな費用上限で区切られておらず、長いCPU負荷になり得る |
|
||||
| `applyPrecisionFallback()` | Workerの結果ごとに反映と全画面統計を実行 | 細切れのアップロード・統計・完了待ちをまとめられる |
|
||||
| `qualityStages()`、`renderQualityStage()` | 深部は原則1回の最終描画。補修と継続が完了するまで新規frameの公開を待つ | 完了時間がそのまま初回表示待ちになる。履歴のない座標ジャンプで特に目立つ |
|
||||
|
||||
### 具体的な規模
|
||||
|
||||
前回の320×240、Seahorse、span `3.4e-13`、初期512反復では、FASTは32768反復まで進んで約36.7秒かかった。FASTとDeepの最終meta・smoothは一致したが、これは両者共通の停止条件・数値判定まで正しいという証明ではない。
|
||||
|
||||
この条件の画素幅は約49.74 bit相当で、現在の最低反復予算は12736。目標を倍増するため最低予算を初めて越えるのは16384で、さらに安定判定によって32768などへ進む。512から32768は反復目標で64倍であり、実際の総演算量が常に64倍という意味ではない。
|
||||
|
||||
現在のchunkは、残り反復数による切り詰めを除くと `max(1, min(256, floor(4,000,000 / activeCount)))`。全画素が継続対象のまま32768反復まで進む仮定では、320×240で少なくとも約631 dispatch、800×600で4096 dispatchになる。各目標境界、補修、参照生成の追加費用は含まない。実際には脱出によって対象が減るため、この例を実測dispatch数として扱わない。ここでいうdispatch数は反復カーネルの回数で、別にindirect引数を準備する小さなdispatchが各回にある。
|
||||
|
||||
## 3. 最初に直すべき判定上の問題
|
||||
|
||||
### 継続経路の終端誤差検査が通常の目標で実行されない
|
||||
|
||||
`DEEP_ACTIVE_CONTINUE_WGSL` の未脱出時の終端処理は `p.targetIter >= p.maxIter` を条件とする。一方、呼び出し側は `p.maxIter=150001`、通常の目標は最大150000である。そのため、この分岐の誤差検査と反復数のmeta更新には通常到達しない。脱出・range等の他の検査は別途存在する。
|
||||
|
||||
提案は、「次へ継続するための状態保存」と「今の目標まで計算した結果の判定」を分離すること。各目標境界で実際の `n` と誤差を記録し、必要な画素だけを補修へ回す。既存の終端分岐を単に有効化すると `return` によって継続キューから画素が脱落するため、state保存と再投入を含めて設計する。
|
||||
|
||||
さらにloop先頭で `s.n>=stop` を先に判定するため、最後の更新で到達した `z_target` の脱出判定は次の継続まで遅れる。表示をその目標で確定するなら、最終stateを評価してから未脱出として採用する必要がある。中間chunkではなく、結果を採用する目標境界で確認する。
|
||||
|
||||
この修正で今まで見えていなかった数値UNKNOWNが一時的に増える可能性がある。改善の評価は、その増加を隠さず、正しい判定後の補修コストと残件数で行う。
|
||||
|
||||
### 未脱出の表現と停止条件を揃える
|
||||
|
||||
Direct DSは未脱出を `FIELD_INTERIOR_LIKELY` として返す一方、別経路ではreason 6となり、継続対象が異なる。同じ予算で比較するには、数値的に判定できたこと、集合内であること、追加反復を待っていることを別々に扱う必要がある。
|
||||
|
||||
また、`pixelEscapeAdvance()` の「新しい脱出が既存の脱出画素の近傍内に収まる」という条件と2回の安定周回は、将来の孤立した脱出や1画素未満の誤差を保証しない。現行の実用的な停止条件として記録し、厳密な内点証明とは呼ばない。
|
||||
|
||||
### 処理量上限は全経路で統一されていない
|
||||
|
||||
400万pixel-iterationsは現行の継続・疎補修の設計値であり、すべてのdispatchに適用できている保証ではない。`computeAccurateDirectHybrid()` のDS queueは初期batchを上限から決めるが、時間による拡大後に同じ上限でclampしていない。12000反復では333画素から500画素へ増えるだけで600万になる。
|
||||
|
||||
`initialNumericRows()` は最小1行なので、`幅×反復数` が予算より大きい場合は1行でも超える。既存の `regression.mjs` はこの例外を許容する検査で、全経路400万以下の証拠にはならない。重いDS/Deep経路は必要なら横方向も分割し、どの適応処理でも最終的に上限へ戻す。FASTの4800万等とは別のカーネル別予算として管理する。GPUの経過時間は演算内容にも依存するので、画素×反復数だけで停止防止を保証しない。
|
||||
|
||||
## 4. 軽量化案と優先順位
|
||||
|
||||
| 優先 | 案 | 期待する効果 | 未確定を増やさない条件 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 上記の状態・誤差判定と処理量上限を整理 | 評価基準と分割の前提を正す | 未検査の画素を確定扱いせず、上限超過時はさらに分割する |
|
||||
| P1 | GPU投入をまとめ、読戻しを間引く | 同期・submit・JS呼び出しを削減 | 各dispatchの上限と投入総量を維持 |
|
||||
| P1 | 正常画素のstateと参照軌道を保持 | 反復0からの重複計算を削減 | 参照原点・精度・計算式が同一であること |
|
||||
| P1 | 数値失敗キューをGPU上で作る | 全画面readback・CPU走査を削減 | キュー被覆とoverflow時の処理を保証 |
|
||||
| P2 | 失敗理由別に補修を選ぶ | 回収できない補修の反復を削減 | 失敗した画素には次の補修手段を残す |
|
||||
| P2 | BigIntの重複計算と一括投入を減らす | CPU負荷・最長待ち時間を削減 | 2精度比較と未採用画素の追跡を維持 |
|
||||
| P3 | 安全な内点判定を加える | 未脱出キュー自体を縮小 | 判定に失敗した点は通常計算へ戻す |
|
||||
| P3 | 画素・タイルごとに反復予算を配分 | 一律の深い反復を削減 | 優先度の低い未確定画素を捨てない |
|
||||
| P4 | 同一視点の段階表示とメモリ上限 | 初回待ち・メモリ圧迫を軽減 | 暫定表示を完成履歴へ昇格させない |
|
||||
|
||||
### P1-A:GPU主導の継続と小さなまとめ投入
|
||||
|
||||
`encodeDeepActiveChunks()` は既に複数passを符号化できる。最初は2~4回程度の有界dispatchをまとめ、件数を読むのはまとまりの最後だけにする。GPU上の件数からindirect dispatchを構築し、対象0の後続passは何も処理しないようにする。
|
||||
|
||||
補修画素の再投入がないまとまりの中ではactive数は増えない。その入口の件数を保守的な上限に使えば、毎回CPUへ戻らず各dispatchの処理量を制限できる。再投入するときはこの前提を更新する。
|
||||
|
||||
4 byteの読戻し自体のために `mapAsync()` の前で全queueの完了を重ねて待つ必要はない。対象バッファのGPU使用完了はmapの成立で扱える。ただし、mapの成立は他バッファや別のpromiseの完了を意味しない。投入量の制御や別用途の完了待ちは分けて残す。[WebGPU仕様のbuffer mapping](https://gpuweb.github.io/gpuweb/#buffer-mapping)
|
||||
|
||||
継続・高コスト補修の各dispatchを400万pixel-iterations以内へ実際に制限したうえで、GPUへ先行投入する時間・総仕事量にも上限を置く。まとめる回数を増やしてWindowsのGPU停止やキャンセル遅延を再発させない。uniformの値がpassごとに変わる場合は専用offsetや別バッファで保持し、submit前の同じuniform領域への上書きで全passが最後の値を見る構成にしない。[WebGPU開発者による複数passのuniform書き込みの説明](https://github.com/gpuweb/gpuweb/discussions/2509)
|
||||
|
||||
### P1-B:正常なstateと参照軌道を使い続ける
|
||||
|
||||
最初に補修キューと継続キューを分離する。数値失敗した画素だけを継続キューから外し、正常画素の `d, w, scaleExp, n, m, errScaled` を保持する。補修した画素は、その結果と参照に対応したstateで再参加させる。stateを復元できない画素に限って再計算する。
|
||||
|
||||
次に、同一原点・同一精度の参照Workerに末尾stateを保持し、目標増加時は軌道の末尾を延長する。既に検査したprefixは同じ条件のまま保持し、新しく追加した部分を検査する。精度変更や原点変更時は別の参照として扱う。
|
||||
|
||||
初期Deep描画からの継続state保存はその次に行う。FAST・Direct・DSのstateをDeepへそのまま移植しない。実装を共通化する場合も、丸め順・誤差・参照の契約を揃えてから採用する。
|
||||
|
||||
参照が短い場合の `refs.request(..., true)` は、現行の第5引数 `fresh` によって該当キャッシュを削除する。旧案の「引数がなく無効」という記述は誤りだった。ただし同じ入力の処理中promiseは再利用され、再生成しても決定的な候補探索は同じ原点を選び得る。改善対象はfreshの追加ではなく、試した原点の記録、失敗分布に基づく別候補の選択、十分な長さを得られない場合の処理である。
|
||||
|
||||
### P1-C:GPU内で失敗画素を集める
|
||||
|
||||
reason別・タイル別の件数、prefix sum、画素indexのscatterをGPU上で作り、局所参照ごとの範囲をそのまま補修へ渡す。既存の疎キューを拡張する形を優先し、別の監査基盤は追加しない。
|
||||
|
||||
CPUへ必要なのは参照を選ぶための小さなタイル要約と、最終的にBigIntへ渡す少数の失敗indexである。全画面metaの読戻しは通常の補修ループから外し、任意の検証や例外処理に限定する。
|
||||
|
||||
overflowや被覆漏れの検査はGPUで集計してまとまりの最後に確認する。件数確認を減らすことと、未処理画素を黙って捨てることを混同しない。
|
||||
|
||||
### P2-A:失敗理由ごとの補修
|
||||
|
||||
| 理由・分布 | 最初に試す処理 | 次の処理 |
|
||||
| --- | --- | --- |
|
||||
| reference-end | 同一原点の軌道延長。実際に脱出する参照なら失敗領域内で参照を再選択 | 局所参照で再計算 |
|
||||
| error-boundが局所に集中 | 既存参照でのDS補修か、回収実績のある局所参照を選ぶ | 改善しなければ高精度補修 |
|
||||
| escape-uncertain | 該当画素だけ精度を上げる | 必要な画素だけBigInt |
|
||||
| range | スケーリングと座標差の表現を確認 | 指数を持つ表現または高精度補修 |
|
||||
| 広く密集した失敗 | タイル単位で参照・精度を選び直す | 大量の画素別BigInt投入を避ける |
|
||||
|
||||
現在も `readNumericalFailureTiles()` はタイル内の最小indexの失敗画素を代表点に使っている。`sumX/sumY` という名前だが重心の総和ではなく、その代表点×件数である。改善は、この既存代表点を基準に失敗の密集度・到達反復・過去の回収費用から少数の代替候補を選ぶこと。単に「中心から失敗画素へ変更する」という新規機能ではない。参照変更でグリッチを減らせる一方、近い参照を選ぶだけで誤差を保証できるわけではない。[摂動法のグリッチと参照選択](https://mathr.co.uk/blog/2014-03-31_perturbation_glitches.html)
|
||||
|
||||
reason 4・7を将来使う場合の経路は予約設計として別途定義する。発生していない理由に専用の常時計算を追加しない。
|
||||
|
||||
補修1回で回収できた画素数、残件数、費用を少数の集計値で保持する。回収の止まった手段を同じ条件で繰り返さず、次の手段へ送る。時間切れの画素は保留として残し、補修完了数へ加算しない。
|
||||
|
||||
### P2-B:BigInt補修を小さくする
|
||||
|
||||
現在の精度比較は `P/P+32 → P+32/P+64 → …` で、隣り合う試行が同じ精度の軌道を再計算する。前回の高精度側の結果を次の低精度側へ使えば、最大4試行の8軌道を5軌道に減らせる。最初の比較で成功する画素にはこの削減は発生しない。
|
||||
|
||||
Workerへの仕事は全残件の等分ではなく、小さな画素数・推定pixel-iterationsで区切る。まず1~2 Workerを基本にして空いたWorkerへ次のバッチを渡し、端末の余裕がある場合だけ増やす。入力が来たら追加投入を止め、長い処理は必要に応じてWorker終了で取り消す。
|
||||
|
||||
同時期に返った結果のindex・meta・smoothをまとめて転送し、GPU scatterで反映する。全画面統計はWorker結果ごとではなく適用バッチごとに1回。2精度で採用できない画素は失敗キューに残す。
|
||||
|
||||
### P3-A:継続が不要な内点を安全に除く
|
||||
|
||||
Directにある主カージオイド・周期2円の判定を、適用可能なタイル・参照表現へ広げる。深い座標を単純にf32へ丸めて判定せず、高精度座標または誤差を含む区間で領域内と確認できた場合だけ採用する。領域から遠いタイルでは判定自体を省ける。[Cardioid and bulb checking](https://mathr.co.uk/blog/2022-11-19_cardioid_and_bulb_checking.html)
|
||||
|
||||
さらに一般の周期成分を対象とするなら、周期候補の検出と内点の確認を分ける。有限精度で同じ値になった、近い位置へ戻った、という理由だけでは確定しない。包含・収縮等を確認できる方式は後段の研究候補とし、全画素で毎反復行わず長く残る画素に絞る。
|
||||
|
||||
主カージオイドと周期2円だけでは、Seahorseや小さなコピー内部の問題を全部解決できない。効果の対象範囲を区別する。
|
||||
|
||||
### P3-B:反復予算を空間精度と切り離す
|
||||
|
||||
画素幅は座標の必要精度を決める。一方、外部の点が何回で脱出するかは軌道に依存するため、`pixelBits × 256` は必要反復数の証明ではない。
|
||||
|
||||
一律の倍増を置き換える候補は、境界近傍、孤立した未脱出成分、長時間残る画素を別キューで管理し、変化の多い部分を優先する方式。低優先度の未確定画素にも一定量を配り、孤立した遅い脱出を永久に取り残さない。8近傍だけへ対象を限定する変更は行わない。
|
||||
|
||||
この変更は従来と最終反復数・停止条件が変わり得る。P1の純粋な実行効率改善と分けて評価する。通常閲覧の有限予算と、明示的な高品質計算の扱いを決め、到達予算と未確定数を内部状態に残す。
|
||||
|
||||
### P4:表示待ちとメモリ負荷を抑える
|
||||
|
||||
最終的な未確定を減らす処理を継続しつつ、現在の視点で計算した暫定結果を段階表示する案を検討する。全体の低解像度計算を単純に追加すると二重計算になるため、同一画素格子のタイル完成表示か、最終計算へ再利用できる段階処理を優先する。暫定画像は完成履歴にしない。
|
||||
|
||||
現在のactive用stateは全画面に32 byte/画素、2本のqueueは合計8 byte/画素。800×600ならこの部分だけで約18.3 MiBあり、meta・smooth・texture・参照は別に必要になる。候補数が少ないときは密なslot配列、候補が多いときは上限付きタイルworkspaceを使い分ける。ただし画素indexとslotの対応、世代、queue境界を明確にする。slot方式でindex対応表を追加すれば、その分のメモリも増える。タイル処理でも生存stateを破棄して毎回0から計算してはならない。
|
||||
|
||||
## 5. 実装・確認の進め方
|
||||
|
||||
| 段階 | 変更範囲 | 確認すること |
|
||||
| --- | --- | --- |
|
||||
| 0 | 終端判定・未確定分類・処理量上限・最低限の集計 | 正常画素と未検査画素を区別できる。継続画素がqueueから消えず、適応batchも上限内 |
|
||||
| 1 | 同期のまとめ、BigIntの同精度結果再利用 | 同じ予算で脱出分類・反復数・smoothが一致。同期数と重複軌道数が減る |
|
||||
| 2 | 失敗キュー分離、正常state保持、参照末尾延長 | 補修後も正常画素の進捗が戻らない。数値UNKNOWNが減るか増えない |
|
||||
| 3 | GPUでのタイル別queueと補修選択 | 全画面読戻しbyte数、BigInt対象数、補修失敗の再試行が減る |
|
||||
| 4 | 内点判定と反復予算・段階表示 | 内点の誤確定を防ぐ。遅い脱出・孤立成分・操作往復で破綻しない |
|
||||
|
||||
自動監査を描画ループへ戻さない。計測は `?test` の明示実行で、初回表示までの時間、数値補修完了までの時間、未確定の理由別件数、仕事量上界、再初期化数、GPU読戻し回数・byte数、CPU補修数を1ケース1レコードへ集約する。現在の `activeCount×chunk` は早期脱出分も含む上界で、実行した反復数そのものではない。実反復を測るならテスト時のみGPU内で集約し、区別して記録する。
|
||||
|
||||
テストは既存の `regression.mjs` と `browser_smoke.mjs` を必要な分だけ拡張する。初期表示、同じSeahorse座標のFAST/Deep、実軸付近、既知の黒領域問題の座標を小さな代表セットとする。最初は同じadapter・解像度・予算でウォームアップ1回+計測3回。少数回の測定からp95は主張しない。
|
||||
|
||||
GPU制御は **検証用Edgeを1インスタンス、タブを1つ** に限定し、ケースを直列実行する。Edge内部の複数プロセスはあり得るが、ケースごとにブラウザーやプロファイルを増やさない。終了時に今回の検証用ブラウザーを閉じ、通常利用のブラウザーは終了対象にしない。
|
||||
|
||||
採用条件は、同条件での数値失敗の減少または非増加、計測した時間・仕事量の削減、既知の表示破綻がないこと。P0で従来未検査の失敗を可視化した直後は、修正後の正しい状態を比較基準とする。速度改善率・任意座標での未確定0は実装前に約束しない。
|
||||
|
||||
既存テストはこの案の採用条件をまだすべて検査しない。特に `numericalFailures=0` とFAST/Deep一致だけではP0の終端検査や共通する誤りを検出できない。既存の比較関数は不一致箇所だけを高精度で確認するため、一致した画素の独立確認も少数必要である。追加する確認ケースと、現行テストが未対応の範囲は実装設計に明記した。
|
||||
|
||||
## 6. 後回しにする案
|
||||
|
||||
- BLA、series approximation等の別の近似加速器の再導入。まず既存計算の重複・同期・補修を直す。
|
||||
- 全画面を常にDS/BigIntにする変更。高精度が必要な画素へ限定する。
|
||||
- Worker・Edge・GPU投入数の無制限な増加。
|
||||
- 誤差guardの緩和、未確定画素の着色だけで件数を減らす変更。
|
||||
- 常時shadow比較、フレームごとの全面ハッシュ、新しい多段階昇格ゲート。
|
||||
|
||||
未使用の `encodeDeepBucketHistogramStats()` や旧背景補修経路などの追加整理は可能だが、実行されていないコードの削除だけで数十秒の描画時間は解消しない。次の実装では、上記の実際に実行される仕事量と待ち合わせを優先する。
|
||||
|
|
@ -1,31 +1,41 @@
|
|||
# Mandelbrot WebGPU v24.2.55
|
||||
# Mandelbrot WebGPU
|
||||
|
||||
## 目的
|
||||
v24.2.54で確認された、FAST深部描画の矩形状/穴状の黒欠損と、数値補修を最終表示前に待つことによるレイテンシ増大を修正する。
|
||||
`index.html` をWebGPU対応のEdge/Chromeで開く。外部ライブラリやビルドは不要。
|
||||
|
||||
## 黒穴の原因
|
||||
COLOR passでは数値UNKNOWNを透明としていたが、PRESENT passが履歴補完を `scale <= 1.035 / offset <= 0.022` に制限していた。そのため通常のズームでも履歴補完が無効になり、透明UNKNOWNがcanvasの黒背景へ落ちた。
|
||||
- 描画は高速方式に統一。画素間隔に応じてDirect f32または参照軌道を使うFAST perturbationへ自動で切り替える。旧「正確」の共有URLもこの方式で開く。
|
||||
- パン/ズーム、配色変更、URLによる表示位置の共有、PNG出力に対応。
|
||||
- 数値未確定画素の補修と未脱出画素の継続計算を終えてから、完成画像を履歴へ保存する。
|
||||
- 計算中は現在の視点の暫定画像を表示する。反復上限は「自動」または指定回数を選べる。未脱出を集合内の証明とは扱わない。
|
||||
- 画面と同じ解像度・1 sampleのPNGは、補修済みの完成画像をそのまま保存する。
|
||||
- 色相変化は表示済みの数値結果を使い、計算中も色を更新する。停止ボタンまたは色相位置の手動操作で停止する。
|
||||
- 描画時間は初期描画から精細化・数値補修・最終GPU処理までの合計。計算中は経過時間、完了時は確定した時間を表示する。
|
||||
|
||||
さらに、その不完全front textureを完成historyとしてcommitする経路があった。局所reference補修が予算内で全UNKNOWNを回収できない場合、タイル境界に沿った黒欠損が完成画像へ固定され得た。
|
||||
BLAと比較専用の監査処理は削除済み。[整理内容と検証範囲](CLEANUP.md)を参照。
|
||||
|
||||
## v24.2.55の契約
|
||||
1. 数値UNKNOWNを含むfrontはstable historyへ昇格しない。
|
||||
2. 通常のパン/ズーム範囲では、数値UNKNOWNを直前のstable frameから再投影して埋める。
|
||||
3. FASTの数値補修は一次画像のpublishをブロックせず、同一viewのbackground taskとして継続する。
|
||||
4. background補修が完了して数値UNKNOWNが0になった時点で初めてhistoryを更新する。
|
||||
5. tiled FAST開始時にmeta/smoothをzero sentinelへ初期化し、最終color前にreason=0の未書込pixelだけbaseline FASTで再計算する。
|
||||
[軽量化の実装結果と測定値](IMPLEMENTATION_RESULTS.md)を参照。承認前の[改善案](PERFORMANCE_PLAN.md)と[実装設計](PERFORMANCE_IMPLEMENTATION.md)も残している。
|
||||
|
||||
`REASON_OPERATION_LIMIT` は数値故障ではなく、maxIterまで未脱出だったpixelなので従来どおり黒表示する。今回の矩形状欠損対策は主に数値UNKNOWN/未書込pixelを対象とする。
|
||||
色相・時間表示の修正と描画方式統合については[ビューワー修正](VIEWER_FIXES.md)を参照。
|
||||
|
||||
## FASTレイテンシ
|
||||
production BLAのCPU側64点shadow計算を廃止した。BLA workerはtable構築だけを行い、採用判定は既存の64点GPU probeへ一本化した。
|
||||
## 検証
|
||||
|
||||
classic Seahorse Valley相当 (654x690, span 3.4e-13, 2900 iter) のNode/V8 workerモデル:
|
||||
- 旧 CPU validate 64点: 約446.9 ms
|
||||
- 新 table build only: 約60.0 ms
|
||||
- 同期prep削減: 約386.9 ms
|
||||
Node.js 22以降で実行する。追加パッケージは不要。
|
||||
|
||||
BLAが不利なviewでは本体FAST計算時間そのものは残る。今回の変更では、数値補修Deep/multi-referenceを一次表示のクリティカルパスから外すことで、黒穴を出さずに操作から画像表示までの待ち時間を短縮する。
|
||||
```sh
|
||||
node tests/regression.mjs
|
||||
```
|
||||
|
||||
## 数値コア
|
||||
v24.2.54から変更したproduction WGSLはFAST_PERTURB_WGSLのみ。通常 `unknownOnly=0` の計算式・perturbation recurrenceは変更していない。追加分はcoverage repair用のearly guardである。
|
||||
構文、現行シェーダーのハッシュ、バックエンド選択、フレーム振り分け、Worker、補修対象と処理量上限を確認する。ハッシュファイルを無条件に再生成してテストを通さないこと。
|
||||
|
||||
実GPUのスモークテストは、独立した一時プロファイルのEdge/Chromiumをリモートデバッグポート9333で起動して実行する。
|
||||
|
||||
```sh
|
||||
node tests/browser_smoke.mjs 9333 all
|
||||
```
|
||||
|
||||
`all` は `reset`、`fast`、`mid`、`legacy`、`axis`、`black`、`kernels`、`cancel`、`budget`、`ui` に置き換えて単独実行できる。描画6ケース(旧方式指定の移行を含む)、キューの被覆、独立高精度サンプル、計算中のキャンセル、指定反復予算、PNGの復号、色相変化、時間表示を確認する。
|
||||
|
||||
起動時は拡張機能と同期を無効にし、専用プロファイルのタブを1つだけにする。テストはそのタブを再利用し、最後に `about:blank` へ戻す。終了後は検証用ブラウザーを閉じる。
|
||||
|
||||
測定が必要な場合だけ `node tests/browser_smoke.mjs 9333 bench` を実行する。初期表示・Seahorse FASTを各1回ウォームアップ+3回測定する。通常の回帰確認ではベンチマークを繰り返さない。
|
||||
|
||||
検証APIと詳細計測は `index.html?test` でのみ有効。通常起動では監査計算を自動実行しない。Strict設定も比較テストを起動しない。
|
||||
|
|
|
|||
|
|
@ -1,75 +0,0 @@
|
|||
# Validation Report — v24.2.55
|
||||
|
||||
## 1. JavaScript / bundle syntax
|
||||
- kernel bundle: PASS
|
||||
- application bundle: PASS
|
||||
|
||||
## 2. production kernel差分
|
||||
v24.2.54とのSHA-256比較。
|
||||
|
||||
変更:
|
||||
- `version`
|
||||
- `FAST_PERTURB_WGSL`
|
||||
|
||||
不変:
|
||||
- Direct
|
||||
- Accurate seed / DS direct
|
||||
- Deep perturbation
|
||||
- Deep correction
|
||||
- COLOR
|
||||
- PRESENT shader本体
|
||||
- BLA candidate / production frame kernels
|
||||
|
||||
FAST_PERTURBの通常recurrenceは不変。`unknownOnly=2` 時だけzero-sentinel pixelを再計算するearly guardを追加。
|
||||
|
||||
## 3. coverage contract
|
||||
PASS:
|
||||
- tiled numeric開始時にmeta/smoothをzero clear
|
||||
- color前にbaseline FAST coverage repair
|
||||
- coverage repairはreason=0 UNKNOWNだけをiteration
|
||||
- 既計算pixelはearly return
|
||||
|
||||
これにより、strip/tile未書込が発生してもrectangular zero-metadata holeをそのままpublishしない。
|
||||
|
||||
## 4. temporal fill / history
|
||||
PASS:
|
||||
- history利用範囲をstable reprojectionと同じ `2.25 / 0.45` へ統一
|
||||
- numeric recovery pending/running時はstable historyへcommitしない
|
||||
- recolor経路も `numericFrameComplete()` を満たさない限りhistoryへcommitしない
|
||||
|
||||
## 5. background numeric recovery
|
||||
PASS (static/control-flow):
|
||||
- FAST primary終了後に数値failureがあれば60ms後にbackground recoveryを予約
|
||||
- local multi-referenceを先行
|
||||
- 残存時だけfull Deep + DS + local recovery
|
||||
- view tokenが変われば結果をpublishしない
|
||||
- recovery中のrecolorはqueueへ退避
|
||||
|
||||
## 6. BLA production prep
|
||||
classic Seahorse Valley相当のworker-model実測:
|
||||
- 旧64-point CPU validation: 446.9 ms
|
||||
- 新build-only: 60.0 ms
|
||||
- 同期prep削減: 386.9 ms
|
||||
|
||||
BLA table buildそのものはほぼ同時間 (58.4 vs 59.4 ms)。削減分は描画前CPU shadow sample計算。
|
||||
|
||||
## 7. BLA safety regression
|
||||
既存289-point Seahorse test、safety=1/64:
|
||||
- classification mismatch: 0
|
||||
- first escape mismatch: 0
|
||||
|
||||
productionではCPU per-view validationを外したが、64-point GPU probeとBLA pixel fallback契約は維持。
|
||||
|
||||
## 8. browser smoke
|
||||
Chromiumでlocalhostからロード:
|
||||
- page JavaScript error: 0
|
||||
- kernelVersion: `24.2.55-no-black-hole-async-recovery`
|
||||
|
||||
このコンテナでは当該起動条件でWebGPU adapterを取得できなかったため、canvasを含む実端末フレーム時間は未測定。
|
||||
|
||||
## 9. release layout
|
||||
- executable HTML: `index.html` の1本のみ
|
||||
- `index.baseline-*.html`: なし
|
||||
|
||||
## 残る性能課題
|
||||
BLA probeがOFFになるSeahorse/Misiurewicz型viewではbaseline FASTのpixel-iteration量が依然支配的。v24.2.55はこのケースのCPU prepと同期補修待ちを削るが、primary perturbation本体を別アルゴリズムで高速化する変更ではない。
|
||||
35
docs/VIEWER_FIXES.md
Normal file
35
docs/VIEWER_FIXES.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# 色相・描画時間・描画方式の修正
|
||||
|
||||
対象:`index.html`。2026-09-06。
|
||||
|
||||
## 色相変化
|
||||
|
||||
従来は数値計算中の色変更を保留し、精細化終了時に保留分を実行する処理も抜けていた。精細化終了後に最新の色を必ず反映するようにした。
|
||||
|
||||
自動色相変化中は、公開済みの数値結果だけを別のGPUバッファへ保持し、独立した色用テクスチャへ描く。計算中のバッファやfront/backを変更せず、GPUキューの順序を保って表示色を更新する。位置が変わった際は保存した視点に応じて再投影する。新しい暫定結果の公開時に色用の数値結果も更新する。
|
||||
|
||||
追加領域は自動色相変化を使う間だけ確保する(12 byte/画素+32 byte、320×240で約0.88 MiB)。停止後の色反映または次の結果公開で解放する。数値再計算はせず、色更新は最大約20回/秒。GPUが処理中の場合のフレームレートは機器の負荷に依存する。
|
||||
|
||||
## 描画時間
|
||||
|
||||
従来の表示は最後の処理段階の時間だった。初期描画・精細化待ち・参照作成・数値補修・最終GPU処理を含む、1回の生成全体の経過時間へ変更した。描画中は250 ms間隔で更新し、完了時にその場で確定する。色変更では計測をやり直さない。
|
||||
|
||||
途中キャンセルとエラーを完了と区別し、古い生成からの完了通知は新しい計測を終了させない。起動時に初期化完了通知から同じ視点を重複生成する処理も抑止した。画面への物理的な走査時間は計測に含まない。
|
||||
|
||||
## 描画方式の統合
|
||||
|
||||
独立した「正確」モードと専用Direct DSの4シェーダー、専用パイプライン、全画面Deep初期計算、未使用の背景Deep切り替えを削除した。描画方式の選択UIはなくし、従来の高速方式へ統一した。旧URLの `rm=accurate` は高速方式で復元し、旧ローカル設定も除去する。
|
||||
|
||||
高速方式でも使うBigInt参照、局所参照、DS補修、CPU高精度補修、未脱出画素の継続計算は維持する。これらの数値シェーダーは変更していない。名前としての「正確」を外すことで、有限反復の描画を数学的な全画素証明と誤解させない。任意の座標・反復予算での数値失敗0を保証するものではない。
|
||||
|
||||
中間倍率の確認で、自動反復の150000回へ正常到達しても安定判定を満たさないと画像を完成扱いにできない問題も見つかった。残った全画素の到達反復数を確認できた場合は有限予算の結果として公開し、「反復上限で完了」と表示する。未脱出は未脱出のまま記録し、内点へ置き換えない。到達不足や数値故障は引き続き完成扱いにしない。
|
||||
|
||||
## 検証
|
||||
|
||||
`node tests/regression.mjs` と、専用Edge 1インスタンス・1タブで `node tests/browser_smoke.mjs 9333 all` を使用する。色相テストはボタンの状態だけでなく、表示に使うGPUテクスチャの画素変化と数値バッファの不変性を確認する。長い数値補修中の色変更、停止、手動操作、完成時の保留色反映、通常描画との数値一致も対象。
|
||||
|
||||
時間表示は段階間の待ちを含む合計、数秒かかる実描画の完了直後の表示、色変更で時間が変わらないことを確認する。旧設定と共有URLの移行、描画6ケース、独立BigIntサンプル、キャンセル、PNGも従来の回帰テストに含める。
|
||||
|
||||
追加した中間倍率(Seahorse、span=1e-6)と旧Deep専用の黒領域は、それぞれ12サンプルの分類と脱出反復数が独立BigInt計算に一致した。一方、連続色付け用smooth値の差は最大約0.0367反復分/0.0516反復分だった。両ケースではsmoothのULP精度を保証せず、偏差をテスト出力に記録する。高速Seahorseと実軸の8 ULP以内という検査は維持している。旧正確モードと同じ色の数値精度を維持したという主張ではない。
|
||||
|
||||
最終版でCPU回帰と実GPUの `all` が通過した。6描画ケースの数値失敗は0。1100×720の実画面でも自動色相変化による色の変化、描画完了直後の「2.32 s」、描画方式の選択肢がないことを確認した。検証用Edgeと専用プロファイルは終了・削除済みで、残存Edgeプロセスは0だった。
|
||||
1783
index.html
1783
index.html
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"environment": "Chromium 144 + SwiftShader/Vulkan, compute-only WebGPU",
|
||||
"size": [320, 220],
|
||||
"iterations": 750,
|
||||
"lateEscapeThreshold": 24,
|
||||
"cases": [
|
||||
{"name": "reset", "center": [-0.5, 0.0], "span": 3.4, "gpuMs": 203.3, "sampledPixels": 1200, "classificationMismatchVsCpuDouble": 0, "escapeIterationMismatchVsCpuDouble": 2},
|
||||
{"name": "seahorse-low", "center": [-0.743643887037151, 0.13182590420533], "span": 0.1, "gpuMs": 160.7, "sampledPixels": 1200, "classificationMismatchVsCpuDouble": 0, "escapeIterationMismatchVsCpuDouble": 0},
|
||||
{"name": "cusp-low", "center": [0.25, 0.0], "span": 0.85, "gpuMs": 87.8, "sampledPixels": 1200, "classificationMismatchVsCpuDouble": 0, "escapeIterationMismatchVsCpuDouble": 0}
|
||||
],
|
||||
"stripOrderTest": {"pixels": 6144, "fullPassVs4RowQueuedStripsMismatch": 0},
|
||||
"note": "GPU time is software-GPU time and is only a relative engineering check."
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"samples": 60000,
|
||||
"mixture": "global Mandelbrot bounds + targeted known boundary neighborhoods",
|
||||
"maxIter": 900,
|
||||
"acceptedRule": "f32 escape iteration < 24",
|
||||
"acceptedSamples": 33637,
|
||||
"acceptedEscapeIterationMismatchVsCpuDouble": 0
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
{
|
||||
"DIRECT_F32_WGSL": {
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
},
|
||||
"DIRECT_ACCURATE_SEED_WGSL": {
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
},
|
||||
"DIRECT_DS_CORRECT_WGSL": {
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
},
|
||||
"FAST_PERTURB_WGSL": {
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
},
|
||||
"DEEP_PERTURB_WGSL": {
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
},
|
||||
"DEEP_CORRECT_WGSL": {
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
},
|
||||
"COLOR_WGSL": {
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
},
|
||||
"PRESENT_WGSL": {
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
},
|
||||
"BLA_GPU_FAST_FRAME_WGSL": {
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922",
|
||||
"FAST_PERTURB_WGSL": "b9db140f03fbd0aa36a5ca7dbbcbfec471e1587ebc632953f9eb73a35fb9a978",
|
||||
"DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65",
|
||||
"DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88",
|
||||
"COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3",
|
||||
"PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc",
|
||||
"BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019",
|
||||
"BLA_GPU_FAST_FRAME_WGSL": "2e4cdbd31d60c22d87fb04847af4d2c1d6de7d213da5cdd27234e5d8f9672553"
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"scenario": "classic Seahorse Valley, 654x690, span 3.4e-13, 2900 iterations, 64-point production gate equivalent",
|
||||
"oldValidate64Ms": 446.9,
|
||||
"oldTableBuildMs": 58.4,
|
||||
"newBuildOnlyMs": 60.0,
|
||||
"newTableBuildMs": 59.4,
|
||||
"estimatedSynchronousPrepSavedMs": 386.9,
|
||||
"note": "Node/V8 worker-model timing, not GPU frame time. The production path now builds the table only; the 64-point decision remains on the GPU probe."
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"version": "9af1429215fe356104bcbe991c98d101bbf241a68a38232533918454505014d3",
|
||||
"DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922",
|
||||
"DIRECT_ACCURATE_SEED_WGSL": "478146d2f8a0bb5608850dcbbdfab183bc6e9105eab2afb4122892f7353349c3",
|
||||
"DIRECT_DS_CORRECT_WGSL": "6c61934c796c67f08ebdee0b7156c8c8d891104f3fd1d2408bad16d9a76f7c00",
|
||||
"FAST_PERTURB_WGSL": "b9db140f03fbd0aa36a5ca7dbbcbfec471e1587ebc632953f9eb73a35fb9a978",
|
||||
"DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65",
|
||||
"DEEP_PERTURB_POSTSTATS_WGSL": "364d916da3d90b049ac24423a8112dd587ebd756fa97c99a6717f869c6b6cfa1",
|
||||
"DEEP_BUCKET_HIST_WGSL": "3458f908fa239b16667e5c82dae25274d5493d647595230574a3d65622a54aaf",
|
||||
"DEEP_BUCKET_HIST_STATS_WGSL": "b464d503703646e3c990e10a0cb40dacdc3677447b7a35e03c80a50bfca23824",
|
||||
"DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da",
|
||||
"DEEP_BUCKET_SCATTER_WGSL": "7575c4fc5f4d5674dfc3997189212ffc6fbe240a9d8b8b3f8ef883cd4eae47a1",
|
||||
"DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88",
|
||||
"DEEP_CORRECT_QUEUE_WGSL": "1337469b0673a4da20670daf220bec5883366e2454acc8c7923e472a5cec7a7f",
|
||||
"COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3",
|
||||
"AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342",
|
||||
"PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc",
|
||||
"BLA_GPU_SHADOW_WGSL": "e830339a150dd5722ca7c58674193174606adaf488bfd817408f2eeb4583514f",
|
||||
"BLA_GPU_CANARY_WGSL": "3c219f657b2890b40f7cfa0297e13a670819d4fed333fcb30cb37196c5755f55",
|
||||
"BLA_GPU_CANARY_APPLY_WGSL": "d02bb3ef007680a687960125ee9dded1fc4b8a4b091a6d924408a2e1f94a05e1",
|
||||
"BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019",
|
||||
"BLA_GPU_FAST_FRAME_WGSL": "2e4cdbd31d60c22d87fb04847af4d2c1d6de7d213da5cdd27234e5d8f9672553"
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"version": "62ebee65d5378e9379394b24229266de255fa2f241a2eecd31fbab0721d2ae00",
|
||||
"DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922",
|
||||
"DIRECT_ACCURATE_SEED_WGSL": "478146d2f8a0bb5608850dcbbdfab183bc6e9105eab2afb4122892f7353349c3",
|
||||
"DIRECT_DS_CORRECT_WGSL": "6c61934c796c67f08ebdee0b7156c8c8d891104f3fd1d2408bad16d9a76f7c00",
|
||||
"FAST_PERTURB_WGSL": "2cc5841740456f10ed3f64005e72ac6ff87d2c38926260a1fcb1521a389013a0",
|
||||
"DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65",
|
||||
"DEEP_PERTURB_POSTSTATS_WGSL": "364d916da3d90b049ac24423a8112dd587ebd756fa97c99a6717f869c6b6cfa1",
|
||||
"DEEP_BUCKET_HIST_WGSL": "3458f908fa239b16667e5c82dae25274d5493d647595230574a3d65622a54aaf",
|
||||
"DEEP_BUCKET_HIST_STATS_WGSL": "b464d503703646e3c990e10a0cb40dacdc3677447b7a35e03c80a50bfca23824",
|
||||
"DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da",
|
||||
"DEEP_BUCKET_SCATTER_WGSL": "7575c4fc5f4d5674dfc3997189212ffc6fbe240a9d8b8b3f8ef883cd4eae47a1",
|
||||
"DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88",
|
||||
"DEEP_CORRECT_QUEUE_WGSL": "1337469b0673a4da20670daf220bec5883366e2454acc8c7923e472a5cec7a7f",
|
||||
"COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3",
|
||||
"AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342",
|
||||
"PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc",
|
||||
"BLA_GPU_SHADOW_WGSL": "e830339a150dd5722ca7c58674193174606adaf488bfd817408f2eeb4583514f",
|
||||
"BLA_GPU_CANARY_WGSL": "3c219f657b2890b40f7cfa0297e13a670819d4fed333fcb30cb37196c5755f55",
|
||||
"BLA_GPU_CANARY_APPLY_WGSL": "d02bb3ef007680a687960125ee9dded1fc4b8a4b091a6d924408a2e1f94a05e1",
|
||||
"BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019",
|
||||
"BLA_GPU_FAST_FRAME_WGSL": "2e4cdbd31d60c22d87fb04847af4d2c1d6de7d213da5cdd27234e5d8f9672553"
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
{
|
||||
"version": "7d56b1b4e3b54eff64b84e98607abb32eb23ae3ab60e1cd6f66f3223993d8c9f",
|
||||
"DIRECT_F32_WGSL": "7146fb3a0969a4f29b3e47d194e5263f8f7da017d84dcf98bddcb29d3c057922",
|
||||
"ACTIVE_PREPARE_WGSL": "27bb3af8310bd0784888f2a648896e1fcf5872b4a9a3ade883ebe95a4868fa6e",
|
||||
"DIRECT_ACTIVE_INIT_WGSL": "a57887e12e91ecdc0272c97e2097268f4b5f05cb467e8a77adfc5263331b2b9b",
|
||||
"DIRECT_ACTIVE_CONTINUE_WGSL": "b0fbfcb9ffccf37b96947ec21c883ad4ce984bbb331c7a3b59a54cdc3e300f93",
|
||||
"ACTIVE_COMPARE_WGSL": "81c37558fd8937fd0560ccb41f73ed66ac5d98d023153ed0b82d8b5a075c3de9",
|
||||
"DIRECT_ACCURATE_SEED_WGSL": "247e1ff1c3d400daf358144b55c6f50c99d337b4e886e0b957e371d39f9c87cf",
|
||||
"DIRECT_DS_CORRECT_WGSL": "e31fa2f8347acc5ea2eb11bbcb3b05a36a3ba72cc1446ded141b1b4207b873e3",
|
||||
"DIRECT_DS_QUEUE_BUILD_WGSL": "95cfcf9cb80e53bd112b0868c035564cb812bdd957de583e2d3ee052bceb1a15",
|
||||
"DIRECT_DS_CORRECT_QUEUE_WGSL": "bb76e1ae09c7494a53e2ae4cad46807f2c671c5afa5bf5649981e1ab60cc2f3e",
|
||||
"FAST_PERTURB_WGSL": "2cc5841740456f10ed3f64005e72ac6ff87d2c38926260a1fcb1521a389013a0",
|
||||
"FAST_PERTURB_POSTSTATS_WGSL": "3680d561d80af6327640d9b5caa127ad67f6a6fc9e499af6dbb98ccdaa261d0c",
|
||||
"DEEP_PERTURB_WGSL": "a631d814fd1c1aa3d4f8af31dcbabaebc5795355e9d72a223a000beb16f96a65",
|
||||
"DEEP_PERTURB_POSTSTATS_WGSL": "364d916da3d90b049ac24423a8112dd587ebd756fa97c99a6717f869c6b6cfa1",
|
||||
"DEEP_ACTIVE_INIT_WGSL": "756373375ad9b21a884ac6906ecf72990612a86ecfde77caf5150fb37cf75604",
|
||||
"DEEP_ACTIVE_RESUME_INIT_WGSL": "a25c0b74bc66b7b91269bc6195ddde8b295f659ea72ec415fcf3fda3d66c126e",
|
||||
"DEEP_ACTIVE_CONTINUE_WGSL": "afe49e748e34d78c01090015c0e2ce5971c0ec6ab7587bba6c25631266acb4f0",
|
||||
"PERTURB_CERTIFIED_INTERIOR_WGSL": "3bd61a7f5cf96e4e52797f5f00a20c848a01fd3ff8eca9a939e8ae87f37568a5",
|
||||
"DEEP_BUCKET_HIST_WGSL": "3458f908fa239b16667e5c82dae25274d5493d647595230574a3d65622a54aaf",
|
||||
"DEEP_BUCKET_HIST_STATS_WGSL": "b464d503703646e3c990e10a0cb40dacdc3677447b7a35e03c80a50bfca23824",
|
||||
"DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da",
|
||||
"DEEP_BUCKET_SCATTER_WGSL": "7575c4fc5f4d5674dfc3997189212ffc6fbe240a9d8b8b3f8ef883cd4eae47a1",
|
||||
"DEEP_CORRECT_WGSL": "112d09df6e7fa9b7aab9e43da2f38c9b0aad45e44296f40054d0e9a7c6077b88",
|
||||
"DEEP_CORRECT_QUEUE_WGSL": "1580c6bab7d1aa2c4eb2c7e8d6f3cc4d32b90b86b9264561269e1857d2826bae",
|
||||
"UNKNOWN_STATS_WGSL": "df1967bcdacf29831403fa53aadaae17ed138b36d1988cf20f6a6ed3e43e519a",
|
||||
"SYMMETRY_COPY_WGSL": "658e1cf2b9f40d8e759abe2269c041f582a80727cb24e2f48a405cb8676d70e1",
|
||||
"FAILURE_TILE_MAP_WGSL": "8308f57151b119363ad456a7e1255c5445ed5e022ccf53e923a4b13ab2a1ca91",
|
||||
"COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3",
|
||||
"AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342",
|
||||
"PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc",
|
||||
"BLA_GPU_SHADOW_WGSL": "e830339a150dd5722ca7c58674193174606adaf488bfd817408f2eeb4583514f",
|
||||
"BLA_GPU_CANARY_WGSL": "3c219f657b2890b40f7cfa0297e13a670819d4fed333fcb30cb37196c5755f55",
|
||||
"BLA_GPU_CANARY_APPLY_WGSL": "d02bb3ef007680a687960125ee9dded1fc4b8a4b091a6d924408a2e1f94a05e1",
|
||||
"BLA_GPU_FAST_CANDIDATE_WGSL": "2a1d2d6ee1fe2ecc55822e58131519f1385d82bcdbd7fc852dc2e39d67ff0019",
|
||||
"BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL": "f55368ed55cda77af550ed2bf38b4027b21e08b6c17d67997e61fcbea17a6c84",
|
||||
"BLA_GPU_FAST_FRAME_WGSL": "1d4f2c2105f0c50a3b5fb1368b262cfe6e0114e20333acdd1b6d8e8093434290",
|
||||
"BLA_GPU_FAST_FRAME_POSTSTATS_WGSL": "6e1d0ac58a1b785075eddb525f565ca8349f50bab2189a537f62bec6a987f6ea"
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"date": "2026-08-25",
|
||||
"scope": "BigInt reference-worker recurrence only; not a browser, GPU, canvas, or end-to-end benchmark",
|
||||
"runtime": "Node worker model",
|
||||
"corpus": {
|
||||
"points": 240,
|
||||
"iterations": 700,
|
||||
"precisionBits": "192..320",
|
||||
"runs": 7
|
||||
},
|
||||
"equivalence": {
|
||||
"checksumMismatch": 0,
|
||||
"separateStateRegressionCases": 2000,
|
||||
"separateStateRegressionMismatch": 0
|
||||
},
|
||||
"median": {
|
||||
"oldMs": 201.5509,
|
||||
"newMs": 115.3558,
|
||||
"speedup": 1.7472,
|
||||
"timeReduction": 0.4277
|
||||
},
|
||||
"runs": [
|
||||
{"oldMs": 222.9260, "newMs": 137.0568},
|
||||
{"oldMs": 206.2790, "newMs": 113.4711},
|
||||
{"oldMs": 218.0533, "newMs": 146.6120},
|
||||
{"oldMs": 201.5509, "newMs": 120.1586},
|
||||
{"oldMs": 163.7775, "newMs": 115.3558},
|
||||
{"oldMs": 166.6713, "newMs": 111.5755},
|
||||
{"oldMs": 172.1890, "newMs": 107.0833}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
[
|
||||
{
|
||||
"iter": 900,
|
||||
"baselineRows": 31,
|
||||
"v53Waits": 31,
|
||||
"v54FastWaits": 8,
|
||||
"v54BlaWaits": 2
|
||||
},
|
||||
{
|
||||
"iter": 1500,
|
||||
"baselineRows": 19,
|
||||
"v53Waits": 50,
|
||||
"v54FastWaits": 13,
|
||||
"v54BlaWaits": 4
|
||||
},
|
||||
{
|
||||
"iter": 3000,
|
||||
"baselineRows": 9,
|
||||
"v53Waits": 105,
|
||||
"v54FastWaits": 27,
|
||||
"v54BlaWaits": 7
|
||||
},
|
||||
{
|
||||
"iter": 6000,
|
||||
"baselineRows": 4,
|
||||
"v53Waits": 235,
|
||||
"v54FastWaits": 59,
|
||||
"v54BlaWaits": 15
|
||||
},
|
||||
{
|
||||
"iter": 12000,
|
||||
"baselineRows": 2,
|
||||
"v53Waits": 470,
|
||||
"v54FastWaits": 118,
|
||||
"v54BlaWaits": 30
|
||||
}
|
||||
]
|
||||
307
tests/browser_smoke.mjs
Normal file
307
tests/browser_smoke.mjs
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
// Connect to a separate Chromium/Edge instance with remote debugging enabled.
|
||||
// Usage: node tests/browser_smoke.mjs [port=9333] [reset|fast|mid|legacy|axis|black|ui|all]
|
||||
import assert from 'node:assert/strict';
|
||||
import {setTimeout as sleep} from 'node:timers/promises';
|
||||
|
||||
const port=Number(process.argv[2]||9333),selection=process.argv[3]||'all';
|
||||
const url=new URL('../index.html?test',import.meta.url).href;
|
||||
const response=await fetch(`http://127.0.0.1:${port}/json/list`);
|
||||
assert.ok(response.ok,'Remote debugging endpoint unavailable');
|
||||
const pages=(await response.json()).filter(t=>t.type==='page');
|
||||
assert.equal(pages.length,1,'Use one dedicated test tab');
|
||||
const target=pages[0],ws=new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve,reject)=>{ws.addEventListener('open',resolve,{once:true});ws.addEventListener('error',reject,{once:true})});
|
||||
let serial=0;const pending=new Map(),errors=[];
|
||||
ws.addEventListener('message',event=>{
|
||||
const m=JSON.parse(event.data);
|
||||
if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);
|
||||
const p=pending.get(m.id);if(!p)return;pending.delete(m.id);clearTimeout(p.timer);
|
||||
if(m.error)p.reject(new Error(JSON.stringify(m.error)));else p.resolve(m.result);
|
||||
});
|
||||
function command(method,params={}){
|
||||
const id=++serial;
|
||||
return new Promise((resolve,reject)=>{const timer=setTimeout(()=>{pending.delete(id);reject(new Error(method+' timeout'))},240000);pending.set(id,{resolve,reject,timer});ws.send(JSON.stringify({id,method,params}))});
|
||||
}
|
||||
async function evaluate(expression){
|
||||
const result=await command('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});
|
||||
if(result.exceptionDetails)throw new Error(result.exceptionDetails.exception?.description||result.exceptionDetails.text);
|
||||
return result.result.value;
|
||||
}
|
||||
const cases=[
|
||||
{id:'reset',re:'-0.5',im:'0',span:'3.4',renderMode:'fast',backend:'direct'},
|
||||
{id:'fast',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',renderMode:'fast',backend:'fast-extended'},
|
||||
{id:'mid',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000001',renderMode:'fast',backend:'fast-extended'},
|
||||
{id:'legacy',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',renderMode:'accurate',backend:'fast-extended'},
|
||||
{id:'axis',re:'-0.75',im:'0',span:'0.02',renderMode:'fast',backend:'direct'},
|
||||
{id:'black',re:'-29466147000382485924219538765424656674342940730553342389512209954887705938978413587',im:'5179018789925933872641942859702187326563010594882200888329237785122225251258352',span:'250637324516887125119843167990565159991251418774166207381437546036409',bits:273,baseIter:350,adaptive:true,fixed:true,renderMode:'accurate',backend:'fast-extended'}
|
||||
];
|
||||
assert.ok(['all','kernels','cancel','bench','budget','ui'].includes(selection)||cases.some(c=>c.id===selection),'Unknown case');
|
||||
|
||||
// Execute the real production kernels on small, fully specified states.
|
||||
// This isolates terminal and queue behavior from render convergence heuristics.
|
||||
async function kernelCases(){
|
||||
const {renderer:r}=globalThis.__MANDEL_TEST__.kernelAccess();
|
||||
await r.ensureDeepActivePipelines();
|
||||
const d=r.device,B=GPUBufferUsage,created=[],reports=[];
|
||||
const check=(value,message)=>{if(!value)throw new Error(message)};
|
||||
const buffer=(size,usage)=>{const b=d.createBuffer({size:Math.max(4,size),usage});created.push(b);return b};
|
||||
async function read(b,bytes){const staging=buffer(bytes,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(b,0,staging,0,bytes);d.queue.submit([e.finish()]);await staging.mapAsync(GPUMapMode.READ);const out=staging.getMappedRange().slice(0);staging.unmap();return out}
|
||||
async function run({count=1,target=3,chunk=1,passes=3,orbit=[0,0,0,0],error=0,m=0,n=0}){
|
||||
const capacity=Math.max(1,count),params=new ArrayBuffer(96),p=new DataView(params);
|
||||
[capacity,1,capacity,1,0,0,150000,orbit.length-1,0,0,capacity,0].forEach((v,i)=>p.setUint32(i*4,v,true));
|
||||
p.setInt32(52,0,true);p.setUint32(84,target,true);p.setUint32(88,chunk,true);
|
||||
const pb=buffer(96,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pb,0,params);
|
||||
const refs=buffer(orbit.length*16,B.STORAGE|B.COPY_DST),refData=new Float32Array(orbit.length*4);orbit.forEach((v,i)=>refData[i*4]=v);d.queue.writeBuffer(refs,0,refData);
|
||||
const states=buffer(capacity*32,B.STORAGE|B.COPY_DST|B.COPY_SRC),initial=new ArrayBuffer(capacity*32),sv=new DataView(initial);
|
||||
for(let i=0;i<count;i++){sv.setUint32(i*32+20,n,true);sv.setUint32(i*32+24,m,true);sv.setFloat32(i*32+28,error,true)}d.queue.writeBuffer(states,0,initial);
|
||||
const queues=[0,1].map(()=>buffer(capacity*4,B.STORAGE|B.COPY_DST|B.COPY_SRC)),counts=[0,1].map(()=>buffer(4,B.STORAGE|B.COPY_DST|B.COPY_SRC)),indirect=buffer(12,B.STORAGE|B.INDIRECT|B.COPY_DST);
|
||||
if(count)d.queue.writeBuffer(queues[0],0,Uint32Array.from({length:count},(_,i)=>i));d.queue.writeBuffer(counts[0],0,Uint32Array.of(count));
|
||||
const meta=buffer(capacity*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buffer(capacity*4,B.STORAGE|B.COPY_SRC);
|
||||
const e=d.createCommandEncoder();let input=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB:states,queues,counts,indirect,meta,smooth,input:0,passes,refsB:refs});d.queue.submit([e.finish()]);
|
||||
const active=new Uint32Array(await read(counts[input],4))[0],field=new Uint32Array(await read(meta,capacity*4));
|
||||
const live=active?Array.from(new Uint32Array(await read(queues[input],active*4))).sort((a,b)=>a-b):[];
|
||||
return{active,field,live,states:new DataView(await read(states,capacity*32)),async resume(nextTarget,nextPasses){p.setUint32(84,nextTarget,true);d.queue.writeBuffer(pb,0,params);const e=d.createCommandEncoder();input=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB:states,queues,counts,indirect,meta,smooth,input,passes:nextPasses,refsB:refs});d.queue.submit([e.finish()]);return{active:new Uint32Array(await read(counts[input],4))[0],field:new Uint32Array(await read(meta,capacity*4))}}};
|
||||
}
|
||||
try{
|
||||
let x=await run({orbit:[0,1,2,5]});check(x.active===0&&x.field[0]===(1<<28|3),'escape exactly at target');
|
||||
x=await run({target:1,n:1,m:1,passes:1,error:.002});check(x.active===0&&x.field[0]===(1<<20|1),'terminal error guard');
|
||||
x=await run({target:2,passes:2,orbit:[0]});check(x.active===0&&x.field[0]===(3<<20),'reference exhaustion');
|
||||
x=await run({target:1,passes:1,orbit:[0],m:1});check(x.active===0&&x.field[0]===(3<<20),'reference index guard');
|
||||
for(const count of [0,1,63,64,65])for(const passes of [1,2,3,4]){
|
||||
const target=passes;x=await run({count,target,passes,orbit:Array(10).fill(0)});
|
||||
check(x.active===count,'queue count');check(x.live.every((v,i)=>v===i),'queue membership');
|
||||
for(let i=0;i<count;i++){check(x.field[i]===(6<<20|target),'target metadata');check(x.states.getUint32(i*32+20,true)===target,'retained iteration')}
|
||||
const next=await x.resume(target+1,1);check(next.active===count,'target resubmission');
|
||||
for(let i=0;i<count;i++)check(next.field[i]===(6<<20|target+1),'next target metadata');
|
||||
}
|
||||
reports.push('target escape/error/reference bounds; 0/1/63/64/65 queues; 1–4 passes; retained states');
|
||||
await r.ensureCorrectionPipelines();await r.ensureCompactActivePipelines();
|
||||
{
|
||||
const width=17,height=5,n=width*height,initial=Uint32Array.from({length:n},(_,i)=>i%7===6?1<<28:(i%7)<<20|i);
|
||||
const p=new Uint32Array(24);p.set([9,3,width,height,3,1,100,0,0,2,width,width+3]);
|
||||
const pb=buffer(96,B.UNIFORM|B.COPY_DST),meta=buffer(n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),buckets=buffer(96,B.STORAGE|B.COPY_DST),stats=buffer(32,B.STORAGE|B.COPY_SRC|B.COPY_DST),queue=buffer(n*4,B.STORAGE|B.COPY_SRC),indirect=buffer(16,B.STORAGE|B.INDIRECT);
|
||||
d.queue.writeBuffer(pb,0,p);d.queue.writeBuffer(meta,0,initial);const e=d.createCommandEncoder();
|
||||
r.encodeDeepBucketHistogram(e,{pbuf:pb,meta,bucketState:buckets,w:9,h:3});r.encodeDeepBucketPrefix(e,{bucketState:buckets,queueStats:stats,indirect});r.encodeDeepBucketScatter(e,{pbuf:pb,meta,bucketState:buckets,queueStats:stats,queue,w:9,h:3});d.queue.submit([e.finish()]);
|
||||
const q=new Uint32Array(await read(stats,32)),expected=[];
|
||||
for(let y=1;y<4;y++)for(let x=3;x<12;x++){const i=y*width+x;if(i%7>=1&&i%7<=5)expected.push(i)}
|
||||
check(q[0]===expected.length&&q[1]===0&&q[2]===expected.length,'tile queue counts');
|
||||
const actual=Array.from(new Uint32Array(await read(queue,q[0]*4))).sort((a,b)=>a-b);check(JSON.stringify(actual)===JSON.stringify(expected),'tile queue exact membership');
|
||||
if(!r.precisionScatter)r.precisionScatter=await r.makeCompute(await r.module('precision-scatter-test',globalThis.MANDEL_WEBGPU_KERNELS.PRECISION_SCATTER_WGSL));
|
||||
const results=new Uint32Array(n*4);for(let i=0;i<n;i++)results.set([i,1<<28|7,0,i%2],i*4);
|
||||
const input=buffer(results.byteLength,B.STORAGE|B.COPY_DST),sm=buffer(n*4,B.STORAGE);d.queue.writeBuffer(input,0,results);
|
||||
const enc=d.createCommandEncoder(),bg=d.createBindGroup({layout:r.precisionScatter.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:input}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:sm}}]}),pass=enc.beginComputePass();pass.setPipeline(r.precisionScatter);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(2);pass.end();d.queue.submit([enc.finish()]);
|
||||
const applied=new Uint32Array(await read(meta,n*4));for(let i=0;i<n;i++)check(applied[i]===(i%2&&i%7>=1&&i%7<=5?(1<<28|7):initial[i]),'scatter preserves completed/operation-limit pixels');
|
||||
}
|
||||
{
|
||||
const pb=buffer(96,B.UNIFORM|B.COPY_DST),p=new Uint32Array(24);p.set([9,1,9,1,0,0,150000,4,0,3,9,0]);p[21]=3;p[22]=1;d.queue.writeBuffer(pb,0,p);
|
||||
const stateB=buffer(3*40,B.STORAGE|B.COPY_SRC),queues=[0,1].map(()=>buffer(12,B.STORAGE|B.COPY_SRC|B.COPY_DST)),counts=[0,1].map(()=>buffer(4,B.STORAGE|B.COPY_SRC|B.COPY_DST)),indirect=buffer(12,B.STORAGE|B.INDIRECT),refsB=buffer(5*16,B.STORAGE),meta=buffer(9*4,B.STORAGE|B.COPY_SRC),sm=buffer(9*4,B.STORAGE);
|
||||
d.queue.writeBuffer(queues[0],0,Uint32Array.of(8,2,5));d.queue.writeBuffer(counts[0],0,Uint32Array.of(3));
|
||||
const e=d.createCommandEncoder();r.encodeDeepActiveResumeInit(e,{pbuf:pb,stateB,queue:queues[0],count:3,pipeline:r.deepCompactInit});
|
||||
const output=r.encodeDeepActiveChunks(e,{pbuf:pb,stateB,queues,counts,indirect,meta,smooth:sm,input:0,passes:3,refsB,pipeline:r.deepCompactContinue});d.queue.submit([e.finish()]);
|
||||
const field=new Uint32Array(await read(meta,36)),states=new DataView(await read(stateB,120));
|
||||
for(let i=0;i<9;i++)check(field[i]===([8,2,5].includes(i)?6<<20|3:0),'compact pixel mapping');
|
||||
for(let i=0;i<3;i++){check(states.getUint32(i*40+32,true)===[8,2,5][i],'compact slot identity');check(states.getUint32(i*40+20,true)===3,'compact state retained')}
|
||||
check(new Uint32Array(await read(counts[output],4))[0]===3,'compact queue retained');
|
||||
}
|
||||
reports.push('tile failure queue exact membership; scatter ownership; compact slot identity');
|
||||
{
|
||||
const width=17,height=5,n=width*height,whole=buffer(n*4,B.STORAGE|B.COPY_SRC),pieces=buffer(n*4,B.STORAGE|B.COPY_SRC),sm=buffer(n*4,B.STORAGE);
|
||||
const unit=1n<<256n,snap={bits:256,re:-unit/2n,im:0n,span:34n*unit/10n},e=d.createCommandEncoder();
|
||||
const encode=(meta,x,y,w,h)=>{const pb=buffer(96,B.UNIFORM|B.COPY_DST);d.queue.writeBuffer(pb,0,r.directParams(w,h,width,height,x,y,100,snap,.5,.5,0,width,y*width+x));r.encodeDirectNumeric(e,{pbuf:pb,meta,smooth:sm,w,h})};
|
||||
encode(whole,0,0,width,height);
|
||||
for(let y=0;y<height;y+=2)for(let x=0;x<width;x+=6)encode(pieces,x,y,Math.min(6,width-x),Math.min(2,height-y));
|
||||
d.queue.submit([e.finish()]);const expected=new Uint32Array(await read(whole,n*4)),actual=new Uint32Array(await read(pieces,n*4));
|
||||
check(expected.every((v,i)=>v!==0&&v===actual[i]),'direct tiles reproduce whole-frame coordinates');
|
||||
}
|
||||
reports.push('direct tile coordinates and full pixel coverage');
|
||||
return reports;
|
||||
}finally{await d.queue.onSubmittedWorkDone();for(const b of created)b.destroy()}
|
||||
}
|
||||
// Read the actual texture selected by presentation, including animation during
|
||||
// numeric work. UI text alone cannot prove that colors are changing.
|
||||
async function displayedColorHash(){
|
||||
const r=globalThis.__MANDEL_TEST__.kernelAccess().renderer,f=r.frame,d=r.device;
|
||||
const texture=r.colorSourceVisible?r.colorSource.texture:f.front;
|
||||
const pitch=Math.ceil(f.w*4/256)*256,b=d.createBuffer({size:pitch*f.h,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ});
|
||||
try{
|
||||
const e=d.createCommandEncoder();e.copyTextureToBuffer({texture},{buffer:b,bytesPerRow:pitch},[f.w,f.h]);d.queue.submit([e.finish()]);
|
||||
await b.mapAsync(GPUMapMode.READ);const data=new Uint8Array(b.getMappedRange()),pixels=new Uint8Array(f.n*4);
|
||||
for(let y=0;y<f.h;y++)pixels.set(data.subarray(y*pitch,y*pitch+f.w*4),y*f.w*4);
|
||||
const digest=await crypto.subtle.digest('SHA-256',pixels);b.unmap();return Array.from(new Uint8Array(digest),x=>x.toString(16).padStart(2,'0')).join('');
|
||||
}finally{b.destroy()}
|
||||
}
|
||||
try{
|
||||
await command('Runtime.enable');
|
||||
await command('Emulation.setDeviceMetricsOverride',{width:320,height:240,deviceScaleFactor:1,mobile:false});
|
||||
await command('Page.navigate',{url});
|
||||
for(let i=0;i<100;i++){if(await evaluate('!!globalThis.__MANDEL_TEST__'))break;await sleep(100)}
|
||||
const ready=await evaluate('globalThis.__MANDEL_TEST__.ensureGpuReady()');
|
||||
assert.equal(ready.ready,true,JSON.stringify(ready));
|
||||
if(selection==='ui'||selection==='all'){
|
||||
const initial=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
||||
assert.equal(initial.renderClock.status,'complete');
|
||||
assert.ok(initial.lastRender>=initial.previewMs+initial.refineMs);
|
||||
if(initial.previewMs)assert.ok(initial.lastRender>=initial.previewMs+initial.refineMs+100,'include refinement delay');
|
||||
assert.equal(await evaluate('!!document.querySelector("#renderMode")'),false);
|
||||
const original=await evaluate('__MANDEL_TEST__.fieldHashes()');
|
||||
const originalColor=await evaluate(`(${displayedColorHash.toString()})()`);
|
||||
await evaluate('document.querySelector("#colorAuto").click()');
|
||||
await sleep(350);
|
||||
assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),originalColor,'animation changes GPU pixels');
|
||||
assert.deepEqual(await evaluate('__MANDEL_TEST__.fieldHashes()'),original,'animation preserves numeric data');
|
||||
const idle=await evaluate('__MANDEL_TEST__.state()');
|
||||
assert.equal(idle.cycle,initial.cycle);assert.equal(idle.lastRender,initial.lastRender);
|
||||
await evaluate('document.querySelector("#colorAuto").click()');
|
||||
await sleep(150);
|
||||
const stopped=await evaluate('__MANDEL_TEST__.state().shift');await sleep(150);
|
||||
assert.equal(await evaluate('__MANDEL_TEST__.state().shift'),stopped);
|
||||
assert.equal(await evaluate('!!__MANDEL_TEST__.kernelAccess().renderer.colorSource'),false,'release animation memory');
|
||||
await evaluate('document.querySelector("#colorAuto").click()');
|
||||
const ongoing=evaluate(`__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false})})`);
|
||||
let active=false;
|
||||
for(let i=0;i<1000;i++){
|
||||
const state=await evaluate('__MANDEL_TEST__.state()');
|
||||
if(state.rendering&&state.provisional&&state.engine.includes('数値補修')){active=true;break}
|
||||
await sleep(20);
|
||||
}
|
||||
assert.ok(active,'animate during expensive repair');
|
||||
const before=await evaluate(`(${displayedColorHash.toString()})()`);
|
||||
await sleep(350);
|
||||
assert.equal(await evaluate('__MANDEL_TEST__.state().rendering'),true);
|
||||
assert.equal(await evaluate('!!__MANDEL_TEST__.kernelAccess().renderer.colorSourceVisible'),true);
|
||||
assert.notEqual(await evaluate(`(${displayedColorHash.toString()})()`),before,'color changes while numeric work is running');
|
||||
assert.match(await evaluate('document.querySelector("#render").textContent'),/^描画中… /);
|
||||
// A manual change made while rendering must be drained at completion.
|
||||
await evaluate('document.querySelector("#colorAuto").click();const shift=document.querySelector("#shift");shift.value="0.65";shift.dispatchEvent(new Event("input",{bubbles:true}))');
|
||||
await ongoing;const final=await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
||||
for(let i=0;i<100;i++){if(await evaluate('!__MANDEL_TEST__.state().recolorPending&&!__MANDEL_TEST__.state().recoloring'))break;await sleep(20)}
|
||||
assert.equal(final.renderClock.status,'complete');assert.ok(final.lastRender>2000);
|
||||
assert.ok(final.lastRender>=final.refineMs);
|
||||
const timing=await evaluate('({label:document.querySelector("#render").textContent,now:performance.now(),state:__MANDEL_TEST__.state(),front:__MANDEL_TEST__.kernelAccess().renderer.frontColorKey})');
|
||||
assert.equal(timing.label,(final.lastRender/1000).toFixed(2)+' s');
|
||||
assert.ok(timing.now-final.renderClock.startedAt-final.lastRender<1000,'time is current without starting another render');
|
||||
assert.equal(Number(timing.front.split(':')[2]),0.65);assert.equal(timing.state.recolorPending,false);
|
||||
const hash=await evaluate('__MANDEL_TEST__.fieldHashes()');
|
||||
const view={...cases[1],bits:448,baseIter:512,adaptive:false};
|
||||
await evaluate(`__MANDEL_TEST__.setView(${JSON.stringify(view)})`);await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
||||
assert.deepEqual(await evaluate('__MANDEL_TEST__.fieldHashes()'),hash,'animated and ordinary renders agree');
|
||||
// Old preferences and shared URLs migrate to the sole supported renderer.
|
||||
const legacyUrl=await evaluate('localStorage.setItem("mandelbrot.renderMode","accurate");const u=new URL(location.href),p=new URLSearchParams(u.hash.slice(1));p.set("rm","accurate");u.hash=p.toString();u.href');
|
||||
await command('Page.navigate',{url:legacyUrl});await command('Page.reload');await sleep(150);
|
||||
await evaluate('__MANDEL_TEST__.waitForNumericComplete()');
|
||||
assert.equal(await evaluate('__MANDEL_TEST__.state().renderMode'),'fast');
|
||||
assert.equal(await evaluate('localStorage.getItem("mandelbrot.renderMode")'),null);
|
||||
console.log(JSON.stringify({ui:'PASS: animation idle/during repair, stop/manual color, numeric equality, total time, legacy migration',renderMs:final.lastRender}));
|
||||
}
|
||||
if(selection==='kernels'||selection==='all'){
|
||||
await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()');
|
||||
console.log(JSON.stringify({kernels:await evaluate(`(${kernelCases.toString()})()`)}));
|
||||
}
|
||||
for(const scenario of cases.filter(c=>selection==='all'||c.id===selection)){
|
||||
const small=['axis','black'].includes(scenario.id);
|
||||
await command('Emulation.setDeviceMetricsOverride',{width:small?64:320,height:small?48:240,deviceScaleFactor:1,mobile:false});
|
||||
const view={bits:448,baseIter:512,adaptive:false,processMode:'standard',...scenario};
|
||||
const result=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__;await t.setView(${JSON.stringify(view)});const state=await t.waitForNumericComplete({timeout:180000});return {state,hashes:await t.fieldHashes(),diagnostics:t.gpuDiagnostics()}})()`);
|
||||
assert.equal(result.state.numericalFailures,0);
|
||||
assert.equal(result.state.frontierConverged,true);
|
||||
assert.equal(result.state.historyReady,true);
|
||||
assert.ok(result.state.backend.endsWith(scenario.backend),result.state.backend);
|
||||
assert.ok(result.state.frontierMaxDispatchWork<=4000000);
|
||||
assert.equal(result.diagnostics.lossReason,'');
|
||||
assert.deepEqual(result.diagnostics.uncapturedErrors,[]);
|
||||
assert.equal(result.diagnostics.compilation.flatMap(c=>c.messages).filter(m=>m.type==='error').length,0);
|
||||
console.log(JSON.stringify({case:scenario.id,backend:result.state.backend,iter:result.state.effectiveIter,ms:result.state.lastRender,hashes:result.hashes}));
|
||||
if(scenario.id!=='reset'){
|
||||
const n=result.hashes.pixels,indices=Array.from(new Set([0,n-1,Math.floor(n/2),...Array.from({length:9},(_,i)=>(i*7919+127)%n)]));
|
||||
const samples=await evaluate(`globalThis.__MANDEL_TEST__.independentSamples(${JSON.stringify(indices)})`);
|
||||
let maxSmoothError=0;
|
||||
for(const p of samples){
|
||||
assert.equal(p.accepted,1,'independent sample inconclusive');
|
||||
assert.equal((p.packed>>>28)===1,(p.oracle>>>28)===1,JSON.stringify(p));
|
||||
if((p.oracle>>>28)===1){
|
||||
assert.equal(p.packed&0xfffff,p.oracle&0xfffff,JSON.stringify(p));
|
||||
// WGSL reconstructs magnitude and evaluates log2 in f32. Compare
|
||||
// smoothing in representable units, separately from exact escape n.
|
||||
const ulp=2**(Math.floor(Math.log2(Math.max(1,Math.abs(p.oracleSmooth))))-23);
|
||||
const error=Math.abs(p.smoothed-p.oracleSmooth);maxSmoothError=Math.max(maxSmoothError,error);
|
||||
// Keep ULP checks for the established FAST/axis samples. The new
|
||||
// cap case and former Deep-only black case use approximate smoothing
|
||||
// in the unified renderer; classification and escape n remain exact
|
||||
// checks, and their smoothing deviations are explicitly reported.
|
||||
if(!['mid','black'].includes(scenario.id))assert.ok(error<=8*ulp,JSON.stringify(p));
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify({case:scenario.id,independentSamples:samples.length,maxSmoothError}));
|
||||
if(scenario.id==='mid'){
|
||||
const cap=await evaluate('(async()=>{const r=__MANDEL_TEST__.kernelAccess().renderer,field=await r.readFieldAll();let below=0,remaining=0;for(const p of field.meta){if((p>>>28)===0){remaining++;if((p>>>20)!==6||(p&0xfffff)!==150000)below++}}return {below,remaining,png:await __MANDEL_TEST__.pngRoundTrip()}})()');
|
||||
assert.equal(result.state.effectiveIter,150000);assert.equal(cap.below,0);assert.ok(cap.remaining>0);assert.equal(cap.png.mismatches,0);
|
||||
}
|
||||
}
|
||||
if(scenario.id==='reset'){
|
||||
// Returning to the same view must reproduce both numerical buffers.
|
||||
await evaluate('(async()=>{const t=globalThis.__MANDEL_TEST__;await t.panPixels(8,0);await t.waitForNumericComplete();await t.panPixels(-8,0);await t.waitForNumericComplete()})()');
|
||||
assert.deepEqual(await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()'),result.hashes);
|
||||
const exports=await evaluate('(async()=>{const t=globalThis.__MANDEL_TEST__;return [await t.smokeExportTile({w:32,h:24}),await t.smokeExportTile({w:32,h:24,ss:2})]})()');
|
||||
for(const item of exports){assert.equal(item.length,item.expected);assert.ok(item.checksum)}
|
||||
const png=await evaluate('globalThis.__MANDEL_TEST__.pngRoundTrip()');
|
||||
assert.equal(png.mismatches,0);assert.equal(png.width,320);assert.equal(png.height,240);
|
||||
console.log(JSON.stringify({png}));
|
||||
}
|
||||
}
|
||||
if(selection==='all'||selection==='budget'){
|
||||
await command('Emulation.setDeviceMetricsOverride',{width:64,height:48,deviceScaleFactor:1,mobile:false});
|
||||
for(const baseIter of [512,4096]){
|
||||
const view={...cases[3],bits:448,baseIter,adaptive:false,continuationBudget:4096};
|
||||
const result=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__;await t.setView(${JSON.stringify(view)});const state=await t.waitForNumericComplete();const r=t.kernelAccess().renderer,field=await r.readFieldAll();let below=0,above=0;for(const p of field.meta){if((p>>>28)===0&&(p>>>20)===6&&(p&0xfffff)!==4096)below++;if((p&0xfffff)>4096)above++}return {state,below,above,png:await t.pngRoundTrip()}})()`);
|
||||
assert.equal(result.state.effectiveIter,4096);assert.equal(result.state.continuationBudget,4096);assert.equal(result.below,0);assert.equal(result.above,0);assert.equal(result.png.mismatches,0);assert.equal(result.png.iter,4096);
|
||||
}
|
||||
console.log('PASS: explicit finite budget, all retained pixels reach target, PNG uses that budget');
|
||||
}
|
||||
if(selection==='all'||selection==='cancel'){
|
||||
await command('Emulation.setDeviceMetricsOverride',{width:320,height:240,deviceScaleFactor:1,mobile:false});
|
||||
const normal={...cases[0],bits:448,baseIter:512,adaptive:false};
|
||||
await evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify(normal)})`);
|
||||
const expected=await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()');
|
||||
const ongoing=evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify({...cases[1],bits:448,baseIter:512,adaptive:false})})`).then(value=>({value}),error=>({error:String(error)}));
|
||||
let during=false;
|
||||
for(let i=0;i<1000;i++){
|
||||
const s=await evaluate('globalThis.__MANDEL_TEST__.state()');
|
||||
if(s.provisional&&(s.engine.includes('数値補修')||s.gpuStage==='precision scatter')){during=true;break}
|
||||
await sleep(20);
|
||||
}
|
||||
assert.ok(during,'exercise cancellation during CPU repair, not after completion');
|
||||
const provisionalPng=await evaluate('globalThis.__MANDEL_TEST__.pngRoundTrip().then(()=>false,()=>true)');
|
||||
assert.equal(provisionalPng,true,'unfinished field must not enter complete PNG path');
|
||||
await evaluate(`globalThis.__MANDEL_TEST__.setView(${JSON.stringify(normal)})`);
|
||||
await ongoing;
|
||||
await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()');
|
||||
assert.deepEqual(await evaluate('globalThis.__MANDEL_TEST__.fieldHashes()'),expected);
|
||||
assert.deepEqual((await evaluate('globalThis.__MANDEL_TEST__.gpuDiagnostics()')).uncapturedErrors,[]);
|
||||
console.log('PASS: cancel during CPU repair, preserve newest frame, reject provisional PNG');
|
||||
}
|
||||
assert.deepEqual(errors,[]);
|
||||
if(selection==='bench'){
|
||||
await evaluate('globalThis.__MANDEL_TEST__.waitForNumericComplete()');
|
||||
for(const scenario of cases.filter(c=>['reset','fast'].includes(c.id))){
|
||||
let expected;
|
||||
for(let run=0;run<4;run++){
|
||||
const view={bits:448,baseIter:512,adaptive:false,processMode:'standard',...scenario};
|
||||
const record=await evaluate(`(async()=>{const t=globalThis.__MANDEL_TEST__,before=t.efficiency();await t.setView(${JSON.stringify(view)});const s=await t.waitForNumericComplete(),after=t.efficiency(),metrics={};for(const k of Object.keys(after))metrics[k]=k==='activeGpuBytes'?after[k]:after[k]-before[k];return {ms:s.lastRender,iter:s.effectiveIter,numericalFailures:s.numericalFailures,operationLimit:s.unknownReasons.operationLimit,firstDisplayMs:s.generationMetrics.at(-1)?.firstDisplayMs,metrics,hashes:await t.fieldHashes()}})()`);
|
||||
if(expected)assert.deepEqual(record.hashes,expected,'repeated view changed numeric output');expected=record.hashes;
|
||||
console.log(JSON.stringify({benchmark:scenario.id,run,warmup:run===0,...record}));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ordinary visits should neither expose diagnostics nor invoke legacy gates.
|
||||
await command('Page.navigate',{url:new URL('../index.html',import.meta.url).href});
|
||||
for(let i=0;i<100;i++){if(await evaluate("!!globalThis.MANDEL_WEBGPU_KERNELS && !!document.querySelector('#engine')"))break;await sleep(100)}
|
||||
assert.equal(await evaluate("typeof globalThis.__MANDEL_TEST__"),'undefined');
|
||||
console.log(`PASS: ${selection}; normal startup exposes no diagnostics`);
|
||||
}catch(error){
|
||||
try{console.error(JSON.stringify(await evaluate('({state:globalThis.__MANDEL_TEST__?.state(),gpu:globalThis.__MANDEL_TEST__?.gpuDiagnostics()})')))}catch{}
|
||||
throw error;
|
||||
}finally{
|
||||
try{await command('Page.navigate',{url:'about:blank'})}catch{}
|
||||
for(const p of pending.values())clearTimeout(p.timer);
|
||||
ws.close();
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import process from 'node:process';
|
||||
|
||||
const port=Number(process.argv[2]||9333);
|
||||
const url=process.argv[3]||'file:///C:/Users/%E3%81%82/Desktop/mandelbrot/index.html?frontier-gate=reset';
|
||||
const timeoutMs=Number(process.argv[4]||300000);
|
||||
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
|
||||
|
||||
async function endpoint(path,options){
|
||||
const response=await fetch(`http://127.0.0.1:${port}${path}`,options);
|
||||
if(!response.ok)throw new Error(`CDP HTTP ${response.status}: ${await response.text()}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
const deadline=Date.now()+timeoutMs;
|
||||
let version;
|
||||
while(Date.now()<deadline){
|
||||
try{version=await endpoint('/json/version');break}catch{await sleep(100)}
|
||||
}
|
||||
if(!version)throw new Error('Edge remote debugging endpoint did not start');
|
||||
|
||||
let target;
|
||||
try{target=await endpoint(`/json/new?${encodeURIComponent(url)}`,{method:'PUT'})}
|
||||
catch{const targets=await endpoint('/json/list');target=targets.find(item=>item.type==='page')}
|
||||
if(!target?.webSocketDebuggerUrl)throw new Error('No debuggable page target');
|
||||
|
||||
const ws=new WebSocket(target.webSocketDebuggerUrl);
|
||||
await new Promise((resolve,reject)=>{ws.addEventListener('open',resolve,{once:true});ws.addEventListener('error',reject,{once:true})});
|
||||
let sequence=0;
|
||||
const pending=new Map();
|
||||
ws.addEventListener('message',event=>{
|
||||
const message=JSON.parse(event.data);
|
||||
if(!message.id)return;
|
||||
const waiter=pending.get(message.id);if(!waiter)return;
|
||||
pending.delete(message.id);
|
||||
if(message.error)waiter.reject(new Error(JSON.stringify(message.error)));else waiter.resolve(message.result);
|
||||
});
|
||||
function command(method,params={}){
|
||||
const id=++sequence;
|
||||
return new Promise((resolve,reject)=>{pending.set(id,{resolve,reject});ws.send(JSON.stringify({id,method,params}))});
|
||||
}
|
||||
|
||||
await command('Runtime.enable');
|
||||
await command('Page.enable');
|
||||
await command('Page.navigate',{url});
|
||||
let result;
|
||||
while(Date.now()<deadline){
|
||||
const evaluated=await command('Runtime.evaluate',{expression:`JSON.stringify({gate:document.documentElement.getAttribute('data-frontier-gate'),body:document.body?.textContent||'',href:location.href})`,returnByValue:true});
|
||||
const value=JSON.parse(evaluated.result.value);
|
||||
if(value.gate){result=value;break}
|
||||
await sleep(250);
|
||||
}
|
||||
try{await command('Browser.close')}catch{}
|
||||
ws.close();
|
||||
if(!result)throw new Error('Frontier gate timed out');
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
26
tests/kernel_hashes.json
Normal file
26
tests/kernel_hashes.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"version": "4a38cadd59b0d30a42b1230a7fe646bb5972c9d49c5b66b3bb908458e3603fa6",
|
||||
"DEEP_COMPACT_INIT_WGSL": "29ee3883412dba774798bbeab8ba9c8744e7e7d3035d7eef1d81f78f9f959e79",
|
||||
"DEEP_COMPACT_CONTINUE_WGSL": "248311ff0acf69b3e7db057ccac08b3aded486cec3f718c0a2afce7de4e28419",
|
||||
"INTERIOR_MASK_WGSL": "00e8a502c23fe3468abab23365bc13cae3e35ecf401dcfe6e9a2f92f810d1eeb",
|
||||
"PRECISION_SCATTER_WGSL": "428ff66314ed0d69bc3bef83d5d0adc554bd89eb4a7137a4a8910f2d1016b03a",
|
||||
"DIRECT_F32_WGSL": "1d5ea335eb8aae5adb9e6d9bcdd46c3054159a83b30e5cf87a4c35aef51edf60",
|
||||
"ACTIVE_PREPARE_WGSL": "27bb3af8310bd0784888f2a648896e1fcf5872b4a9a3ade883ebe95a4868fa6e",
|
||||
"FAST_PERTURB_WGSL": "9f249f71619b2b9e8eff2e34699e8c8075a30feb9169774453953c82597b5f85",
|
||||
"FAST_PERTURB_POSTSTATS_WGSL": "8031e332beeb7b4d5c06b785e86e62ae6e0629a35d307a7480d974dd16e79029",
|
||||
"DEEP_PERTURB_WGSL": "0f42cd0ed34d02a4c563ebec1ae8fe5f09b45386073aed109c0e6542077a29dd",
|
||||
"DEEP_PERTURB_POSTSTATS_WGSL": "ef3e0bc2b1205747569b6aa17db3deb847a66787b0ef68219aa25bd0061aa5bb",
|
||||
"DEEP_ACTIVE_RESUME_INIT_WGSL": "a25c0b74bc66b7b91269bc6195ddde8b295f659ea72ec415fcf3fda3d66c126e",
|
||||
"DEEP_ACTIVE_CONTINUE_WGSL": "c4afcb504ec0fdc8fe18c67d8dced6e45d99cba0ec2e4dfc015ac6eb36856edd",
|
||||
"DEEP_BUCKET_HIST_WGSL": "3458f908fa239b16667e5c82dae25274d5493d647595230574a3d65622a54aaf",
|
||||
"DEEP_BUCKET_PREFIX_WGSL": "e8fb3f566d69ef87a61f1aa61f4c56ec26614711d7897afa2f8bfdcf5088c3da",
|
||||
"DEEP_BUCKET_SCATTER_WGSL": "7575c4fc5f4d5674dfc3997189212ffc6fbe240a9d8b8b3f8ef883cd4eae47a1",
|
||||
"DEEP_CORRECT_WGSL": "1ee151d376f4c56c870bb7ba5811b3d015bb93a02c9e3e02e9be96d673311eda",
|
||||
"DEEP_CORRECT_QUEUE_WGSL": "3ae43622225872a255dd96bf35becaca6bd44f558852b71ec93d13c02b57732b",
|
||||
"UNKNOWN_STATS_WGSL": "df1967bcdacf29831403fa53aadaae17ed138b36d1988cf20f6a6ed3e43e519a",
|
||||
"SYMMETRY_COPY_WGSL": "658e1cf2b9f40d8e759abe2269c041f582a80727cb24e2f48a405cb8676d70e1",
|
||||
"FAILURE_TILE_MAP_WGSL": "8308f57151b119363ad456a7e1255c5445ed5e022ccf53e923a4b13ab2a1ca91",
|
||||
"COLOR_WGSL": "4ef141430a5255cc8b78459b1a50fdd2ff31cf37eeb872a957d43794c59b09a3",
|
||||
"AA_RESOLVE_WGSL": "f353adafc837c170da1627f8b0cfd924d9748954376e4a427190917da4c92342",
|
||||
"PRESENT_WGSL": "651bf13de25b023c3f0ab0d0287b1d27cfd8e11677aca53629ce0a8526c7c5bc"
|
||||
}
|
||||
|
|
@ -1,306 +0,0 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {correctnessCases,performanceCases,interactionStabilityViews,productionPerformanceThresholds,evaluateProductionPerformance,runCorrectnessCorpus,runPerformanceCorpus,runInteractionStabilityCorpus} from './optimization_webgpu_corpus.mjs';
|
||||
|
||||
const root=fileURLToPath(new URL('../',import.meta.url));
|
||||
const html=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
|
||||
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
|
||||
assert.equal(scripts.length,2);
|
||||
for(const code of scripts)new vm.Script(code);
|
||||
const precisionWorker=scripts[1].match(/function precisionFallbackWorkerSource\(\)\{return String\.raw`([\s\S]*?)`\}/);
|
||||
assert.ok(precisionWorker);
|
||||
new vm.Script(precisionWorker[1]);
|
||||
|
||||
const context={globalThis:null};context.globalThis=context;vm.createContext(context);
|
||||
new vm.Script(scripts[0]).runInContext(context);
|
||||
const kernels=context.MANDEL_WEBGPU_KERNELS;
|
||||
const expected=JSON.parse(await fs.readFile(new URL('./KERNEL_HASHES_V24_3_0.json',import.meta.url),'utf8'));
|
||||
const actual={};
|
||||
for(const [key,value] of Object.entries(kernels)){
|
||||
if(typeof value==='string')actual[key]=crypto.createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
assert.deepEqual(actual,expected);
|
||||
assert.equal(correctnessCases.length,12);
|
||||
assert.equal(performanceCases.length,6);
|
||||
assert.equal(interactionStabilityViews.length,3);
|
||||
assert.ok(correctnessCases.some(x=>x.id==='reported-black-regression'&&x.fixed&&x.frontierCheck));
|
||||
assert.ok(correctnessCases.some(x=>x.id==='moderate-1e5-fast'));
|
||||
assert.ok(correctnessCases.some(x=>x.id==='moderate-1e5-accurate'));
|
||||
assert.ok(correctnessCases.some(x=>x.id==='moderate-1e8-fast'));
|
||||
assert.ok(correctnessCases.some(x=>x.id==='moderate-1e8-accurate'));
|
||||
assert.ok(performanceCases.some(x=>x.id==='fast-1e14-800x600'&&x.absoluteP95Ms<2000));
|
||||
assert.equal(productionPerformanceThresholds.targetP50Reduction,.20);
|
||||
assert.equal(productionPerformanceThresholds.targetP95Reduction,.15);
|
||||
assert.equal(typeof runCorrectnessCorpus,'function');
|
||||
assert.equal(typeof runPerformanceCorpus,'function');
|
||||
assert.equal(typeof runInteractionStabilityCorpus,'function');
|
||||
assert.equal(evaluateProductionPerformance({records:[{id:'target',target:true,summary:{recovery:{p50:79,p95:84}}}]},{records:[{id:'target',summary:{recovery:{p50:100,p95:100}}}]}).pass,true);
|
||||
assert.equal(evaluateProductionPerformance({records:[{id:'target',target:true,summary:{recovery:{p50:90,p95:90}}}]},{records:[{id:'target',summary:{recovery:{p50:100,p95:100}}}]}).pass,false);
|
||||
const corpusSource=await fs.readFile(new URL('./optimization_webgpu_corpus.mjs',import.meta.url),'utf8');
|
||||
assert.match(corpusSource,/assert\.equal\(result\.numericComplete,true/);
|
||||
assert.match(corpusSource,/assert\.equal\(numeric,0,testCase\.id\+' final numeric UNKNOWN'/);
|
||||
assert.match(corpusSource,/assert\.equal\(result\.state\.historyReady,true/);
|
||||
assert.match(corpusSource,/assert\.equal\(compile\.productionEligible,expectProduction\)/);
|
||||
assert.match(corpusSource,/assert\.equal\(compile\.candidateEnabled,true\)/);
|
||||
assert.match(corpusSource,/await t\.waitForNumericComplete\(\{timeout\}\)/);
|
||||
|
||||
assert.match(kernels.DIRECT_ACCURATE_SEED_WGSL,/if\(n>=p\.lateThreshold\)\{fieldMeta\[out\]=pack_unknown/);
|
||||
assert.match(kernels.DIRECT_DS_CORRECT_WGSL,/var zr2=DS\(0\.0,0\.0\);var zi2=DS\(0\.0,0\.0\)/);
|
||||
assert.match(kernels.DIRECT_DS_QUEUE_BUILD_WGSL,/atomicAdd\(&queueStats\[2\],1u\)/);
|
||||
assert.match(kernels.DIRECT_DS_CORRECT_QUEUE_WGSL,/atomicAdd\(&queueStats\[6\],1u\)/);
|
||||
assert.match(kernels.DIRECT_DS_CORRECT_QUEUE_WGSL,/let zri=ds_mul\(zr,zi\)/);
|
||||
assert.match(html,/accurateQueuedPixels/);
|
||||
assert.match(html,/q\.selected===q\.enqueued&&q\.enqueued===q\.dispatchCount&&q\.dispatchCount===q\.processed/);
|
||||
assert.match(html,/accurateQueueFallbacks/);
|
||||
assert.match(html,/encodeDeepBucketHistogram\(e/);
|
||||
assert.match(html,/q\.selected===q\.enqueued&&q\.enqueued===q\.dispatchCount&&q\.dispatchCount===q\.processed/);
|
||||
assert.match(html,/reusePreview/);
|
||||
assert.match(html,/canUseStableReprojection\(\)\|\|fastNeedsExtended\(snapshot\(\),canvas\.width\)\)return\[finalIter\]/);
|
||||
assert.match(html,/numericInFlightBudgetMs/);
|
||||
assert.match(html,/const ADAPTIVE_ITER_QUANTUM=64/);
|
||||
assert.match(html,/function pixelPrecisionBits\(snap=snapshot\(\),w=canvas\.width,guard=32\)/);
|
||||
assert.match(html,/pixelLog2=log2FixedAt\(snap\.span,snap\.bits\)-Math\.log2\(Math\.max\(1,w\)\)/);
|
||||
assert.match(html,/targetBits=pixelPrecisionBits\(snap,width,40\)/);
|
||||
assert.match(html,/function fastReferenceBits\(snap,w=canvas\.width\)\{return pixelPrecisionBits\(snap,w,32\)\}/);
|
||||
assert.match(html,/const bits=baseBits\+attempt\*32,a=pixel\(s,indices\[i\],bits\),b=pixel\(s,indices\[i\],bits\+32\)/);
|
||||
assert.match(html,/for\(let attempt=0;attempt<4&&!accepted\[i\];attempt\+\+\)/);
|
||||
assert.match(html,/for\(let i=0;i<meta\.length;i\+\+\)/);
|
||||
assert.match(html,/for\(let start=0;start<indices\.length;start\+=chunkSize\)/);
|
||||
assert.doesNotMatch(html,/UNRESOLVED_BLACK_MIN_ZOOM_EXP|colorUnresolvedInsteadOfBlack|guardedFastDeep|fast-quality-floor-before-1e100/);
|
||||
assert.match(html,/Math\.ceil\(raw\/quantum\)\*quantum/);
|
||||
assert.match(html,/this\.blaFastDisabled=true;this\.blaFastOptIn=false/);
|
||||
assert.doesNotMatch(html,/deep-stable-bootstrap|forceStableDeep/);
|
||||
assert.match(html,/A FAST frame is not publishable while any numeric UNKNOWN remains/);
|
||||
assert.match(html,/if\(initialFailed>0\)\{const mr=await recoverNumericalUnknownTiles/);
|
||||
assert.match(html,/if\(numericalFailureCount\(stats\)>0\)\{const pf=await recoverResidualPrecision/);
|
||||
assert.match(html,/if\(isFinal\)\{const remaining=numericalFailureCount.*frameを公開しません/);
|
||||
assert.match(html,/viewRe:snap\.re,viewIm:snap\.im/);
|
||||
assert.match(html,/function referenceOriginMatchesView\(/);
|
||||
assert.match(html,/!referenceOriginMatchesView\(src,snap\)/);
|
||||
assert.match(html,/stabilityPolicy:/);
|
||||
assert.match(html,/fastBackgroundDeepOnFailureOnly:true,deepProductionActive:false,deepCorrectionDispatchWork:PIXEL_FRONTIER_WORK/);
|
||||
const fastStableStart=html.indexOf('}else if(isFinal&&fastExtended){');
|
||||
const fastStableSource=html.slice(fastStableStart,html.indexOf('}else if(isFinal){',fastStableStart));
|
||||
assert.doesNotMatch(fastStableSource,/scheduleFastNumericRecovery/);
|
||||
assert.match(fastStableSource,/recoverNumericalUnknownTiles/);
|
||||
assert.match(fastStableSource,/recoverResidualPrecision/);
|
||||
assert.match(html,/ensureAccuratePipelines/);
|
||||
assert.match(html,/ensureCorrectionPipelines/);
|
||||
assert.match(html,/precisionFallbackWorkerSource/);
|
||||
assert.match(html,/recoverResidualPrecision/);
|
||||
assert.match(html,/estimatedNetMs/);
|
||||
assert.match(kernels.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL,/fn baseline_from\(s0:S,pre:u32\)->Result/);
|
||||
assert.match(kernels.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL,/var anchor=s;loop/);
|
||||
assert.match(kernels.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL,/anchor=s;\}\}/);
|
||||
assert.doesNotMatch(kernels.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL,/baseline\(g,st\)/);
|
||||
assert.equal((kernels.BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL.match(/baseline_from\(anchor,st\)/g)||[]).length,7);
|
||||
assert.ok(kernels.BLA_GPU_FAST_FRAME_WGSL.includes('baseline_from(anchor,st)'));
|
||||
assert.match(kernels.ACTIVE_PREPARE_WGSL,/indirect\[0\]=\(n\+63u\)\/64u/);
|
||||
assert.match(kernels.DIRECT_ACTIVE_CONTINUE_WGSL,/struct DirectState\{zr:f32,zi:f32,n:u32/);
|
||||
assert.match(kernels.DIRECT_ACTIVE_CONTINUE_WGSL,/atomicLoad\(&countIn\[0\]\)/);
|
||||
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/struct DeepState\{d:vec2<f32>,w:vec2<f32>,scaleExp:i32,n:u32,m:u32,errScaled:f32\}/);
|
||||
assert.match(kernels.DEEP_ACTIVE_INIT_WGSL,/states\[local\]=DeepState/);
|
||||
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/let out=p\.outputBase\+\(local\/p\.tileW\)\*p\.outputStride/);
|
||||
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/if\(s\.m>p\.refLen\)/);
|
||||
assert.match(kernels.DEEP_ACTIVE_CONTINUE_WGSL,/if\(s\.m>=p\.refLen\)/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/struct I\{lo:f32,hi:f32\}/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/fn outward\(/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/lhs\.hi<rhs\.lo/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/bulb\.hi<0\.0625/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/if\(p\.refLen<1u\)\{proven\[i\]=0u;return;\}/);
|
||||
assert.match(kernels.PERTURB_CERTIFIED_INTERIOR_WGSL,/let ref=refs\[1u\]/);
|
||||
assert.match(html,/runDirectActiveShadow/);
|
||||
assert.match(html,/dispatchWorkgroupsIndirect\(indirect,0\)/);
|
||||
assert.match(html,/direct-active-shadow-resume/);
|
||||
assert.match(html,/resumedFromPreview:previewIter>0/);
|
||||
assert.match(html,/runDeepActiveShadow/);
|
||||
assert.match(html,/computeDeepActiveFrame/);
|
||||
assert.match(html,/workPerDispatch=PIXEL_FRONTIER_WORK,maxPassesPerSubmit=1/);
|
||||
assert.match(html,/const workBudget=deep\?4000000:48000000/);
|
||||
assert.match(html,/const PIXEL_FRONTIER_PRODUCTION=true,PIXEL_FRONTIER_WORK=4000000,PIXEL_FRONTIER_MAX_ITER=150000/);
|
||||
assert.match(html,/PIXEL_FRONTIER_CANDIDATE=.*frontier-gate/);
|
||||
assert.match(html,/PIXEL_FRONTIER_ENABLED=PIXEL_FRONTIER_PRODUCTION\|\|PIXEL_FRONTIER_CANDIDATE/);
|
||||
assert.match(html,/function operationLimitFrontier\(meta,w,h\)/);
|
||||
assert.match(html,/function pixelFrontierIterationFloor\(snap,w,initialIter\)/);
|
||||
assert.match(html,/iter>=minimumIter&&stableRounds>=2/);
|
||||
assert.match(html,/count\*iter/);
|
||||
assert.match(html,/pixel frontier queue invariant failed/);
|
||||
assert.match(html,/compareCurrentToFrontierBaseline/);
|
||||
assert.match(html,/productionEligible:PIXEL_FRONTIER_PRODUCTION/);
|
||||
assert.match(kernels.DEEP_CORRECT_QUEUE_WGSL,/bitcast<u32>\(p\._numeric2\)\+localQi/);
|
||||
assert.match(html,/const maxBatch=Math\.max\(1,Math\.floor\(PIXEL_FRONTIER_WORK\/Math\.max\(1,iter\)\)\);let offset=0/);
|
||||
assert.match(html,/this\.deepParams\(f\.w,f\.h,f\.w,f\.h,0,0,iter,snap,\.5,\.5,0,refX,refY,2,f\.w,0,offset\)/);
|
||||
assert.match(html,/maxPixels=Math\.max\(1,Math\.floor\(4000000\/Math\.max\(1,iter\)\)\),tileW=Math\.min\(f\.w,maxPixels\)/);
|
||||
assert.match(html,/correctUnknownFrame\(snap,iter,token,referencePixel,numericalFailureCount\(stats\)\)/);
|
||||
assert.match(html,/ensureDeepActiveWorkspace/);
|
||||
assert.doesNotMatch(html,/return this\.computeDeepActiveFrame\(snap,iter,token,forceStrict,referencePixel\)/);
|
||||
assert.match(html,/return this\.computePerturbFrameTiled\(snap,iter,true,token,forceStrict,referencePixel,false,colorStyle,colorKey\)/);
|
||||
assert.match(html,/state\.gpuInitFailed=true;markDirty\(false\);setTimeout/);
|
||||
assert.match(html,/deep-active-shadow-resume/);
|
||||
assert.match(html,/deep-active-compare-shadow/);
|
||||
assert.match(html,/runtime\.deepActiveShadowLatest=result/);
|
||||
assert.match(html,/runCertifiedInteriorShadow/);
|
||||
assert.match(html,/precisionFallback\.request\(snap,iter,w,h,gpu\.provenIndices,FIELD_INTERIOR_PROVEN\)/);
|
||||
assert.match(html,/falsePositive===0&&inconclusive===0/);
|
||||
assert.match(html,/oracleBits:\[oracleBits,oracleBits\+32\]/);
|
||||
assert.match(html,/productionEligible:false/);
|
||||
assert.match(html,/waitForPrimary\(/);
|
||||
assert.match(html,/waitForFinal\(/);
|
||||
assert.match(html,/waitForNumericComplete\(/);
|
||||
assert.match(html,/fieldHashes\(\)/);
|
||||
assert.match(html,/readFieldAll\(\)/);
|
||||
assert.match(html,/generationMetrics:\[\]/);
|
||||
assert.match(html,/primaryNumericalFailures/);
|
||||
assert.match(html,/numericQuiescentMs/);
|
||||
assert.match(html,/historyCommitMs/);
|
||||
assert.match(html,/SYMMETRY_COPY_WGSL/);
|
||||
assert.match(html,/canUseRealAxisSymmetry/);
|
||||
assert.match(html,/encodeSymmetryCopy/);
|
||||
assert.match(html,/FAILURE_TILE_MAP_WGSL/);
|
||||
assert.match(html,/readNumericalFailureTiles/);
|
||||
assert.match(html,/tileReferencePool=Array\.from\(\{length:3\}/);
|
||||
assert.match(html,/Promise\.all\(batch\.map/);
|
||||
assert.match(html,/if\(p\.key===key\)return p\.promise/);
|
||||
assert.match(html,/prefetchFinalReference/);
|
||||
assert.match(html,/this\.exportWs=\[\]/);
|
||||
assert.match(html,/exportRingDepth\(tile=this\.exportTileSize\(\)\)/);
|
||||
assert.match(html,/Promise\.allSettled\(batch\.map\(item=>item\.promise\)\)/);
|
||||
assert.match(html,/if\(failed\)throw failed\.reason/);
|
||||
assert.match(html,/performance\.now\(\)-lastYield<24/);
|
||||
assert.doesNotMatch(html,/await png\.rows\(band\);await new Promise\(requestAnimationFrame\)/);
|
||||
assert.match(html,/frameReadback\(kind,size\)/);
|
||||
assert.match(html,/length:3.*kind\+'-readback-'/);
|
||||
const statsReadbackSource=html.slice(html.indexOf('async readUnresolvedStats'),html.indexOf('async readUnresolved(){'));
|
||||
assert.match(statsReadbackSource,/this\.frameReadback\('unknown-stats'/);
|
||||
assert.doesNotMatch(statsReadbackSource,/destroy\(r\)/);
|
||||
assert.match(html,/cachedBindGroup\(pipeline,entries\)/);
|
||||
assert.match(html,/cachedTextureView\(texture\)/);
|
||||
assert.match(html,/this\.cachedBindGroup\(this\.direct/);
|
||||
assert.match(html,/this\.cachedBindGroup\(this\.present/);
|
||||
assert.match(html,/this\.clearBindGroupCache\(\)/);
|
||||
const historyCommitSource=html.slice(html.indexOf('commitHistory(view='),html.indexOf('invalidateHistory(){'));
|
||||
assert.match(historyCommitSource,/f\.history=f\.front/);
|
||||
assert.doesNotMatch(historyCommitSource,/copyTextureToTexture/);
|
||||
assert.match(html,/colorTarget\(f=this\.frame\)/);
|
||||
{
|
||||
const roles={front:'a',back:'b',history:'c',spare:'c'};
|
||||
const commit=()=>{roles.history=roles.front};
|
||||
const render=()=>{if(roles.back===roles.history){const old=roles.history;roles.back=roles.spare;roles.spare=old}assert.notEqual(roles.back,roles.history);assert.notEqual(roles.back,roles.front);[roles.front,roles.back]=[roles.back,roles.front]};
|
||||
for(let i=0;i<1000;i++){if(i%7===0||i%11===0)commit();else render();assert.equal(new Set([roles.front,roles.back,roles.history,roles.spare]).size,3)}
|
||||
}
|
||||
const recoverySource=html.slice(html.indexOf('async function recoverNumericalUnknownTiles'),html.indexOf('async function recoverResidualPrecision'));
|
||||
assert.match(recoverySource,/readNumericalFailureTiles/);
|
||||
assert.match(recoverySource,/const currentMeta=await r\.readMetaAll\(\)/);
|
||||
assert.match(recoverySource,/Uint32Array\.from\(tileIndices\)/);
|
||||
assert.match(html,/p\.unknownOnly==2u && reason!=0u/);
|
||||
assert.match(html,/fast-coverage-repair-and-color/);
|
||||
assert.match(html,/reprojectionSafe\(rawHistory,2\.25,\.45\)/);
|
||||
assert.match(html,/scheduleFastNumericRecovery/);
|
||||
assert.match(html,/runFastNumericRecovery/);
|
||||
assert.match(html,/numericFrameComplete\(\)/);
|
||||
assert.match(html,/requestBuild\(\{ctx,snap,iter,width,height,refPixel,safety=1\/64\}\)/);
|
||||
assert.match(html,/type:'build'/);
|
||||
assert.match(html,/buildOnly=d\.type==='build'/);
|
||||
assert.equal((await fs.readdir(root)).filter(name=>name.startsWith('index.baseline')&&name.endsWith('.html')).length,0);
|
||||
|
||||
let randomState=0x12345678;
|
||||
function randomU32(){randomState=(Math.imul(randomState,1664525)+1013904223)>>>0;return randomState}
|
||||
function roundShift(value,bits){const negative=value<0n,absolute=negative?-value:value,half=1n<<(BigInt(bits)-1n),rounded=(absolute+half)>>BigInt(bits);return negative?-rounded:rounded}
|
||||
function orbitOld(bits,re,im,iterations){const bailout=16n*(1n<<BigInt(bits));let zr=0n,zi=0n;const states=[];for(let n=0;n<iterations;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);states.push([zr,zi,mag]);if(mag>bailout)return{escape:n+1,states}}return{escape:0,states}}
|
||||
function orbitNew(bits,re,im,iterations){const bailout=16n*(1n<<BigInt(bits));let zr=0n,zi=0n,zr2=0n,zi2=0n;const states=[];for(let n=0;n<iterations;n++){zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;zr2=roundShift(zr*zr,bits);zi2=roundShift(zi*zi,bits);const mag=zr2+zi2;states.push([zr,zi,mag]);if(mag>bailout)return{escape:n+1,states}}return{escape:0,states}}
|
||||
|
||||
for(let sample=0;sample<2000;sample++){
|
||||
const bits=80+(randomU32()%177),one=1n<<BigInt(bits);
|
||||
const re=(BigInt(randomU32()%7000001)-3500000n)*one/1000000n;
|
||||
const im=(BigInt(randomU32()%4000001)-2000000n)*one/1000000n;
|
||||
const iterations=20+(randomU32()%180);
|
||||
assert.deepEqual(orbitNew(bits,re,im,iterations),orbitOld(bits,re,im,iterations));
|
||||
}
|
||||
|
||||
const f=Math.fround;
|
||||
function ds(h,l=0){return{h:f(h),l:f(l)}}
|
||||
function quick(a,b){const q=f(a+b),e=f(b-f(q-a));return ds(q,e)}
|
||||
function sum(a,b){const q=f(a+b),bb=f(q-a),e=f(f(a-f(q-bb))+f(b-bb));return ds(q,e)}
|
||||
function product(a,b){const q=f(a*b),ca=f(4097*a),ah=f(ca-f(ca-a)),al=f(a-ah),cb=f(4097*b),bh=f(cb-f(cb-b)),bl=f(b-bh);let e=f(f(ah*bh)-q);e=f(e+f(ah*bl));e=f(e+f(al*bh));e=f(e+f(al*bl));return ds(q,e)}
|
||||
function add(a,b){const q=sum(a.h,b.h);return quick(q.h,f(f(q.l+a.l)+b.l))}
|
||||
function neg(a){return ds(-a.h,-a.l)}
|
||||
function sub(a,b){return add(a,neg(b))}
|
||||
function mul(a,b){const q=product(a.h,b.h),e=f(f(f(q.l+f(a.h*b.l))+f(a.l*b.h))+f(a.l*b.l));return quick(q.h,e)}
|
||||
function scale(a,b){const q=product(a.h,b);return quick(q.h,f(q.l+f(a.l*b)))}
|
||||
function sameDs(a,b){return Object.is(a.h,b.h)&&Object.is(a.l,b.l)}
|
||||
function dsOrbit(cr,ci,iterations,carrySquares){let zr=ds(0),zi=ds(0),zr2=ds(0),zi2=ds(0);const states=[];for(let n=0;n<iterations;n++){if(!carrySquares){zr2=mul(zr,zr);zi2=mul(zi,zi)}const zri=mul(zr,zi);zr=add(sub(zr2,zi2),cr);zi=add(scale(zri,2),ci);const nextZr2=mul(zr,zr),nextZi2=mul(zi,zi),mag=add(nextZr2,nextZi2);states.push({zr,zi,mag});zr2=nextZr2;zi2=nextZi2}return states}
|
||||
|
||||
for(let sample=0;sample<600;sample++){
|
||||
const cr=ds(f((randomU32()/0xffffffff)*3.5-2.5),f((randomU32()/0xffffffff-.5)*1e-7));
|
||||
const ci=ds(f((randomU32()/0xffffffff)*2.4-1.2),f((randomU32()/0xffffffff-.5)*1e-7));
|
||||
const iterations=8+(randomU32()%64),oldStates=dsOrbit(cr,ci,iterations,false),newStates=dsOrbit(cr,ci,iterations,true);
|
||||
assert.equal(newStates.length,oldStates.length);
|
||||
for(let i=0;i<oldStates.length;i++){
|
||||
assert.ok(sameDs(newStates[i].zr,oldStates[i].zr));
|
||||
assert.ok(sameDs(newStates[i].zi,oldStates[i].zi));
|
||||
assert.ok(sameDs(newStates[i].mag,oldStates[i].mag));
|
||||
}
|
||||
}
|
||||
|
||||
function directChunkOrbit(cr,ci,iterations,chunk){let zr=f(0),zi=f(0),n=0,escape=0,mag=f(0);while(n<iterations&&!escape){const stop=Math.min(iterations,n+chunk);while(n<stop){const zr2=f(zr*zr),zi2=f(zi*zi);zi=f(f(f(2*zr)*zi)+ci);zr=f(f(zr2-zi2)+cr);n++;mag=f(f(zr*zr)+f(zi*zi));if(mag>4){escape=n;break}}}return{zr,zi,n,escape,mag}}
|
||||
for(let sample=0;sample<1000;sample++){
|
||||
const cr=f((randomU32()/0xffffffff)*3.5-2.5),ci=f((randomU32()/0xffffffff)*2.4-1.2),iterations=64+(randomU32()%900),chunk=[64,128,192,256][randomU32()%4];
|
||||
assert.deepEqual(directChunkOrbit(cr,ci,iterations,chunk),directChunkOrbit(cr,ci,iterations,iterations));
|
||||
}
|
||||
|
||||
function deepChunkState(reference,d,scaleExp,iterations,chunk){let w=[f(0),f(0)],n=0,m=0,err=f(Math.pow(2,-24)*Math.max(Math.abs(d[0]),Math.abs(d[1])));while(n<iterations){const stop=Math.min(iterations,n+chunk);while(n<stop){const r=reference[m],delta=[f(w[0]*Math.pow(2,scaleExp)),f(w[1]*Math.pow(2,scaleExp))],sq=[f(f(w[0]*w[0])-f(w[1]*w[1])),f(f(2*w[0])*w[1])],linear=[f(2*f(f(r[0]*w[0])-f(r[1]*w[1]))),f(2*f(f(r[0]*w[1])+f(r[1]*w[0])))],refAbs=Math.max(Math.abs(r[0]),Math.abs(r[1])),wAbs=Math.max(Math.abs(w[0]),Math.abs(w[1])),dAbs=Math.max(Math.abs(d[0]),Math.abs(d[1])),deltaAbs=Math.max(Math.abs(delta[0]),Math.abs(delta[1])),sqAbs=Math.max(Math.abs(f(sq[0]*Math.pow(2,scaleExp))),Math.abs(f(sq[1]*Math.pow(2,scaleExp))));err=f(f(f(2*refAbs)+f(2*deltaAbs))*err+f(Math.pow(2,-24)*f(f(f(2*refAbs)*wAbs)+sqAbs+dAbs+1e-30)));w=[f(f(linear[0]+f(sq[0]*Math.pow(2,scaleExp)))+d[0]),f(f(linear[1]+f(sq[1]*Math.pow(2,scaleExp)))+d[1])];m++;n++;const mm=Math.max(Math.abs(w[0]),Math.abs(w[1]),Math.abs(d[0]),Math.abs(d[1]));if(mm>65536){w=[f(w[0]/65536),f(w[1]/65536)];d=[f(d[0]/65536),f(d[1]/65536)];err=f(err/65536);scaleExp+=16}}}return{w,d,scaleExp,n,m,err}}
|
||||
for(let sample=0;sample<1000;sample++){
|
||||
const iterations=32+(randomU32()%480),reference=Array.from({length:iterations+1},()=>[f((randomU32()/0xffffffff-.5)*2),f((randomU32()/0xffffffff-.5)*2)]),d=[f((randomU32()/0xffffffff-.5)*1e-3),f((randomU32()/0xffffffff-.5)*1e-3)],scaleExp=-20-(randomU32()%60),chunk=1+(randomU32()%256);
|
||||
assert.deepEqual(deepChunkState(reference,[...d],scaleExp,iterations,chunk),deepChunkState(reference,[...d],scaleExp,iterations,iterations));
|
||||
}
|
||||
|
||||
for(let sample=0;sample<500;sample++){
|
||||
const w=64+(randomU32()%512),h=32+(randomU32()%256),iterations=350+(randomU32()%5000),maxPixels=Math.max(1,Math.floor(4000000/iterations)),tileW=Math.min(w,maxPixels),rows=Math.max(1,Math.min(128,Math.floor(maxPixels/tileW))),coverage=new Uint8Array(w*h);
|
||||
for(let y=0;y<h;y+=rows){const th=Math.min(rows,h-y);for(let x=0;x<w;x+=tileW){const tw=Math.min(tileW,w-x);assert.ok(tw*th*iterations<=4000000);for(let yy=0;yy<th;yy++)for(let xx=0;xx<tw;xx++)coverage[(y+yy)*w+x+xx]++}}
|
||||
for(const count of coverage)assert.equal(count,1);
|
||||
}
|
||||
|
||||
function adaptiveBudget(screenBits,q,base=350,quantum=64){const raw=48*Math.max(0,q)*Math.max(0,screenBits),bonus=raw>0?Math.ceil(raw/quantum)*quantum:0;return Math.min(12000,Math.max(base,base+bonus))}
|
||||
for(const q of [.72,1,1.25,1.5]){
|
||||
let prior=0;
|
||||
for(let i=0;i<=2000;i++){
|
||||
const screenBits=i/20,value=adaptiveBudget(screenBits,q);
|
||||
assert.ok(value>=350+Math.floor(48*q*screenBits));
|
||||
assert.equal((value-350)%64,0);
|
||||
assert.ok(value>=prior);prior=value;
|
||||
}
|
||||
}
|
||||
assert.equal(adaptiveBudget(Math.log2(800)+12*Math.LOG2E*Math.LN10,1),adaptiveBudget(Math.log2(800)+12.05*Math.LOG2E*Math.LN10,1));
|
||||
|
||||
function frontierModel(meta,w,h){const out=[];for(let y=0;y<h;y++)for(let x=0;x<w;x++){const i=y*w+x,m=meta[i]>>>0;if(((m>>>28)&3)!==0||((m>>>20)&255)!==6)continue;let edge=false;for(let oy=-1;oy<=1&&!edge;oy++)for(let ox=-1;ox<=1;ox++){if(!ox&&!oy)continue;const xx=x+ox,yy=y+oy;if(xx>=0&&xx<w&&yy>=0&&yy<h&&((meta[yy*w+xx]>>>28)&3)===1){edge=true;break}}if(edge)out.push(i)}return out}
|
||||
const opLimit=(6<<20)|350,escaped=(1<<28)|12,interior=(3<<28)|350,frontierFixture=new Uint32Array(25).fill(interior);frontierFixture[12]=escaped;for(const i of [6,7,8,11,13,16,17,18])frontierFixture[i]=opLimit;frontierFixture[0]=opLimit;
|
||||
assert.deepEqual(frontierModel(frontierFixture,5,5),[6,7,8,11,13,16,17,18]);
|
||||
for(let sample=0;sample<500;sample++){const iter=1+(randomU32()%150000),count=1+(randomU32()%200000),chunk=Math.max(1,Math.floor(4000000/iter));for(let start=0;start<count;start+=chunk)assert.ok(Math.min(chunk,count-start)*iter<=4000000)}
|
||||
|
||||
function pixelBitsAt(spanLog2,width,sourceBits=512,guard=32){const pixelLog2=spanLog2-Math.log2(Math.max(1,width)),need=Math.ceil(Math.max(0,-pixelLog2))+Math.max(16,guard|0);return Math.max(96,Math.min(sourceBits+64,need))}
|
||||
assert.equal(pixelBitsAt(Math.log2(3.4)-8*Math.LOG2E*Math.LN10,1920),96);
|
||||
assert.ok(pixelBitsAt(Math.log2(3.4)-100*Math.LOG2E*Math.LN10,1920)>350);
|
||||
assert.ok(pixelBitsAt(-400,3840)>pixelBitsAt(-400,960));
|
||||
assert.equal(pixelBitsAt(-400,1920,256),320);
|
||||
|
||||
export const result={
|
||||
syntaxScripts:scripts.length,
|
||||
precisionWorkerSyntax:true,
|
||||
kernelHashes:Object.keys(actual).length,
|
||||
bigintOrbitCases:2000,
|
||||
dsOrbitCases:600,
|
||||
directActiveChunkCases:1000,
|
||||
deepActiveChunkCases:1000,
|
||||
boundedCorrectionTileCases:500,
|
||||
adaptiveIterationCases:8004,
|
||||
pixelPrecisionCases:4,
|
||||
frontierAdjacencyCases:1,
|
||||
frontierWorkBoundCases:500,
|
||||
webGpuCorrectnessCases:correctnessCases.length,
|
||||
webGpuPerformanceCases:performanceCases.length,
|
||||
webGpuStabilityViews:interactionStabilityViews.length,
|
||||
status:'pass'
|
||||
};
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
import assert from 'node:assert/strict';
|
||||
|
||||
export const correctnessCases=[
|
||||
{id:'reset',re:'-0.5',im:'0',span:'3.4',bits:256,baseIter:750,adaptive:false,processMode:'validate',renderMode:'fast',activeDirect:true,certified:true,frontierCheck:true},
|
||||
{id:'cusp-low',re:'0.25',im:'0',span:'0.85',bits:256,baseIter:900,adaptive:false,processMode:'validate',renderMode:'accurate',certified:true},
|
||||
{id:'seahorse-low',re:'-0.743643887037151',im:'0.13182590420533',span:'0.1',bits:320,baseIter:1200,adaptive:false,processMode:'validate',renderMode:'accurate'},
|
||||
{id:'moderate-1e5-fast',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000034',bits:384,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast',frontierCheck:true},
|
||||
{id:'moderate-1e5-accurate',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000034',bits:384,baseIter:350,adaptive:true,processMode:'standard',renderMode:'accurate',frontierCheck:true},
|
||||
{id:'moderate-1e8-fast',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000000034',bits:384,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast',frontierCheck:true},
|
||||
{id:'moderate-1e8-accurate',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000000034',bits:384,baseIter:350,adaptive:true,processMode:'standard',renderMode:'accurate',frontierCheck:true},
|
||||
{id:'seahorse-deep',re:'-0.743643887037151',im:'0.13182590420533',span:'0.00000000000034',bits:448,baseIter:2900,adaptive:false,processMode:'validate',renderMode:'accurate',activeDeep:true,frontierCheck:true},
|
||||
{id:'interior-heavy',re:'0',im:'0',span:'0.5',bits:320,baseIter:1500,adaptive:false,processMode:'validate',renderMode:'accurate',certified:true},
|
||||
{id:'real-axis-boundary',re:'-0.75',im:'0',span:'0.02',bits:384,baseIter:3000,adaptive:false,processMode:'validate',renderMode:'accurate',activeDeep:true,certified:true,frontierCheck:true},
|
||||
{id:'misiurewicz-hard',re:'-0.10109636384562',im:'0.95628651080914',span:'0.00000002',bits:384,baseIter:3000,adaptive:false,processMode:'validate',renderMode:'accurate',activeDeep:true,frontierCheck:true},
|
||||
{id:'reported-black-regression',re:'-29466147000382485924219538765424656674342940730553342389512209954887705938978413587',im:'5179018789925933872641942859702187326563010594882200888329237785122225251258352',span:'250637324516887125119843167990565159991251418774166207381437546036409',bits:273,baseIter:350,adaptive:true,processMode:'standard',renderMode:'accurate',fixed:true,frontierCheck:true}
|
||||
];
|
||||
|
||||
export const performanceCases=[
|
||||
{id:'accurate-320x220-750',viewport:{width:320,height:220},view:correctnessCases[2],iterations:750},
|
||||
{id:'bla-654x690-2900',viewport:{width:654,height:690},view:correctnessCases[7],iterations:2900},
|
||||
{id:'fast-1e14-800x600',viewport:{width:800,height:600},view:{id:'fast-1e14',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000000000000034',bits:448,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast'},target:true,absoluteP50Ms:1000,absoluteP95Ms:1500},
|
||||
{id:'accurate-1e14-800x600',viewport:{width:800,height:600},view:{id:'accurate-1e14',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000000000000034',bits:448,baseIter:350,adaptive:true,processMode:'standard',renderMode:'accurate'},target:true,absoluteP50Ms:1500,absoluteP95Ms:2000},
|
||||
{id:'hd-reset-1500',viewport:{width:1920,height:1080},view:correctnessCases[0],iterations:1500},
|
||||
{id:'hd-boundary-3000',viewport:{width:1920,height:1080},view:correctnessCases[9],iterations:3000}
|
||||
];
|
||||
|
||||
export const productionPerformanceThresholds={targetP50Reduction:.20,targetP95Reduction:.15,nonTargetP50Regression:.05,nonTargetP95Regression:.10};
|
||||
|
||||
export const interactionStabilityViews=[
|
||||
{id:'near-a',re:'-0.743643887037151',im:'0.13182590420533',span:'0.0000000000034',bits:448,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast'},
|
||||
{id:'near-b',re:'-0.743643887037151',im:'0.13182590420533',span:'0.000000000003030253189',bits:448,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast'},
|
||||
{id:'near-a-repeat',re:'-0.743643887037151',im:'0.13182590420533',span:'0.0000000000034',bits:448,baseIter:350,adaptive:true,processMode:'standard',renderMode:'fast'}
|
||||
];
|
||||
|
||||
function numericalReasonTotal(reasons={}){return (reasons.errorBound||0)+(reasons.escapeUncertain||0)+(reasons.referenceEnd||0)+(reasons.rebaseGap||0)+(reasons.range||0)}
|
||||
function operationLimit(reasons={}){return reasons.operationLimit||0}
|
||||
|
||||
async function evaluateCase(page,testCase,options){
|
||||
return page.evaluate(async({testCase,options})=>{
|
||||
const t=globalThis.__MANDEL_TEST__;if(!t)throw new Error('__MANDEL_TEST__ is unavailable');
|
||||
await t.setView(testCase);await t.waitForFinal({timeout:options.timeout});
|
||||
let numericComplete=true,numericError='';try{await t.waitForNumericComplete({timeout:options.timeout})}catch(error){numericComplete=false;numericError=String(error&&error.message||error)}
|
||||
const state=t.state(),hashes=await t.fieldHashes(),diagnostics=t.gpuDiagnostics();
|
||||
let activeDirect=null,activeDeep=null,certified=null,frontier=null;
|
||||
if(testCase.activeDirect)activeDirect=await t.runActiveDirectShadow({chunkSize:128});
|
||||
if(testCase.activeDeep)activeDeep=await t.runActiveDeepShadow({width:options.shadowWidth,height:options.shadowHeight,chunkSize:128,strict:true});
|
||||
if(testCase.certified)certified=await t.runCertifiedInteriorShadow({width:options.oracleWidth,height:options.oracleHeight,samples:options.oracleSamples});
|
||||
if(testCase.frontierCheck)frontier=await t.compareCurrentToFrontierBaseline({strict:true});
|
||||
return{state,hashes,diagnostics,numericComplete,numericError,activeDirect,activeDeep,certified,frontier};
|
||||
},{testCase,options});
|
||||
}
|
||||
|
||||
function auditCase(testCase,result,expected){
|
||||
const reasons=result.state.unknownReasons||{},numeric=numericalReasonTotal(reasons),op=operationLimit(reasons),coverage=Math.max(0,(result.state.unresolved||0)-numeric-op),errors=(result.diagnostics?.compilation||[]).flatMap(x=>x.messages||[]).filter(x=>x.type==='error');
|
||||
assert.equal(result.state.kernelVersion,'24.3.0-optimized-unknown-pipeline',testCase.id+' kernel version');
|
||||
assert.equal(coverage,0,testCase.id+' coverage UNKNOWN');
|
||||
assert.equal(errors.length,0,testCase.id+' WGSL compilation errors');
|
||||
assert.equal(result.diagnostics?.uncapturedErrors?.length||0,0,testCase.id+' uncaptured GPU errors');
|
||||
assert.equal(result.diagnostics?.lossReason||'','',testCase.id+' device loss');
|
||||
assert.equal(result.numericComplete,true,testCase.id+' numeric completion: '+result.numericError);
|
||||
assert.equal(numeric,0,testCase.id+' final numeric UNKNOWN');
|
||||
assert.equal(result.state.historyReady,true,testCase.id+' completed history commit');
|
||||
if(result.activeDirect)assert.equal(result.activeDirect.gatePassed,true,testCase.id+' Direct active mismatch');
|
||||
if(result.activeDeep)assert.equal(result.activeDeep.gatePassed,true,testCase.id+' Deep active mismatch');
|
||||
if(result.certified)assert.equal(result.certified.gatePassed,true,testCase.id+' certified interior oracle mismatch');
|
||||
if(result.frontier)assert.equal(result.frontier.gatePassed,true,testCase.id+' production frontier mismatch');
|
||||
assert.equal(result.state.frontierConverged,true,testCase.id+' frontier convergence');
|
||||
assert.ok((result.state.frontierMaxDispatchWork||0)<=4000000,testCase.id+' frontier TDR work bound');
|
||||
if(expected){assert.equal(result.hashes.metaSha256,expected.metaSha256,testCase.id+' meta hash');assert.equal(result.hashes.smoothSha256,expected.smoothSha256,testCase.id+' smooth hash')}
|
||||
return{coverageUnknown:coverage,numericalUnknown:numeric,operationLimit:op};
|
||||
}
|
||||
|
||||
export async function runCorrectnessCorpus(page,{timeout=180000,viewport={width:480,height:320},shadowWidth=160,shadowHeight=112,oracleWidth=320,oracleHeight=220,oracleSamples=289,expectedHashes={},expectProduction=false}={}){
|
||||
if(page.setViewportSize)await page.setViewportSize(viewport);
|
||||
const compile=await page.evaluate(()=>globalThis.__MANDEL_TEST__.compileActiveShadow()),records=[];
|
||||
assert.equal(compile.productionEligible,expectProduction);
|
||||
assert.equal(compile.candidateEnabled,true);
|
||||
assert.equal(compile.pixelFrontier,true);
|
||||
assert.equal(compile.frontierWorkLimit,4000000);
|
||||
for(const testCase of correctnessCases){const started=Date.now(),result=await evaluateCase(page,testCase,{timeout,shadowWidth,shadowHeight,oracleWidth,oracleHeight,oracleSamples}),audit=auditCase(testCase,result,expectedHashes[testCase.id]);records.push({id:testCase.id,elapsedMs:Date.now()-started,audit,...result})}
|
||||
const adapter=await page.evaluate(()=>globalThis.__MANDEL_TEST__.gpuDiagnostics()?.adapterInfo||{});
|
||||
return{version:'24.3.0',kind:'correctness',date:new Date().toISOString(),expectProduction,compile,adapter,records};
|
||||
}
|
||||
|
||||
function percentile(values,p){const sorted=[...values].sort((a,b)=>a-b);if(!sorted.length)return Infinity;const index=Math.min(sorted.length-1,Math.max(0,Math.ceil(sorted.length*p)-1));return sorted[index]}
|
||||
function summarizeRuns(samples){const primary=samples.map(x=>Number(x.metric?.primaryMs??x.wallMs)),recovery=samples.map(x=>Number(x.metric?.numericQuiescentMs??x.metric?.historyCommitMs??x.metric?.finalMs??x.wallMs));return{runs:samples.length,primary:{p50:percentile(primary,.50),p95:percentile(primary,.95)},recovery:{p50:percentile(recovery,.50),p95:percentile(recovery,.95)}}}
|
||||
export function evaluateProductionPerformance(candidate,baseline,{thresholds=productionPerformanceThresholds}={}){const failures=[],byId=new Map((baseline?.records||[]).map(x=>[x.id,x]));for(const record of candidate?.records||[]){const base=byId.get(record.id),summary=record.summary||summarizeRuns(record.runs||[]);if(record.absoluteP50Ms&&summary.recovery.p50>record.absoluteP50Ms)failures.push(record.id+' recovery p50 '+summary.recovery.p50+' > '+record.absoluteP50Ms);if(record.absoluteP95Ms&&summary.recovery.p95>record.absoluteP95Ms)failures.push(record.id+' recovery p95 '+summary.recovery.p95+' > '+record.absoluteP95Ms);if(!base){failures.push(record.id+' missing baseline');continue}const bs=base.summary||summarizeRuns(base.runs||[]),p50=(summary.recovery.p50-bs.recovery.p50)/Math.max(1,bs.recovery.p50),p95=(summary.recovery.p95-bs.recovery.p95)/Math.max(1,bs.recovery.p95);if(record.target){if(p50>-thresholds.targetP50Reduction)failures.push(record.id+' target p50 reduction '+p50);if(p95>-thresholds.targetP95Reduction)failures.push(record.id+' target p95 reduction '+p95)}else{if(p50>thresholds.nonTargetP50Regression)failures.push(record.id+' non-target p50 regression '+p50);if(p95>thresholds.nonTargetP95Regression)failures.push(record.id+' non-target p95 regression '+p95)}}return{pass:failures.length===0,failures}}
|
||||
|
||||
export async function runPerformanceCorpus(page,{runs=10,timeout=240000,includeLarge=false}={}){
|
||||
const cases=includeLarge?performanceCases:performanceCases.slice(0,2),records=[];
|
||||
for(const scenario of cases){if(page.setViewportSize)await page.setViewportSize(scenario.viewport);const view=scenario.iterations?{...scenario.view,baseIter:scenario.iterations,adaptive:false,processMode:'standard'}:{...scenario.view},samples=[];for(let run=0;run<runs+1;run++){const started=Date.now();await page.evaluate(async({view,timeout})=>{const t=globalThis.__MANDEL_TEST__;await t.setView(view);await t.waitForNumericComplete({timeout})},{view,timeout});const state=await page.evaluate(()=>globalThis.__MANDEL_TEST__.state()),metric=state.generationMetrics.at(-1),diagnostics=await page.evaluate(()=>globalThis.__MANDEL_TEST__.gpuDiagnostics());assert.equal(state.numericalFailures,0,scenario.id+' numerical failures');assert.equal(diagnostics?.uncapturedErrors?.length||0,0,scenario.id+' uncaptured GPU errors');assert.equal(diagnostics?.lossReason||'','',scenario.id+' device loss');if(run>0)samples.push({wallMs:Date.now()-started,metric})}records.push({id:scenario.id,viewport:scenario.viewport,target:!!scenario.target,absoluteP50Ms:scenario.absoluteP50Ms||0,absoluteP95Ms:scenario.absoluteP95Ms||0,runs:samples,summary:summarizeRuns(samples)})}
|
||||
const exports=await page.evaluate(async()=>({ss1:await globalThis.__MANDEL_TEST__.smokeExportTile({w:128,h:96,strict:true,ss:1}),ss2:await globalThis.__MANDEL_TEST__.smokeExportTile({w:128,h:96,strict:true,ss:2})}));
|
||||
assert.equal(exports.ss1.length,exports.ss1.expected);assert.equal(exports.ss2.length,exports.ss2.expected);
|
||||
const adapter=await page.evaluate(()=>globalThis.__MANDEL_TEST__.gpuDiagnostics()?.adapterInfo||{});
|
||||
return{version:'24.3.0',kind:'performance',date:new Date().toISOString(),adapter,records,exports};
|
||||
}
|
||||
|
||||
export async function runInteractionStabilityCorpus(page,{timeout=240000,expectProduction=false}={}){
|
||||
if(page.setViewportSize)await page.setViewportSize({width:800,height:600});
|
||||
const policy=await page.evaluate(()=>globalThis.__MANDEL_TEST__.stabilityPolicy());
|
||||
assert.equal(policy.adaptiveIterationQuantum,64);
|
||||
assert.equal(policy.blaFastProductionOptIn,false);
|
||||
assert.equal(policy.blaFastDisabled,true);
|
||||
assert.equal(policy.pixelFrontierProduction,expectProduction);
|
||||
assert.equal(policy.pixelFrontierCandidate,true);
|
||||
assert.equal(policy.pixelFrontierEnabled,true);
|
||||
assert.equal(policy.pixelFrontierWorkLimit,4000000);
|
||||
const records=[];
|
||||
for(const view of interactionStabilityViews){
|
||||
const result=await page.evaluate(async({view,timeout})=>{const t=globalThis.__MANDEL_TEST__;await t.setView(view);await t.waitForNumericComplete({timeout});return{state:t.state(),hashes:await t.fieldHashes(),policy:t.stabilityPolicy()}},{view,timeout});
|
||||
assert.notEqual(result.state.backend,'fast-bla',view.id+' timing-selected backend');
|
||||
assert.equal(result.state.numericalFailures,0,view.id+' numerical failures');
|
||||
records.push({id:view.id,...result});
|
||||
}
|
||||
assert.equal(records[0].state.iter,records[1].state.iter,'nearby zoom iteration envelope');
|
||||
assert.equal(records[0].hashes.metaSha256,records[2].hashes.metaSha256,'repeat meta hash');
|
||||
assert.equal(records[0].hashes.smoothSha256,records[2].hashes.smoothSha256,'repeat smooth hash');
|
||||
const adapter=await page.evaluate(()=>globalThis.__MANDEL_TEST__.gpuDiagnostics()?.adapterInfo||{});
|
||||
return{version:'24.3.0',kind:'interaction-stability',date:new Date().toISOString(),expectProduction,policy,adapter,records};
|
||||
}
|
||||
|
||||
export async function runWebGpuCorpus(page,options={}){const correctness=await runCorrectnessCorpus(page,options.correctness),stability=await runInteractionStabilityCorpus(page,options.stability),performance=await runPerformanceCorpus(page,options.performance);return{correctness,stability,performance}}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
import {correctnessCases,performanceCases,interactionStabilityViews,evaluateProductionPerformance} from './optimization_webgpu_corpus.mjs';
|
||||
|
||||
const root=fileURLToPath(new URL('../',import.meta.url));
|
||||
const artifactPath=process.argv[2];
|
||||
const phase=process.argv[3]||'preflight';
|
||||
assert.ok(artifactPath,'usage: node tests/production_gate.mjs <gate-bundle.json> [preflight|production]');
|
||||
assert.ok(phase==='preflight'||phase==='production','phase must be preflight or production');
|
||||
|
||||
const bundle=JSON.parse(await fs.readFile(artifactPath,'utf8'));
|
||||
const index=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
|
||||
const production=/const PIXEL_FRONTIER_PRODUCTION=true,PIXEL_FRONTIER_WORK=4000000/.test(index);
|
||||
assert.equal(production,phase==='production','index production flag does not match gate phase');
|
||||
assert.ok(Array.isArray(bundle.candidateRuns)&&bundle.candidateRuns.length>=2,'at least two candidate adapter runs are required');
|
||||
assert.ok(Array.isArray(bundle.baselineRuns)&&bundle.baselineRuns.length>=2,'matching baseline runs are required');
|
||||
|
||||
function adapterKey(run){const a=run?.correctness?.adapter||run?.performance?.adapter||{};return[a.vendor,a.architecture,a.device,a.description].map(x=>String(x||'')).join('|')}
|
||||
function gpuErrors(diagnostics){return(diagnostics?.compilation||[]).flatMap(x=>x.messages||[]).filter(x=>x.type==='error').length+(diagnostics?.uncapturedErrors?.length||0)+(diagnostics?.lossReason?1:0)}
|
||||
function auditCandidate(run){
|
||||
assert.equal(run?.correctness?.expectProduction,phase==='production','correctness phase mismatch');
|
||||
assert.equal(run?.correctness?.compile?.productionEligible,phase==='production','compile production eligibility mismatch');
|
||||
assert.equal(run?.correctness?.compile?.candidateEnabled,true,'candidate path was not enabled');
|
||||
assert.equal(run?.correctness?.records?.length,correctnessCases.length,'incomplete correctness corpus');
|
||||
for(const record of run.correctness.records){
|
||||
assert.equal(record.audit?.coverageUnknown,0,record.id+' coverage UNKNOWN');
|
||||
assert.equal(record.audit?.numericalUnknown,0,record.id+' numeric UNKNOWN');
|
||||
assert.equal(record.numericComplete,true,record.id+' numeric completion');
|
||||
assert.equal(record.state?.historyReady,true,record.id+' history commit');
|
||||
assert.equal(record.state?.frontierConverged,true,record.id+' frontier convergence');
|
||||
assert.ok((record.state?.frontierMaxDispatchWork||0)<=4000000,record.id+' dispatch work limit');
|
||||
if((record.audit?.operationLimit||0)>0)assert.ok(record.state.frontierIter>=record.state.frontierMinimumIter,record.id+' stopped below pixel-scale iteration floor');
|
||||
assert.equal(gpuErrors(record.diagnostics),0,record.id+' GPU diagnostics');
|
||||
if(record.frontier)assert.equal(record.frontier.gatePassed,true,record.id+' frontier oracle');
|
||||
if(record.activeDirect)assert.equal(record.activeDirect.gatePassed,true,record.id+' Direct active shadow');
|
||||
if(record.activeDeep)assert.equal(record.activeDeep.gatePassed,true,record.id+' Deep active shadow');
|
||||
if(record.certified)assert.equal(record.certified.gatePassed,true,record.id+' certified interior shadow');
|
||||
}
|
||||
assert.equal(run?.stability?.expectProduction,phase==='production','stability phase mismatch');
|
||||
assert.equal(run?.stability?.records?.length,interactionStabilityViews.length,'incomplete interaction corpus');
|
||||
const stability=run.stability.records;
|
||||
assert.equal(stability[0].hashes.metaSha256,stability[2].hashes.metaSha256,'round-trip meta instability');
|
||||
assert.equal(stability[0].hashes.smoothSha256,stability[2].hashes.smoothSha256,'round-trip smooth instability');
|
||||
assert.equal(run?.performance?.records?.length,performanceCases.length,'full performance corpus is required');
|
||||
for(const record of run.performance.records){
|
||||
assert.ok((record.runs?.length||0)>=10,record.id+' requires 10 measured runs');
|
||||
assert.ok(Number.isFinite(record.summary?.recovery?.p50)&&Number.isFinite(record.summary?.recovery?.p95),record.id+' summary missing');
|
||||
}
|
||||
}
|
||||
|
||||
const candidateKeys=new Set();
|
||||
const baselineByAdapter=new Map(bundle.baselineRuns.map(run=>[adapterKey(run),run]));
|
||||
const performance=[];
|
||||
for(const run of bundle.candidateRuns){
|
||||
auditCandidate(run);
|
||||
const key=adapterKey(run);assert.ok(key.replaceAll('|',''),'candidate adapter identity is empty');candidateKeys.add(key);
|
||||
const baseline=baselineByAdapter.get(key);assert.ok(baseline,'missing same-adapter baseline for '+key);
|
||||
const result=evaluateProductionPerformance(run.performance,baseline.performance);assert.equal(result.pass,true,result.failures.join('\n'));
|
||||
performance.push({adapter:key,...result});
|
||||
}
|
||||
assert.ok(candidateKeys.size>=2,'candidate evidence must cover at least two distinct adapters');
|
||||
|
||||
const output={pass:true,phase,adapters:[...candidateKeys],correctnessCases:correctnessCases.length,performanceCases:performanceCases.length,interactionCases:interactionStabilityViews.length,performance};
|
||||
process.stdout.write(JSON.stringify(output,null,2)+'\n');
|
||||
189
tests/regression.mjs
Normal file
189
tests/regression.mjs
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import vm from 'node:vm';
|
||||
import {createHash} from 'node:crypto';
|
||||
|
||||
const html=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');
|
||||
const scripts=[...html.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
|
||||
assert.equal(scripts.length,2);
|
||||
for(const script of scripts)new vm.Script(script);
|
||||
assert.doesNotMatch(html,/\bBLA\b|bla[A-Z]|useBla|run\w*Shadow|Canary|frontier-gate/);
|
||||
|
||||
// Pin the reviewed kernels; numerical behavior is also exercised on the GPU.
|
||||
const kernelContext={};vm.runInNewContext(scripts[0],kernelContext);
|
||||
const kernels=kernelContext.MANDEL_WEBGPU_KERNELS;
|
||||
const expected=JSON.parse(await fs.readFile(new URL('./kernel_hashes.json',import.meta.url),'utf8'));
|
||||
assert.deepEqual(Object.fromEntries(Object.entries(kernels).map(([k,v])=>[k,createHash('sha256').update(v).digest('hex')])),expected);
|
||||
|
||||
function appContext(search=''){
|
||||
const element={width:800,height:600,clientWidth:800,clientHeight:600,style:{},addEventListener(){},classList:{toggle(){}},setAttribute(){}};
|
||||
const context={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element},navigator:{hardwareConcurrency:4},location:{search},performance,URLSearchParams,TextEncoder,console,addEventListener(){}};
|
||||
const code=scripts[1].slice(0,scripts[1].indexOf('// ── boot / teardown'))+
|
||||
'globalThis.internal={state,runtime,refs,refinePixelFrontier,setTestPresentation:fn=>{presentProvisional=fn},WebGpuRenderer,chooseBackend,startRenderClock,finishRenderClock,renderTimeLabel,fromDec,operationLimitIndices,numericalFailureIndices,pixelPrecisionBits,precisionBatchSize,certifiedInteriorTiles,referenceWorkerSource,fastReferenceWorkerSource,precisionFallbackWorkerSource};})();';
|
||||
vm.runInNewContext(code,context);
|
||||
return context;
|
||||
}
|
||||
const normal=appContext(),diagnostic=appContext('?test');
|
||||
assert.equal(normal.__MANDEL_TEST__,undefined);
|
||||
assert.equal(typeof diagnostic.__MANDEL_TEST__.state,'function');
|
||||
const a=normal.internal;
|
||||
assert.equal(a.runtime.generationMetrics.length,0);
|
||||
|
||||
// Wall time survives stage boundaries; stale completions cannot finish a new
|
||||
// generation. No real timer remains running after completion or cancellation.
|
||||
{
|
||||
const c=appContext(),clock=c.internal;let now=100,live=0;
|
||||
c.performance={now:()=>now};c.setInterval=()=>{live++;return live};c.clearInterval=()=>{live--};
|
||||
clock.startRenderClock(1,now);now=2240;
|
||||
assert.equal(clock.renderTimeLabel(),'描画中… 2.14 s');
|
||||
clock.finishRenderClock(1);assert.equal(live,0);assert.equal(clock.state.lastRender,2140);
|
||||
now=5000;assert.equal(clock.renderTimeLabel(),'2.14 s');
|
||||
clock.startRenderClock(2,now);now=5400;clock.finishRenderClock(1);
|
||||
assert.equal(clock.state.renderClock.status,'running');assert.equal(live,1);
|
||||
clock.finishRenderClock(2,'cancelled');assert.equal(clock.renderTimeLabel(),'中断 400 ms');assert.equal(live,0);
|
||||
}
|
||||
|
||||
// Exercise actual backend selection, including both perturbation paths.
|
||||
const backendCases=[['3.4','fast','direct'],['0.00000000000034','fast','fast-extended'],['3.4','accurate','direct'],['0.00000000000034','accurate','fast-extended']];
|
||||
for(const [span,mode,expectedBackend] of backendCases){
|
||||
a.state.renderMode=mode;
|
||||
const snap={bits:256,re:a.fromDec('-0.743643887037151'),im:a.fromDec('0.13182590420533'),span:a.fromDec(span)};
|
||||
assert.equal(a.chooseBackend(snap,800).backend,expectedBackend);
|
||||
}
|
||||
|
||||
// Recovery must distinguish iteration limits from numerical failures.
|
||||
const fixture=Uint32Array.from([6<<20,1<<20,2<<20,3<<20,4<<20,5<<20,1<<28,3<<28,0]);
|
||||
assert.deepEqual(Array.from(a.operationLimitIndices(fixture)),[0]);
|
||||
assert.deepEqual(Array.from(a.numericalFailureIndices(fixture)),[1,2,3,4,5]);
|
||||
|
||||
const renderer=Object.create(a.WebGpuRenderer.prototype);
|
||||
for(const width of [320,800,1920,3840])for(const iter of [350,2900,12000,150000])for(const deep of [false,true]){
|
||||
const {width:tileWidth,rows}=renderer.numericTileShape(width,iter,deep);
|
||||
assert.ok(rows>=1&&tileWidth>=1);
|
||||
assert.ok(tileWidth*rows*iter<=(deep?4000000:48000000));
|
||||
assert.ok(renderer.adaptNumericRows(rows,100,600,deep)<=rows);
|
||||
const coverage=new Uint8Array(width*3);
|
||||
for(let y=0;y<3;y+=rows)for(let x=0;x<width;x+=tileWidth)
|
||||
for(let dy=0;dy<Math.min(rows,3-y);dy++)for(let dx=0;dx<Math.min(tileWidth,width-x);dx++)coverage[(y+dy)*width+x+dx]++;
|
||||
assert.ok(coverage.every(n=>n===1),'tile coverage');
|
||||
assert.ok(a.precisionBatchSize(iter,256)<=64);
|
||||
}
|
||||
|
||||
// Catch dangling renderer calls after removing whole diagnostic subsystems.
|
||||
const rendererSource=scripts[1].slice(scripts[1].indexOf('class WebGpuRenderer{'),scripts[1].indexOf('function numericalFailureCount'));
|
||||
for(const [,method] of rendererSource.matchAll(/this\.(\w+)\(/g))assert.equal(typeof a.WebGpuRenderer.prototype[method],'function',method);
|
||||
|
||||
// Test actual frame routing, especially the argument following removed BLA plans.
|
||||
{
|
||||
const r=Object.create(a.WebGpuRenderer.prototype);let actualContext;
|
||||
Object.assign(r,{ready:Promise.resolve(),device:{},ensureFastPipelines:async()=>{},ensureFrame:()=>({}),setFastContext(ctx){actualContext=ctx},computePerturbFrameTiled:async(...args)=>{assert.equal(args[2],false);return true}});
|
||||
const ctx={referenceId:'fast-test'};
|
||||
assert.equal(await r.computeFrame({},350,0,null,true,ctx),true);
|
||||
assert.equal(actualContext,ctx);
|
||||
}
|
||||
|
||||
// Run every surviving Worker, including real reference generation and fallback.
|
||||
function worker(source,message){
|
||||
let result;const c={self:{},postMessage:r=>{result=r},performance};
|
||||
vm.runInNewContext(source,c);c.self.onmessage({data:message});
|
||||
assert.ok(result);assert.notEqual(result.type,'error',result.error);return result;
|
||||
}
|
||||
const scale=1n<<256n;
|
||||
for(const center of [0n,-scale,scale]){
|
||||
const message={type:'build',id:1,key:'test',bits:256,sourceBits:256,targetBits:128,re:center.toString(),im:'0',span:'0',width:1,height:1,iter:64};
|
||||
const deep=worker(a.referenceWorkerSource(),message),fast=worker(a.fastReferenceWorkerSource(),message);
|
||||
assert.equal(deep.checkpointMismatch,false);
|
||||
assert.equal(deep.refLen,fast.refLen);
|
||||
assert.deepEqual(Buffer.from(deep.refs),Buffer.from(fast.refs));
|
||||
assert.equal(deep.escape,center===scale?3:0);
|
||||
}
|
||||
for(const [center,escaped] of [[0n,false],[scale*3n,true]]){
|
||||
const result=worker(a.precisionFallbackWorkerSource(),{type:'solve',id:1,bits:256,targetBits:128,re:center.toString(),im:'0',span:'0',width:1,height:1,iter:64,terminalClass:-1,indices:Uint32Array.of(0).buffer});
|
||||
assert.equal(new Uint8Array(result.accepted)[0],1);
|
||||
assert.equal((new Uint32Array(result.meta)[0]>>>28)===1,escaped);
|
||||
}
|
||||
// Reuse a live reference Worker across increasing and decreasing budgets.
|
||||
{
|
||||
let result;const c={self:{},postMessage:r=>{result=r},performance};
|
||||
vm.runInNewContext(a.referenceWorkerSource(),c);
|
||||
const message={type:'build',id:1,bits:256,targetBits:128,re:'0',im:'0',span:'0',width:1,height:1,fixedRe:'0',fixedIm:'0'};
|
||||
let id;
|
||||
for(const iter of [64,256,512,128]){
|
||||
c.self.onmessage({data:{...message,iter,key:String(iter)}});
|
||||
assert.equal(result.checkpointMismatch,false);assert.equal(result.refLen,iter);
|
||||
const independent=worker(a.referenceWorkerSource(),{...message,iter});
|
||||
assert.deepEqual(Buffer.from(result.refs),Buffer.from(independent.refs));
|
||||
if(id)assert.equal(result.referenceId,id);id=result.referenceId;
|
||||
if(iter===512){assert.equal(result.extendedFrom,256);assert.ok(result.checkpointCount<=3)}
|
||||
}
|
||||
c.self.onmessage({data:{...message,iter:512,targetBits:192}});
|
||||
assert.notEqual(result.referenceId,id);assert.equal(result.extendedFrom,0);
|
||||
}
|
||||
// Instrument the actual fallback recurrence: failed comparisons must reuse
|
||||
// the preceding high-precision result (five orbits instead of eight).
|
||||
{
|
||||
let result;const calls=[];const c={self:{},performance,postMessage:r=>{result=r},record:b=>calls.push(b)};
|
||||
vm.runInNewContext(a.precisionFallbackWorkerSource(),c);
|
||||
vm.runInNewContext('pixel=(s,i,bits)=>{record(bits);return {escape:bits,smooth:bits}}',c);
|
||||
c.self.onmessage({data:{type:'solve',id:1,bits:256,targetBits:128,re:'0',im:'0',span:'0',width:1,height:1,iter:64,indices:Uint32Array.of(0).buffer}});
|
||||
assert.deepEqual(calls,[128,160,192,224,256]);assert.equal(new Uint8Array(result.accepted)[0],0);
|
||||
}
|
||||
// A late CPU completion must be rejected before any GPU write or allocation.
|
||||
assert.equal(await renderer.applyPrecisionFallback({indices:Uint32Array.of(0)},a.state.token-1),null);
|
||||
// Exact rational certificates include only strict interior rectangles. Probe
|
||||
// both sides of the cardioid cusp and the period-2 boundary at 256-bit depth.
|
||||
{
|
||||
const one=1n<<256n;
|
||||
for(const [re,expected] of [[0n,true],[-one,true],[one/4n,false],[one/4n-1n,true],[one/4n+1n,false],[-5n*one/4n,false],[-5n*one/4n+1n,true],[-5n*one/4n-1n,false]]){
|
||||
const result=a.certifiedInteriorTiles({bits:256,re,im:0n,span:0n},1,1);
|
||||
assert.equal(result.pixels===1,expected,re.toString());
|
||||
}
|
||||
const touching=a.certifiedInteriorTiles({bits:256,re:one/4n,im:0n,span:one/100n},32,32);
|
||||
assert.equal(touching.pixels,0,'rectangle crossing cusp must stay in the normal queue');
|
||||
}
|
||||
// Exercise the actual frontier controller with one repaired pixel and one
|
||||
// healthy survivor. GPU state/queue correctness is tested in browser_smoke.
|
||||
{
|
||||
const snap={bits:256,re:0n,im:0n,span:a.fromDec('3.4')};Object.assign(a.state,snap);
|
||||
const ctx={referenceId:'fixture',key:'fixture',source:snap,refLen:4096};
|
||||
a.refs.request=async()=>ctx;a.refs.requestFixed=async()=>ctx;a.setTestPresentation(async()=>true);
|
||||
const field=Uint32Array.of(6<<20|512,6<<20|512),replayed=[];let initializations=0,advances=0;
|
||||
const stats=()=>({total:2,reasons:{errorBound:(field[0]>>20)===1?1:0,operationLimit:(field[0]>>20)===1?1:2}});
|
||||
const r={certifyInterior:async()=>0,readMetaAll:async()=>field.slice(),readUnresolvedStats:async()=>stats(),setDeepContext(){},
|
||||
beginOperationLimitContinuation:async()=>{initializations++;return{active:2,pixelIterations:0,dispatches:0}},
|
||||
advancePriorityTarget:async(session,_snap,target)=>{advances++;assert.ok(target>512);field[0]=advances===1?1<<20:field[0];field[1]=6<<20|target;session.active=1;session.pixelIterations+=1;session.dispatches++;return session},
|
||||
correctUnknownFrame:async()=>{field[0]=6<<20|1024;return{mode:'queue'}},
|
||||
readActiveIndices:async()=>Uint32Array.of(1),
|
||||
refineOperationLimitIndices:async(_snap,target,_token,_ctx,_pixel,indices)=>{replayed.push(Array.from(indices));for(const i of indices)field[i]=6<<20|target;return{stats:stats()}}
|
||||
};
|
||||
const result=await a.refinePixelFrontier(r,snap,512,a.state.token,stats());
|
||||
assert.equal(initializations,1,'a repair must not reset the healthy session');
|
||||
assert.equal(advances,2);assert.deepEqual(replayed,[[0]]);assert.equal(result.converged,true);
|
||||
assert.equal(field[0]&0xfffff,result.iter);assert.equal(field[1]&0xfffff,result.iter);
|
||||
}
|
||||
{
|
||||
const prior=new Uint32Array(5100).fill(6<<20|512);prior[0]=1<<28|10;
|
||||
const q={kind:'queue'},c={kind:'count'},steps=[];let entries=Uint32Array.from({length:5000},(_,i)=>i+1);
|
||||
const r=Object.create(a.WebGpuRenderer.prototype),session={active:5000,progress:512,f:{w:100,h:51},queues:[q,q],counts:[c,c],input:0};
|
||||
r.device={queue:{writeBuffer:(target,_offset,data)=>{if(target.kind==='queue')entries=Uint32Array.from(data)}}};
|
||||
r.readActiveIndices=async()=>entries.slice();
|
||||
r.continueOperationLimitActive=async(s,_snap,target)=>{steps.push({count:s.active,target});s.progress=target;return s};
|
||||
const result=await r.advancePriorityTarget(session,{},4096,a.state.token,{}, {},prior);
|
||||
assert.equal(steps.length,2);assert.ok(steps[0].count<steps[1].count);
|
||||
assert.equal(steps[0].count+steps[1].count,5000);assert.ok(steps.every(s=>s.target===4096));
|
||||
assert.equal(result.active,5000);assert.equal(result.progress,4096);
|
||||
assert.deepEqual(Array.from(entries).sort((x,y)=>x-y),Array.from({length:5000},(_,i)=>i+1));
|
||||
}
|
||||
// Exhausting the automatic finite budget is a valid image only when every
|
||||
// remaining pixel reached it. A stale lower-iteration field is still rejected.
|
||||
{
|
||||
const snap={bits:256,re:0n,im:0n,span:a.fromDec('3.4')};Object.assign(a.state,snap,{continuationBudget:0});
|
||||
const stats={total:1,reasons:{operationLimit:1}};
|
||||
for(const [n,complete] of [[150000,true],[149999,false]]){
|
||||
const r={certifyInterior:async()=>0,readMetaAll:async()=>Uint32Array.of(6<<20|n),readUnresolvedStats:async()=>stats};
|
||||
const result=await a.refinePixelFrontier(r,snap,150000,a.state.token,stats);
|
||||
assert.equal(result.converged,complete);assert.equal(result.membershipCertified,false);
|
||||
assert.equal(result.policy,'automatic-finite-cap');
|
||||
}
|
||||
}
|
||||
console.log(`PASS: scripts, ${Object.keys(kernels).length-1} pinned shaders, routing, tile coverage/work caps, reference extension, precision reuse, cancellation, generation timing, finite cap`);
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
from pathlib import Path
|
||||
import re, subprocess, tempfile, json
|
||||
ROOT=Path(__file__).resolve().parents[1]
|
||||
CUR=ROOT/'index.html'
|
||||
OLD=ROOT/'tests'/'KERNEL_HASHES_V24_2_54.json'
|
||||
NEW=ROOT/'tests'/'KERNEL_HASHES_V24_3_0.json'
|
||||
CORPUS=ROOT/'tests'/'optimization_webgpu_corpus.mjs'
|
||||
|
||||
def scripts(path): return re.findall(r'<script[^>]*>(.*?)</script>',path.read_text(),re.S)
|
||||
|
||||
def syntax(path):
|
||||
for i,code in enumerate(scripts(path)):
|
||||
q=Path(tempfile.gettempdir())/f'mandel_v55_check_{i}.js'; q.write_text(code)
|
||||
r=subprocess.run(['node','--check',str(q)],capture_output=True,text=True)
|
||||
assert r.returncode==0, r.stderr
|
||||
|
||||
def kernels(path):
|
||||
code=scripts(path)[0]
|
||||
q=Path(tempfile.gettempdir())/'mandel_v55_kernel_dump.js'
|
||||
q.write_text(code+"\nconst crypto=require('crypto');const G=globalThis.MANDEL_WEBGPU_KERNELS;const out={};for(const k of Object.keys(G)){if(typeof G[k]==='string')out[k]=crypto.createHash('sha256').update(G[k]).digest('hex')}console.log(JSON.stringify(out));")
|
||||
r=subprocess.run(['node',str(q)],capture_output=True,text=True,check=True)
|
||||
return json.loads(r.stdout.strip().splitlines()[-1])
|
||||
|
||||
syntax(CUR)
|
||||
old=json.loads(OLD.read_text()); cur=kernels(CUR); expected=json.loads(NEW.read_text())
|
||||
assert cur==expected
|
||||
changed=[k for k,v in old.items() if cur.get(k)!=v]
|
||||
assert {'version','DIRECT_ACCURATE_SEED_WGSL','DIRECT_DS_CORRECT_WGSL','FAST_PERTURB_WGSL'}.issubset(changed), changed
|
||||
html=CUR.read_text()
|
||||
assert CORPUS.exists() and 'runCorrectnessCorpus' in CORPUS.read_text() and 'runPerformanceCorpus' in CORPUS.read_text() and 'runInteractionStabilityCorpus' in CORPUS.read_text()
|
||||
assert "version:'24.3.0-optimized-unknown-pipeline'" in html
|
||||
assert 'if(n>=p.lateThreshold){fieldMeta[out]=pack_unknown' in html
|
||||
assert 'var zr2=DS(0.0,0.0);var zi2=DS(0.0,0.0)' in html
|
||||
assert 'DIRECT_DS_QUEUE_BUILD_WGSL' in html and 'DIRECT_DS_CORRECT_QUEUE_WGSL' in html
|
||||
assert 'accurateQueuedPixels' in html and 'accurateQueueFallbacks' in html
|
||||
assert 'UNKNOWN_STATS_WGSL' in html and 'encodeUnknownStats' in html
|
||||
assert 'encodeDeepBucketHistogram(e' in html and 'encodeQueuedDeepCorrection(e' in html
|
||||
assert 'q.selected===q.enqueued&&q.enqueued===q.dispatchCount&&q.dispatchCount===q.processed' in html
|
||||
assert 'reusePreview' in html and 'numericInFlightBudgetMs' in html
|
||||
assert 'canUseStableReprojection()||fastNeedsExtended(snapshot(),canvas.width))return[finalIter]' in html
|
||||
assert 'const ADAPTIVE_ITER_QUANTUM=64' in html and 'Math.ceil(raw/quantum)*quantum' in html
|
||||
assert 'function pixelPrecisionBits(snap=snapshot(),w=canvas.width,guard=32)' in html
|
||||
assert 'targetBits=pixelPrecisionBits(snap,width,40)' in html and 'fastReferenceBits(snap,w=canvas.width)' in html
|
||||
assert 'const bits=baseBits+attempt*32,a=pixel(s,indices[i],bits),b=pixel(s,indices[i],bits+32)' in html
|
||||
assert 'for(let attempt=0;attempt<4&&!accepted[i];attempt++)' in html
|
||||
assert 'for(let start=0;start<indices.length;start+=chunkSize)' in html
|
||||
assert 'UNRESOLVED_BLACK_MIN_ZOOM_EXP' not in html and 'colorUnresolvedInsteadOfBlack' not in html and 'guardedFastDeep' not in html
|
||||
assert 'this.blaFastDisabled=true;this.blaFastOptIn=false' in html
|
||||
assert 'deep-stable-bootstrap' not in html and 'forceStableDeep' not in html
|
||||
assert 'A FAST frame is not publishable while any numeric UNKNOWN remains' in html
|
||||
assert 'if(initialFailed>0){const mr=await recoverNumericalUnknownTiles' in html
|
||||
assert 'if(numericalFailureCount(stats)>0){const pf=await recoverResidualPrecision' in html
|
||||
assert "if(isFinal){const remaining=numericalFailureCount" in html and 'frameを公開しません' in html
|
||||
assert 'viewRe:snap.re,viewIm:snap.im' in html and 'referenceOriginMatchesView(src,snap)' in html
|
||||
assert 'fastBackgroundDeepOnFailureOnly:true,deepProductionActive:false,deepCorrectionDispatchWork:PIXEL_FRONTIER_WORK' in html
|
||||
assert 'ensureAccuratePipelines' in html and 'ensureCorrectionPipelines' in html
|
||||
assert 'precisionFallbackWorkerSource' in html and 'recoverResidualPrecision' in html
|
||||
assert 'estimatedNetMs' in html and "reason:enabled?'probe-pass':!operationGate?'probe-cost':'probe-net-time'" in html
|
||||
replay=cur['BLA_GPU_FAST_REPLAY_CANDIDATE_WGSL']
|
||||
assert 'fn baseline_from(s0:S,pre:u32)->Result' in replay and 'var anchor=s;loop' in replay
|
||||
assert 'baseline(g,st)' not in replay and replay.count('baseline_from(anchor,st)')==7
|
||||
assert 'anchor=s;}}' in replay and 'baseline_from(anchor,st)' in cur['BLA_GPU_FAST_FRAME_WGSL']
|
||||
assert 'SYMMETRY_COPY_WGSL' in html and 'canUseRealAxisSymmetry' in html and 'encodeSymmetryCopy' in html
|
||||
assert 'FAILURE_TILE_MAP_WGSL' in html and 'readNumericalFailureTiles' in html
|
||||
assert "tileReferencePool=Array.from({length:3}" in html and 'Promise.all(batch.map' in html
|
||||
assert 'if(p.key===key)return p.promise' in html and 'prefetchFinalReference' in html
|
||||
assert 'this.exportWs=[]' in html and 'exportRingDepth(tile=this.exportTileSize())' in html
|
||||
assert 'Promise.allSettled(batch.map(item=>item.promise))' in html and 'if(failed)throw failed.reason' in html and 'performance.now()-lastYield<24' in html
|
||||
assert 'await png.rows(band);await new Promise(requestAnimationFrame)' not in html
|
||||
assert 'frameReadback(kind,size)' in html and "this.frameReadback('unknown-stats'" in html
|
||||
stats_readback=html[html.index('async readUnresolvedStats'):html.index('async readUnresolved(){')]
|
||||
assert 'destroy(r)' not in stats_readback
|
||||
assert 'cachedBindGroup(pipeline,entries)' in html and 'cachedTextureView(texture)' in html
|
||||
assert 'this.cachedBindGroup(this.direct' in html and 'this.cachedBindGroup(this.present' in html
|
||||
history_commit=html[html.index('commitHistory(view='):html.index('invalidateHistory(){')]
|
||||
assert 'f.history=f.front' in history_commit and 'copyTextureToTexture' not in history_commit
|
||||
assert 'colorTarget(f=this.frame)' in html and 'spare:history' in html
|
||||
assert 'ACTIVE_PREPARE_WGSL' in html and 'DIRECT_ACTIVE_CONTINUE_WGSL' in html
|
||||
assert 'DEEP_ACTIVE_CONTINUE_WGSL' in html and 'PERTURB_CERTIFIED_INTERIOR_WGSL' in html
|
||||
assert 'runDirectActiveShadow' in html and 'dispatchWorkgroupsIndirect(indirect,0)' in html
|
||||
assert 'direct-active-shadow-resume' in html and 'resumedFromPreview:previewIter>0' in html
|
||||
assert 'runDeepActiveShadow' in html and 'deep-active-shadow-resume' in html
|
||||
assert 'deep-active-compare-shadow' in html and 'runtime.deepActiveShadowLatest=result' in html
|
||||
assert 'computeDeepActiveFrame' in html and 'workPerDispatch=PIXEL_FRONTIER_WORK,maxPassesPerSubmit=1' in html
|
||||
assert 'const workBudget=deep?4000000:48000000' in html
|
||||
assert 'maxPixels=Math.max(1,Math.floor(4000000/Math.max(1,iter))),tileW=Math.min(f.w,maxPixels)' in html
|
||||
assert 'PIXEL_FRONTIER_PRODUCTION=true,PIXEL_FRONTIER_WORK=4000000,PIXEL_FRONTIER_MAX_ITER=150000' in html
|
||||
assert 'PIXEL_FRONTIER_ENABLED=PIXEL_FRONTIER_PRODUCTION||PIXEL_FRONTIER_CANDIDATE' in html
|
||||
assert 'function operationLimitFrontier(meta,w,h)' in html and 'refineOperationLimitIndices' in html
|
||||
assert 'function pixelFrontierIterationFloor(snap,w,initialIter)' in html and 'iter>=minimumIter&&stableRounds>=2' in html
|
||||
assert 'pixel frontier queue invariant failed' in html and 'compareCurrentToFrontierBaseline' in html
|
||||
assert 'correctUnknownFrame(snap,iter,token,referencePixel,numericalFailureCount(stats))' in html
|
||||
assert 'ensureDeepActiveWorkspace' in html and 'return this.computeDeepActiveFrame(snap,iter,token,forceStrict,referencePixel)' not in html
|
||||
assert 'return this.computePerturbFrameTiled(snap,iter,true,token,forceStrict,referencePixel,false,colorStyle,colorKey)' in html
|
||||
assert 'states[local]=DeepState' in html and 'let out=p.outputBase+(local/p.tileW)*p.outputStride' in html
|
||||
assert 'state.gpuInitFailed=true;markDirty(false);setTimeout' in html
|
||||
assert 'runCertifiedInteriorShadow' in html and 'gpu.provenIndices,FIELD_INTERIOR_PROVEN' in html
|
||||
assert 'falsePositive===0&&inconclusive===0' in html and 'oracleBits:[oracleBits,oracleBits+32]' in html
|
||||
assert 'waitForPrimary(' in html and 'waitForFinal(' in html and 'waitForNumericComplete(' in html
|
||||
assert 'fieldHashes()' in html and 'readFieldAll()' in html
|
||||
assert 'generationMetrics:[]' in html and 'primaryNumericalFailures' in html
|
||||
assert 'numericQuiescentMs' in html and 'historyCommitMs' in html
|
||||
recovery=html[html.index('async function recoverNumericalUnknownTiles'):html.index('async function recoverResidualPrecision')]
|
||||
assert 'readNumericalFailureTiles' in recovery and 'const currentMeta=await r.readMetaAll()' in recovery
|
||||
assert 'Uint32Array.from(tileIndices)' in recovery
|
||||
assert 'p.unknownOnly==2u && reason!=0u' in html
|
||||
assert "fast-coverage-repair-and-color" in html
|
||||
assert 'reprojectionSafe(rawHistory,2.25,.45)' in html
|
||||
assert 'scheduleFastNumericRecovery' in html and 'runFastNumericRecovery' in html
|
||||
assert 'numericFrameComplete()' in html
|
||||
assert "requestBuild({ctx,snap,iter,width,height,refPixel,safety=1/64})" in html
|
||||
assert "type:'build'" in html and 'buildOnly=d.type===\'build\'' in html
|
||||
assert not list(ROOT.glob('index.baseline*.html'))
|
||||
print('PASS: syntax; bounded Accurate seed; DS square reuse; shared UNKNOWN stats; sparse correction invariants; preview/reference reuse; bounded GPU bursts')
|
||||
Loading…
Add table
Add a link
Reference in a new issue