This commit is contained in:
33333-33333 2026-08-23 17:12:29 +09:00
commit de177e8896
58 changed files with 3904 additions and 2847 deletions

95
AUDIT_V24.md Normal file
View file

@ -0,0 +1,95 @@
# v24.1.3 completion audit
## 判定
**v24.1.2で実ブラウザからWGSL reserved-word parse errorが報告されたため、従来のrelease判定を撤回。v24.1.3でshader命名とfallback負荷を修正し、静的・CPU数値モデル・build gateを再実行してPASS。実WebGPU adapter acceptanceはこの環境では引き続き未実行。**
実GPU未実行を「PASS」とは扱いません。
## 修正済みの重大事項
1. f32量子化BLAによるmembership反転
- production BLAを完全除去。
2. BLAなしperturbationのfalse bounded反例
- roundoff error boundを導入し、既知反例を`UNKNOWN`へ退避。
3. recolor/render token race
- `recolorPending`方式へ変更。
4. redundant Strict retry
- 1-pass policyへ整理し、大画面queue/indirect dispatchを削除。
5. Exportの4 readback/tile
- 4 sampleを1 command streamで処理しGPU resolve後1 readback。
6. Exportのtile resource churn
- 512² reusable GPU workspaceへ変更。
7. 16K巨大Canvas
- streaming PNGへ変更。
8. deep JS fallback誤描画
- deep fallbackを明示拒否。
9. WGSL parse error (`meta` / `smooth` reserved words)
- storage変数を`fieldMeta` / `fieldSmooth`へ改名。WGSL予約語を全shaderで走査する回帰テストを追加。
10. WebGPU shader/pipeline failure時のCPU暴走
- shader/pipelineエラーではJavaScript full-frame fallbackを起動せず停止。WebGPU自体がない場合だけ軽量fallbackを使用。
11. 画面描画負荷
- standard screen budgetをdesktop約4M→1.5M、小型端末約2M→0.75Mへ低減。power約0.5M、fine最大約3M、Strict最大約2M。
12. WebGPU canvas context claim
- pipeline成功後へ遅延。
13. e340以深の座標表示0化
- fixed-point exact decimal formatterの340桁capを撤去。
## CPU numeric regression
`guarded-production-equation-cpu-f32-model-not-real-gpu`:
- period2-cusp-z14: false escaped 0 / false bounded 0 / UNKNOWN 0
- period2-cusp-z20: 0 / 0 / 0
- period2-cusp-z100: 0 / 0 / 0
- period3-interior: 0 / 0 / 0
- period2-cusp-e280: 0 / 0 / 0
- period2-cusp-e400: 0 / 0 / 0
- swirly-seahorses-z12: false escaped 0 / false bounded 0 / UNKNOWN 71 of 187
BalancedとStrictの両方で上記membership gateを満たします。swirlyの未確定率は既知の制約で、誤分類より保守退避を優先しています。
Shallow f32 modelは693 sampleでfalse escaped 0 / false bounded 0。escape iteration count差は2 sampleあり、membership certificationを名乗らない理由の一つです。
## BLA status
production sourceにBLA builder/evaluator/node/levelはありません。再導入にはf32係数量子化を含む誤差上界付き受入条件が必要です。
## Export audit
- tile max 512²
- reusable GPU workspace
- 2×2 AA: one readback/tile
- unresolved counterをRGBA readback末尾へ同梱
- PNG signature / IHDR / CRC / multiple IDAT / inflate scanlineをNode modelで検証
- abort path settles without unhandled rejection
## Real WebGPU acceptance
`tests/webgpu-acceptance.html`は以下を実adapterで検証します。
- shader compilation
- standard / Strict deep dispatch
- BigInt reference guard
- sampled membership
- dense known regressions
- Export 1× / 2×2 readback
- uncaptured validation errors
stable scenesはsampled UNKNOWNを許容しません。swirlyは25 sample中12以上の確定を要求します。
この作業環境のChromiumは`navigator.gpu`を公開しないため、このgateは未実行です。`audit/v24-real-webgpu-status.json`に環境結果を記録します。
## v24.1.3 hotfix verification
- WGSL 16.2 reserved-word scan: PASS (5 shader modules)
- `meta` / `smooth` WGSL identifier occurrences: 0
- source contract: PASS (28 checks)
- `npm test`: PASS
- clean `npm run build`: PASS
- source ↔ standalone/hosted hashes: identical
- shader/pipeline init failure: CPU full-frame fallback is not entered and failed init is not retried every render
- normal desktop screen budget: ~1.5M px (v24.1.2 ~4M px)
- no-WebGPU/no-adapter fallback budget: ~0.25M px
実WebGPU adapter上のv24.1.3 compile/dispatchだけは、このコンテナではadapterを得られないため未実行です。v24.1.2で報告されたreserved-word parse failureそのものは、仕様予約語の除去とreserved-token回帰gateによってsource上修正済みです。

View file

@ -36,8 +36,11 @@ powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-wasm.ps1 -Prom
## 信頼境界
公式配布物のSHA-256を照合したclang 17.0.6とNode.js v22.18.0を作業領域内で使用し、
復元sourceのcompile、generated-WASM golden、SIMD/scalar同値性、画素中心契約を実行済みです。
clang 17.0.6とNode.js v22.18.0を作業領域内で使用し、復元sourceのcompile、
generated-WASM golden、SIMD/scalar同値性、画素中心契約を実行済みです。tool配布archive自体の
URLSHA-256は現repositoryに保持されていないため、「公式archiveのhash照合」は再監査可能な証拠には
含めません。再監査できる信頼境界はversionflags、source hash、生成manifest、golden結果です。
Provenance status: tool archive hash not retained.
golden合格後に8 payloadと`kernels.js`を昇格し、HostedStandalone成果物も再生成しました。
実測結果と生成manifestのSHA-256は`audit/v23-source-baseline.json`へ記録しています。

View file

@ -2,7 +2,7 @@
## 判定
`IMPROVEMENT_PROPOSAL.md`のうち、計測結果に依存しないPhase 0〜3Aの実装項目はコード、build、非ブラウザ試験へ反映した。sourceABI数値pixel mappingWASM Module clone配布物のgateは合格している。
`IMPROVEMENT_PROPOSAL.md`のうち、Phase 0Aと計測結果に依存しないPhase 1〜3Aの実装項目はコード、build、非ブラウザ試験へ反映した。sourceABI数値pixel mappingWASM Module clone配布物の保存済みgateは合格している。v22成果物が残っていないため、v22比較項目は`not-verifiable`でありpassには含めない。
ただし、固定実機を必要とするbrowser acceptanceは実行環境にin-app Browserが存在しないため未実施である。性能修正後に再開した接続監査でも3回連続のgoal turnで利用可能Browser 0件となり、2026-08-22T10:48:56.1197927Zに外部状態待ちのblockerとして再確定した。Node固定sceneではdeep BLAの高いMPPを確認し、標準deepを約1.4秒の実測時間予算へ変更済みだが、これはCanvas、Worker、入力、downloadを含むbrowser gateの代替ではない。またPhase 3BとPhase 4は、提案書自身がbrowser telemetryで支配コストまたは誤差を確認した場合だけ実施すると定義した条件付き項目であり、trigger判定も未実施である。したがって本監査は「実装・非ブラウザgate合格、実機受入blocked」と判定する。
@ -36,7 +36,7 @@
| deepBLAcolor `WebAssembly.Module` structured clone・instantiate | pass |
| 256320-bit direct agreement、analytic interior整数証明 | pass |
| reference PP+64 checkpoint agreement・故意の不一致検出 | pass |
| Node固定scene runtime budget | pass。shallow約0.34〜0.91秒/MP、seahorse deep BLA約19.6〜36.6秒/MPのrunを観測し、deep標準を固定画素数から実測時間適応へ変更 |
| Node固定scene計測/予算式契約 | pass。shallow 2 sceneと、出典付きのswirly-seahorses混在境界sceneでdeep BLAを測定し、deep標準を固定画素数から実測時間適応へ変更。保存値は端末負荷に依存する特性値であり、時間上限そのものをassertするgateではない |
| 全backend pixel mapping・2×4× tile seam | pass、851 sample |
| 配布shallow SIMD pixel contract | pass、1,440 sample、mismatch 0 |
| kernel sourceABI、payload checksum、HostedStandalone build | pass |
@ -44,10 +44,12 @@
機械可読なsource基準値は`audit/v23-source-baseline.json`、未実施browser gateは`audit/v23-browser-baseline.json`に記録した。
追加のbrowser非依存監査は`audit/v23-browserless-baseline.json`へ保存した。固定Node.js v22.18.0で7 executable tests、sourcedocument contract、配布hashを再実行してpassしたが、scopeは`browserless-current-artifacts``fullAcceptance: false`である。v22比較は`not-verifiable`、source-WASM buildはprior evidence reused、runtimeは測定値であり性能合格ではない。
## 条件付き項目
Phase 3Bの1 shared memory統合、immutable referenceBLA table共有、WASM hot loopのatomic cancel、およびPhase 4のdouble-double、interval/error ball、scaled BLA、multi-reference、WebGPUは無条件要件ではない。`IMPROVEMENT_PROPOSAL.md` 3.3、7.7、Phase 3B4の規定どおり、固定browser計測でcopy、memory、10^-280以深、glitch、reference生成のどれが支配的か判明した場合だけ着手する。現在はModule clone、buffer pool、短いchunk、guarded directで安全な前段を実装済みだが、telemetryがないため条件成立を主張しない。
## Browser受入の再開条件
`tests/browser-benchmark.html`をHTTPで開けるin-app Browserを接続し、mobiledesktop4Kの実viewport・DPRで実行する。runnerはsceneごとのcanvas SHA-256、PreviewCoveredRefined、idle write、long task、managed memory、deep request、wheel settle、再着色、Export完了取消、accessibilityを採取し、`acceptance`を出力する。合格JSONを`audit/v23-browser-baseline.json`へ保存した時点で、固定実機に対する最終受入を判定できる。
`tests/browser-benchmark.html`をHTTPで開けるin-app Browserを接続し、mobiledesktop4Kを要求DPRの別contextで、HostedStandalone別に実行する。runnerはraw pixel golden、30回以上のPreview、Covered実Refined、idle write、long task、managed取得可能ならUA memory、deep request、非zero wheel paint、再着色、Export完了取消再現性、keyboard操作を採取する。observed GPU memory、visible focus、page zoom/pinch、screen-reader順序の外部証拠と統合し、全profile・全buildが揃った時点で固定実機の最終受入を判定できる。

View file

@ -2,7 +2,7 @@
## 結論
`IMPROVEMENT_PROPOSAL.md`で定義したv23の実装、ローカル数値検証、WASM再現build、HostedStandalone成果物の生成まで完了した。実ブラウザでのみ測定できる性能・表示・操作・download検証は、今回の環境にin-app browser bindingがないため未実行である。したがって、実装と非ブラウザgateは合格、browser acceptance gateは保留として扱う。
`IMPROVEMENT_PROPOSAL.md`のv22向け設計判断をrenderer v23へ反映し、ローカル数値検証、WASM再現build、HostedStandalone成果物の生成まで実施した。提案は履歴文書であり、修正版のcoverageExport受入契約を現行基準とする。実ブラウザでのみ測定できる性能・表示・操作・download検証は、今回の環境にin-app browser bindingがないため未実行である。したがって、実装と確認済みの非ブラウザgate、未確認のbrowser acceptanceを分けて扱う。
## 実装済み
@ -19,7 +19,7 @@
- adaptive 2×4× AA、linear-light resolve、byte-budget detail cacheを実装した。
- 深部Worker、deep/BLA/color WASM、reference資産を遅延生成し、Worker数を実測とmemory budgetで増減する。
- deep/BLA/colorはmainで各1回だけcompileし、compile済み`WebAssembly.Module`をWorkerへstructured cloneする。Worker sourceから埋込payload、palette LUT、RGBA生成の重複を除き、Workerは数値fieldだけを返す。
- 通常jobから学習したstrip時間とWorker数をkernel version・端末特性keyで永続化し、次回起動で再利用する。
- 通常jobから学習したstrip時間とWorker数をkernel version・端末帯域keyで永続化し、30日TTL、schema version、storage拒否fallbackを付けて次回起動で再利用する。
- fixed-point direct、perturbation、rebase、BLA、局所guarded directを共通tile schedulerへ接続した。
- deep切替を固定zoom閾値ではなくpixel stepとULPの比、hysteresisで決める。
- deep切替とguard bitsへ直近orbitのglitch未確定補修率を加え、精度昇格時は低精度で作った基準軌道を破棄して再構築する。
@ -27,6 +27,7 @@
- f64解析・収縮判定は`INTERIOR_LIKELY`のまま保持し、ValidatedではPP+64双方の固定小数点整数不等式で主カージオイド周期2球を証明できたsampleだけを`INTERIOR_PROVEN`へ昇格する。
- 精度優先のdeep描画では、基準軌道もPP+64で独立再計算して疎なcheckpointとescape位置を照合し、不一致時はglobal reference全体を32 bit昇格して作り直す。
- 1×2×4×custom、1×2×2 AA、BalancedValidated、進捗、取消に対応する独立tile exportを実装した。
- Balanced Exportは未確定数をmetadataへ記録して完了可能とし、Validated Exportは未確定sampleが残る場合にfinal PNGを出さず明示失敗する。
- Export sidecarへViewSpec、precisioniteration policy、未確定sample数、backend、全kernel SHA-256、色空間、encoder、paletteを記録する。
- version付きURL、戻る進む、座標・spanの正確値入力copy、UndoRedoを実装した。
- 初回UI、一般statusと診断status、mobile compact status、keyboard操作、live region、visible focus、44px target、reduced motiontransparencyを実装した。
@ -37,7 +38,7 @@
- `src/shallow_kernel.c``src/deep_kernel.c``src/bla_kernel_v18.c``src/color_kernel.c``src/abi.json`を配置した。
- `toolchain.lock.json`でclang 17.0.6、target、SIMDscalar flagsを固定した。
- 公式SHA-256を照合したclang 17.0.6とNode.js v22.18.0で8 WASMを再buildした。
- clang 17.0.6とNode.js v22.18.0で8 WASMを再buildした。tool配布archiveのURLSHA-256はrepositoryに保持されていないため、再監査可能な証拠範囲はversionflags、source hash、生成manifest、golden結果までとする。
- shallow、deep、BLA、colorのgoldenとSIMDscalar同値gateに合格後、payloadと`kernels.js`を昇格した。
- `scripts/build-kernels.ps1`は同一payloadだけを自動deduplicateし、異なるCOLOR variantsも正しく保持する。
- `scripts/build-hosted.ps1`は現manifest以外の古いcontent-hashed WASMを削除する。
@ -48,7 +49,7 @@
- Standalone: `dist/standalone/`
- 再現build: `build/wasm-v23/`
現在の非圧縮基準値は、Hosted first view 142,046 bytes、Standalone 170,593 bytesである。詳細なfile hashは`audit/v23-source-baseline.json`に記録した。
現在の非圧縮基準値は、Hosted first view 144,044 bytes、Standalone 172,591 bytesである。詳細なfile hashは`audit/v23-source-baseline.json`に記録した。v22成果物が残っていないため、v22の転送bytescompile timeに対するnon-regressionは判定不能であり、このv23値を今後の比較起点とする。
## 合格済みgate
@ -64,7 +65,7 @@
- 本体、shallow Worker、deep WorkerのJavaScript構文: pass
- mock DOM初期化とv23 diagnostics: pass
- compile済みdeepBLAcolor Moduleのstructured clone・instantiate: pass
- Node固定scene性能基準と処理量契約: pass`audit/v23-node-performance.json`
- Node固定sceneの計測記録と処理量予算式の構造契約: pass`audit/v23-node-performance.json`。browser時間目標の達成を意味しない
- HostedStandalone buildと全payload SHA-256: pass
toolchainを利用できる環境では次で省略なしに再実行できる。
@ -75,7 +76,7 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/test-all.ps1 -Re
## Browser acceptance gate
`tests/browser-benchmark.html``tests/scenes.json`のmobiledesktop4K profileと6 sceneを走査し、Preview、Covered、Refined、idle schedulerwrite、long task、logical memory、canvas SHA-256をJSON化する。desktop/z0ではwheel連打、省電力がPreviewで停止すること、標準がCovered後にAA・追加反復を開始しないこと、即時再着色、64px Export、Validated Export取消、機械判定できるaccessibilityも実行し、結果へacceptance判定を付ける。今回の環境ではbrowser bindingがなかったため、`audit/v23-browser-baseline.json`意図的に`not-run`のままである。
`tests/browser-benchmark.html`1 runを1 profile・1 buildへ限定し、`?profile=desktop&target=hosted`のように実行する。iframe寸法によるDPR模擬を廃止し、非zero wheelのtransform-to-paint、30回以上のPreview、実際のFine→Refined、raw pixel golden、long task、Export metadata完了前download再現性、keyboard操作を自動判定する。HostedとStandaloneを別に開く。visual golden未登録、observed browser/GPU memory、visible focus、page zoom/pinch、screen-reader順序は外部gateとして残り、自動項目が通ってもfull acceptanceを返さない。今回の環境ではbrowser bindingがなかったため、`audit/v23-browser-baseline.json``not-run`のままである。
残る実機確認:
@ -87,3 +88,5 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/test-all.ps1 -Re
- keyboard-only、focus、44px target、live status、page zoomとcanvas pinchの操作確認
このgateが合格するまで、v23の実装完了と数値gate合格は主張できるが、固定実機に対する性能目標の達成は主張しない。
browser非依存の現行成果物監査は`audit/v23-browserless-baseline.json`に保存した。公式SHA-256を照合したNode.js v22.18.0 portable binaryで7 executable testsが合格し、sourcedocument contract、851 pixel mapping、1,440 pixel contract、Module cloneを再確認した。deep runtimeは出典付きの混在境界sceneで特性を測るが、値は`measured-not-acceptance`である。v22比較は`not-verifiable`、source-WASM再buildは保存済みprior evidenceの再利用として明示している。

View file

@ -1,9 +1,12 @@
# マンデルブロ集合ビューワー 軽量・高精細化 改善提案
- 対象: Mandelbrot ∞ Zoom v22
- 対象baseline: Mandelbrot ∞ Zoom v22
- 文書status: v22に対する実装前提案。採用項目はrenderer v23へ実装済みであり、現行状態と残存gateは`IMPLEMENTATION_REPORT.md``COMPLETION_AUDIT.md`を正とする。
- Document status: historical-v22 / implemented-v23
- 証拠境界: v22成果物とGit snapshotは現workspaceに残っていないため、v22の行番号・bytesは履歴記録であり再監査可能なbaselineではない。以後の比較基準はversion付きJSONとSHA-256を必須とする。
- 作成日: 2026-08-22
- 対象ファイル: `index.html`, `script.js`, `kernels.js`, `src/bla_kernel_v18.c`
- 性格: 実装前の技術提案。効果値は、既存監査の引用を除き検証すべき目標または仮説である。
- 性格: 設計判断を保存する履歴文書兼、修正版の品質契約。効果値は、version付き監査JSONの引用を除き検証すべき目標または仮説である。
## 1. 結論
@ -27,12 +30,14 @@
| 配布の軽さ | 初回ロード・解析・WASMコンパイルが小さい | 転送bytes、起動時に取得/compileする資産 |
| 実行の軽さ | 操作中と待機中にCPU/GPUを浪費しない | long task、idle CPU、Canvas draw回数、消費電力 |
| メモリの軽さ | DPR・Worker数・参照長が増えても予算内に収める | peak memory、cache bytes、Workerごとの固定領域 |
| ラスター精細度 | 表示ピクセルを実サンプルで覆い、境界を適切にAAする | target coverage、samples/pixel、継ぎ目 |
| ラスター精細度 | 表示ピクセルを実サンプルで覆い、境界を適切にAAする | sample coverage、resolved coverage、samples/pixel、継ぎ目 |
| 数値精度 | 座標・反復・BLA近似の誤差を検出し、未解決を黒と混同しない | 高精度referenceとの差、unresolved数、検証モード |
| 出力精細度 | 表示プレビューとは独立して、指定寸法を完了後に保存する | 出力寸法、AA、再現性、未完了pixel数 |
「Canvasが4Kサイズである」だけでは高精細とはみなさない。低解像度フレームを4K Canvasへ拡大した状態と、4K分のサンプルを実計算した状態を区別する。
本書のcoverageは次の2値へ分ける。sample coverageは宣言したiteration/precision policyで新ViewSpecのsampleを計算済みのtarget pixel比率であり、`UNRESOLVED`も計算済みsampleとして含む。resolved coverageは`ESCAPED`またはpolicy上の`INTERIOR_PROVEN`へ確定した比率である。Coveredはsample coverage 100%、Validatedはsample coverage 100%に加えてunresolved 0を要求する。
## 3. 現状評価
### 3.1 残す価値が高い部分
@ -47,7 +52,7 @@
既存のv18監査では、v17比のbase-frame改善が2.07〜5.15倍で、テストした内部早期終了は黒/外部分類不一致0だった (`AUDIT.md:60-82`, `AUDIT.md:163-176`)。この実績があるため、BLAや内部早期終了を一律に廃止する理由はない。
ただし、現行コードはv22であるのに、性能監査と最終ベンチの表題・データはv18である。既存値は設計判断の参考には使えるが、v22の受入基準にはできない。最初に同じsceneをv22で再計測する必要がある
提案作成時の現行コードはv22だったのに、性能監査と最終ベンチの表題・データはv18だった。既存値は設計判断の参考には使えるが、v22の受入基準にはできない。当初は同じsceneのv22再計測を必要条件としたが、現在は不変なv22成果物が残っていないため比較を`not-verifiable`とし、version固定したv23以後のbaselineだけを回帰判定へ使う
### 3.2 主要なギャップ
@ -99,19 +104,19 @@
|---|---|---|---|---|
| Reprojected | wheel / pinch / pan継続中 | 既存フレームのtransformのみ | 入力停止待ち | プレビュー |
| Preview | 入力停止80〜120ms後 | 時間予算内の低解像度。近似BLA可 | 最優先tileを表示 | 高速描画 |
| Covered | Preview後 | 選択した処理モードのtarget gridを全域tile計算 | target coverage 100%、再投影pixel 0 | 全域描画 68% |
| Covered | Preview後 | 選択した処理モードのtarget gridを全域tile計算 | sample coverage 100%、再投影pixel 0。未確定数は別表示 | 全域描画 68% |
| Refined | Covered後のidle | 境界・不確実tileだけ2×/4× adaptive AA | 対象tile完了 | 境界AA完了 |
| Validated | 精度優先時またはExport | 宣言した有限反復・精度policyを局所照合 | checks合格かつunresolved 0。残る場合は別状態「検証未完了・未確定N」 | 検証完了 / 検証未完了 |
| Validated | 精度優先時またはValidated Export | 宣言した有限反復・精度policyを局所照合 | checks合格かつunresolved 0。残る場合は別状態「検証未完了・未確定N」 | 検証完了 / 検証未完了 |
重要な契約は次の通り。
- Previewは粗くてよいが、新しい未解決画素を「内部」と断定して黒にしない。
- Coveredはズーム深度に関係なく、確定したScreen Canvas gridを新しいViewSpecの実sampleだけで全域計算する。再投影した旧frameは描画中のpresentationにだけ使い、coverageへ数えない。
- 標準モードは明示したeffective DPR、精細モードは原則native DPR、Exportは指定gridをtargetにする。DPRを画素予算で下げた場合は実解像度を表示する。
- Coveredはズーム深度に関係なく、確定したScreen Canvas gridを新しいViewSpecの実sampleだけで全域計算する。再投影した旧frameは描画中のpresentationにだけ使い、sample coverageへ数えない。計算済み`UNRESOLVED`はsample coverageへ数えるがresolved coverageへは数えない。
- native DPRは上限であり、標準・精細とも明示したpixel/memory budget内で使う。精細は時間適応で解像度を下げないが、memory/pixel上限は維持する。DPRを制限した場合はeffective DPRと実解像度を表示する。
- Refinedは単なる2倍補間ではなく、複数の実サンプルをlinear-lightでresolveする。
- Validatedは数学的なマンデルブロ集合所属証明ではなく、宣言した有限反復・precision policyの照合完了を表す。全sampleへ誤差境界または証明手続きを適用する将来モードだけを`Certified`と呼ぶ。
- 「高精細」と「数値検証完了」を同じチェックボックスにしない。
- Exportは表示Canvasとは独立したjobであり、view、寸法、反復方針、AA、乱数seedを固定してから開始する。
- Exportは表示Canvasとは独立したjobであり、view、寸法、反復方針、AA、precision tier、乱数seedを固定してから開始する。Balanced ExportとValidated Exportを区別し、前者は未確定数をmetadataへ記録して完了可能、後者だけをunresolved 0必須とする。
処理モードと描画状態は分ける。
@ -120,7 +125,9 @@
| 省電力 | 自動Coveredなし。Previewは最大約1MP | Preview |
| 標準 | shallowはmobile低memory約2MP、desktop約4MP。deepはPreview実測から約1.4秒のCovered予算へeffective DPRを下げる | Covered。AA・未確定追加反復は自動実行しない |
| 精細 | mobile低memory約4MP、desktop約8MP。上限時は実解像度を明示 | Refined。未確定追加反復は境界候補だけを上限付きで行う |
| 精度優先 / Export | 指定gridとprecision policy | Validated。将来はCertifiedを選択可 |
| 精度優先 | 指定gridとprecision policy | Validated。将来はCertifiedを選択可 |
Exportは処理モードではなく独立jobである。Balanced / Validatedのprecision tierを選び、それぞれRefined相当 / Validated相当の完了条件を適用する。
## 5. 推奨アーキテクチャ
@ -196,7 +203,7 @@ Previewの目安は80〜120ms、停止後baseは400〜700msとし、その後は
- deep main-thread WASM、deep worker source、Worker poolは、数値条件が閾値へ近づいた時点で準備する。
- Workerは最初1〜2個だけ作り、実jobのthroughputから必要時だけ増やす。
- 現在のReal Wisdomは深部HQへ到達した各ページセッションのidle時に1回、最大4 Workerなら合計10回のmini renderを追加する (`script.js:333-348`, `script.js:698`)。通常jobから受動学習し、能動ベンチは診断時または不確実時だけにする。
- 学習結果はkernel versionと端末特性をkeyに永続化する
- 学習結果はkernel versionと最小限の端末帯域特性をkeyに永続化する。schema versionと保存時刻を持たせ、既定TTL 30日で失効させる。storage拒否時はsession内学習だけで動作し、raw timing履歴や過剰なfingerprint属性を保存しない
### 6.5 WASMとメモリを統合・動的化する — P1
@ -260,7 +267,7 @@ cr = centerRe + span * (2*x + 1 - width) / (2*width)
ci = centerIm + span * (height - 2*y - 1) / (2*width)
```
JS f64、shallow WASM、deep WASM、BLA、direct fallback、detail、exportについて、同一pixelが同一座標になるcross-backend testを追加する。整数格子式だけで現在のtile seamが実証されたわけではないが、規約を統一して将来のbackend差を防ぐ。この変更は既存URLのview中心を変えない一方、画像sampleは半画素分変化し得るため、renderer/golden image versionを上げる。
canonical座標は上の有理数とする。fixed-point化では分子の符号と対称なround-to-nearest、exact halfはaway-from-zeroへ丸める`roundDiv`をABI契約とし、積を先に計算してから1回だけ除算する。JS f64、shallow WASM、deep WASM、BLA、direct fallback、detail、exportについて、同一pixelがcanonical座標を各backendの表現可能範囲で再現するcross-backend testを追加する。「backend差0」は整数のfixed-point表現または分類・iteration契約に対して用い、f64座標は宣言ULP以内を基準にする。整数格子式だけで現在のtile seamが実証されたわけではないが、規約を統一して将来のbackend差を防ぐ。この変更は既存URLのview中心を変えない一方、画像sampleは半画素分変化し得るため、renderer/golden image versionを上げる。
### 7.2 Covered完了後にadaptive AAする — P0/P1
@ -309,7 +316,7 @@ adaptive AAを再着色可能にするには、境界pixelのsubsampleごとのs
- `ESCAPED`: 発散が確認されたもの
- `UNRESOLVED`: iteration/work/precision上限へ達したもの
Coveredの描画中は未解決領域へ中立色または直前結果をpresentationとして重ねられるが、それを新ViewSpecのcoverageへ数えない。未解決の境界tileだけ反復を段階的に継続する。これなら、旧work capを復活させずに計算量を局所化できる。
Coveredの描画中は未解決領域へ中立色または直前結果をpresentationとして重ねられる。旧frameの再投影はsample coverageへ数えないが、新ViewSpecでpolicy上限まで計算した`UNRESOLVED`はsample coverageへ数え、resolved coverageと未確定数で別に監査する。精細では未解決の境界tileだけ反復を段階的に継続する。これなら、旧work capを復活させずに計算量を局所化できる。
収縮による内部早期終了は既存監査で有効だったためBalancedでは`INTERIOR_LIKELY`として維持する。Validatedでは判定根拠をconfidenceへ記録し、policyが要求する場合は誤差境界付き解析判定・周期証明へ上げられないものを`UNRESOLVED`へ戻す。
@ -336,16 +343,16 @@ Coveredの描画中は未解決領域へ中立色または直前結果をpresent
- reference orbitをchunk化し、固定150,001点・最大140,000反復の上限をmemory budgetへ置き換える。
- 上限へ達した場合は「∞」を暗黙に名乗らず、現在の反復/参照/精度上限と未解決数を表示する。
multi-referenceやdouble-doubleは高難度である。v22基準計測で、reference生成、glitch fallback、10^-280以深のどれが実際に支配的か確認してから実装する。
multi-referenceやdouble-doubleは高難度である。version固定した現rendererのbrowser計測で、reference生成、glitch fallback、10^-280以深のどれが実際に支配的か確認してから実装する。v22欠落分を現renderer値で遡及的に代用しない。
### 7.8 Exportを独立pipelineにする — P0/P1
`PNG`を次の2操作へ分ける。
- Quick snapshot: 現在見えているCanvasを保存。Previewである可能性を明示する。
- High-quality export: 1× / 2× / 4× / custom、AA、Balanced / Validated、進捗、取消を指定する。Certifiedは誤差境界経路を実装した後に追加する。
- High-quality export: 1× / 2× / 4× / custom、AA、Balanced / Validated、進捗、取消を指定する。Balancedは未確定sampleをmetadataへ記録して完了可能、Validatedはchecks合格かつunresolved 0だけをfinalとして出す。Certifiedは誤差境界経路を実装した後に追加する。
High-quality exportでは、固定したViewSpecをtile描画し、全tile完了後にだけencodeする。座標、span、bits、iteration policy、palette、AA、precision tier、backend、kernel hash、SIMD/scalar、色空間、encoder、renderer versionをPNG metadataまたはsidecar JSONへ記録する。同じ条件では規定許容差内で再現し、bit単位再現が必要な場合はscalar mathとencoderまで固定する。
High-quality exportでは、固定したViewSpecをtile描画し、全tile完了後にだけencodeする。開始前に最大辺、sample数、予約bytesをpreflightし、予算超過・allocation失敗は解像度を黙って下げず明示失敗する。座標、span、bits、iteration policy、palette、AA、precision tier、backend、kernel hash、SIMD/scalar、色空間、encoder、renderer versionをPNG metadataまたはsidecar JSONへ記録する。同じ条件では規定許容差内で再現し、bit単位再現が必要な場合はscalar mathとencoderまで固定する。
## 8. 既存仕様の改廃
@ -370,7 +377,7 @@ High-quality exportでは、固定したViewSpecをtile描画し、全tile完了
| world-space cache | 維持・再設計 | style非依存field cache、quadtree key、byte budget |
| base iteration + adaptive slider | 変更 | Auto policyは維持。生値はAdvancedへ移し、未確定数を表示 |
| view hash / URL共有 | 強化 | version付きViewSpec、gesture settle保存、戻る/進む復元 |
| 「∞ Zoom」表記 | 条件付き維持 | 参照長・iteration・precision上限と未確定数を併記 |
| 「∞ Zoom」表記 | 廃止 | 有限の参照長・iteration・precision上限を持つため「Deep Zoom」とし、現在の上限と未確定数を併記 |
| UI初期非表示 | 変更 | 初回だけ操作ヒントを表示し、以後は利用者設定を復元 |
低優先度の整理対象として、未使用の`MILLION``last``sx/sy`、未使用`pilot`引数、常にfalseの`strictBoundary`、v22時点で同一内容だったCOLOR SIMD/scalar payloadがある。性能改善というより、仕様とコードのずれを減らすためにテスト後に削除する。実装後の再現buildではCOLOR variantsが異なるため、generatorは同一時だけ自動deduplicateする。
@ -390,16 +397,28 @@ High-quality exportでは、固定したViewSpecをtile描画し、全tile完了
## 10. 実装ロードマップ
### Phase 0 — v22基準化
P0/P1/P2は利用者影響のseverityであり、実装順ではない。各項目はphase、依存先、mandatory / telemetry-conditional、未通過gateを追跡する。後段実装を先行できても、前段gateを通過したとは扱わない。
### Phase 0A — browser非依存の証拠基準化
- shallow/deep/colorを含む全kernel sourceを回収または復元し、固定toolchain、compile flags、生成script、payload checksumを揃える。
- z0 / z14 / z20 / z100 / period-3 interior / boundary / 10^-280付近を固定sceneにする。
- z0 / z14 / z20 / z100 / period-3 interior / boundary / 10^-280付近をversion付き固定sceneにする。中心座標はspan相応のguard digitsを持つdecimalかexact rationalとし、precision kind、oracle、期待分類、iteration policyをscene schemaへ保存する。
- v22成果物が残っている場合は全対象hashとbaselineをimmutableに保存する。残っていない場合は「v22比較不能」と機械可読に記録し、後からv23値で代用しない。
- 現行`exactDeepPixel`とは独立した、複数精度一致と誤差上限を持つ高精度reference実装で、分類、iteration、smooth valueを比較する。
- source、ABI、pixel mapping、tile seam、SIMD/scalar、解析的内部証明、Module clone、配布物hash、文書内の生成値を同じcommandで検証する。
- v18監査はarchiveと明記し、現renderer baselineを別JSONにする。
Exit gate: 全kernelをsourceから再現buildでき、version付きsceneとoracleで数値baselineを再生成でき、全成果物hashと文書内の生成値が一致する。過去baseline欠落は明示的な`not-verifiable`でありpassにしない。
### Phase 0B — 固定browser baseline
- desktop、mobile相当、4Kでcold / warmを測る。
- first preview、Covered完了、Refined完了、idle job/draw、peak memory、long taskを記録する。
- 現行`exactDeepPixel`とは独立した、複数精度一致と誤差上限を持つ高精度reference実装で、分類、iteration、smooth valueを比較する。
- v18監査はarchiveと明記し、v22 baselineを別JSONにする。
- Hosted / Standalone、SIMD / scalar、Worker / Workerなしを対象構成matrixに従って分けて実行する。
- viewportのCSS寸法だけでDPRを模擬しない。mobile / desktop / 4Kは要求DPRを持つ別browser contextまたは別実機runとし、各runの実viewport・実DPR・browser build・OS・電源・試行数を保存する。
- visualはraw pixelまたはlinear-light fieldのversion付きgoldenと比較し、hash文字列が生成できただけではpassにしない。
Exit gate: 全kernelを再現buildでき、v22の速度・memory・数値baselineを同じcommandで再生成できる。
Exit gate: 対象profileごとのbrowser acceptanceが合格し、cold / warm、操作p95、visual、idle、long task、observed memory、Export、accessibilityのraw結果を保存する。binding不在などで未実施の場合、後続実装は進められるがbrowser性能・操作・表示の達成を主張しない
### Phase 1A — Runtime quick wins
@ -420,7 +439,7 @@ Exit gate: idle時に予約RAF/timer/background job 0、wheel中full render 0、
- renderer/golden image versionを上げる。
- detail BLAをbase HQ以上へ修正する。
Exit gate: 同一sample座標のbackend差0、tile境界回帰0、既存URLのview中心不変を確認する。
Exit gate: canonical rationalに対するfixed-point整数一致とf64の宣言ULP基準、tile境界回帰0、既存URLのview中心不変を確認する。
### Phase 2 — Tile/field pipeline
@ -429,11 +448,11 @@ Exit gate: 同一sample座標のbackend差0、tile境界回帰0、既存URLのvi
- shallowもWorker化し、全backendを共通tile protocolへする。
- Reprojected / Preview / Covered / Refined状態機械を導入する。
- palette非依存field bufferと即時再着色を導入する。
- 処理モード別target coverage 100%を実装する。
- 処理モード別sample coverage 100%とresolved coverage未確定数の分離を実装する。
- ULPベースのshallow/deep切替を導入する。
- `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を導入し、局所反復継続を実装する。
Exit gate: target coverage 100%、再投影pixelの誤加算0、false `INTERIOR_PROVEN` 0、logical memory budget内。
Exit gate: sample coverage 100%、再投影pixelの誤加算0、未確定数の独立計上、false `INTERIOR_PROVEN` 0、logical memory budget内。
### Phase 3A — Refine / Export
@ -465,35 +484,37 @@ Phase 4は、telemetryで支配コストまたは誤差が確認された項目
## 11. 受入基準
browser時間値は、profileごとに固定したbrowser build・OS・viewport・実DPR・電源条件で、warmup後30回以上を採取してp95を算出する。単発値、同期event handler時間、異なるsceneを混ぜた分位点は代用しない。Visual golden、数値oracle、threshold policyには独立したversion IDを与え、threshold欠落時はpassにしない。
| 分野 | 基準案 |
|---|---|
| Idle | 最終更新2秒後に予約RAF/timer/background job 0、Canvas/DOM write 0回/秒。CPUは同環境blank baselineとの差を補助指標にする |
| 操作 | transform反映p95 < 16ms、main thread 50ms超long task 0、wheel中に新規full renderを開始しない |
| Preview | 基準端末・固定sceneでgesture settle後p95 < 120ms |
| Covered | target Screen Canvas coverage 100%、effective DPRを表示し、再投影pixelをcoverageへ数えない。scene別時間budgetはPhase 0後に確定 |
| Refined | 対象pixelは記録済み実sampleのlinear-light resolve。Canvas補間だけをAAと数えず、heuristicであることを明示 |
| 数値 | false `ESCAPED` 0、false `INTERIOR_PROVEN` 0、escape iteration差・smooth誤差・`INTERIOR_LIKELY` mismatch・UNRESOLVED数を別々に記録 |
| Pixel mapping | 全backendで同一pixelのworld座標が一致し、tile境界に1px/半pxの回帰がない |
| Export | 指定寸法どおり、全tile完了前はfinal fileを出さない、取消可能、宣言したbackend/encoder条件で許容差内再現 |
| 操作 | 最後の実入力eventから、その入力を反映した再投影frameのpaint完了までのp95 < 16ms、計測区間のmain thread 50ms超long task 0、継続wheel中に新規full renderを開始しない |
| Preview | settle期限到達からPreview paint完了まで、profile・sceneごとの30回以上でp95 < 120ms |
| Covered | target Screen Canvasのsample coverage 100%、effective DPRを表示し、再投影pixelをsample coverageへ数えない。resolved coverageとunresolved数を別記録。scene別時間budgetはPhase 0B後に確定 |
| Refined | Fine modeで実際に`REFINED`へ到達し、対象pixelは記録済み実sampleのlinear-light resolve。subsample accountingとversion付きgoldenを比較し、Canvas補間だけをAAと数えない |
| 数値 | version付きcorpusoraclethreshold policyを必須とする。Validatedではcorpus上のfalse `ESCAPED` 0、false `INTERIOR_PROVEN` 0、unresolved 0。escape iteration差・smooth誤差・`INTERIOR_LIKELY` mismatchはpolicyの明示閾値で判定し、閾値未定義ならfail |
| Pixel mapping | canonical rationalとties-away-from-zeroの`roundDiv`を基準に、fixed-point backendは期待整数と一致、f64 backendは宣言ULP以内。tile境界に1px/半pxの回帰がない |
| Export | 指定寸法どおり、全tile完了前はfinal fileを出さない、取消可能、metadata必須項目とsample countを検査。Balancedはunresolved数を記録、Validatedは0。宣言したbackend/encoder条件とversion付きgoldenの許容差内で再現 |
| Memory | managed typed array/WASM/cacheは暫定mobile 96 MiB、desktop 192 MiB以内。Canvas/GPUを含むobserved peakは固定環境で別記録 |
| Startup | Hosted shallow表示ではdeep取得/compile 0。Standaloneはdeep復号/compileとWorker生成0 |
| 配布 | shallow first viewの取得bytes・compile timeをv22 baselineより増やさず、Hostedではdeep到達前のdeep request 0 |
| Accessibility | keyboard-only操作、visible focus、44px target、label/name、live status、page zoom、reduced motion/transparencyが合格 |
| Regression | v22 scene corpusをCIで実行し、速度・memory・分類・例外を履歴化する |
| 配布 | v22 baseline欠落のため初回比較は`not-verifiable`とする。v23の同一request集合・Content-Encoding・cache条件を新baselineとして固定し、以後の取得bytes・compile timeを回帰判定する。Hostedではdeep到達前のdeep request 0 |
| Accessibility | keyboard-onlyで全操作を実行し、visible focus、44px target、label/name、live status、page zoomとCanvas pinchの共存、reduced motion/transparencyの実動作が合格。静的属性の存在だけではpassにしない |
| Regression | version付き現行scene corpusをCIで実行し、速度・memory・分類・例外を履歴化する。各metricのwarn/fail閾値をpolicyへ保存する |
端末依存の時間値は、対象端末を固定して初めて合否に使う。絶対時間だけでなく、同一scene・同一pixel数の比率とcorrectnessを主要指標にする。
端末依存の時間値は、対象端末を固定して初めて合否に使う。絶対時間だけでなく、同一scene・同一pixel数の比率とcorrectnessを主要指標にする。browser非依存gateはsourceABIhash数値oraclepixel mappingscene精度document consistencyまでを証明し、paint、DPR、GPU、入力、download、実accessibilityの代替とはみなさない。
## 12. 推奨する最初の実装単位
最初の変更セットは、次の範囲に限定すると効果を測りやすい。
1. v22 baselineと全WASMの再現buildを用意する。
1. v22 baselineの有無を機械可読に確定し、現renderer baselineと全WASMの再現buildを用意する。
2. 常時RAFと毎frame DOM更新を廃止する。
3. wheelをgestureとしてデバウンスし、非表示時のjobを止める。
4. Screen Canvasへpixel budgetを設ける。
5. deep poolを必要時まで作らない。
6. 既存`q`のままPreview時間予算へ`renderPerf`を接続する。
7. v22用のidle・gesture・startup・memory回帰テストを追加する。
7. version付き現renderer用のidle・gesture・startup・memory回帰テストを追加する。
この段階ではBLAカーネルやpixel contractをまだ変えない。まずidle負荷、入力応答、Canvas/初期memory、Preview時間を改善する。画素中心変更は、全kernelを再現buildできるPhase 1Bで行う。
@ -522,7 +543,7 @@ Phase 4は、telemetryで支配コストまたは誤差が確認された項目
| perturbation | 高精度な基準軌道との差分だけを低精度で追跡する摂動法 |
| rebase | 差分が不安定になったとき、基準軌道上の別位置へ基準を移す処理 |
| DPR / effective DPR | OS/browserが示すdevice pixel比 / pixel budget適用後に実際に使う比率 |
| target coverage | 選択したScreen Canvas gridのうち、新ViewSpecで実計算済みの割合 |
| sample / resolved coverage | 選択したScreen Canvas gridのうち、新ViewSpecでpolicy上限まで実計算済みの割合 / `ESCAPED`またはpolicy上の`INTERIOR_PROVEN`へ確定した割合 |
| AA | Anti-Aliasing。1 pixel内の複数sampleをresolveして境界のジャギーを減らす処理 |
| SIMD | 1命令で複数データを処理するWASM最適化 |
| ULP | その浮動小数値付近で表現できる隣接値の間隔 |
@ -542,4 +563,4 @@ Phase 4は、telemetryで支配コストまたは誤差が確認された項目
| OffscreenCanvasなし | Workerはfield/RGBA bufferを返し、main Canvasへ合成 | Export encodeと合成の一部がmain thread |
| Standalone / `file://` | 埋込WASM互換build。deepの復号・compileは遅延 | streaming、HTTP cache、COOP/COEP、Service Workerなし |
最低対応browser versionとoffline cache方針は、Phase 0で実機matrixを作って確定する。
最低対応browser versionとoffline cache方針は、Phase 0Bで実機matrixを作って確定する。

115
MIGRATION_V24.md Normal file
View file

@ -0,0 +1,115 @@
# v23 → v24.1.3 WebGPU migration
## v24.1.3 browser hotfix
実WebGPUブラウザで`direct` shaderのparse時に、WGSL予約語`meta` / `smooth`をstorage変数名へ使用していることが判明しました。両識別子を`fieldMeta` / `fieldSmooth`へ改名し、WGSL 16.2 reserved-word listを全shader sourceへ機械照合するtestを追加しました。
同時に、shader初期化失敗を通常のWebGPU非対応と同じ扱いにしてCPU f64 rendererへ落としていた挙動を廃止しました。旧挙動は数百万pixel × 数百iterationをmain-thread fallbackで実行し、shader errorの直後にUIが極端に重くなる原因でした。v24.1.3ではshader/pipeline failureは描画を停止して明示し、CPU fallbackは`navigator.gpu`がない/adapterがない場合だけ使います。screen pixel budgetも標準約1.5Mdesktopへ縮小しました。
## 最終アーキテクチャ
```text
Main thread
UI / BigInt ViewState
├─ shallow ─────────────────────────────┐
│ │
└─ deep → Reference Worker │
BigInt orbit P+64 │
checkpoint guard P+128 │
hi/lo f32 packing │
│ │
└──────────────┐ │
▼ ▼
WebGPU numeric
f32 direct / guarded
rescaled perturbation
fieldMeta + fieldSmooth field
GPU color / AA
GPU presentation
```
CPU側に残る高精度処理はview座標とreference orbit生成だけで、pixel rendererではありません。
## BLAを最終版から外した理由
初期WebGPU版ではCPU f64でBLAを作り、GPU用f32係数へ量子化して評価していました。しかし`swirly-seahorses-z12`を61×39へ高密度化した回帰で、BLAあり経路がbounded pixelをescapedへ反転する反例を検出しました。
f64で導出した受入半径へf32係数量子化誤差を安全に織り込む設計が未確立だったため、閾値調整で残すのではなくproduction BLAを完全に除去しました。
## Deep numeric policy
Deepではpixel offsetを
```text
delta = w * 2^scaleExp
```
として保持します。通常反復はf32の正規化座標で行い、値域が偏った時だけ16 bit単位でrescaleします。
さらにroundoff上界を追跡します。
- escape候補で `|z| - error > 2` の時だけescapedを確定
- max iteration到達時もabsolute errorがpolicy閾値以下の場合だけ`INTERIOR_LIKELY`
- 不確かな場合は`UNKNOWN`
閾値:
```text
Balanced: 1e-3
Strict: 1e-4
```
Strictは同じ1-pass perturbationの受入閾値を厳しくするpolicyです。以前の「Fast → unresolved queue → Strict再実行」は、BLA撤去後には同じ式を二度計算するだけだったため削除しました。
## 既知反例の固定
`swirly-seahorses-z12`の61×39 gridで旧版が誤った3 pixelをregressionへ固定しています。
```text
(25,12) oracle bounded
(26,15) oracle bounded
(27,25) oracle escaped @ 1957
```
v24.1.3ではBalanced/Strictともこれらを誤分類せず`UNKNOWN`へ退避します。
## Presentation / recolor
数値field (`fieldMeta`, `fieldSmooth`) はGPU常駐です。色変更はGPU recolorだけで、Mandelbrot反復を再実行しません。
描画中にrecolor操作が入った場合はrender tokenを破壊せず`recolorPending`へ集約し、数値frame完了後に最新色を適用します。旧版にあった`rendering=true`残留レースを避けています。
## Export
旧巨大Canvas方式は廃止しました。
- max side 16,384
- 512² tile
- reusable GPU export workspace
- 2×2 AA: 4 GPU samples → GPU linear-light resolve → 1 readback/tile
- PNG: scanline bands → deflate stream → PNG chunks
- sidecar: exact BigInt view + `unresolvedSamples`
raw 16K RGBA全体をCanvasへ保持しないため、GPU texture limit回避だけでなくCPU raw-imageメモリも抑えます。
## Fallback
WebGPU contextを早期取得すると、pipeline初期化失敗後に同じCanvasを2Dへ切り替えられません。v24.1.3ではshader/pipeline作成を先に行い、成功後にWebGPU canvas contextを取得します。
WebGPU unavailable時はshallow f64 fallbackのみです。deepをNumberへunderflowさせて描画する経路は削除しました。
## 削除したv23 production資産
- deep WASM renderer / Worker pool
- BLA WASM renderer
- color WASM
- BigInt direct pixel renderer
- CPU field → ImageData hot path
- Canvas detail tile cache
- Validated direct Export
BigInt direct相当はテストoracleにのみ存在します。

134
README.md
View file

@ -1,70 +1,110 @@
# Mandelbrot Deep Zoom v23
# Mandelbrot Deep Zoom v24.1.3 — WebGPU
軽量な操作表示と、画素中心規約・数値field・境界AA・高精度出力を分離したマンデルブロ集合ビューアーです。無条件の「∞」表記は廃止し、現在のbit数、反復上限、未確定数を表示します。
v23のCPU/WASM deep pixel rendererを撤去し、画素計算・field・彩色・再投影・高解像度ExportをWebGPUへ移した版です。
設計判断は[IMPROVEMENT_PROPOSAL.md](IMPROVEMENT_PROPOSAL.md)、実装・検証状況は[IMPLEMENTATION_REPORT.md](IMPLEMENTATION_REPORT.md)、提案ごとの対応と保留条件は[COMPLETION_AUDIT.md](COMPLETION_AUDIT.md)を参照してください。
## 数値構成
## 実行
- ViewState: BigInt固定小数点。深度に応じてbit数を自動拡張。
- Shallow: WebGPU `f32` direct iteration。f32座標分解能に十分な余裕があるviewだけで使用。
- Deep reference: 専用WorkerでBigInt固定小数点orbitを1本生成。referenceはview精度より64 bit高く生成し、さらに+64 bitのguard orbitでcheckpoint照合。
- Deep pixels: WebGPU `guarded rescaled f32 perturbation`。spanはmantissa + exponentへ分離し、`1e-400`級でもpixel offsetをf32 absolute値へ潰さない。
- Error handling: roundoff上界がescape/bounded判定へ影響し得るpixelは `FIELD_UNKNOWN` にする。UNKNOWNを内部点へ偽装しない。
- BLA: **productionでは無効・未搭載**。旧f32量子化BLAで境界pixelのmembership反転を再現したため、再導入していない。
Standalone版は`index.html``script.js``kernels.js`の3ファイルを同じディレクトリに置き、`index.html`を直接開けます。
旧v23の`Validated direct`は削除しています。v24.1.3のStrictは誤差許容閾値を厳しくする保守的GPU policyで、任意精度direct全画素証明ではありません。PNG sidecarは常に `membershipCertified:false`す。
配信用Hosted版はPowerShellから生成します。
## 描画パイプライン
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ./scripts/build-hosted.ps1
通常表示ではfieldをGPUに保持します。
```text
BigInt ViewState
└─ deep時: BigInt reference Worker
WebGPU direct / guarded perturbation
fieldMeta + fieldSmooth GPU buffers
GPU color / optional boundary smoothing
offscreen texture
GPU reprojection / canvas
```
生成先は`dist/hosted/`です。`dist/wasm/`も同じoriginで配信し、`_headers`相当のCOOP/COEPとWASM MIME設定を反映してください。Standalone成果物は`./scripts/build-standalone.ps1`で生成できます。
パレット・cycle・shift・HQ変更は数値fieldを再計算せずrecolorします。pan / wheel / pinch操作中は直前frameをGPUで再投影し、settle後に新しい数値frameを計算します。
## v23の主要仕様
## Export
- 静止時の常時RAFとDOM更新を廃止し、invalidate時だけ描画します。
- wheel/pinch/pan中は既存frameの再投影だけを行い、110ms停止後にPreviewを1回開始します。
- `Reprojected → Preview → Covered → Refined → Validated/検証未完了`を内部状態として分離します。
- Coveredは選択したeffective DPRのCanvas全域を実sampleで計算します。固定2100px HQ上限はありません。
- 全backendのpixel mappingは`(x+0.5, y+0.5)`です。
- escape値と分類をpalette非依存fieldに保持し、色変更では集合を再計算しません。
- `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を区別し、未確定を内部点と同じ黒として扱いません。
- 境界detailは2×2 subsample fieldを保持し、linear-lightでresolveします。
- High-quality exportは指定寸法、2×2 AA、進捗、取消、JSON sidecarに対応します。現在の表示を即時保存するQuick snapshotも別操作です。
- deep Workerは必要時に1基だけ生成し、deepを離れた時点で大容量instanceとreferenceを退役させます。
- cacheとScreen Canvasは端末別の論理byte/pixel budgetで管理します。
高解像度PNGは最大辺16,384 pxです。
処理モードの自動到達点:
- 512×512以下のGPU tileで計算。
- Export用GPU buffers/textures/readback bufferは固定workspaceを再利用し、tileごとの大量生成を避ける。
- 2×2 AAは4 sampleをGPUで計算し、GPUでlinear-light resolveした後、tileにつき1回だけreadback。
- 全画像Canvasを確保せず、scanline bandを`CompressionStream('deflate')`へ送りPNGを構築。
- sidecar JSONへ`unresolvedSamples`を記録。
| モード | 自動処理 |
|---|---|
| 省電力 | 約1MP以下のPreviewで停止 |
| 標準 | shallowは2〜4MP。deepは48px幅の初回測定後、約1.4秒予算へ全域gridを調整。境界AAと未確定追加反復は既定で行わない |
| 精細 | 4〜8MPの全域描画、境界AA、境界候補の上限付き追加反復 |
| 精度優先 | 精細処理後にPP+64照合を含む検証 |
`unresolvedSamples > 0`は、数値policyがそのsampleを安全に分類できなかったことを意味します。
## 互換構成
## WebGPU unavailable
| 構成 | shallow | deep | 出力 | 備考 |
|---|---|---|---|---|
| Hosted + SIMD + Worker | WASM SIMD Worker | BLA/perturbation Worker | 全機能 | 推奨 |
| Hosted + SIMDなし | scalar payload | scalar deep/BLA | 全機能 | SIMD検査後にscalarを取得 |
| Hosted + Workerなし | main WASM | high-precision BigInt direct | 全機能、低速 | deep WASMは取得せずdirectへ移行 |
| Standalone `file://` | 埋込WASM | 埋込Worker/WASM | 全機能 | 3ファイル、オフライン可 |
| OffscreenCanvasなし | Canvas 2D | 同左 | 全機能 | 現行標準経路 |
| SharedArrayBufferなし | tile/chunk取消 | tile/chunk取消 | 全機能 | 現行標準経路 |
WebGPUが利用できない場合は浅部のみJavaScript f64 fallbackを使います。deep zoomは誤画像を出さず、「このズーム深度はWebGPUが必要です」と表示します。
## 検査
WebGPU自体が存在しない場合だけ軽量な浅部JavaScript fallbackを使います。**WGSL/shader/pipeline初期化エラー時はCPU全画面fallbackへ自動移行しません**。エラーを表示して停止し、shader不具合を隠したままCPUを占有しない設計です。`webgpu` canvas contextはpipeline作成成功後に取得します。high-performance adapterが得られない場合は通常のadapter requestも再試行します。
固定toolchainを含む全gateは次で実行します。
## v24.1.3 hotfix
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ./scripts/test-all.ps1 -RequireToolchain
実ブラウザで発覚したWGSL parse errorを修正しました。WGSL 16.2で予約されている`meta` / `smooth`をstorage-buffer変数名に使っていたため、`fieldMeta` / `fieldSmooth`へ変更しています。全shaderをWGSL reserved-word一覧へ照合するNode gate `tests/v24-wgsl-reserved.mjs`も追加しました。
表示負荷も見直し、標準モードのscreen pixel budgetをdesktop約1.5M / 小型端末約0.75Mへ縮小しました旧版は約4M / 2M。省電力は約0.5M、精細は最大約3M、Strictは最大約2Mです。WebGPU非対応時のCPU fallbackは約0.25M pixelに制限します。
## Standalone
`index.html`, `gpu-kernels.js`, `script.js`の3ファイルで動作します。外部WGSL fetchはありません。
```text
index.html
gpu-kernels.js
script.js
```
toolchainなしでsource contractだけ確認する場合は次を実行します。
ブラウザが`file://`上でWebGPUを許可しない構成ではlocalhost/HTTPSで開いてください
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ./tests/source-contract.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File ./tests/kernel-source-contract.ps1
## Test
```bash
npm test
npm run build
```
WASM checksumは`./scripts/extract-wasm.ps1`で再生成します。Nodeテストは本体Worker構文、WASM Module clone、数値精度、解析的内部証明、pixel mappingcontractを検査します。固定sceneは`tests/scenes.json`にあります。HTTPでworkspaceを配信すれば、`tests/browser-benchmark.html`からcold/warm描画、Covered/Refined、idle write、logical memoryをscene corpus単位で採取できます。
`npm test`は以下を検査します。
WASM source buildと昇格gateは[BUILD_REPRODUCIBILITY.md](BUILD_REPRODUCIBILITY.md)を参照してください。v18の旧性能監査は履歴資料であり、v23の基準値ではありません。
- JS syntax / source contracts
- WGSL reserved-word token audit (`meta`, `smooth`, `ref`等を含む仕様予約語)
- 旧C/WASM deep assetがproduction treeに残っていないこと
- BigInt reference Worker + guard checkpoints
- shallow f32 CPU model vs BigInt oracle
- guarded perturbation CPU f32 model vs BigInt oracle
- `swirly-seahorses-z12`高密度回帰と既知3反例
- BLAがproductionから除去されていること
- tile/pixel geometry
- `1e-400` / `1e-1000` coordinate formatting
- streaming PNG structure / CRC / inflate / abort
- real-WebGPU acceptance harnessのJS syntax
### 実GPU acceptance
`tests/webgpu-acceptance.html`をWebGPU対応browserで開きます。これは実adapter上でshader compile / compute / readbackを行い、BigInt oracleと比較します。
必須gate:
- stable corpus scene: sampled `UNKNOWN = 0`
- swirly scene: 25 sample中12以上を確定
- `falseEscaped = 0`
- `falseBounded = 0`
- reference guard mismatch = 0
- 既知dense反例で誤分類しない
- 1× / 2×2 Export smokeの未確定sample = 0
- uncaptured WebGPU validation error = 0
このリポジトリを生成した実行環境では`navigator.gpu`が公開されなかったため、実adapter gateだけは未実行です。静的/CPU-model gateの代替ではありません。

View file

@ -0,0 +1,82 @@
{
"environment": "Chromium 144 headless, 780x441 viewport, container-local",
"scenes": [
{
"name": "seahorse_z14",
"bits": 288,
"first": {
"render": "789 ms / 3490 回 · BLA 54.395ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.7424271106719971
},
"base": {
"render": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.28958845138549805
},
"engine": "WASM SIMD ×2 · BLA e-28 + リベース / 288 bit",
"renderText": "288 ms / 3490 回 · BLA 52.502ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z20",
"bits": 320,
"first": {
"render": "401 ms / 4509 回 · BLA 5.023ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.4155466556549072
},
"base": {
"render": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.26917600631713867
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 320 bit",
"renderText": "238 ms / 4509 回 · BLA 4.768ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z100",
"bits": 576,
"first": {
"render": "482 ms / 17085 回 · BLA 0.076ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.5742058753967285
},
"base": {
"render": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.5136466026306152
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース / 576 bit",
"renderText": "474 ms / 17085 回 · BLA 0.069ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "period3_z20",
"bits": 320,
"first": {
"render": "1420 ms / 4509 回 · BLA 0.395ms/kpx · 内部省略 100%",
"badge": "深部リベース",
"saw": true,
"wall": 1.5150198936462402
},
"base": {
"render": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
"badge": "深部リベース",
"saw": true,
"wall": 0.8927633762359619
},
"engine": "WASM SIMD ×2 · BLA e-23 + リベース · 内部早期終了 100% / 320 bit",
"renderText": "781 ms / 4509 回 · BLA 0.373ms/kpx · 内部省略 100%",
"canvas": "780x441"
}
],
"errorCount": 0
}

View file

@ -10,7 +10,9 @@
"outcome": "blocked",
"recovery": "Connect an in-app Browser, then rerun tests/browser-benchmark.html. Unrelated browser-control backends are intentionally not substituted. Node runtime-budget coverage is available in audit/v23-node-performance.json but does not replace the browser gate."
},
"runner": "tests/browser-benchmark.html",
"runner": "tests/browser-benchmark.html?profile=<mobile|desktop|4k>&target=<hosted|standalone>",
"executionModel": "one real-DPR profile and one build target per run; iframe sizing does not emulate DPR",
"requiredTargets": ["hosted", "standalone"],
"requiredProfiles": ["mobile", "desktop", "4k"],
"acceptancePending": ["visual output", "requested viewport and actual DPR fidelity", "interaction p95", "Preview/Covered/Refined timing", "Power stops at Preview", "Standard stops at Covered without AA/continuation", "idle writes", "long tasks", "observed browser/GPU peak memory", "export download/cancel", "keyboard/focus/target-size/live-status/page-zoom accessibility"]
}

View file

@ -0,0 +1,267 @@
{
"format": "mandelbrot-browserless-audit-v23",
"generatedUtc": "2026-08-22T12:35:37.5161338Z",
"status": "pass",
"scope": "browserless-current-artifacts",
"fullAcceptance": false,
"rendererVersion": 23,
"node": {
"version": "v22.18.0",
"archive": {
"file": ".tmp-node-v22.18.0-win-x64.zip",
"sha256": "c95d8a7e1c99e669cc08c9f1176e068c1f50847c37908fcb8c35b62482366511",
"source": "https://nodejs.org/dist/v22.18.0/node-v22.18.0-win-x64.zip"
}
},
"contracts": {
"kernel": {
"status": "pass",
"modules": 4,
"compiler": "17.0.6",
"pixelContract": "sample centers at x + 0.5, y + 0.5"
},
"source": {
"status": "pass",
"rendererVersion": 23,
"wasmPayloads": 8,
"scriptBytes": 129807,
"htmlBytes": 11395
},
"document": {
"status": "pass",
"proposalContract": "revised-v23",
"scenes": 7,
"viewports": 3,
"numericPolicy": "numeric-policy-v23",
"hashedFiles": 13,
"browserRunner": "static-contract-pass",
"browserlessEvidence": "pass-with-explicit-limits"
}
},
"executableTests": {
"js-syntax.mjs": {
"status": "pass",
"rendererVersion": 23,
"lastPass": "preview",
"automaticTarget": "COVERED",
"fieldIterations": [
7,
100
],
"modeProbe": {
"targets": {
"power": "PREVIEW",
"standard": "COVERED",
"fine": "REFINED",
"validate": "VALIDATED"
},
"coldDeepPreview": [
48,
36
]
},
"referenceCheckpoints": {
"agreement": true,
"catchesMismatch": true,
"count": 17,
"bits": 320
},
"deepWorkerModuleInit": true,
"browserHarnessSyntax": true,
"sources": {
"appBytes": 129807,
"shallowWorkerCharacters": 1509,
"deepWorkerCharacters": 8788
}
},
"module-clone.mjs": {
"status": "pass",
"structuredClone": true,
"results": [
{
"name": "deep-simd.wasm",
"bytes": 4866,
"exports": [
"render_perturb_rebase_rect"
]
},
{
"name": "bla-simd.wasm",
"bytes": 4959,
"exports": [
"build_bla",
"render_bla_rect_v2"
]
},
{
"name": "color-simd.wasm",
"bytes": 1387,
"exports": [
"smooth_batch"
]
}
]
},
"precision-reference.mjs": {
"status": "pass",
"precisions": [
256,
320
],
"results": [
{
"id": "outside",
"p": 3,
"guarded": 3,
"stable": true
},
{
"id": "boundary-escape",
"p": 33,
"guarded": 33,
"stable": true
},
{
"id": "period3-center",
"p": 4000,
"guarded": 4000,
"stable": true
}
]
},
"analytic-interior.mjs": {
"status": "pass",
"checked": 14,
"precisions": [
256,
320
],
"proof": "integer cardioid/period-2 inequalities"
},
"pixel-mapping.mjs": {
"status": "pass",
"samples": 851,
"backends": [
"shallow",
"deep",
"bla"
],
"subsampleScales": [
2,
4
],
"contract": "centered-rational",
"float64UlpMax": 4,
"fixedPointRounding": "nearest-ties-away-from-zero"
},
"pixel-contract.mjs": {
"status": "pass",
"backend": "simd",
"samples": 1440,
"mismatches": 0,
"contract": "(x+0.5,y+0.5)"
},
"runtime-budget.mjs": {
"format": "mandelbrot-node-runtime-budget-v23",
"generatedUtc": "2026-08-22T12:35:33.428Z",
"node": "v22.18.0",
"status": "measured",
"acceptance": false,
"sceneCorpus": "mandelbrot-scene-corpus-v2",
"measurementNote": "Runtime characterization only; browser acceptance has separate thresholds and evidence.",
"contracts": {
"previewBudget110": true,
"modeTargets": true,
"screenBudgets": true,
"boundedContinuation": true,
"coldDeepCap": true,
"measuredDeepBudget": true,
"viewportIndependentFloor": true
},
"shallow": [
{
"id": "overview-350",
"width": 640,
"height": 400,
"iterations": 350,
"pixels": 256000,
"medianMs": 85.583,
"msPerMegapixel": 334.30859375,
"meanIterations": 78.3253515625,
"samplesMs": [
85.583,
88.88799999999998,
84.14580000000001,
84.10900000000004,
92.09339999999997
]
},
{
"id": "boundary-900",
"width": 512,
"height": 320,
"iterations": 900,
"pixels": 163840,
"medianMs": 113.91780000000006,
"msPerMegapixel": 695.2990722656253,
"meanIterations": 154.49669189453124,
"samplesMs": [
115.31709999999998,
113.91780000000006,
112.1979,
112.90809999999988,
117.25350000000003
]
}
],
"deep": {
"id": "swirly-seahorses-z12-bla-2000",
"width": 256,
"height": 144,
"iterations": 2000,
"pixels": 36864,
"blaEntries": 12,
"buildMs": 0.27119999999990796,
"medianMs": 395.4109000000003,
"msPerMegapixel": 10726.207139756953,
"unresolved": 0,
"samplesMs": [
388.1887999999999,
394.21360000000004,
413.6929,
395.4109000000003,
396.3185000000003
]
}
}
},
"runtime": {
"status": "measured-not-acceptance",
"report": "audit/v23-browserless-runtime.json",
"reason": "Runtime values are environment/load dependent and no browser paint/input/GPU threshold is asserted."
},
"sourceWasmBuild": {
"status": "prior-evidence-reused",
"manifest": {
"file": "build/wasm-v23/manifest.json",
"bytes": 4193,
"sha256": "cc388d664f642181d45a110ea5b44c47524050f8bf904ebd115e8e24c0f58a18"
},
"reason": "Verified generated manifest reused; fixed toolchain was removed after the successful gate."
},
"v22Comparison": {
"status": "not-verifiable",
"reason": "No immutable v22 artifacts, hashes, or versioned baseline are present."
},
"browserAcceptance": {
"status": "not-run",
"report": "audit/v23-browser-baseline.json",
"nonSubstitutable": [
"actual DPR and visual output",
"transform-to-paint and Preview p95",
"long tasks and observed browser/GPU memory",
"download/cancel behavior",
"keyboard/focus/page-zoom/screen-reader behavior"
]
}
}

View file

@ -0,0 +1,73 @@
{
"format": "mandelbrot-node-runtime-budget-v23",
"generatedUtc": "2026-08-22T12:35:33.428Z",
"node": "v22.18.0",
"status": "measured",
"acceptance": false,
"sceneCorpus": "mandelbrot-scene-corpus-v2",
"measurementNote": "Runtime characterization only; browser acceptance has separate thresholds and evidence.",
"contracts": {
"previewBudget110": true,
"modeTargets": true,
"screenBudgets": true,
"boundedContinuation": true,
"coldDeepCap": true,
"measuredDeepBudget": true,
"viewportIndependentFloor": true
},
"shallow": [
{
"id": "overview-350",
"width": 640,
"height": 400,
"iterations": 350,
"pixels": 256000,
"medianMs": 85.583,
"msPerMegapixel": 334.30859375,
"meanIterations": 78.3253515625,
"samplesMs": [
85.583,
88.88799999999998,
84.14580000000001,
84.10900000000004,
92.09339999999997
]
},
{
"id": "boundary-900",
"width": 512,
"height": 320,
"iterations": 900,
"pixels": 163840,
"medianMs": 113.91780000000006,
"msPerMegapixel": 695.2990722656253,
"meanIterations": 154.49669189453124,
"samplesMs": [
115.31709999999998,
113.91780000000006,
112.1979,
112.90809999999988,
117.25350000000003
]
}
],
"deep": {
"id": "swirly-seahorses-z12-bla-2000",
"width": 256,
"height": 144,
"iterations": 2000,
"pixels": 36864,
"blaEntries": 12,
"buildMs": 0.27119999999990796,
"medianMs": 395.4109000000003,
"msPerMegapixel": 10726.207139756953,
"unresolved": 0,
"samplesMs": [
388.1887999999999,
394.21360000000004,
413.6929,
395.4109000000003,
396.3185000000003
]
}
}

View file

@ -1,12 +1,12 @@
{
"format": "mandelbrot-source-baseline-v23",
"generatedUtc": "2026-08-22T10:48:34.5532410Z",
"generatedUtc": "2026-08-22T12:30:30.4538464Z",
"rendererVersion": 23,
"contract": {
"status": "pass",
"rendererVersion": 23,
"wasmPayloads": 8,
"scriptBytes": 129452,
"scriptBytes": 129807,
"htmlBytes": 11395
},
"hosted": {
@ -23,8 +23,8 @@
},
{
"file": "dist/hosted/script.js",
"bytes": 129452,
"sha256": "97a1a3d897fa0f7bce3a30727a0e90a767799356b12899c074fdb04968506ace"
"bytes": 129807,
"sha256": "6ffebe51532fb8a141e9b1939e586d76b6c79db18786c06713e67ffe8d4ca78a"
},
{
"file": "dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm",
@ -32,9 +32,10 @@
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
}
],
"firstViewUncompressedBytes": 143689,
"deepRequestsBeforeDeepView": 0,
"note": "Transfer compression, parse, compile, and runtime timings require the fixed browser benchmark."
"firstViewUncompressedBytes": 144044,
"deepRequestContractBeforeDeepView": 0,
"deepRequestMeasurementStatus": "not-run",
"note": "The zero is a static loader contract, not a measured request count. Transfer compression, request count, parse, compile, and runtime timings require the fixed Hosted browser benchmark."
},
"standalone": {
"files": [
@ -45,8 +46,8 @@
},
{
"file": "dist/standalone/script.js",
"bytes": 129452,
"sha256": "97a1a3d897fa0f7bce3a30727a0e90a767799356b12899c074fdb04968506ace"
"bytes": 129807,
"sha256": "6ffebe51532fb8a141e9b1939e586d76b6c79db18786c06713e67ffe8d4ca78a"
},
{
"file": "dist/standalone/kernels.js",
@ -54,7 +55,7 @@
"sha256": "e97726c09af138da92b331c376766c1128c10aa3c361b83750356824262ea1a0"
}
],
"uncompressedBytes": 172236
"uncompressedBytes": 172591
},
"sourceBuild": {
"files": [

View file

@ -0,0 +1,6 @@
{
"status": "pass",
"kind": "bla-disabled-contract",
"reason": "dense swirly regression demonstrated unsafe f32-quantized BLA classifications",
"productionBla": false
}

View file

@ -0,0 +1,266 @@
{
"status": "pass",
"kind": "guarded-production-equation-cpu-f32-model-not-real-gpu",
"thresholds": {
"balanced": 0.001,
"strict": 0.0001
},
"report": [
{
"mode": "balanced",
"id": "period2-cusp-z14",
"bits": 285,
"limit": 1200,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "balanced",
"id": "swirly-seahorses-z12",
"bits": 279,
"limit": 2000,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 71,
"known": 116,
"exactEscape": 110,
"mismatch": []
},
{
"mode": "balanced",
"id": "period2-cusp-z20",
"bits": 305,
"limit": 1800,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "balanced",
"id": "period2-cusp-z100",
"bits": 571,
"limit": 2400,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "balanced",
"id": "period3-interior",
"bits": 267,
"limit": 1200,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "balanced",
"id": "period2-cusp-e280",
"bits": 1171,
"limit": 2400,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "balanced",
"id": "period2-cusp-e400",
"bits": 1569,
"limit": 1600,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "period2-cusp-z14",
"bits": 285,
"limit": 1200,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "swirly-seahorses-z12",
"bits": 279,
"limit": 2000,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 71,
"known": 116,
"exactEscape": 110,
"mismatch": []
},
{
"mode": "strict",
"id": "period2-cusp-z20",
"bits": 305,
"limit": 1800,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "period2-cusp-z100",
"bits": 571,
"limit": 2400,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "period3-interior",
"bits": 267,
"limit": 1200,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "period2-cusp-e280",
"bits": 1171,
"limit": 2400,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
},
{
"mode": "strict",
"id": "period2-cusp-e400",
"bits": 1569,
"limit": 1600,
"guardMismatch": 0,
"falseEscaped": 0,
"falseBounded": 0,
"unknown": 0,
"known": 187,
"exactEscape": 0,
"mismatch": []
}
],
"denseRegression": [
{
"mode": "balanced",
"x": 25,
"y": 12,
"oracle": 2000,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 2.1991906643574973e+21
}
},
{
"mode": "strict",
"x": 25,
"y": 12,
"oracle": 2000,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 2.1991906643574973e+21
}
},
{
"mode": "balanced",
"x": 26,
"y": 15,
"oracle": 2000,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 7.768308137076909e+21
}
},
{
"mode": "strict",
"x": 26,
"y": 15,
"oracle": 2000,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 7.768308137076909e+21
}
},
{
"mode": "balanced",
"x": 27,
"y": 25,
"oracle": 1957,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 2084483903264.6707
}
},
{
"mode": "strict",
"x": 27,
"y": 25,
"oracle": 1957,
"gpu": {
"kind": "unknown",
"n": 2000,
"reason": "error-bound",
"err": 2084483903264.6707
}
}
]
}

View file

@ -0,0 +1,24 @@
{
"status": "v24.1.3-real-webgpu-rerun-pending",
"gate": "real-webgpu-compile-dispatch-numeric-acceptance",
"environment": {
"browser": "Chromium 144.0.7559.96 Debian GNU/Linux 13",
"display": "container/Xvfb",
"navigatorGpuPreviouslyObserved": false,
"latestAttempt": "GPU command-buffer/context initialization failed before acceptance completed",
"adapterAccepted": false
},
"attemptedModes": [
"headless Chromium default",
"--enable-unsafe-webgpu",
"--enable-unsafe-webgpu --use-angle=swiftshader",
"Xvfb + unsafe WebGPU"
],
"interpretation": "The reported real-browser v24.1.2 parse failure is fixed in source in v24.1.3. This container still cannot obtain a real WebGPU adapter, so v24.1.3 compile/dispatch must be rerun on a WebGPU-capable browser/device.",
"externalObservation": {
"version": "24.1.2",
"result": "shader-parse-failed",
"error": "WGSL reserved identifiers meta and smooth in direct shader",
"hotfix": "renamed to fieldMeta/fieldSmooth; reserved-word static gate added"
}
}

View file

@ -0,0 +1,196 @@
{
"format": "mandelbrot-v24.1.3-release-manifest",
"generatedAt": "2026-08-23T16:17:33+09:00",
"files": [
{
"path": "AUDIT_V24.md",
"bytes": 4405,
"sha256": "ef09e435b24a64bdbf0872e9f182c31ab2edea5afd0c959a5681d2b9be87ddbf"
},
{
"path": "MIGRATION_V24.md",
"bytes": 5185,
"sha256": "2a4fe1754c521901b0e1f2c99d46426055fd3dc2932da93df12ea01a15469839"
},
{
"path": "README.md",
"bytes": 5532,
"sha256": "5d3e7db74400db1fb0585f23b966d1eab08f3729fe5f253ef533060f65f1790d"
},
{
"path": "audit/legacy-v23/final-browser-benchmark.json",
"bytes": 2686,
"sha256": "bca2e141c101722f8a077d43346c2c28b5d8d7c99e70415730031065c8ab5a1c"
},
{
"path": "audit/v24-bla-status.json",
"bytes": 177,
"sha256": "84587eb2183b1e6a2837f5018e74f32bc968c536d64cee7cf5fed55f7d3b21df"
},
{
"path": "audit/v24-cpu-numeric-model.json",
"bytes": 5391,
"sha256": "f25e830d831f828b84f327670b5be5e9afcf6118164b2d217a9eae24443f84bf"
},
{
"path": "audit/v24-real-webgpu-status.json",
"bytes": 1066,
"sha256": "9791925d9ddb890644b895d4cd659d7377d7c48dd94df86495fef04ee1325c8a"
},
{
"path": "audit/v24-release-verification.json",
"bytes": 290,
"sha256": "6cd0dd2b293bae482e8cbd511caa50aebe34597ee2c12b442de75fa7a937dccd"
},
{
"path": "audit/v24-test-summary.json",
"bytes": 3373,
"sha256": "260173d15640a9681ae049f3e1c2707c774cf2af7a4fc4d6475115e7be344e8c"
},
{
"path": "dist/hosted/_headers",
"bytes": 68,
"sha256": "3d11be43c945209eb7e4503e384ac3007c42ee4022cfc23fc005f6217bda8d1b"
},
{
"path": "dist/hosted/gpu-kernels.js",
"bytes": 12196,
"sha256": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a"
},
{
"path": "dist/hosted/index.html",
"bytes": 11416,
"sha256": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc"
},
{
"path": "dist/hosted/script.js",
"bytes": 58865,
"sha256": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61"
},
{
"path": "dist/standalone/gpu-kernels.js",
"bytes": 12196,
"sha256": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a"
},
{
"path": "dist/standalone/index.html",
"bytes": 11416,
"sha256": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc"
},
{
"path": "dist/standalone/script.js",
"bytes": 58865,
"sha256": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61"
},
{
"path": "gpu-kernels.js",
"bytes": 12196,
"sha256": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a"
},
{
"path": "index.html",
"bytes": 11416,
"sha256": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc"
},
{
"path": "package.json",
"bytes": 199,
"sha256": "0fa1b8155f09356fba73e94c8db6428f1367aefef40efd116ad89c89a8a8a13f"
},
{
"path": "script.js",
"bytes": 58865,
"sha256": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61"
},
{
"path": "scripts/build.mjs",
"bytes": 657,
"sha256": "84706582dd01d6de28cbc0fcdba9758c3f6f72a3d5693b74736109b35819997f"
},
{
"path": "scripts/test-all.mjs",
"bytes": 818,
"sha256": "1fbebd942d5c3ee94a1cbb449a29168c50b0bc8d3285cbae1c7c01eb188b29ec"
},
{
"path": "tests/legacy-v23/numeric-policy-v23.json",
"bytes": 787,
"sha256": "7afe223d27715a79e34f8c9ea11993f1c37e75bc5d49a6b3284f412d8171a00d"
},
{
"path": "tests/scenes.json",
"bytes": 3225,
"sha256": "a92c5535c54106ce18823443244f115bf5a2a3d42abfc4ee3573f38e8234e553"
},
{
"path": "tests/v24-acceptance-contract.mjs",
"bytes": 818,
"sha256": "4bc444c694934d8bfd4a56ab597ab9fe834867ed6bc97a36c8182f99c25e39a6"
},
{
"path": "tests/v24-bla-model.mjs",
"bytes": 677,
"sha256": "04f22c659b61635792b9662b718ec9ae6140189a3daf553ac580b841ee48bd6b"
},
{
"path": "tests/v24-coordinate-format.mjs",
"bytes": 1253,
"sha256": "a993f04f12fceb4d38c16206636f7951d8bc4afad63a57491e0164bb3575ca3a"
},
{
"path": "tests/v24-cpu-numeric-model.mjs",
"bytes": 8197,
"sha256": "8f24ccd80bf00b5b13e09a1b31bddf6df1c17dc8d7541e04ea1e7421165a2ce0"
},
{
"path": "tests/v24-direct-model.mjs",
"bytes": 2452,
"sha256": "9c9ba9cfeaf5e352059a639ba1014ae7a3b0382309a634ee128bc99e62ee3dc5"
},
{
"path": "tests/v24-geometry-contract.mjs",
"bytes": 1177,
"sha256": "9403a8d1efe6d0d5547bd7ebaa966578dea1ba7630f094fc117e0ae4fe395996"
},
{
"path": "tests/v24-index-contract.mjs",
"bytes": 461,
"sha256": "ffeeccba4b72613f061c7add2f8cdecc03a6c01e2e5a01d4f2a2cf53f94345ef"
},
{
"path": "tests/v24-png-stream-model.mjs",
"bytes": 3411,
"sha256": "ca4dee6252bf6545310ba52c1433e82e83886d04cdb1feb20c7ab08045f431a7"
},
{
"path": "tests/v24-reference-worker.mjs",
"bytes": 2140,
"sha256": "4d637c549cdda2c28666f5bfe2925c5742103c44b752c337cadbe9bc9df6431d"
},
{
"path": "tests/v24-source-contract.mjs",
"bytes": 3805,
"sha256": "ce3e8dc4be17f19af362087772e7a17b59d7e8e007c5e19f00153899fe0a20af"
},
{
"path": "tests/v24-tree-contract.mjs",
"bytes": 691,
"sha256": "933c8c5035806584ee3ffc7645b14aa60f262920528ebb8e5e47666023174770"
},
{
"path": "tests/v24-wgsl-reserved.mjs",
"bytes": 2260,
"sha256": "98c153ac038852d3402f36ee18ce1f8263de1a016240b4363f01d84a31f0f0ab"
},
{
"path": "tests/webgpu-acceptance.html",
"bytes": 405,
"sha256": "89b03a08f248b048eead508fc3599057a8cf5afc1d9f846de206a69272dd95bd"
},
{
"path": "tests/webgpu-acceptance.js",
"bytes": 6072,
"sha256": "c38fbeca176a464bfb6d4a0685f66469065ba3392a65f0617215a85e3fccbeb0"
}
]
}

View file

@ -0,0 +1,12 @@
{
"status": "pass",
"version": "24.1.3",
"generatedAt": "2026-08-23T16:17:15+09:00",
"preZipCleanCopy": {
"npmTest": "pass",
"npmBuild": "pass"
},
"wgslReservedWords": "pass",
"sourceDistHashesMatch": true,
"realWebGpu": "pending: container has no usable adapter"
}

View file

@ -0,0 +1,94 @@
{
"status": "v24.1.3-static-cpu-build-pass-real-webgpu-rerun-pending",
"rendererVersion": 24,
"shaderVersion": "24.1.3",
"packageVersion": "24.1.3",
"production": {
"legacyDeepAssets": 0,
"bigIntPixelRenderer": false,
"referenceWorker": "BigInt fixed-point reference at viewBits+64 with viewBits+128 guard checkpoints",
"gpuDeep": "guarded rescaled f32 perturbation",
"productionBla": false,
"strictRetryPass": false,
"gpuFieldColorPresentation": true,
"recolorWithoutNumericRecompute": true,
"reusableExportWorkspace": true,
"streamingPng": true,
"gpuAaResolve2x2": true,
"maxExportSide": 16384,
"membershipCertified": false
},
"nodeGates": {
"status": "pass",
"directFalseEscaped": 0,
"directFalseBounded": 0,
"deepFalseEscaped": 0,
"deepFalseBounded": 0,
"deepGuardMismatch": 0,
"swirlyUnknownBalanced": 71,
"swirlyKnownBalanced": 116,
"denseKnownCounterexamples": "all conservatively UNKNOWN",
"coordinateDepths": [
"1e-400",
"1e-1000"
],
"pngStreamModel": "pass",
"acceptanceHarnessContract": "pass"
},
"build": {
"status": "pass",
"standaloneFiles": [
"index.html",
"gpu-kernels.js",
"script.js"
],
"hostedFiles": [
"index.html",
"gpu-kernels.js",
"script.js",
"_headers"
]
},
"realWebGPU": {
"status": "rerun-pending-for-v24.1.3",
"v24.1.2ObservedFailure": "reserved-word parse error from real WebGPU browser",
"v24.1.3SourceFix": "static reserved-word gate passes",
"containerAdapterAccepted": false
},
"generatedAt": "2026-08-23T16:17:15+09:00",
"hotfix": {
"wgslReservedWords": "pass: all 5 WGSL modules contain no WGSL 16.2 reserved-word tokens",
"renamedIdentifiers": [
"meta -> fieldMeta",
"smooth -> fieldSmooth"
],
"shaderFailureFallback": "CPU full-frame fallback disabled for shader/pipeline failures",
"screenPixelBudget": {
"power": 524288,
"standardDesktop": 1572864,
"standardSmall": 786432,
"fineDesktop": 3145728,
"strictDesktop": 2097152,
"noWebGpuFallback": 262144
},
"errorBadge": "compiler/runtime errors truncated to 120 chars in status UI; full diagnostics retained"
},
"hashes": {
"index.html": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc",
"script.js": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61",
"gpu-kernels.js": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a",
"dist/standalone/index.html": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc",
"dist/standalone/script.js": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61",
"dist/standalone/gpu-kernels.js": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a",
"dist/hosted/index.html": "adf0dc65d2cfbb6ca56494651d40fc6874aedcd088f4170650d97fdde9c0e5dc",
"dist/hosted/script.js": "eca75aaba2e859ab73d79a46a76329ffb41c438e413bf24f7a4b21328f8f0e61",
"dist/hosted/gpu-kernels.js": "aab457adce3cf1bf52eca6416c7c9570f4feb3579580a8cc95ae2425f8aea68a"
},
"cleanCopyVerification": {
"directory": "v24_hotfix_verify_pre",
"npmTest": "pass",
"npmBuild": "pass",
"sourceHashesMatch": true,
"sourceDistHashesMatch": true
}
}

10
dist/hosted/_headers vendored
View file

@ -1,11 +1,3 @@
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
X-Content-Type-Options: nosniff
/wasm/*
Content-Type: application/wasm
Cache-Control: public, max-age=31536000, immutable
/hosted/*
Cache-Control: no-cache
Referrer-Policy: no-referrer

258
dist/hosted/gpu-kernels.js vendored Normal file
View file

@ -0,0 +1,258 @@
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x0fffffffu;
const STATUS_UNRESOLVED:u32=0xfffffffeu;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
fn pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=64.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, _numeric0:u32, _numeric1:u32, _numeric2:u32,
spanMant:f32, spanExp:i32, sampleX:f32, sampleY:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct UnresolvedHead{ remaining:atomic<u32>, _p0:u32, _p1:u32, _p2:u32 };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32){
fieldMeta[out]=pack_meta(n,FIELD_UNKNOWN); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-0.5*f32(p.fullW))/f32(p.fullW);
let dy=(0.5*f32(p.fullH)-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2<f32>(p.spanMant*dx,p.spanMant*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
var errScaled=64.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n);}
return;
}
if(m>p.refLen){mark_unresolved(out,n);return;}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMant*dx,p.spanMant*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=64.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n);return;}
let r=refs[m];
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=64.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
const COLOR_WGSL=String.raw`
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(20,22,30)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
let n=f32(m&0x0fffffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
if(p.edgeAA!=0u){
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
let x=i32(gid.x); let y=i32(gid.y);
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x0fffffffu)-i32(m&0x0fffffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
`;
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.1.3',DIRECT_F32_WGSL,DEEP_PERTURB_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
});
})();

View file

@ -1,10 +1,10 @@
<!doctype html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#050813">
<title>Mandelbrot Deep Zoom v23</title>
<title>Mandelbrot Deep Zoom v24.1.3 WebGPU</title>
<style>
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
@ -34,13 +34,13 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大"></button><button id="zout" aria-label="中心を縮小"></button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">精度優先</option></select><output></output></div>
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0.001" max="0.05" step="0.0005" value="0.008"><output id="cycleO" for="cycle">0.0080</output></div>
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
<details><summary>詳細設定・正確な座標</summary>
<div class="line"><label for="iters">基準反復</label><input id="iters" type="range" min="100" max="2500" step="25" value="350"><output id="itersO" for="iters">350</output></div>
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> 境界AAを追加</label></div>
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> GPU境界平滑化</label></div>
<div class="exact-grid"><label for="coordReInput">実部</label><input id="coordReInput"><label for="coordImInput">虚部</label><input id="coordImInput"><label for="coordSpanInput">表示幅</label><input id="coordSpanInput"></div>
<div class="mini-actions"><button id="coordApply">座標を適用</button><button id="coordCopy">正確値をコピー</button><button id="undoView">戻す</button><button id="redoView">進む</button></div>
</details>
@ -57,13 +57,13 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
<label for="exportWidth">px</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×高速</option><option value="2">2×2 AA</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="validated">Validated direct</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="strict">保守的 (Strict)</option></select>
</div>
<progress id="exportProgress" max="1" value="0" hidden></progress>
<div id="exportStatus" role="status" aria-live="polite"></div>
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
</dialog>
<script type="module" src="./hosted-loader.js"></script>
<script src="gpu-kernels.js"></script>
<script src="script.js"></script>
</body>
</html>

1132
dist/hosted/script.js vendored

File diff suppressed because it is too large Load diff

258
dist/standalone/gpu-kernels.js vendored Normal file
View file

@ -0,0 +1,258 @@
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x0fffffffu;
const STATUS_UNRESOLVED:u32=0xfffffffeu;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
fn pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=64.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, _numeric0:u32, _numeric1:u32, _numeric2:u32,
spanMant:f32, spanExp:i32, sampleX:f32, sampleY:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct UnresolvedHead{ remaining:atomic<u32>, _p0:u32, _p1:u32, _p2:u32 };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32){
fieldMeta[out]=pack_meta(n,FIELD_UNKNOWN); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-0.5*f32(p.fullW))/f32(p.fullW);
let dy=(0.5*f32(p.fullH)-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2<f32>(p.spanMant*dx,p.spanMant*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
var errScaled=64.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n);}
return;
}
if(m>p.refLen){mark_unresolved(out,n);return;}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMant*dx,p.spanMant*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=64.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n);return;}
let r=refs[m];
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=64.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
const COLOR_WGSL=String.raw`
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(20,22,30)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
let n=f32(m&0x0fffffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
if(p.edgeAA!=0u){
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
let x=i32(gid.x); let y=i32(gid.y);
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x0fffffffu)-i32(m&0x0fffffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
`;
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.1.3',DIRECT_F32_WGSL,DEEP_PERTURB_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
});
})();

View file

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#050813">
<title>Mandelbrot Deep Zoom v23</title>
<title>Mandelbrot Deep Zoom v24.1.3 WebGPU</title>
<style>
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
@ -34,13 +34,13 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大"></button><button id="zout" aria-label="中心を縮小"></button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">精度優先</option></select><output></output></div>
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0.001" max="0.05" step="0.0005" value="0.008"><output id="cycleO" for="cycle">0.0080</output></div>
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
<details><summary>詳細設定・正確な座標</summary>
<div class="line"><label for="iters">基準反復</label><input id="iters" type="range" min="100" max="2500" step="25" value="350"><output id="itersO" for="iters">350</output></div>
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> 境界AAを追加</label></div>
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> GPU境界平滑化</label></div>
<div class="exact-grid"><label for="coordReInput">実部</label><input id="coordReInput"><label for="coordImInput">虚部</label><input id="coordImInput"><label for="coordSpanInput">表示幅</label><input id="coordSpanInput"></div>
<div class="mini-actions"><button id="coordApply">座標を適用</button><button id="coordCopy">正確値をコピー</button><button id="undoView">戻す</button><button id="redoView">進む</button></div>
</details>
@ -57,13 +57,13 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
<label for="exportWidth">px</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×高速</option><option value="2">2×2 AA</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="validated">Validated direct</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="strict">保守的 (Strict)</option></select>
</div>
<progress id="exportProgress" max="1" value="0" hidden></progress>
<div id="exportStatus" role="status" aria-live="polite"></div>
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
</dialog>
<script src="kernels.js"></script>
<script src="gpu-kernels.js"></script>
<script src="script.js"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"format": "mandelbrot-wasm-manifest-v1",
"generatedUtc": "2026-08-22T10:48:30.5652322Z",
"generatedUtc": "2026-08-22T12:30:26.3193366Z",
"input": "kernels.js",
"payloads": [
{

258
gpu-kernels.js Normal file
View file

@ -0,0 +1,258 @@
(()=>{'use strict';
const COMMON=String.raw`
const FIELD_UNKNOWN:u32=0u;
const FIELD_ESCAPED:u32=1u;
const FIELD_INTERIOR_LIKELY:u32=2u;
const FIELD_INTERIOR_PROVEN:u32=3u;
const ITER_MASK:u32=0x0fffffffu;
const STATUS_UNRESOLVED:u32=0xfffffffeu;
fn pack_meta(n:u32, cls:u32)->u32 { return (n & ITER_MASK) | ((cls & 3u) << 28u); }
fn cmul(a:vec2<f32>, b:vec2<f32>)->vec2<f32>{
return vec2<f32>(a.x*b.x-a.y*b.y, a.x*b.y+a.y*b.x);
}
fn maxabs(v:vec2<f32>)->f32 { return max(abs(v.x),abs(v.y)); }
const F32_U:f32=5.960464477539063e-8;
fn pow2_safe(e:i32)->f32 {
if(e < -126){ return 0.0; }
if(e > 126){ return 8.507059e37; }
return ldexp(1.0,e);
}
fn safe_abs_error(errScaled:f32, scaleExp:i32, z:vec2<f32>, delta:vec2<f32>)->f32{
let propagated=abs(errScaled*pow2_safe(scaleExp));
let reconstruction=64.0*F32_U*(maxabs(z)+maxabs(delta)+1.0e-30);
return propagated+reconstruction;
}
fn scaled_to_f32(v:vec2<f32>, e:i32)->vec2<f32>{
if(e < -126){ return vec2<f32>(0.0); }
if(e > 126){ return vec2<f32>(8.507059e37); }
return ldexp(v,vec2<i32>(e));
}
fn smooth_escape(n:u32, mag2:f32)->f32{
let u=log2(max(4.0000005,mag2));
return f32(n)+1.0-log2(max(1.0e-20,0.5*u));
}
`;
const DIRECT_F32_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, strict:u32,
centerRe:f32, centerIm:f32, span:f32, sampleX:f32,
sampleY:f32, _p0:f32, _p1:f32, _p2:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read_write> fieldSmooth:array<f32>;
fn analytic(cr:f32,ci:f32)->bool{
let y2=ci*ci; let x=cr-0.25; let q=x*x+y2;
let lhs=q*(q+x); let rhs=0.25*y2;
let margin=16.0*F32_U*(abs(lhs)+abs(rhs)+1.0);
if(lhs<rhs-margin){return true;}
let x2=cr+1.0; let bulb=x2*x2+y2;
let bulbMargin=16.0*F32_U*(abs(bulb)+0.0625+1.0);
return bulb<0.0625-bulbMargin;
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX;
let gy=f32(p.tileY+gid.y)+p.sampleY;
let scale=p.span/f32(p.fullW);
let cr=p.centerRe+(gx-0.5*f32(p.fullW))*scale;
let ci=p.centerIm+(0.5*f32(p.fullH)-gy)*scale;
if(analytic(cr,ci)){
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_PROVEN); fieldSmooth[out]=0.0; return;
}
var zr=0.0; var zi=0.0; var n=0u;
loop{
if(n>=p.maxIter){break;}
let zr2=zr*zr; let zi2=zi*zi;
zi=2.0*zr*zi+ci; zr=zr2-zi2+cr; n+=1u;
let mag=zr*zr+zi*zi;
if(mag>4.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED); fieldSmooth[out]=smooth_escape(n,mag); return;}
}
fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY); fieldSmooth[out]=0.0;
}
`;
// Deep path: high-precision CPU reference + guarded rescaled f32 perturbation.
const DEEP_PERTURB_WGSL=COMMON+String.raw`
struct Params{
tileW:u32, tileH:u32, fullW:u32, fullH:u32,
tileX:u32, tileY:u32, maxIter:u32, refLen:u32,
strict:u32, _numeric0:u32, _numeric1:u32, _numeric2:u32,
spanMant:f32, spanExp:i32, sampleX:f32, sampleY:f32,
};
struct RefPoint{ hi:vec2<f32>, lo:vec2<f32> };
struct UnresolvedHead{ remaining:atomic<u32>, _p0:u32, _p1:u32, _p2:u32 };
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> refs:array<RefPoint>;
@group(0) @binding(2) var<storage,read_write> fieldMeta:array<u32>;
@group(0) @binding(3) var<storage,read_write> fieldSmooth:array<f32>;
@group(0) @binding(4) var<storage,read_write> unresolved:UnresolvedHead;
fn mark_unresolved(out:u32,n:u32){
fieldMeta[out]=pack_meta(n,FIELD_UNKNOWN); fieldSmooth[out]=0.0;
atomicAdd(&unresolved.remaining,1u);
}
fn render_pixel(out:u32,gx:f32,gy:f32,strictMode:bool){
let dx=(gx-0.5*f32(p.fullW))/f32(p.fullW);
let dy=(0.5*f32(p.fullH)-gy)/f32(p.fullW);
// dc = d * 2^scaleExp. Keep d and w in one shared scale.
var d=vec2<f32>(p.spanMant*dx,p.spanMant*dy);
var w=vec2<f32>(0.0);
var scaleExp=p.spanExp;
var n=0u; var m=0u; var operations=0u;
var errScaled=64.0*F32_U*maxabs(d);
loop{
if(n>=p.maxIter){
let rpEnd=refs[min(m,p.refLen)];
let deltaEnd=scaled_to_f32(w,scaleExp);
let zEnd=rpEnd.hi+(rpEnd.lo+deltaEnd);
let errAbs=safe_abs_error(errScaled,scaleExp,zEnd,deltaEnd);
let limit=select(1.0e-3,1.0e-4,strictMode);
if(errAbs<=limit){fieldMeta[out]=pack_meta(p.maxIter,FIELD_INTERIOR_LIKELY);fieldSmooth[out]=0.0;}else{mark_unresolved(out,n);}
return;
}
if(m>p.refLen){mark_unresolved(out,n);return;}
let rp=refs[m];
let delta=scaled_to_f32(w,scaleExp);
let z=rp.hi+(rp.lo+delta);
let mag=dot(z,z);
if(mag>4.0){
let errAbs=safe_abs_error(errScaled,scaleExp,z,delta);
if(length(z)-errAbs>2.0){fieldMeta[out]=pack_meta(n,FIELD_ESCAPED);fieldSmooth[out]=smooth_escape(n,mag);return;}
mark_unresolved(out,n);return;
}
// Rebase only when dc remains numerically representable in the new scale.
if(m>0u && dot(delta,delta)>0.0 && mag<dot(delta,delta)){
if(p.spanExp-scaleExp < -96){mark_unresolved(out,n);return;}
errScaled=safe_abs_error(errScaled,scaleExp,z,delta);
w=z; d=scaled_to_f32(vec2<f32>(p.spanMant*dx,p.spanMant*dy),p.spanExp); scaleExp=0; m=0u;
errScaled+=64.0*F32_U*maxabs(d);
continue;
}
if(m>=p.refLen){mark_unresolved(out,n);return;}
let r=refs[m];
let refAbs=maxabs(r.hi)+maxabs(r.lo);
let wAbs=maxabs(w); let dAbs=maxabs(d); let p2=abs(pow2_safe(scaleExp));
let gain=2.0*refAbs+2.0*wAbs*p2;
let roundErr=64.0*F32_U*(2.0*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1.0e-30);
errScaled=gain*errScaled+roundErr;
let linear=2.0*(cmul(r.hi,w)+cmul(r.lo,w));
// delta^2 / 2^scaleExp = w^2 * 2^scaleExp
let sq=cmul(w,w)*pow2_safe(scaleExp);
w=linear+sq+d; m+=1u; n+=1u; operations+=1u;
if(maxabs(w)>=1.0e30 || maxabs(d)>=1.0e30){mark_unresolved(out,n);return;}
let mm=max(maxabs(w),maxabs(d));
if(mm>65536.0){
w*=0.0000152587890625; d*=0.0000152587890625; errScaled*=0.0000152587890625; scaleExp+=16;
}else if(mm>0.0 && mm<0.0000152587890625 && scaleExp>p.spanExp){
w*=65536.0; d*=65536.0; errScaled*=65536.0; scaleExp-=16;
}
if(scaleExp>126 || errScaled!=errScaled || errScaled>1.0e35){mark_unresolved(out,n);return;}
if(operations>p.maxIter*2u+2048u){mark_unresolved(out,n);return;}
}
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.tileW||gid.y>=p.tileH){return;}
let out=gid.y*p.tileW+gid.x;
let gx=f32(p.tileX+gid.x)+p.sampleX; let gy=f32(p.tileY+gid.y)+p.sampleY;
render_pixel(out,gx,gy,p.strict!=0u);
}
`;
const COLOR_WGSL=String.raw`
struct Params{
width:u32,height:u32,palette:u32,edgeAA:u32,
cycle:f32,shift:f32,_p0:f32,_p1:f32,
};
@group(0) @binding(0) var<uniform> p:Params;
@group(0) @binding(1) var<storage,read> fieldMeta:array<u32>;
@group(0) @binding(2) var<storage,read> fieldSmooth:array<f32>;
@group(0) @binding(3) var outTex:texture_storage_2d<rgba8unorm,write>;
fn hsv(h:f32,s:f32,v:f32)->vec3<f32>{
let x=fract(h)*6.0; let i=i32(floor(x)); let f=x-floor(x); let pp=v*(1.0-s); let q=v*(1.0-s*f); let t=v*(1.0-s*(1.0-f));
if(i==0){return vec3<f32>(v,t,pp);} if(i==1){return vec3<f32>(q,v,pp);} if(i==2){return vec3<f32>(pp,v,t);} if(i==3){return vec3<f32>(pp,q,v);} if(i==4){return vec3<f32>(t,pp,v);} return vec3<f32>(v,pp,q);
}
fn current_palette(t0:f32)->vec3<f32>{
let t=select(2.0-2.0*t0,2.0*t0,t0<=0.5);
if(t<0.11){return mix(vec3<f32>(4,10,27),vec3<f32>(12,53,79),smoothstep(0.0,0.11,t))/255.0;}
if(t<0.25){return mix(vec3<f32>(12,53,79),vec3<f32>(31,156,184),smoothstep(0.11,0.25,t))/255.0;}
if(t<0.38){return mix(vec3<f32>(31,156,184),vec3<f32>(91,226,234),smoothstep(0.25,0.38,t))/255.0;}
if(t<0.50){return mix(vec3<f32>(91,226,234),vec3<f32>(66,53,151),smoothstep(0.38,0.50,t))/255.0;}
if(t<0.62){return mix(vec3<f32>(66,53,151),vec3<f32>(139,49,170),smoothstep(0.50,0.62,t))/255.0;}
if(t<0.73){return mix(vec3<f32>(139,49,170),vec3<f32>(232,72,145),smoothstep(0.62,0.73,t))/255.0;}
if(t<0.84){return mix(vec3<f32>(232,72,145),vec3<f32>(255,137,64),smoothstep(0.73,0.84,t))/255.0;}
if(t<0.93){return mix(vec3<f32>(255,137,64),vec3<f32>(255,211,99),smoothstep(0.84,0.93,t))/255.0;}
return mix(vec3<f32>(255,211,99),vec3<f32>(255,250,223),smoothstep(0.93,1.0,t))/255.0;
}
fn base_color(i:u32)->vec3<f32>{
let m=fieldMeta[i]; let cls=(m>>28u)&3u;
if(cls==0u){return vec3<f32>(20,22,30)/255.0;} if(cls!=1u){return vec3<f32>(0.0);}
let sm=fieldSmooth[i]; let phase=fract(p.shift+sm*p.cycle); var c=vec3<f32>(0.0);
if(p.palette==1u){c=hsv(phase,0.92,1.0);}else if(p.palette==2u){let g=(22.0+233.0*(0.5-0.5*cos(6.283185307*phase)))/255.0;c=vec3<f32>(g);}else{c=current_palette(phase);}
let n=f32(m&0x0fffffffu); let edge=clamp(log(1.0+n)/log(1.0+max(8.0,n+32.0)),0.0,1.0); let mixv=0.34+0.66*pow(edge,0.38);
let floorc=select(vec3<f32>(2,5,15)/255.0,vec3<f32>(8.0/255.0),p.palette==2u); return mix(floorc,c,mixv);
}
fn linearize(c:vec3<f32>)->vec3<f32>{return pow(c,vec3<f32>(2.2));}
fn delinearize(c:vec3<f32>)->vec3<f32>{return pow(max(c,vec3<f32>(0.0)),vec3<f32>(1.0/2.2));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
if(gid.x>=p.width||gid.y>=p.height){return;} let i=gid.y*p.width+gid.x; var c=base_color(i);
if(p.edgeAA!=0u){
let m=fieldMeta[i]; let cls=(m>>28u)&3u; var boundary=false; var sum=linearize(c); var cnt=1.0;
let x=i32(gid.x); let y=i32(gid.y);
for(var oy=-1;oy<=1;oy+=1){for(var ox=-1;ox<=1;ox+=1){if(ox==0&&oy==0){continue;} let xx=x+ox;let yy=y+oy;if(xx<0||yy<0||xx>=i32(p.width)||yy>=i32(p.height)){continue;}let j=u32(yy)*p.width+u32(xx);let mj=fieldMeta[j];let cj=(mj>>28u)&3u;if(cj!=cls||abs(i32(mj&0x0fffffffu)-i32(m&0x0fffffffu))>2){boundary=true;}sum+=linearize(base_color(j));cnt+=1.0;}}
if(boundary){c=delinearize(sum/cnt);}
}
textureStore(outTex,vec2<i32>(gid.xy),vec4<f32>(c,1.0));
}
`;
const AA_RESOLVE_WGSL=String.raw`
@group(0) @binding(0) var a:texture_2d<f32>;
@group(0) @binding(1) var b:texture_2d<f32>;
@group(0) @binding(2) var c:texture_2d<f32>;
@group(0) @binding(3) var d:texture_2d<f32>;
@group(0) @binding(4) var outTex:texture_storage_2d<rgba8unorm,write>;
fn to_linear(x:f32)->f32{return select(x/12.92,pow((x+0.055)/1.055,2.4),x>0.04045);}
fn to_srgb(x0:f32)->f32{let x=clamp(x0,0.0,1.0);return select(12.92*x,1.055*pow(x,1.0/2.4)-0.055,x>0.0031308);}
fn lin3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_linear(v.x),to_linear(v.y),to_linear(v.z));}
fn srgb3(v:vec3<f32>)->vec3<f32>{return vec3<f32>(to_srgb(v.x),to_srgb(v.y),to_srgb(v.z));}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) gid:vec3<u32>){
let size=textureDimensions(a); if(gid.x>=size.x||gid.y>=size.y){return;}
let q=vec2<i32>(gid.xy);
let sum=lin3(textureLoad(a,q,0).rgb)+lin3(textureLoad(b,q,0).rgb)+lin3(textureLoad(c,q,0).rgb)+lin3(textureLoad(d,q,0).rgb);
textureStore(outTex,q,vec4<f32>(srgb3(sum*0.25),1.0));
}
`;
const PRESENT_WGSL=String.raw`
struct Params{scaleX:f32,scaleY:f32,offsetX:f32,offsetY:f32};
@group(0) @binding(0) var samp:sampler;
@group(0) @binding(1) var tex:texture_2d<f32>;
@group(0) @binding(2) var<uniform> p:Params;
struct VSOut{@builtin(position) pos:vec4<f32>,@location(0) uv:vec2<f32>};
@vertex fn vs(@builtin(vertex_index) i:u32)->VSOut{
var pos=array<vec2<f32>,3>(vec2<f32>(-1.0,-1.0),vec2<f32>(3.0,-1.0),vec2<f32>(-1.0,3.0));
var uv=array<vec2<f32>,3>(vec2<f32>(0.0,1.0),vec2<f32>(2.0,1.0),vec2<f32>(0.0,-1.0));
var o:VSOut;o.pos=vec4<f32>(pos[i],0.0,1.0);o.uv=uv[i];return o;
}
@fragment fn fs(in:VSOut)->@location(0) vec4<f32>{
let uv=vec2<f32>(0.5)+(in.uv-vec2<f32>(0.5))*vec2<f32>(p.scaleX,p.scaleY)+vec2<f32>(p.offsetX,p.offsetY);
if(any(uv<vec2<f32>(0.0))||any(uv>vec2<f32>(1.0))){return vec4<f32>(0.0196,0.0314,0.0745,1.0);} return textureSampleLevel(tex,samp,uv,0.0);
}
`;
globalThis.MANDEL_WEBGPU_KERNELS=Object.freeze({
version:'24.1.3',DIRECT_F32_WGSL,DEEP_PERTURB_WGSL,COLOR_WGSL,AA_RESOLVE_WGSL,PRESENT_WGSL
});
})();

View file

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta name="theme-color" content="#050813">
<title>Mandelbrot Deep Zoom v23</title>
<title>Mandelbrot Deep Zoom v24.1.3 WebGPU</title>
<style>
:root{color-scheme:dark;--panel:rgba(7,12,25,.88);--line:rgba(255,255,255,.12);--text:#f7f8ff;--muted:#a9b3ca;--accent:#61dbe9}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#050813;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}body{-webkit-user-select:none;user-select:none}
@ -34,13 +34,13 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
<div id="controls" class="panel">
<div class="toolbar"><button id="zin" aria-label="中心を拡大"></button><button id="zout" aria-label="中心を縮小"></button><button id="reset">リセット</button><button id="png">出力</button></div>
<div class="group">
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">精度優先</option></select><output></output></div>
<div class="line"><label for="processMode">処理モード</label><select id="processMode"><option value="power">省電力</option><option value="standard" selected>標準</option><option value="fine">精細</option><option value="validate">保守的 (Strict)</option></select><output></output></div>
<div class="line"><label for="palette">彩色</label><select id="palette"><option value="0">昼夜</option><option value="1">虹色</option><option value="2">白黒</option></select><output></output></div>
<div class="line"><label for="cycle">色周期</label><input id="cycle" type="range" min="0.001" max="0.05" step="0.0005" value="0.008"><output id="cycleO" for="cycle">0.0080</output></div>
<div class="line"><label for="shift">色相位置</label><input id="shift" type="range" min="0" max="1" step="0.005" value="0.18"><output id="shiftO" for="shift">.18</output></div>
<details><summary>詳細設定・正確な座標</summary>
<div class="line"><label for="iters">基準反復</label><input id="iters" type="range" min="100" max="2500" step="25" value="350"><output id="itersO" for="iters">350</output></div>
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> 境界AAを追加</label></div>
<div class="checks"><label><input id="adaptive" type="checkbox" checked> 反復回数を自動調整</label><label><input id="hq" type="checkbox"> GPU境界平滑化</label></div>
<div class="exact-grid"><label for="coordReInput">実部</label><input id="coordReInput"><label for="coordImInput">虚部</label><input id="coordImInput"><label for="coordSpanInput">表示幅</label><input id="coordSpanInput"></div>
<div class="mini-actions"><button id="coordApply">座標を適用</button><button id="coordCopy">正確値をコピー</button><button id="undoView">戻す</button><button id="redoView">進む</button></div>
</details>
@ -57,13 +57,13 @@ dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-r
<label for="exportScale">出力倍率</label><select id="exportScale"><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option><option value="0">カスタム幅</option></select>
<label for="exportWidth">px</label><input id="exportWidth" type="number" min="64" max="16384" step="1">
<label for="exportAA">サブサンプル</label><select id="exportAA"><option value="1">1×高速</option><option value="2">2×2 AA</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="validated">Validated direct</option></select>
<label for="exportPrecision">精度方針</label><select id="exportPrecision"><option value="balanced">Balanced</option><option value="strict">保守的 (Strict)</option></select>
</div>
<progress id="exportProgress" max="1" value="0" hidden></progress>
<div id="exportStatus" role="status" aria-live="polite"></div>
<div class="export-actions"><button id="exportCancel" type="button">閉じる</button><button id="exportQuick" type="button">表示を即時保存</button><button id="exportStart" class="primary" type="button">PNGを生成</button></div>
</dialog>
<script src="kernels.js"></script>
<script src="gpu-kernels.js"></script>
<script src="script.js"></script>
</body>
</html>

10
package.json Normal file
View file

@ -0,0 +1,10 @@
{
"name": "mandelbrot-webgpu-v24",
"version": "24.1.3",
"private": true,
"type": "module",
"scripts": {
"test": "node scripts/test-all.mjs",
"build": "node scripts/build.mjs"
}
}

1132
script.js

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,90 @@
param(
[Parameter(Mandatory=$true)][string]$NodePath,
[string]$NodeArchivePath,
[string]$OutputPath = 'audit/v23-browserless-baseline.json'
)
$ErrorActionPreference = 'Stop'
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
$node = (Resolve-Path -LiteralPath $NodePath).Path
$expectedNodeArchiveSha256 = 'c95d8a7e1c99e669cc08c9f1176e068c1f50847c37908fcb8c35b62482366511'
function Run-PowerShellJson([string]$Path) {
$raw = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $Path
if ($LASTEXITCODE -ne 0) { throw "PowerShell gate failed: $Path" }
$raw | ConvertFrom-Json
}
function Run-NodeJson([string]$Path, [string[]]$Arguments = @()) {
$raw = & $node $Path @Arguments
if ($LASTEXITCODE -ne 0) { throw "Node gate failed: $Path" }
($raw | Select-Object -Last 1) | ConvertFrom-Json
}
$nodeVersion = (& $node --version | Select-Object -First 1)
if ($nodeVersion -ne 'v22.18.0') { throw "Expected Node v22.18.0, got $nodeVersion" }
$nodeArchive = $null
if ($NodeArchivePath) {
$archive = (Resolve-Path -LiteralPath $NodeArchivePath).Path
$actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expectedNodeArchiveSha256) { throw "Node archive SHA-256 mismatch: $actual" }
$nodeArchive = [ordered]@{
file=[System.IO.Path]::GetFileName($archive)
sha256=$actual
source='https://nodejs.org/dist/v22.18.0/node-v22.18.0-win-x64.zip'
}
}
$kernelContract = Run-PowerShellJson (Join-Path $workspace 'tests/kernel-source-contract.ps1')
$sourceContract = Run-PowerShellJson (Join-Path $workspace 'tests/source-contract.ps1')
$documentContract = Run-PowerShellJson (Join-Path $workspace 'tests/document-contract.ps1')
$tests = [ordered]@{}
foreach ($name in @('js-syntax.mjs','module-clone.mjs','precision-reference.mjs','analytic-interior.mjs','pixel-mapping.mjs','pixel-contract.mjs')) {
$tests[$name] = Run-NodeJson (Join-Path $workspace "tests/$name")
}
$runtimePath = Join-Path $workspace 'audit/v23-browserless-runtime.json'
$tests['runtime-budget.mjs'] = Run-NodeJson (Join-Path $workspace 'tests/runtime-budget.mjs') @('--out',$runtimePath)
$sourceBaseline = Get-Content -LiteralPath (Join-Path $workspace 'audit/v23-source-baseline.json') -Raw -Encoding UTF8 | ConvertFrom-Json
$report = [ordered]@{
format='mandelbrot-browserless-audit-v23'
generatedUtc=[DateTime]::UtcNow.ToString('o')
status='pass'
scope='browserless-current-artifacts'
fullAcceptance=$false
rendererVersion=23
node=[ordered]@{ version=$nodeVersion; archive=$nodeArchive }
contracts=[ordered]@{
kernel=$kernelContract
source=$sourceContract
document=$documentContract
}
executableTests=$tests
runtime=[ordered]@{
status='measured-not-acceptance'
report='audit/v23-browserless-runtime.json'
reason='Runtime values are environment/load dependent and no browser paint/input/GPU threshold is asserted.'
}
sourceWasmBuild=[ordered]@{
status=if ($sourceBaseline.sourceBuild.goldenStatus -eq 'pass') { 'prior-evidence-reused' } else { 'not-run' }
manifest=$sourceBaseline.sourceBuild.generatedManifest
reason=$sourceBaseline.sourceBuild.reason
}
v22Comparison=[ordered]@{
status='not-verifiable'
reason='No immutable v22 artifacts, hashes, or versioned baseline are present.'
}
browserAcceptance=[ordered]@{
status='not-run'
report='audit/v23-browser-baseline.json'
nonSubstitutable=@(
'actual DPR and visual output',
'transform-to-paint and Preview p95',
'long tasks and observed browser/GPU memory',
'download/cancel behavior',
'keyboard/focus/page-zoom/screen-reader behavior'
)
}
}
$target = Join-Path $workspace $OutputPath
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $target -Encoding UTF8
Write-Output $target

8
scripts/build.mjs Normal file
View file

@ -0,0 +1,8 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url),files=['index.html','gpu-kernels.js','script.js'];
for(const variant of ['standalone','hosted']){
const dir=new URL(`../dist/${variant}/`,import.meta.url);await fs.rm(dir,{recursive:true,force:true});await fs.mkdir(dir,{recursive:true});
for(const f of files)await fs.copyFile(new URL('../'+f,import.meta.url),new URL(f,dir));
}
await fs.writeFile(new URL('../dist/hosted/_headers',import.meta.url),`/*\n X-Content-Type-Options: nosniff\n Referrer-Policy: no-referrer\n`);
console.log(JSON.stringify({status:'pass',outputs:['dist/standalone','dist/hosted'],files},null,2));

View file

@ -75,8 +75,9 @@ $baseline = [ordered]@{
hosted = [ordered]@{
firstViewFiles = $hostedFiles
firstViewUncompressedBytes = ($hostedFiles | Measure-Object bytes -Sum).Sum
deepRequestsBeforeDeepView = 0
note = 'Transfer compression, parse, compile, and runtime timings require the fixed browser benchmark.'
deepRequestContractBeforeDeepView = 0
deepRequestMeasurementStatus = 'not-run'
note = 'The zero is a static loader contract, not a measured request count. Transfer compression, request count, parse, compile, and runtime timings require the fixed Hosted browser benchmark.'
}
standalone = [ordered]@{
files = $standaloneFiles

7
scripts/test-all.mjs Normal file
View file

@ -0,0 +1,7 @@
import {spawnSync} from 'node:child_process';
const commands=[
['node',['--check','script.js']],['node',['--check','gpu-kernels.js']],['node',['--check','tests/webgpu-acceptance.js']],
...['v24-wgsl-reserved.mjs','v24-source-contract.mjs','v24-index-contract.mjs','v24-tree-contract.mjs','v24-reference-worker.mjs','v24-direct-model.mjs','v24-cpu-numeric-model.mjs','v24-bla-model.mjs','v24-geometry-contract.mjs','v24-coordinate-format.mjs','v24-png-stream-model.mjs','v24-acceptance-contract.mjs'].map(f=>['node',['tests/'+f]])
];
for(const [cmd,args] of commands){const r=spawnSync(cmd,args,{stdio:'inherit'});if(r.status!==0)process.exit(r.status??1)}
console.log(JSON.stringify({status:'pass',suite:'v24-static-and-cpu-model',realWebGPU:'run tests/webgpu-acceptance.html in a WebGPU-capable browser'},null,2));

View file

@ -8,6 +8,7 @@ function Run-CheckedPowerShell([string]$Path) {
}
Run-CheckedPowerShell (Join-Path $workspace 'tests\kernel-source-contract.ps1')
Run-CheckedPowerShell (Join-Path $workspace 'tests\source-contract.ps1')
Run-CheckedPowerShell (Join-Path $workspace 'tests\document-contract.ps1')
$node = Get-Command node -ErrorAction SilentlyContinue
if ($node) {
@ -29,4 +30,12 @@ if ($clang -and $node -and ((& $clang.Source --version | Select-Object -First 1)
} elseif ($RequireToolchain) { throw 'clang 17.0.6 is required by -RequireToolchain.' }
else { Write-Warning 'clang 17.0.6 is unavailable; source-WASM build/golden was skipped.' }
[ordered]@{ status='pass'; nodeTests=[bool]$node; sourceWasmGolden=$sourceGolden } | ConvertTo-Json
$status = if ($node -and $sourceGolden) { 'pass' } else { 'partial' }
[ordered]@{
status=$status
sourceContracts=$true
documentContract=$true
nodeTests=[bool]$node
sourceWasmGolden=$sourceGolden
note=if ($status -eq 'partial') { 'Use -RequireToolchain for a full pass; skipped gates are not pass.' } else { $null }
} | ConvertTo-Json

View file

@ -1,27 +1,323 @@
<!doctype html>
<meta charset="utf-8">
<title>Mandelbrot v23 browser benchmark</title>
<style>body{font:14px system-ui;margin:20px;background:#111827;color:#eef2ff}button{padding:8px 14px}iframe{position:fixed;left:-20000px;top:0;border:0}pre{white-space:pre-wrap}</style>
<h1>v23 browser benchmark</h1>
<p>HTTP(S)でこのworkspaceを配信して実行します。iframeの実寸と実DPRを結果へ記録します。</p>
<button id="run">Run corpus</button>
<title>Mandelbrot v23 browser acceptance runner</title>
<style>
body{font:14px system-ui;margin:20px;background:#111827;color:#eef2ff}
button{padding:8px 14px}
iframe{position:fixed;left:-20000px;top:0;border:0}
pre{white-space:pre-wrap}
</style>
<h1>v23 browser acceptance runner</h1>
<p>
HTTP(S)で実行します。1回のrunは1 profile・1 buildだけを検査します。
例: <code>?profile=desktop&amp;target=hosted</code>
iframe寸法ではDPRを模擬できないため、要求DPRを持つbrowser contextごとに実行してください。
</p>
<button id="run">Run selected profile</button>
<pre id="output">Ready.</pre>
<iframe id="app" width="1440" height="900"></iframe>
<script type="module">
const out=document.querySelector('#output'),frame=document.querySelector('#app');
const wait=ms=>new Promise(r=>setTimeout(r,ms));
function fixed(decimal,bits){let s=String(decimal).toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);let[m,e='0']=s.split('e'),[i,f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-Number(e);if(places<0){digits+='0'.repeat(-places);places=0}let v=BigInt(digits)*(1n<<BigInt(bits))/(10n**BigInt(places));return neg?-v:v}
function bitsFor(span){const n=Math.abs(Number(span));return Number.isFinite(n)&&n>0?Math.max(256,Math.ceil(-Math.log2(n))+256):1280}
function hash(scene){const bits=bitsFor(scene.span),p=new URLSearchParams({v:'23',b:String(bits),re:String(fixed(scene.re,bits)),im:String(fixed(scene.im,bits)),sp:String(fixed(scene.span,bits)),pal:'0',cy:'.008',sh:'.18',it:'350',ad:'1'});return'#'+p}
async function poll(fn,timeout,label){const started=performance.now();while(performance.now()-started<timeout){let value;try{value=fn()}catch{}if(value)return{value,ms:performance.now()-started};await wait(25)}throw new Error('timeout: '+label)}
const p95=values=>values.slice().sort((a,b)=>a-b)[Math.min(values.length-1,Math.floor(values.length*.95))]||0;
async function canvasHash(win){const blob=await new Promise(resolve=>win.document.querySelector('#view').toBlob(resolve,'image/png'));const digest=await crypto.subtle.digest('SHA-256',await blob.arrayBuffer());return[...new Uint8Array(digest)].map(x=>x.toString(16).padStart(2,'0')).join('')}
function accessibilityAudit(win){const doc=win.document,failures=[],interactive=[...doc.querySelectorAll('button,select,input,summary')];for(const el of interactive){const target=el.type==='checkbox'?el.closest('label')||el:el,r=target.getBoundingClientRect();if(r.width<44||r.height<44)failures.push('target:'+el.id);if(el.matches('input:not([type=checkbox]),select')&&el.id&&!doc.querySelector(`label[for="${el.id}"]`))failures.push('label:'+el.id)}if(!doc.querySelector('#view[tabindex][aria-label]'))failures.push('canvas-keyboard-name');if(!doc.querySelector('[aria-live]'))failures.push('live-status');if(/user-scalable\s*=\s*no/i.test(doc.querySelector('meta[name=viewport]')?.content||''))failures.push('page-zoom-disabled');const css=[...doc.querySelectorAll('style')].map(x=>x.textContent).join('\n');if(!css.includes('prefers-reduced-motion'))failures.push('reduced-motion');if(!css.includes('prefers-reduced-transparency'))failures.push('reduced-transparency');return{pass:failures.length===0,failures,interactiveCount:interactive.length}}
async function interactionAudit(win){const canvas=win.document.querySelector('#view'),before=win.__MANDEL_DIAG__.snapshot(),durations=[];for(let i=0;i<16;i++){const t=performance.now();canvas.dispatchEvent(new win.WheelEvent('wheel',{deltaY:0,clientX:canvas.clientWidth/2,clientY:canvas.clientHeight/2,cancelable:true}));durations.push(performance.now()-t)}const during=win.__MANDEL_DIAG__.snapshot();await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d&&!d.rendering&&!d.scheduler.wheelActive&&d.lastPass==='preview'?d:null},120000,'wheel settle preview');await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d&&!d.rendering&&d.coverage>=.999&&['COVERED','RESOLVING','REFINING','REFINED'].includes(d.drawState)?d:null},180000,'wheel settle covered');return{dispatchP95Ms:p95(durations),renderStartsDuringGesture:during.runtimeMetrics.renderStartsDuringGesture-before.runtimeMetrics.renderStartsDuringGesture,renderStartsBeforeSettle:during.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts}}
async function modeAudit(win){const select=win.document.querySelector('#processMode'),hq=win.document.querySelector('#hq');select.value='power';select.dispatchEvent(new win.Event('change',{bubbles:true}));await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d.automaticTarget==='PREVIEW'&&!d.rendering&&d.lastPass==='preview'?d:null},120000,'power preview');const powerBefore=win.__MANDEL_DIAG__.snapshot();await wait(900);const power=win.__MANDEL_DIAG__.snapshot(),powerPass=power.automaticTarget==='PREVIEW'&&power.lastPass==='preview'&&!power.rendering&&!power.scheduler.timer&&!power.detailActive&&!hq.checked&&power.screen.pixelBudget<=1048576&&power.runtimeMetrics.renderStarts===powerBefore.runtimeMetrics.renderStarts;select.value='standard';select.dispatchEvent(new win.Event('change',{bubbles:true}));await poll(()=>{const d=win.__MANDEL_DIAG__.snapshot();return d.automaticTarget==='COVERED'&&!d.rendering&&d.lastPass==='covered'&&d.coverage>=.999?d:null},180000,'standard covered');await wait(900);const standard=win.__MANDEL_DIAG__.snapshot(),standardPass=standard.automaticTarget==='COVERED'&&standard.lastPass==='covered'&&!standard.detailActive&&!standard.scheduler.unknownTimer&&!hq.checked&&standard.screen.pixelBudget<=4194304;return{pass:powerPass&&standardPass,power:{pass:powerPass,target:power.automaticTarget,pixelBudget:power.screen.pixelBudget,renderStartsAfterStable:power.runtimeMetrics.renderStarts-powerBefore.runtimeMetrics.renderStarts},standard:{pass:standardPass,target:standard.automaticTarget,pixelBudget:standard.screen.pixelBudget,detailActive:standard.detailActive,unknownTimer:standard.scheduler.unknownTimer}}}
async function recolorAudit(win){const select=win.document.querySelector('#palette'),before=win.__MANDEL_DIAG__.snapshot();select.value=before.palette===1?'2':'1';select.dispatchEvent(new win.Event('change',{bubbles:true}));await new Promise(resolve=>win.requestAnimationFrame(()=>win.requestAnimationFrame(resolve)));const after=win.__MANDEL_DIAG__.snapshot();return{pass:after.palette!==before.palette&&after.runtimeMetrics.renderStarts===before.runtimeMetrics.renderStarts,renderStarts:after.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts}}
async function exportAudit(win){const doc=win.document,captured=[],nativeCreate=win.URL.createObjectURL.bind(win.URL),nativeClick=win.HTMLAnchorElement.prototype.click;win.URL.createObjectURL=blob=>{const url=nativeCreate(blob);captured.push({url,blob});return url};win.HTMLAnchorElement.prototype.click=function(){};try{doc.querySelector('#exportScale').value='0';doc.querySelector('#exportWidth').value='64';doc.querySelector('#exportAA').value='1';doc.querySelector('#exportPrecision').value='balanced';doc.querySelector('#exportStart').click();await poll(()=>!win.__MANDEL_DIAG__.snapshot().exporting&&captured.length>=2,120000,'small export');const png=captured.find(x=>x.blob.type==='image/png')?.blob,json=captured.find(x=>x.blob.type==='application/json')?.blob;if(!png||!json)throw new Error('export files missing');const bytes=new Uint8Array(await png.arrayBuffer()),width=(bytes[16]<<24)|(bytes[17]<<16)|(bytes[18]<<8)|bytes[19],height=(bytes[20]<<24)|(bytes[21]<<16)|(bytes[22]<<8)|bytes[23],meta=JSON.parse(await json.text()),completed={width,height,metadataComplete:meta.determinism?.allTilesCompleted===true,sampleCount:meta.sampleCount,kernelHashes:!!meta.kernelSha256};const prior=captured.length;doc.querySelector('#exportWidth').value='256';doc.querySelector('#exportPrecision').value='validated';doc.querySelector('#exportStart').click();doc.querySelector('#exportCancel').click();await poll(()=>!win.__MANDEL_DIAG__.snapshot().exporting,120000,'export cancel');return{pass:width===64&&height===Math.round(64*win.document.querySelector('#view').height/win.document.querySelector('#view').width)&&captured.length===prior,completed,cancelledWithoutDownload:captured.length===prior}}finally{win.URL.createObjectURL=nativeCreate;win.HTMLAnchorElement.prototype.click=nativeClick}}
async function runScene(scene,viewport){frame.width=viewport.cssWidth;frame.height=viewport.cssHeight;const loaded=new Promise((resolve,reject)=>{frame.onload=resolve;frame.onerror=reject});const started=performance.now();frame.src='../index.html'+hash(scene);await loaded;const win=frame.contentWindow;const label=viewport.id+'/'+scene.id;const preview=await poll(()=>{const d=win.__MANDEL_DIAG__?.snapshot();return d&&d.drawState==='PREVIEW'&&!d.rendering&&d.lastPass==='preview'?d:null},120000,'preview '+label);const previewSnapshot=preview.value,covered=await poll(()=>{const d=win.__MANDEL_DIAG__?.snapshot();return d&&['COVERED','RESOLVING','REFINING','REFINED','VALIDATING','VALIDATED','VALIDATION_INCOMPLETE'].includes(d.drawState)&&d.coverage>=.999?d:null},180000,'covered '+label);const refined=await poll(()=>{const d=win.__MANDEL_DIAG__?.snapshot();return d&&!d.detailActive&&!d.rendering&&!d.validating&&['COVERED','REFINED','VALIDATED','VALIDATION_INCOMPLETE'].includes(d.drawState)?d:null},240000,'refined '+label);const exercise=viewport.id==='desktop'&&scene.id==='z0',interaction=exercise?await interactionAudit(win):null,modes=exercise?await modeAudit(win):null,recolor=exercise?await recolorAudit(win):null,exportResult=exercise?await exportAudit(win):null,accessibility=exercise?accessibilityAudit(win):null,visualSha256=await canvasHash(win),resources=win.performance.getEntriesByType('resource').map(e=>String(e.name)),assetRequests={deep:resources.filter(x=>/\/(deep|bla|color)-/.test(x)),allWasm:resources.filter(x=>/\.wasm(?:$|\?)/.test(x))};await wait(2000);const before=win.__MANDEL_DIAG__.snapshot();await wait(1000);const after=win.__MANDEL_DIAG__.snapshot(),idle={raf:after.scheduler.raf,timer:after.scheduler.timer,pointerSettle:after.scheduler.pointerSettle,unknownTimer:after.scheduler.unknownTimer,backgroundJobs:after.scheduler.backgroundJobs,canvasWrites:after.runtimeMetrics.canvasWrites-before.runtimeMetrics.canvasWrites,domWrites:after.runtimeMetrics.domWrites-before.runtimeMetrics.domWrites,renderStarts:after.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts};return{id:scene.id,profile:viewport.id,requestedViewport:viewport,actualViewport:{cssWidth:win.innerWidth,cssHeight:win.innerHeight},actualDevicePixelRatio:win.devicePixelRatio,totalMs:performance.now()-started,previewMs:preview.ms,coveredMs:covered.ms,refinedMs:refined.ms,preview:previewSnapshot,final:after,idle,visualSha256,assetRequests,interaction,modes,recolor,export:exportResult,accessibility}}
function evaluate(results){const exercise=results.find(x=>x.interaction),checks={idle:results.every(x=>!x.idle.raf&&!x.idle.timer&&!x.idle.pointerSettle&&!x.idle.unknownTimer&&x.idle.backgroundJobs===0&&x.idle.canvasWrites===0&&x.idle.domWrites===0&&x.idle.renderStarts===0),covered:results.every(x=>x.final.coverage>=.999&&x.final.hqTarget===x.final.frame),memory:results.every(x=>x.final.memory.managedBytes<=x.final.memory.budget),visual:results.every(x=>/^[0-9a-f]{64}$/.test(x.visualSha256)),startup:results.filter(x=>x.id==='z0').every(x=>x.assetRequests.deep.length===0),interaction:!!exercise&&exercise.interaction.dispatchP95Ms<16&&exercise.interaction.renderStartsDuringGesture===0&&exercise.interaction.renderStartsBeforeSettle===0,modes:exercise?.modes?.pass===true,recolor:exercise?.recolor?.pass===true,export:exercise?.export?.pass===true,accessibility:exercise?.accessibility?.pass===true,profileFidelity:results.every(x=>x.actualViewport.cssWidth===x.requestedViewport.cssWidth&&x.actualViewport.cssHeight===x.requestedViewport.cssHeight&&Math.abs(x.actualDevicePixelRatio-x.requestedViewport.dpr)<.01),previewBudget:results.every(x=>x.preview.lastRender<120)};const failures=Object.entries(checks).filter(([,pass])=>!pass).map(([name])=>name);return{pass:failures.length===0,checks,failures}}
document.querySelector('#run').onclick=async()=>{document.querySelector('#run').disabled=true;try{const corpus=await(await fetch('./scenes.json',{cache:'no-store'})).json(),results=[];for(const viewport of corpus.viewports)for(const scene of corpus.scenes){out.textContent='Running '+viewport.id+'/'+scene.id+'…\n'+JSON.stringify(results,null,2);results.push(await runScene(scene,viewport))}const report={format:'mandelbrot-browser-baseline-v23',generatedUtc:new Date().toISOString(),userAgent:navigator.userAgent,hostDevicePixelRatio:devicePixelRatio,profiles:corpus.viewports,results,acceptance:evaluate(results)};out.textContent=JSON.stringify(report,null,2);globalThis.__BENCHMARK_RESULT__=report}catch(error){out.textContent=String(error?.stack||error)}finally{document.querySelector('#run').disabled=false}};
const out=document.querySelector('#output');
const frame=document.querySelector('#app');
const query=new URLSearchParams(location.search);
const selectedProfile=query.get('profile')||'desktop';
const target=query.get('target')||'hosted';
const previewTrials=Math.max(30,Number(query.get('trials'))||30);
if(!['hosted','standalone'].includes(target))throw new Error('target must be hosted or standalone');
const appPath=target==='hosted'?'../dist/hosted/index.html':'../index.html';
const wait=ms=>new Promise(resolve=>setTimeout(resolve,ms));
const nextFrame=win=>new Promise(resolve=>win.requestAnimationFrame(resolve));
function fixed(decimal,bits){
let s=String(decimal).toLowerCase(),neg=s.startsWith('-');
if(neg)s=s.slice(1);
let pair=s.split('e'),mantissa=pair[0],exponent=Number(pair[1]||0),parts=mantissa.split('.');
let digits=((parts[0]||'0')+(parts[1]||'')).replace(/^0+(?=\d)/,'')||'0';
let places=(parts[1]||'').length-exponent;
if(places<0){digits+='0'.repeat(-places);places=0}
let value=BigInt(digits)*(1n<<BigInt(bits))/(10n**BigInt(places));
return neg?-value:value
}
function bitsFor(span){
const n=Math.abs(Number(span));
return Number.isFinite(n)&&n>0?Math.max(256,Math.ceil(-Math.log2(n))+256):1280
}
function hash(scene){
const bits=bitsFor(scene.span);
const p=new URLSearchParams({
v:'23',b:String(bits),re:String(fixed(scene.re,bits)),im:String(fixed(scene.im,bits)),
sp:String(fixed(scene.span,bits)),pal:'0',cy:'.008',sh:'.18',it:'350',ad:'1'
});
return '#'+p
}
async function poll(fn,timeout,label){
const started=performance.now();
while(performance.now()-started<timeout){
let value;
try{value=fn()}catch{}
if(value)return{value,ms:performance.now()-started};
await wait(25)
}
throw new Error('timeout: '+label)
}
const p95=values=>values.slice().sort((a,b)=>a-b)[Math.min(values.length-1,Math.ceil(values.length*.95)-1)]||0;
async function sha256(bytes){
const digest=await crypto.subtle.digest('SHA-256',bytes);
return[...new Uint8Array(digest)].map(x=>x.toString(16).padStart(2,'0')).join('')
}
async function canvasAudit(win,scene,profile){
const canvas=win.document.querySelector('#view');
const context=canvas.getContext('2d',{willReadFrequently:true});
const pixels=context.getImageData(0,0,canvas.width,canvas.height).data;
const hashValue=await sha256(pixels.buffer);
let min=255,max=0;
for(let i=0;i<pixels.length;i+=4){min=Math.min(min,pixels[i],pixels[i+1],pixels[i+2]);max=Math.max(max,pixels[i],pixels[i+1],pixels[i+2])}
const expected=scene.visualGoldens?.[target]?.[profile.id]||null;
return{hash:hashValue,expected,nonBlank:max>min,pass:!!expected&&hashValue===expected&&max>min}
}
async function accessibilityAudit(win){
const doc=win.document,failures=[],interactive=[...doc.querySelectorAll('button,select,input,summary')];
for(const el of interactive){
const targetEl=el.type==='checkbox'?el.closest('label')||el:el,r=targetEl.getBoundingClientRect();
if(r.width<44||r.height<44)failures.push('target:'+el.id);
if(el.matches('input:not([type=checkbox]),select')&&el.id&&!doc.querySelector('label[for="'+el.id+'"]'))failures.push('label:'+el.id)
}
const canvas=doc.querySelector('#view');
if(!canvas?.matches('[tabindex][aria-label]'))failures.push('canvas-keyboard-name');
if(!doc.querySelector('[aria-live]'))failures.push('live-status');
if(/user-scalable\s*=\s*no/i.test(doc.querySelector('meta[name=viewport]')?.content||''))failures.push('page-zoom-disabled');
const css=[...doc.querySelectorAll('style')].map(x=>x.textContent).join('\n');
if(!css.includes('prefers-reduced-motion'))failures.push('reduced-motion');
if(!css.includes('prefers-reduced-transparency'))failures.push('reduced-transparency');
canvas.focus();
const before=win.location.hash;
canvas.dispatchEvent(new win.KeyboardEvent('keydown',{key:'ArrowRight',bubbles:true,cancelable:true}));
const keyboard=await poll(()=>win.location.hash!==before,2000,'keyboard navigation').then(()=>true).catch(()=>false);
if(!keyboard)failures.push('keyboard-navigation');
if(doc.activeElement!==canvas)failures.push('canvas-focus');
return{
pass:failures.length===0,failures,interactiveCount:interactive.length,keyboard,
pendingExternal:['visible-focus visual review','browser page-zoom and canvas-pinch coexistence','screen-reader announcement order']
}
}
async function interactionAudit(win){
const canvas=win.document.querySelector('#view'),before=win.__MANDEL_DIAG__.snapshot(),paintDurations=[];
for(let i=0;i<16;i++){
const writes=win.__MANDEL_DIAG__.snapshot().runtimeMetrics.canvasWrites,t=performance.now();
canvas.dispatchEvent(new win.WheelEvent('wheel',{
deltaY:i%2?6:-4,clientX:canvas.clientWidth/2,clientY:canvas.clientHeight/2,cancelable:true
}));
await poll(()=>win.__MANDEL_DIAG__.snapshot().runtimeMetrics.canvasWrites>writes,1000,'wheel transform paint');
paintDurations.push(performance.now()-t);
await nextFrame(win)
}
const during=win.__MANDEL_DIAG__.snapshot();
const preview=await poll(()=>{
const d=win.__MANDEL_DIAG__.snapshot();
return d&&!d.rendering&&!d.scheduler.wheelActive&&d.lastPass==='preview'?d:null
},120000,'wheel settle preview');
await poll(()=>{
const d=win.__MANDEL_DIAG__.snapshot();
return d&&!d.rendering&&d.coverage>=1&&d.drawState==='COVERED'?d:null
},180000,'wheel settle covered');
const after=win.__MANDEL_DIAG__.snapshot();
return{
paintP95Ms:p95(paintDurations),
previewAfterSettleMs:Math.max(0,preview.ms-110),
renderStartsDuringGesture:during.runtimeMetrics.renderStartsDuringGesture-before.runtimeMetrics.renderStartsDuringGesture,
renderStartsBeforeSettle:during.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts,
longTasks:after.runtimeMetrics.longTasks-before.runtimeMetrics.longTasks
}
}
async function modeAudit(win){
const select=win.document.querySelector('#processMode'),hq=win.document.querySelector('#hq'),previewAfterSettle=[];
select.value='power';
select.dispatchEvent(new win.Event('change',{bubbles:true}));
for(let i=0;i<previewTrials;i++){
const canvas=win.document.querySelector('#view'),started=performance.now();
canvas.dispatchEvent(new win.WheelEvent('wheel',{
deltaY:1,clientX:canvas.clientWidth/2,clientY:canvas.clientHeight/2,cancelable:true
}));
await poll(()=>{
const d=win.__MANDEL_DIAG__.snapshot();
return d.automaticTarget==='PREVIEW'&&!d.rendering&&!d.scheduler.wheelActive&&d.lastPass==='preview'?d:null
},120000,'power preview trial');
previewAfterSettle.push(Math.max(0,performance.now()-started-110))
}
const powerBefore=win.__MANDEL_DIAG__.snapshot();
await wait(900);
const power=win.__MANDEL_DIAG__.snapshot();
const powerPass=power.automaticTarget==='PREVIEW'&&power.lastPass==='preview'&&!power.rendering&&!power.scheduler.timer&&!power.detailActive&&!hq.checked&&power.screen.pixelBudget<=1048576&&power.runtimeMetrics.renderStarts===powerBefore.runtimeMetrics.renderStarts;
select.value='standard';
select.dispatchEvent(new win.Event('change',{bubbles:true}));
await poll(()=>{
const d=win.__MANDEL_DIAG__.snapshot();
return d.automaticTarget==='COVERED'&&!d.rendering&&d.lastPass==='covered'&&d.coverage>=1&&d.drawState==='COVERED'?d:null
},180000,'standard covered');
await wait(900);
const standard=win.__MANDEL_DIAG__.snapshot();
const standardPass=standard.automaticTarget==='COVERED'&&standard.lastPass==='covered'&&!standard.detailActive&&!standard.scheduler.unknownTimer&&!hq.checked&&standard.screen.pixelBudget<=4194304;
select.value='fine';
select.dispatchEvent(new win.Event('change',{bubbles:true}));
const fineResult=await poll(()=>{
const d=win.__MANDEL_DIAG__.snapshot();
return d.automaticTarget==='REFINED'&&!d.rendering&&!d.detailActive&&d.drawState==='REFINED'?d:null
},240000,'fine refined');
const fine=fineResult.value;
const finePass=fine.coverage>=1&&fine.drawState==='REFINED';
return{
pass:powerPass&&standardPass&&finePass&&previewAfterSettle.length>=30&&p95(previewAfterSettle)<120,
previewTrials:previewAfterSettle.length,previewP95AfterSettleMs:p95(previewAfterSettle),
power:{pass:powerPass,target:power.automaticTarget,pixelBudget:power.screen.pixelBudget},
standard:{pass:standardPass,target:standard.automaticTarget,pixelBudget:standard.screen.pixelBudget},
fine:{pass:finePass,target:fine.automaticTarget,refinedMs:fineResult.ms}
}
}
async function recolorAudit(win){
const select=win.document.querySelector('#palette'),before=win.__MANDEL_DIAG__.snapshot();
select.value=before.palette===1?'2':'1';
select.dispatchEvent(new win.Event('change',{bubbles:true}));
await nextFrame(win);await nextFrame(win);
const after=win.__MANDEL_DIAG__.snapshot();
return{pass:after.palette!==before.palette&&after.runtimeMetrics.renderStarts===before.runtimeMetrics.renderStarts,renderStarts:after.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts}
}
async function exportAudit(win){
const doc=win.document,captured=[],nativeCreate=win.URL.createObjectURL.bind(win.URL),nativeClick=win.HTMLAnchorElement.prototype.click;
win.URL.createObjectURL=blob=>{
const url=nativeCreate(blob);
captured.push({url,blob,progress:Number(doc.querySelector('#exportProgress').value),active:win.__MANDEL_DIAG__.snapshot().exporting});
return url
};
win.HTMLAnchorElement.prototype.click=function(){};
try{
async function balancedExport(){
const start=captured.length;
doc.querySelector('#exportScale').value='0';
doc.querySelector('#exportWidth').value='64';
doc.querySelector('#exportAA').value='2';
doc.querySelector('#exportPrecision').value='balanced';
doc.querySelector('#exportStart').click();
await poll(()=>!win.__MANDEL_DIAG__.snapshot().exporting&&captured.length>=start+2,120000,'small export');
return captured.slice(start)
}
const first=await balancedExport(),second=await balancedExport();
const png=first.find(x=>x.blob.type==='image/png'),json=first.find(x=>x.blob.type==='application/json');
const png2=second.find(x=>x.blob.type==='image/png');
if(!png||!json||!png2)throw new Error('export files missing');
const bytes=new Uint8Array(await png.blob.arrayBuffer()),width=(bytes[16]<<24)|(bytes[17]<<16)|(bytes[18]<<8)|bytes[19],height=(bytes[20]<<24)|(bytes[21]<<16)|(bytes[22]<<8)|bytes[23];
const meta=JSON.parse(await json.blob.text()),expectedHeight=Math.round(64*doc.querySelector('#view').height/doc.querySelector('#view').width);
const deterministic=await sha256(await png.blob.arrayBuffer())===await sha256(await png2.blob.arrayBuffer());
const completed=meta.determinism?.allTilesCompleted===true&&meta.sampleCount===width*height*4&&!!meta.kernelSha256&&typeof meta.unresolvedSamples==='number';
const noEarlyDownload=[...first,...second].every(x=>x.progress>=1);
const prior=captured.length;
doc.querySelector('#exportWidth').value='256';
doc.querySelector('#exportPrecision').value='validated';
doc.querySelector('#exportStart').click();
doc.querySelector('#exportCancel').click();
await poll(()=>!win.__MANDEL_DIAG__.snapshot().exporting,120000,'export cancel');
const cancelledWithoutDownload=captured.length===prior;
return{pass:width===64&&height===expectedHeight&&completed&&noEarlyDownload&&deterministic&&cancelledWithoutDownload,width,height,completed,noEarlyDownload,deterministic,cancelledWithoutDownload}
}finally{
win.URL.createObjectURL=nativeCreate;
win.HTMLAnchorElement.prototype.click=nativeClick
}
}
async function observedMemory(win){
if(typeof win.performance.measureUserAgentSpecificMemory!=='function')return{status:'unsupported'};
try{
const result=await win.performance.measureUserAgentSpecificMemory();
return{status:'measured',bytes:result.bytes}
}catch(error){return{status:'error',error:String(error?.message||error)}}
}
async function runScene(scene,profile){
frame.width=profile.cssWidth;frame.height=profile.cssHeight;
const loaded=new Promise((resolve,reject)=>{frame.onload=resolve;frame.onerror=reject});
const started=performance.now();
frame.src=appPath+hash(scene);
await loaded;
const win=frame.contentWindow,label=profile.id+'/'+scene.id;
const preview=await poll(()=>{
const d=win.__MANDEL_DIAG__?.snapshot();
return d&&d.drawState==='PREVIEW'&&!d.rendering&&d.lastPass==='preview'?d:null
},120000,'preview '+label);
const covered=await poll(()=>{
const d=win.__MANDEL_DIAG__?.snapshot();
return d&&d.drawState==='COVERED'&&d.coverage>=1&&!d.rendering?d:null
},180000,'covered '+label);
const initial=covered.value,visual=await canvasAudit(win,scene,profile);
const exercise=scene.id==='z0';
const interaction=exercise?await interactionAudit(win):null;
const modes=exercise?await modeAudit(win):null;
const recolor=exercise?await recolorAudit(win):null;
const exportResult=exercise?await exportAudit(win):null;
const accessibility=exercise?await accessibilityAudit(win):null;
const resources=win.performance.getEntriesByType('resource').map(e=>String(e.name));
const assetRequests={deep:resources.filter(x=>/\/(deep|bla|color)-/.test(x)),allWasm:resources.filter(x=>/\.wasm(?:$|\?)/.test(x))};
await wait(2000);
const before=win.__MANDEL_DIAG__.snapshot();
await wait(1000);
const after=win.__MANDEL_DIAG__.snapshot();
const idle={
raf:after.scheduler.raf,timer:after.scheduler.timer,pointerSettle:after.scheduler.pointerSettle,
unknownTimer:after.scheduler.unknownTimer,backgroundJobs:after.scheduler.backgroundJobs,
canvasWrites:after.runtimeMetrics.canvasWrites-before.runtimeMetrics.canvasWrites,
domWrites:after.runtimeMetrics.domWrites-before.runtimeMetrics.domWrites,
renderStarts:after.runtimeMetrics.renderStarts-before.runtimeMetrics.renderStarts
};
return{
id:scene.id,profile:profile.id,target,requestedViewport:profile,
actualViewport:{cssWidth:win.innerWidth,cssHeight:win.innerHeight},actualDevicePixelRatio:win.devicePixelRatio,
totalMs:performance.now()-started,previewMs:preview.ms,coveredMs:covered.ms,
preview:preview.value,initial,final:after,idle,visual,assetRequests,interaction,modes,recolor,
export:exportResult,accessibility,memoryObserved:await observedMemory(win)
}
}
function evaluate(results,profile){
const exercise=results.find(x=>x.interaction);
const checks={
idle:results.every(x=>!x.idle.raf&&!x.idle.timer&&!x.idle.pointerSettle&&!x.idle.unknownTimer&&x.idle.backgroundJobs===0&&x.idle.canvasWrites===0&&x.idle.domWrites===0&&x.idle.renderStarts===0),
covered:results.every(x=>x.initial.coverage>=1&&x.initial.hqTarget===x.initial.frame),
managedMemory:results.every(x=>x.final.memory.managedBytes<=x.final.memory.budget),
visual:results.every(x=>x.visual.pass),
startup:results.filter(x=>x.id==='z0').every(x=>target==='hosted'?x.assetRequests.deep.length===0:x.initial.deepAssets.workers===0),
interaction:!!exercise&&exercise.interaction.paintP95Ms<16&&exercise.interaction.renderStartsDuringGesture===0&&exercise.interaction.renderStartsBeforeSettle===0&&exercise.interaction.longTasks===0,
preview:exercise?.modes?.previewTrials>=30&&exercise?.modes?.previewP95AfterSettleMs<120,
modes:exercise?.modes?.pass===true,
recolor:exercise?.recolor?.pass===true,
export:exercise?.export?.pass===true,
accessibilityAutomated:exercise?.accessibility?.pass===true,
profileFidelity:results.every(x=>x.actualViewport.cssWidth===profile.cssWidth&&x.actualViewport.cssHeight===profile.cssHeight&&Math.abs(x.actualDevicePixelRatio-profile.dpr)<.01)
};
const failures=Object.entries(checks).filter(([,pass])=>!pass).map(([name])=>name);
const pendingExternal=[
'observed browser/GPU peak memory review',
'visible-focus and rendered-output visual review',
'page zoom and canvas pinch coexistence',
'screen-reader live announcement order'
];
return{pass:false,automatedPass:failures.length===0,checks,failures,pendingExternal}
}
document.querySelector('#run').onclick=async()=>{
document.querySelector('#run').disabled=true;
try{
const corpus=await(await fetch('./scenes.json',{cache:'no-store'})).json();
const profile=corpus.viewports.find(x=>x.id===selectedProfile);
if(!profile)throw new Error('Unknown profile: '+selectedProfile);
const results=[];
for(const scene of corpus.scenes){
out.textContent='Running '+profile.id+'/'+scene.id+' on '+target+'…\n'+JSON.stringify(results,null,2);
results.push(await runScene(scene,profile))
}
const report={
format:'mandelbrot-browser-baseline-v23',generatedUtc:new Date().toISOString(),target,
userAgent:navigator.userAgent,hostDevicePixelRatio:devicePixelRatio,profile,results,
acceptance:evaluate(results,profile)
};
out.textContent=JSON.stringify(report,null,2);
globalThis.__BENCHMARK_RESULT__=report
}catch(error){
out.textContent=String(error?.stack||error)
}finally{
document.querySelector('#run').disabled=false
}
};
</script>

133
tests/document-contract.ps1 Normal file
View file

@ -0,0 +1,133 @@
$ErrorActionPreference = 'Stop'
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
function Assert-Contract([bool]$Condition, [string]$Message) {
if (-not $Condition) { throw $Message }
}
function Decimal-Fraction([string]$Text) {
if ($Text -notmatch '^([+-]?)(\d+)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$') {
throw "Invalid decimal: $Text"
}
$negative = $Matches[1] -eq '-'
$fraction = if ($null -eq $Matches[3]) { '' } else { $Matches[3] }
$digits = ($Matches[2] + $fraction).TrimStart('0')
if (-not $digits) { $digits = '0' }
$numerator = [System.Numerics.BigInteger]::Parse($digits)
if ($negative) { $numerator = -$numerator }
$exponent = if ($Matches[4]) { [int]$Matches[4] } else { 0 }
$places = $fraction.Length - $exponent
if ($places -ge 0) {
$denominator = [System.Numerics.BigInteger]::Pow([System.Numerics.BigInteger]10, $places)
} else {
$numerator *= [System.Numerics.BigInteger]::Pow([System.Numerics.BigInteger]10, -$places)
$denominator = [System.Numerics.BigInteger]1
}
[pscustomobject]@{ numerator=$numerator; denominator=$denominator }
}
function Assert-ExactCoordinate($Scene, [string]$Axis) {
$decimal = Decimal-Fraction ([string]$Scene.$Axis)
$prefix = if ($Axis -eq 're') { 're' } else { 'im' }
$numerator = [System.Numerics.BigInteger]::Parse([string]$Scene.coordinate.($prefix + 'Numerator'))
$denominator = [System.Numerics.BigInteger]::Parse([string]$Scene.coordinate.($prefix + 'Denominator'))
Assert-Contract ($denominator -gt 0) "$($Scene.id): $Axis denominator must be positive."
Assert-Contract ($decimal.numerator * $denominator -eq $numerator * $decimal.denominator) "$($Scene.id): $Axis decimal does not match its exact rational."
}
$proposal = Get-Content -LiteralPath (Join-Path $workspace 'IMPROVEMENT_PROPOSAL.md') -Raw -Encoding UTF8
Assert-Contract ($proposal -match 'Document status: historical-v22 / implemented-v23') 'Proposal status/provenance is missing.'
Assert-Contract ($proposal -match 'sample coverage.+UNRESOLVED.+resolved coverage') 'Coverage semantics are not separated.'
Assert-Contract ($proposal -match 'Balanced Export.+Validated Export') 'Export precision tiers are not separated.'
Assert-Contract ($proposal -match 'exact half.+away-from-zero') 'Fixed-point tie rounding is not specified.'
Assert-Contract ($proposal -match 'Phase 0A' -and $proposal -match 'Phase 0B') 'Browserless and browser gates are not separated.'
Assert-Contract ($proposal -match 'threshold.+pass') 'Undefined acceptance thresholds must fail.'
$scenePath = Join-Path $workspace 'tests\scenes.json'
$scenes = Get-Content -LiteralPath $scenePath -Raw -Encoding UTF8 | ConvertFrom-Json
$policy = Get-Content -LiteralPath (Join-Path $workspace 'tests\numeric-policy-v23.json') -Raw -Encoding UTF8 | ConvertFrom-Json
Assert-Contract ($scenes.format -eq 'mandelbrot-scene-corpus-v2') 'Unexpected scene corpus version.'
Assert-Contract ($scenes.pixelContract -eq 'centered-rational-ties-away-from-zero') 'Unexpected scene pixel contract.'
Assert-Contract ($scenes.numericPolicy -eq $policy.id) 'Scene corpus and numeric policy IDs differ.'
Assert-Contract ($policy.validated.falseEscapedMax -eq 0 -and $policy.validated.falseInteriorProvenMax -eq 0 -and $policy.validated.unresolvedMax -eq 0) 'Validated zero-error thresholds changed.'
$ids = @{}
$maxViewportPixels = ($scenes.viewports | ForEach-Object { [double]$_.cssWidth * [double]$_.dpr } | Measure-Object -Maximum).Maximum
$viewportDigits = [Math]::Ceiling([Math]::Log10($maxViewportPixels))
foreach ($scene in $scenes.scenes) {
Assert-Contract (-not $ids.ContainsKey($scene.id)) "Duplicate scene ID: $($scene.id)"
$ids[$scene.id] = $true
Assert-Contract ($null -ne $scene.coordinate -and $null -ne $scene.oracle) "$($scene.id): coordinate/oracle metadata missing."
if ($scene.coordinate.kind -eq 'exact-rational') {
Assert-ExactCoordinate $scene 're'
Assert-ExactCoordinate $scene 'im'
} elseif ($scene.coordinate.kind -eq 'decimal') {
$reDigits = ([regex]::Replace([string]$scene.re, '[^0-9]', '')).TrimStart('0').Length
$imDigits = ([regex]::Replace([string]$scene.im, '[^0-9]', '')).TrimStart('0').Length
$actualDigits = [Math]::Min($reDigits, $imDigits)
Assert-Contract ($actualDigits -eq [int]$scene.coordinate.significantDigits) "$($scene.id): significantDigits metadata is stale."
$span = [Math]::Abs([double]::Parse([string]$scene.span, [Globalization.CultureInfo]::InvariantCulture))
$depthDigits = [Math]::Ceiling(-[Math]::Log10($span))
$guard = $actualDigits - $depthDigits - $viewportDigits
Assert-Contract ($guard -eq [int]$scene.coordinate.guardDigitsAtViewport -and $guard -ge 4) "$($scene.id): coordinate guard digits are insufficient or stale."
} else {
throw "$($scene.id): unsupported coordinate kind $($scene.coordinate.kind)"
}
}
foreach ($required in @('z0','period2-cusp-z14','swirly-seahorses-z12','period2-cusp-z20','period2-cusp-z100','period3-interior','period2-cusp-e280')) {
Assert-Contract ($ids.ContainsKey($required)) "Required scene missing: $required"
}
$runtimeTest = Get-Content -LiteralPath (Join-Path $workspace 'tests\runtime-budget.mjs') -Raw -Encoding UTF8
Assert-Contract ($runtimeTest -match "sceneById\('swirly-seahorses-z12'\)" -and $runtimeTest -match "acceptance:false") 'Runtime characterization must use the published mixed-boundary scene and remain non-acceptance evidence.'
$runner = Get-Content -LiteralPath (Join-Path $workspace 'tests\browser-benchmark.html') -Raw -Encoding UTF8
Assert-Contract ($runner -match "target==='hosted'.+dist/hosted/index\.html") 'Browser runner does not target the Hosted build.'
Assert-Contract ($runner -match "selectedProfile=query\.get\('profile'\)" -and $runner -notmatch 'for\s*\(const profile of corpus\.viewports\)') 'Browser runner must execute one real-DPR profile per run.'
Assert-Contract ($runner -notmatch 'deltaY\s*:\s*0') 'Wheel audit still uses a zero-delta event.'
Assert-Contract ($runner -match 'previewTrials>=30' -and $runner -match "drawState==='REFINED'") 'Preview p95 or actual Refined checks are missing.'
Assert-Contract ($runner -match 'visualGoldens' -and $runner -match 'hashValue===expected') 'Visual acceptance lacks a golden comparison.'
Assert-Contract ($runner -match 'interaction\.longTasks===0') 'Long tasks are not part of browser acceptance.'
Assert-Contract ($runner -match 'completed&&noEarlyDownload&&deterministic&&cancelledWithoutDownload') 'Export evidence is not part of the pass condition.'
Assert-Contract ($runner -match 'pendingExternal' -and $runner -match 'return\{pass:false,automatedPass') 'Browser-only/manual residual gates can be falsely marked complete.'
$baseline = Get-Content -LiteralPath (Join-Path $workspace 'audit\v23-source-baseline.json') -Raw -Encoding UTF8 | ConvertFrom-Json
Assert-Contract ($baseline.hosted.deepRequestMeasurementStatus -eq 'not-run') 'Static deep-request contract is mislabeled as a measurement.'
$records = @($baseline.hosted.firstViewFiles) + @($baseline.standalone.files) + @($baseline.sourceBuild.files)
foreach ($record in $records) {
$path = Join-Path $workspace ([string]$record.file).Replace('/', [System.IO.Path]::DirectorySeparatorChar)
Assert-Contract (Test-Path -LiteralPath $path) "Baseline file missing: $($record.file)"
$item = Get-Item -LiteralPath $path
$hash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
Assert-Contract ($item.Length -eq [long]$record.bytes -and $hash -eq [string]$record.sha256) "Baseline bytes/hash mismatch: $($record.file)"
}
$implementation = Get-Content -LiteralPath (Join-Path $workspace 'IMPLEMENTATION_REPORT.md') -Raw -Encoding UTF8
Assert-Contract ($implementation -match 'Hosted first view ([\d,]+) bytes') 'Hosted implementation size statement missing.'
$hostedDoc = [long](($Matches[1] -replace ',', ''))
Assert-Contract ($implementation -match 'Standalone ([\d,]+) bytes') 'Standalone implementation size statement missing.'
$standaloneDoc = [long](($Matches[1] -replace ',', ''))
Assert-Contract ($hostedDoc -eq [long]$baseline.hosted.firstViewUncompressedBytes -and $standaloneDoc -eq [long]$baseline.standalone.uncompressedBytes) 'Implementation size statement is stale.'
$completion = Get-Content -LiteralPath (Join-Path $workspace 'COMPLETION_AUDIT.md') -Raw -Encoding UTF8
Assert-Contract ($completion -notmatch '0\.91|36\.6') 'Completion audit cites performance values absent from the saved v23 JSON.'
$buildDoc = Get-Content -LiteralPath (Join-Path $workspace 'BUILD_REPRODUCIBILITY.md') -Raw -Encoding UTF8
Assert-Contract ($buildDoc -match 'Provenance status: tool archive hash not retained') 'Toolchain provenance boundary is overstated.'
$browserless = Get-Content -LiteralPath (Join-Path $workspace 'audit\v23-browserless-baseline.json') -Raw -Encoding UTF8 | ConvertFrom-Json
Assert-Contract ($browserless.status -eq 'pass' -and $browserless.scope -eq 'browserless-current-artifacts' -and -not $browserless.fullAcceptance) 'Browserless audit scope/status is misleading.'
Assert-Contract ($browserless.node.version -eq 'v22.18.0' -and $browserless.node.archive.sha256 -eq 'c95d8a7e1c99e669cc08c9f1176e068c1f50847c37908fcb8c35b62482366511') 'Pinned Node provenance is missing.'
foreach ($test in $browserless.executableTests.psobject.Properties) {
Assert-Contract ($test.Value.status -eq 'pass' -or $test.Name -eq 'runtime-budget.mjs') "Browserless executable gate failed: $($test.Name)"
}
Assert-Contract ($browserless.runtime.status -eq 'measured-not-acceptance' -and $browserless.v22Comparison.status -eq 'not-verifiable' -and $browserless.browserAcceptance.status -eq 'not-run') 'Residual gates are overstated.'
[ordered]@{
status='pass'
proposalContract='revised-v23'
scenes=@($scenes.scenes).Count
viewports=@($scenes.viewports).Count
numericPolicy=$policy.id
hashedFiles=$records.Count
browserRunner='static-contract-pass'
browserlessEvidence='pass-with-explicit-limits'
} | ConvertTo-Json

View file

@ -3,7 +3,8 @@ import vm from 'node:vm';
const root = new URL('../', import.meta.url);
const kernels = await fs.readFile(new URL('kernels.js', root), 'utf8');
let app = await fs.readFile(new URL('script.js', root), 'utf8');
const appSource = await fs.readFile(new URL('script.js', root), 'utf8');
let app = appSource;
const browserHarness = await fs.readFile(new URL('tests/browser-benchmark.html', root), 'utf8');
const browserModule = browserHarness.match(/<script type="module">([\s\S]*?)<\/script>/)?.[1];
if (!browserModule) throw new Error('Browser benchmark module not found.');
@ -148,5 +149,9 @@ console.log(JSON.stringify({
referenceCheckpoints,
deepWorkerModuleInit: true,
browserHarnessSyntax: true,
sources: { appBytes: app.length, shallowWorkerBytes: sources.shallow.length, deepWorkerBytes: sources.deep.length }
sources: {
appBytes: Buffer.byteLength(appSource),
shallowWorkerCharacters: sources.shallow.length,
deepWorkerCharacters: sources.deep.length
}
}));

View file

@ -0,0 +1,30 @@
{
"format": "mandelbrot-numeric-policy-v1",
"id": "numeric-policy-v23",
"rendererVersion": 23,
"pixelContract": {
"mapping": "centered-rational",
"fixedPointRounding": "nearest-ties-away-from-zero",
"fixedPointIntegerMismatchMax": 0,
"float64CoordinateUlpMax": 4,
"tileSeamPixelMismatchMax": 0
},
"validated": {
"falseEscapedMax": 0,
"falseInteriorProvenMax": 0,
"unresolvedMax": 0,
"escapeIterationAbsMax": 0,
"magnitudeRelativeErrorMax": 1e-12,
"referenceGuardBits": 64
},
"balanced": {
"falseEscapedMax": 0,
"falseInteriorProvenMax": 0,
"interiorLikelyMismatch": "record",
"unresolved": "record"
},
"color": {
"smoothCorrectionRelativeErrorMax": 0.00003,
"simdScalarRelativeErrorMax": 1e-7
}
}

View file

@ -0,0 +1,30 @@
{
"format": "mandelbrot-numeric-policy-v1",
"id": "numeric-policy-v23",
"rendererVersion": 23,
"pixelContract": {
"mapping": "centered-rational",
"fixedPointRounding": "nearest-ties-away-from-zero",
"fixedPointIntegerMismatchMax": 0,
"float64CoordinateUlpMax": 4,
"tileSeamPixelMismatchMax": 0
},
"validated": {
"falseEscapedMax": 0,
"falseInteriorProvenMax": 0,
"unresolvedMax": 0,
"escapeIterationAbsMax": 0,
"magnitudeRelativeErrorMax": 1e-12,
"referenceGuardBits": 64
},
"balanced": {
"falseEscapedMax": 0,
"falseInteriorProvenMax": 0,
"interiorLikelyMismatch": "record",
"unresolved": "record"
},
"color": {
"smoothCorrectionRelativeErrorMax": 0.00003,
"simdScalarRelativeErrorMax": 1e-7
}
}

View file

@ -1,12 +1,79 @@
const assert=(ok,message)=>{if(!ok)throw new Error(message)};
const close=(a,b)=>Math.abs(a-b)<=1e-15*Math.max(1,Math.abs(a),Math.abs(b));
const bitsBuffer=new ArrayBuffer(8),bitsView=new DataView(bitsBuffer);
function ulp(value){
if(!Number.isFinite(value))return Infinity;
if(value===0)return Number.MIN_VALUE;
bitsView.setFloat64(0,value,false);
let bits=bitsView.getBigUint64(0,false);
bits+=value>0?1n:-1n;
bitsView.setBigUint64(0,bits,false);
return Math.abs(bitsView.getFloat64(0,false)-value)
}
const closeUlp=(a,b,limit=4)=>Math.abs(a-b)<=limit*Math.max(Number.MIN_VALUE,ulp(a),ulp(b));
const roundDiv=(value,denominator)=>{
const negative=value<0n,absolute=negative?-value:value;
const quotient=(absolute+denominator/2n)/denominator;
return negative?-quotient:quotient
};
assert(roundDiv(1n,2n)===1n&&roundDiv(-1n,2n)===-1n,'half ties must round away from zero');
assert(roundDiv(3n,2n)===2n&&roundDiv(-3n,2n)===-2n,'roundDiv must be sign symmetric');
const view={re:-.743643887037151,im:.13182590420533,span:3.4e-12,w:37,h:23};
const world=(x,y,w=view.w,h=view.h,span=view.span)=>[view.re+(x+.5-w*.5)*span/w,view.im+(h*.5-y-.5)*span/w];
const shallow=(x,y)=>{const scale=view.span/view.w,shiftedRe=view.re+scale*.5,shiftedIm=view.im-scale*.5;return[shiftedRe+(x-view.w*.5)*scale,shiftedIm+(view.h*.5-y)*scale]};
const deep=(x,y)=>{const scale=view.span/view.w,offR=scale*.5,offI=-scale*.5;return[view.re+offR+scale*(x-view.w*.5),view.im+offI+scale*(view.h*.5-y)]};
const bla=(x,y)=>{const scale=view.span/view.w,offR=scale*.5,offI=-scale*.5;return[view.re+offR+view.span*(x/view.w-.5),view.im+offI+view.span*((.5*view.h-y)/view.w)]};
let samples=0;for(let y=0;y<view.h;y++)for(let x=0;x<view.w;x++){const expected=world(x,y);for(const [name,actual]of[['shallow',shallow(x,y)],['deep',deep(x,y)],['bla',bla(x,y)]]){assert(close(expected[0],actual[0])&&close(expected[1],actual[1]),`${name} mapping mismatch ${x},${y}`)}samples++}
for(const sampleScale of[2,4]){const tile={x:7,y:5,w:11,h:9},W=view.w*sampleScale,H=view.h*sampleScale;for(let phase=0;phase<sampleScale*sampleScale;phase++){const left=(phase%sampleScale)*tile.w,top=((phase/sampleScale)|0)*tile.h;for(let yy=0;yy<tile.h;yy++)for(let xx=0;xx<tile.w;xx++){const gx=tile.x*sampleScale+left+xx,gy=tile.y*sampleScale+top+yy,expected=world(gx,gy,W,H,view.span);const direct=[view.re+(gx+.5-W*.5)*view.span/W,view.im+(H*.5-gy-.5)*view.span/W];assert(close(expected[0],direct[0])&&close(expected[1],direct[1]),`tile seam ${sampleScale}x phase ${phase}`)}}}
const first=world(0,0),last=world(view.w-1,view.h-1);assert(close((first[0]+last[0])/2,view.re),'view center moved on x');assert(close((first[1]+last[1])/2,view.im),'view center moved on y');
const result={status:'pass',samples,backends:['shallow','deep','bla'],subsampleScales:[2,4],contract:'(x+0.5,y+0.5)'};
console.log(JSON.stringify(result));export default result;
const world=(x,y,w=view.w,h=view.h,span=view.span)=>[
view.re+(x+.5-w*.5)*span/w,
view.im+(h*.5-y-.5)*span/w
];
const shallow=(x,y)=>{
const scale=view.span/view.w,shiftedRe=view.re+scale*.5,shiftedIm=view.im-scale*.5;
return[shiftedRe+(x-view.w*.5)*scale,shiftedIm+(view.h*.5-y)*scale]
};
const deep=(x,y)=>{
const scale=view.span/view.w,offR=scale*.5,offI=-scale*.5;
return[view.re+offR+scale*(x-view.w*.5),view.im+offI+scale*(view.h*.5-y)]
};
const bla=(x,y)=>{
const scale=view.span/view.w,offR=scale*.5,offI=-scale*.5;
return[view.re+offR+view.span*(x/view.w-.5),view.im+offI+view.span*((.5*view.h-y)/view.w)]
};
let samples=0;
for(let y=0;y<view.h;y++)for(let x=0;x<view.w;x++){
const expected=world(x,y);
for(const [name,actual]of[['shallow',shallow(x,y)],['deep',deep(x,y)],['bla',bla(x,y)]]){
assert(closeUlp(expected[0],actual[0])&&closeUlp(expected[1],actual[1]),name+' mapping mismatch '+x+','+y)
}
samples++
}
for(const sampleScale of[2,4]){
const tile={x:7,y:5,w:11,h:9},W=view.w*sampleScale,H=view.h*sampleScale;
for(let phase=0;phase<sampleScale*sampleScale;phase++){
const left=(phase%sampleScale)*tile.w,top=((phase/sampleScale)|0)*tile.h;
for(let yy=0;yy<tile.h;yy++)for(let xx=0;xx<tile.w;xx++){
const gx=tile.x*sampleScale+left+xx,gy=tile.y*sampleScale+top+yy,expected=world(gx,gy,W,H,view.span);
const direct=[view.re+(gx+.5-W*.5)*view.span/W,view.im+(H*.5-gy-.5)*view.span/W];
assert(closeUlp(expected[0],direct[0])&&closeUlp(expected[1],direct[1]),'tile seam '+sampleScale+'x phase '+phase)
}
}
}
const first=world(0,0),last=world(view.w-1,view.h-1);
assert(closeUlp((first[0]+last[0])/2,view.re),'view center moved on x');
assert(closeUlp((first[1]+last[1])/2,view.im),'view center moved on y');
const fixedBits=320n,fixedOne=1n<<fixedBits;
const fixedView={re:-3n*fixedOne/4n,im:0n,span:1n<<220n,w:37,h:23};
const fixedWorld=(x,y,w=fixedView.w,h=fixedView.h)=>[
fixedView.re+roundDiv(fixedView.span*BigInt(2*x+1-w),BigInt(2*w)),
fixedView.im+roundDiv(fixedView.span*BigInt(h-2*y-1),BigInt(2*w))
];
for(let y=0;y<fixedView.h;y++)for(let x=0;x<fixedView.w;x++){
const point=fixedWorld(x,y),mirror=fixedWorld(fixedView.w-1-x,fixedView.h-1-y);
assert(point[0]+mirror[0]===2n*fixedView.re,'fixed x symmetry mismatch '+x+','+y);
assert(point[1]+mirror[1]===2n*fixedView.im,'fixed y symmetry mismatch '+x+','+y)
}
const result={
status:'pass',samples,backends:['shallow','deep','bla'],subsampleScales:[2,4],
contract:'centered-rational',float64UlpMax:4,fixedPointRounding:'nearest-ties-away-from-zero'
};
console.log(JSON.stringify(result));
export default result;

View file

@ -5,6 +5,12 @@ import { fileURLToPath } from 'node:url';
const root=path.resolve(fileURLToPath(new URL('..',import.meta.url)));
const wasmDir=path.join(root,'build','wasm-v23');
const corpus=JSON.parse(await fs.readFile(path.join(root,'tests','scenes.json'),'utf8'));
const sceneById=id=>{
const scene=corpus.scenes.find(candidate=>candidate.id===id);
if(!scene)throw new Error('Scene missing: '+id);
return scene
};
const load=async name=>(await WebAssembly.instantiate(await fs.readFile(path.join(wasmDir,name)),{})).instance.exports;
const median=values=>values.slice().sort((a,b)=>a-b)[values.length>>1];
const assert=(ok,message)=>{if(!ok)throw new Error(message)};
@ -20,7 +26,7 @@ async function benchmarkShallow(){
function reference(cr,ci,limit){const rr=new Float64Array(limit+1),ri=new Float64Array(limit+1);let zr=0,zi=0;for(let n=0;n<=limit;n++){rr[n]=zr;ri[n]=zi;const nr=zr*zr-zi*zi+cr;zi=2*zr*zi+ci;zr=nr}return{rr,ri}}
async function benchmarkDeep(){
const ex=await load('bla-simd.wasm'),c={id:'seahorse-bla-2000',re:-.743643887037151,im:.13182590420533,span:3.4e-14,w:256,h:144,iter:2000},ref=reference(c.re,c.im,c.iter),scale=c.span/c.w;
const scene=sceneById('swirly-seahorses-z12'),ex=await load('bla-simd.wasm'),c={id:'swirly-seahorses-z12-bla-2000',re:Number(scene.re),im:Number(scene.im),span:Number(scene.span),w:256,h:144,iter:2000},ref=reference(c.re,c.im,c.iter),scale=c.span/c.w;
new Float64Array(ex.memory.buffer,ex.refs_r_ptr(),150001).set(ref.rr);new Float64Array(ex.memory.buffer,ex.refs_i_ptr(),150001).set(ref.ri);
const tBuild=performance.now(),entries=ex.build_bla(c.iter,Math.hypot(c.span*.5,c.span*c.h/(2*c.w)),2**-32),buildMs=performance.now()-tBuild;assert(entries>0,'BLA table build failed');
const run=()=>ex.render_bla_rect_v2(c.span,scale*.5,-scale*.5,c.re,c.im,c.iter,c.w,c.h,0,0,c.w,c.h,c.iter,0,0,1);run();const samples=[];for(let i=0;i<5;i++){const t=performance.now();assert(run()===c.w*c.h,'BLA output size');samples.push(performance.now()-t)}const ms=median(samples),counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),c.w*c.h),unresolved=counts.reduce((n,v)=>n+(v>=0xfffffffe),0);return{id:c.id,width:c.w,height:c.h,iterations:c.iter,pixels:c.w*c.h,blaEntries:entries,buildMs,medianMs:ms,msPerMegapixel:ms*1e6/(c.w*c.h),unresolved,samplesMs:samples}
@ -36,6 +42,6 @@ const source=await fs.readFile(path.join(root,'script.js'),'utf8'),contracts={
viewportIndependentFloor:/minDpr=Math\.min\(1,64\/Math\.max\(cssW,cssH\)\)/.test(source)
};
assert(Object.values(contracts).every(Boolean),`runtime budget contract failed: ${JSON.stringify(contracts)}`);
const report={format:'mandelbrot-node-runtime-budget-v23',generatedUtc:new Date().toISOString(),node:process.version,contracts,shallow:await benchmarkShallow(),deep:await benchmarkDeep()};
const report={format:'mandelbrot-node-runtime-budget-v23',generatedUtc:new Date().toISOString(),node:process.version,status:'measured',acceptance:false,sceneCorpus:corpus.format,measurementNote:'Runtime characterization only; browser acceptance has separate thresholds and evidence.',contracts,shallow:await benchmarkShallow(),deep:await benchmarkDeep()};
const outputArg=process.argv.indexOf('--out');if(outputArg>=0){const target=path.resolve(process.argv[outputArg+1]);await fs.writeFile(target,JSON.stringify(report,null,2)+'\n','utf8')}
console.log(JSON.stringify(report));

View file

@ -1,14 +1,72 @@
{
"format": "mandelbrot-scene-corpus-v1",
"format": "mandelbrot-scene-corpus-v2",
"rendererVersion": 23,
"pixelContract": "centered",
"pixelContract": "centered-rational-ties-away-from-zero",
"numericPolicy": "numeric-policy-v23",
"scenes": [
{"id":"z0","re":"-0.5","im":"0","span":"3.4","tags":["shallow","overview"]},
{"id":"seahorse-z14","re":"-0.743643887037151","im":"0.13182590420533","span":"3.4e-14","tags":["deep","boundary"]},
{"id":"seahorse-z20","re":"-0.743643887037151","im":"0.13182590420533","span":"3.4e-20","tags":["deep","boundary","warm-reference"]},
{"id":"seahorse-z100","re":"-0.743643887037151","im":"0.13182590420533","span":"3.4e-100","tags":["deep","precision"]},
{"id":"period3-interior","re":"-0.122561166876","im":"0.744861766619","span":"1e-8","tags":["interior","periodic"]},
{"id":"deep-cliff-e280","re":"-0.743643887037151","im":"0.13182590420533","span":"1e-280","tags":["deep","scaled-bla-boundary"]}
{
"id": "z0",
"re": "-0.5",
"im": "0",
"span": "3.4",
"coordinate": {"kind":"exact-rational","reNumerator":"-1","reDenominator":"2","imNumerator":"0","imDenominator":"1"},
"oracle": {"kind":"overview","membership":"mixed"},
"tags": ["shallow","overview"]
},
{
"id": "period2-cusp-z14",
"re": "-0.75",
"im": "0",
"span": "3.4e-14",
"coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"},
"oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"},
"tags": ["deep","boundary","parabolic"]
},
{
"id": "swirly-seahorses-z12",
"re": "-0.7453983606667815",
"im": "0.1125046349959942",
"span": "3.52e-12",
"coordinate": {"kind":"exact-rational","reNumerator":"-7453983606667815","reDenominator":"10000000000000000","imNumerator":"1125046349959942","imDenominator":"10000000000000000"},
"oracle": {"kind":"published-performance-scene","membership":"mixed","iterations":2000,"source":"https://github.com/ckormanyos/mandelbrot#swirly-seahorses-and-mini-mandelbrot"},
"tags": ["deep","boundary","performance"]
},
{
"id": "period2-cusp-z20",
"re": "-0.75",
"im": "0",
"span": "3.4e-20",
"coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"},
"oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"},
"tags": ["deep","boundary","warm-reference","parabolic"]
},
{
"id": "period2-cusp-z100",
"re": "-0.75",
"im": "0",
"span": "3.4e-100",
"coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"},
"oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"},
"tags": ["deep","precision","parabolic"]
},
{
"id": "period3-interior",
"re": "-0.122561166876653619975245551820735654052",
"im": "0.744861766619744236593170428604392367240",
"span": "1e-8",
"coordinate": {"kind":"decimal","significantDigits":39,"guardDigitsAtViewport":27},
"oracle": {"kind":"polynomial-root","polynomial":"c^3+2c^2+c+1","membership":"interior","period":3},
"tags": ["interior","periodic"]
},
{
"id": "period2-cusp-e280",
"re": "-0.75",
"im": "0",
"span": "1e-280",
"coordinate": {"kind":"exact-rational","reNumerator":"-3","reDenominator":"4","imNumerator":"0","imDenominator":"1"},
"oracle": {"kind":"known-boundary","id":"main-cardioid-period2-root","membership":"boundary"},
"tags": ["deep","deep-scaled-boundary","parabolic"]
}
],
"viewports": [
{"id":"mobile","cssWidth":390,"cssHeight":844,"dpr":3},

View file

@ -47,6 +47,8 @@ Assert-NotContains $script 'n<36' 'Fixed 36-tile refinement cap returned.'
Assert-Contains $script 'detailCacheBudget\(\)' 'Byte-budget detail cache is missing.'
Assert-Contains $script 'function memoryLedger' 'Logical memory ledger is missing.'
Assert-Contains $script 'function deepWisdomStorageKey' 'Versioned per-device wisdom persistence is missing.'
Assert-Contains $script 'DEEP_WISDOM_TTL_MS=30\*24\*60\*60\*1000' 'Persisted wisdom has no 30-day TTL.'
Assert-Contains $script 'savedAt:Date\.now\(\)' 'Persisted wisdom has no save timestamp.'
Assert-Contains $script 'promoteState\(required\+32-available\);invalidateReferenceOrbit\(\)' 'Orbit-condition precision promotion does not rebuild the reference.'
Assert-Contains $script 'function verifyReferenceCheckpoints' 'P/P+64 reference-orbit checkpoint verification is missing.'
Assert-Contains $script 'state\.processMode!==''validate''' 'Reference-orbit checkpoint verification is not gated to precision-first rendering.'
@ -58,6 +60,7 @@ Assert-NotContains $script 'const SIMD=\$\{JSON\.stringify\(DEEP_SIMD_B64\)\}' '
Assert-Contains $script 'highPrecisionDirectPixelAsync' 'Yielding high-precision direct verifier is missing.'
Assert-Contains $script "state\.processMode==='validate'\|\|task\.tile\.score>1\.35" 'Validated detail subsamples are not guarded-direct.'
Assert-Contains $script 'kernelSha256:globalThis\.MANDEL_KERNEL_META' 'Export kernel identity metadata is missing.'
Assert-Contains $script 'exportForcePrecision&&unresolvedSamples' 'Validated Export can emit a final file with unresolved samples.'
Assert-NotContains $script 'exactDeepPixel' 'Misleading exactDeepPixel alias returned.'
Assert-Contains $script 'deepEngineNeeded' 'ULP-based engine selection is missing.'
Assert-NotContains $script 'scheduleRealWisdom' 'Default active Real Wisdom benchmark returned.'

View file

@ -0,0 +1,10 @@
import fs from 'node:fs/promises';
const s=await fs.readFile(new URL('./webgpu-acceptance.js',import.meta.url),'utf8');
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
must(/row\.known<12/.test(s),'swirly minimum-known gate missing');
must(/row\.unknown!==0/.test(s),'stable-scene UNKNOWN=0 gate missing');
must(/falseEscaped/.test(s)&&/falseBounded/.test(s)&&/guardMismatch/.test(s),'membership/reference gates missing');
must(/dense false escape/.test(s)&&/dense false bounded/.test(s),'dense regression gates missing');
must(/exportSmoke\.unresolved!==0/.test(s)&&/exportSmokeAA\.unresolved!==0/.test(s),'export unresolved gate missing');
must(/uncapturedErrors/.test(s),'uncaptured WebGPU error gate missing');
console.log(JSON.stringify({status:'pass',checks:6,stableUnknownMax:0,swirlyKnownMin:12},null,2));

4
tests/v24-bla-model.mjs Normal file
View file

@ -0,0 +1,4 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8');
const failures=[];if(/buildBla\(|BlaNode|useBla|blaLevels/.test(script+kernels))failures.push('BLA code remains in production');if(!/blaEnabled:false/.test(script))failures.push('sidecar/diagnostics do not declare BLA disabled');if(failures.length)throw new Error(failures.join('; '));
console.log(JSON.stringify({status:'pass',kind:'bla-disabled-contract',reason:'dense swirly regression demonstrated unsafe f32-quantized BLA classifications',productionBla:false},null,2));

View file

@ -0,0 +1,7 @@
function rd(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const[m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits);if(neg)n=-n;return[n,10n**BigInt(places)]}
function fixed(t,b){const[n,d]=decRatio(t);return rd(n*(1n<<BigInt(b)),d)}
function fmt(v,bits){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(bits),q=v*scale>>BigInt(bits);let s=q.toString().padStart(bits+1,'0');s=s.slice(0,-bits)+'.'+s.slice(-bits);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function parseFixed(s,bits){const[n,d]=decRatio(s);return n*(1n<<BigInt(bits))/d}
for(const [text,bits] of [['1e-400',1569],['1e-1000',3562],['-0.75',1569]]){const v=fixed(text,bits),s=fmt(v,bits);if(s==='0'||s==='-0')throw new Error('deep formatter collapsed '+text);const r=parseFixed(s,bits);if(r!==v)throw new Error('exact decimal roundtrip failed '+text);}
console.log(JSON.stringify({status:'pass',kind:'exact-fixed-decimal-format',deep:['1e-400','1e-1000']},null,2));

View file

@ -0,0 +1,47 @@
import fs from 'node:fs/promises';
const corpus=JSON.parse(await fs.readFile(new URL('./scenes.json',import.meta.url),'utf8'));const map=new Map(corpus.scenes.map(s=>[s.id,s]));map.set('period2-cusp-e400',{id:'period2-cusp-e400',re:'-0.75',im:'0',span:'1e-400'});const F=Math.fround;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function decRatio(t){let s=String(t).trim().toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const [m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-e;if(places<0){digits+='0'.repeat(-places);places=0}let n=BigInt(digits),d=10n**BigInt(places);if(neg)n=-n;return[n,d]}
function decFixed(t,b){const[n,d]=decRatio(t);return roundDiv(n*(1n<<BigInt(b)),d)}function bitsForSpan(t){const s=String(t).toLowerCase(),a=s.split('e'),m=Math.abs(Number(a[0])),e=a[1]?Number(a[1]):0,l2=Math.log2(m)+e*Math.log2(10);return Math.max(256,Math.ceil(-l2)+240)}function snap(scene){const bits=bitsForSpan(scene.span);return{bits,re:decFixed(scene.re,bits),im:decFixed(scene.im,bits),span:decFixed(scene.span,bits)}}
function fixedNum(v,bits){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),x=top*Math.pow(2,sh-bits);return neg?-x:x}function log2FixedAt(v,b){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-b}function spanME(s){const l=log2FixedAt(s.span,s.bits),exp=Math.floor(l);return{mant:F(Math.pow(2,l-exp)),exp}}
function pixel(s,w,h,x,y,b=s.bits){const re=align(s.re,s.bits,b),im=align(s.im,s.bits,b),span=align(s.span,s.bits,b),den=BigInt(2*w);return[re+roundDiv(span*BigInt(2*x+1-w),den),im+roundDiv(span*BigInt(h-2*y-1),den)]}
function direct(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,b)+roundShift(zi*zi,b)>bail)return n+1}return limit}
function buildRef(s,limit){const b=s.bits+64,cr=align(s.re,s.bits,b),ci=align(s.im,s.bits,b);let zr=0n,zi=0n;const hi=new Float32Array((limit+1)*2),lo=new Float32Array((limit+1)*2);for(let n=0;n<=limit;n++){const rr=fixedNum(zr,b),ri=fixedNum(zi,b),hr=F(rr),hii=F(ri);hi[n*2]=hr;hi[n*2+1]=hii;lo[n*2]=F(rr-hr);lo[n*2+1]=F(ri-hii);if(n===limit)break;const zr2=roundShift(zr*zr,b),zi2=roundShift(zi*zi,b);zi=roundShift(2n*zr*zi,b)+ci;zr=zr2-zi2+cr}return{hi,lo,refLen:limit}}
function cmul(ar,ai,br,bi){return[F(F(ar*br)-F(ai*bi)),F(F(ar*bi)+F(ai*br))]}function scale(v,e){if(e<-126)return 0;if(e>126)return F(8.507059e37);return F(v*Math.pow(2,e))}
function perturb(ref,s,w,h,x,y,limit,strict=false){const se=spanME(s),dx=F((x+.5-.5*w)/w),dy=F((.5*h-y-.5)/w),U=2**-24;let dr=F(se.mant*dx),di=F(se.mant*dy),wr=0,wi=0,e=se.exp,n=0,m=0,ops=0,err=64*U*Math.max(Math.abs(dr),Math.abs(di));while(true){if(n>=limit){const rp=Math.min(m,ref.refLen)*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),ea=Math.abs(err*2**e)+64*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30),threshold=strict?1e-4:1e-3;return ea<=threshold?{kind:'bounded',n,err:ea}:{kind:'unknown',n,reason:'error-bound',err:ea}}if(m>ref.refLen)return{kind:'unknown',n,reason:'ref'};const rp=m*2,delr=scale(wr,e),deli=scale(wi,e),zr=F(ref.hi[rp]+F(ref.lo[rp]+delr)),zi=F(ref.hi[rp+1]+F(ref.lo[rp+1]+deli)),mag=F(F(zr*zr)+F(zi*zi)),ea=Math.abs(err*2**e)+64*U*(Math.max(Math.abs(zr),Math.abs(zi))+Math.max(Math.abs(delr),Math.abs(deli))+1e-30);if(mag>4){if(Math.hypot(zr,zi)-ea>2)return{kind:'escaped',n,err:ea};return{kind:'unknown',n,reason:'escape-uncertain',err:ea}}const dmag=F(F(delr*delr)+F(deli*deli));if(m>0&&dmag>0&&mag<dmag){if(se.exp-e<-96)return{kind:'unknown',n,reason:'rebase-gap'};err=ea;wr=zr;wi=zi;dr=scale(F(se.mant*dx),se.exp);di=scale(F(se.mant*dy),se.exp);e=0;m=0;err+=64*U*Math.max(Math.abs(dr),Math.abs(di));continue}if(m>=ref.refLen)return{kind:'unknown',n,reason:'ref-end'};const rr=ref.hi[rp],ri=ref.hi[rp+1],lr=ref.lo[rp],li=ref.lo[rp+1],refAbs=Math.max(Math.abs(rr),Math.abs(ri))+Math.max(Math.abs(lr),Math.abs(li)),wAbs=Math.max(Math.abs(wr),Math.abs(wi)),dAbs=Math.max(Math.abs(dr),Math.abs(di)),p2=Math.abs(2**e),gain=2*refAbs+2*wAbs*p2,roundErr=64*U*(2*refAbs*wAbs+wAbs*wAbs*p2+dAbs+1e-30);err=gain*err+roundErr;const[a,b]=cmul(rr,ri,wr,wi),[c,d]=cmul(lr,li,wr,wi),[sqR,sqI]=cmul(wr,wi,wr,wi);wr=F(F(F(2*a)+F(2*c))+F(scale(sqR,e)+dr));wi=F(F(F(2*b)+F(2*d))+F(scale(sqI,e)+di));m++;n++;ops++;if(!Number.isFinite(wr)||!Number.isFinite(wi)||!Number.isFinite(err)||Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di))>=1e30||err>1e35)return{kind:'unknown',n,reason:'range'};const mm=Math.max(Math.abs(wr),Math.abs(wi),Math.abs(dr),Math.abs(di));if(mm>65536){wr=F(wr/65536);wi=F(wi/65536);dr=F(dr/65536);di=F(di/65536);err/=65536;e+=16}else if(mm>0&&mm<1/65536&&e>se.exp){wr=F(wr*65536);wi=F(wi*65536);dr=F(dr*65536);di=F(di*65536);err*=65536;e-=16}if(e>126||ops>limit*2+2048)return{kind:'unknown',n,reason:'guard'}}}
const cfg=[['period2-cusp-z14',1200],['swirly-seahorses-z12',2000],['period2-cusp-z20',1800],['period2-cusp-z100',2400],['period3-interior',1200],['period2-cusp-e280',2400],['period2-cusp-e400',1600]],w=17,h=11,pts=[];
for(let y=0;y<h;y++)for(let x=0;x<w;x++)pts.push([x,y]);
const report=[];
for(const mode of [{name:'balanced',strict:false},{name:'strict',strict:true}]){
for(const[id,limit]of cfg){
const sc=map.get(id),s=snap(sc),ref=buildRef(s,limit);
let guardMismatch=0,falseEscaped=0,falseBounded=0,unknown=0,exactEscape=0;
const mismatch=[];
for(const[x,y]of pts){
const p=pixel(s,w,h,x,y),g=pixel(s,w,h,x,y,s.bits+64),a=direct(p[0],p[1],s.bits,limit),ag=direct(g[0],g[1],s.bits+64,limit);
if(a!==ag){guardMismatch++;continue}
const r=perturb(ref,s,w,h,x,y,limit,mode.strict);
if(r.kind==='unknown'){unknown++;continue}
if(r.kind==='escaped'&&a===limit)falseEscaped++;
if(r.kind==='bounded'&&a<limit)falseBounded++;
if(r.kind==='escaped'&&a<limit&&r.n===a)exactEscape++;
if(((r.kind==='escaped'&&a<limit&&r.n!==a)||(r.kind==='bounded'&&a<limit))&&mismatch.length<5)mismatch.push({x,y,oracle:a,gpu:r});
}
report.push({mode:mode.name,id,bits:s.bits,limit,guardMismatch,falseEscaped,falseBounded,unknown,known:pts.length-unknown,exactEscape,mismatch});
}
}
const denseScene=map.get('swirly-seahorses-z12'),denseS=snap(denseScene),denseRef=buildRef(denseS,2000),densePoints=[[25,12],[26,15],[27,25]],dense=[];
for(const[x,y]of densePoints){
const p=pixel(denseS,61,39,x,y),g=pixel(denseS,61,39,x,y,denseS.bits+64),a=direct(p[0],p[1],denseS.bits,2000),ag=direct(g[0],g[1],denseS.bits+64,2000);
if(a!==ag)throw new Error('dense oracle guard mismatch '+x+','+y);
for(const mode of [{name:'balanced',strict:false},{name:'strict',strict:true}]){
const r=perturb(denseRef,denseS,61,39,x,y,2000,mode.strict);
if(r.kind==='escaped'&&a===2000)throw new Error('dense false escape '+JSON.stringify({mode:mode.name,x,y,a,r}));
if(r.kind==='bounded'&&a<2000)throw new Error('dense false bounded '+JSON.stringify({mode:mode.name,x,y,a,r}));
dense.push({mode:mode.name,x,y,oracle:a,gpu:r});
}
}
if(report.some(r=>r.guardMismatch||r.falseEscaped||r.falseBounded))throw new Error('numeric classification failure '+JSON.stringify(report));
const allUnknownStrict=report.filter(r=>r.mode==='strict'&&r.unknown===pts.length);
if(allUnknownStrict.length)throw new Error('strict mode became all UNKNOWN: '+JSON.stringify(allUnknownStrict));
console.log(JSON.stringify({status:'pass',kind:'guarded-production-equation-cpu-f32-model-not-real-gpu',thresholds:{balanced:1e-3,strict:1e-4},report,denseRegression:dense},null,2));

View file

@ -0,0 +1,13 @@
import fs from 'node:fs/promises';
const corpus=JSON.parse(await fs.readFile(new URL('./scenes.json',import.meta.url),'utf8'));
const scene=corpus.scenes.find(s=>s.id==='z0'); const F=Math.fround;
function rd(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function rs(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
function dec(t,b){let s=String(t).toLowerCase(),neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const[m,e0='0']=s.split('e'),e=Number(e0),[i='0',f='']=m.split('.');let dg=(i+f).replace(/^0+(?=\d)/,'')||'0',p=f.length-e;if(p<0){dg+='0'.repeat(-p);p=0}let n=BigInt(dg);if(neg)n=-n;return rd(n*(1n<<BigInt(b)),10n**BigInt(p))}
function pixel(re,im,span,b,w,h,x,y){const den=BigInt(2*w);return[re+rd(span*BigInt(2*x+1-w),den),im+rd(span*BigInt(h-2*y-1),den)]}
function oracle(cr,ci,b,limit){const bail=4n<<BigInt(b);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const a=rs(zr*zr,b),c=rs(zi*zi,b);zi=rs(2n*zr*zi,b)+ci;zr=a-c+cr;if(rs(zr*zr,b)+rs(zi*zi,b)>bail)return n+1}return limit}
function direct(cr,ci,limit){cr=F(cr);ci=F(ci);const U=2**-24,y2=F(ci*ci),x=F(cr-.25),q=F(F(x*x)+y2),lhs=F(q*F(q+x)),rhs=F(.25*y2),margin=F(16*U*(Math.abs(lhs)+Math.abs(rhs)+1));if(lhs<F(rhs-margin))return limit;const x2=F(cr+1),bulb=F(F(x2*x2)+y2),bulbMargin=F(16*U*(Math.abs(bulb)+.0625+1));if(bulb<F(.0625-bulbMargin))return limit;let zr=0,zi=0;for(let n=0;n<limit;n++){const a=F(zr*zr),c=F(zi*zi);zi=F(F(F(2*zr)*zi)+ci);zr=F(F(a-c)+cr);if(F(F(zr*zr)+F(zi*zi))>4)return n+1}return limit}
const bits=256,re=dec(scene.re,bits),im=dec(scene.im,bits),span=dec(scene.span,bits),W=33,H=21,limit=800;let falseEscaped=0,falseBounded=0,guardMismatch=0,countMismatch=0,tested=0;
for(let y=0;y<H;y++)for(let x=0;x<W;x++){const [cr,ci]=pixel(re,im,span,bits,W,H,x,y),[gr,gi]=pixel(re<<64n,im<<64n,span<<64n,bits+64,W,H,x,y),a=oracle(cr,ci,bits,limit),g=oracle(gr,gi,bits+64,limit);if(a!==g){guardMismatch++;continue}const rr=Number(cr)/2**bits,ii=Number(ci)/2**bits,r=direct(rr,ii,limit);tested++;if(r<limit&&a===limit)falseEscaped++;if(r===limit&&a<limit)falseBounded++;if(r<limit&&a<limit&&r!==a)countMismatch++}
if(falseEscaped||falseBounded)throw new Error(JSON.stringify({falseEscaped,falseBounded,guardMismatch,countMismatch}));
console.log(JSON.stringify({status:'pass',kind:'direct-f32-cpu-model-not-real-gpu',tested,guardMismatch,falseEscaped,falseBounded,countMismatch},null,2));

View file

@ -0,0 +1,6 @@
const must=(x,m)=>{if(!x)throw new Error(m)};
for(const [W,H] of [[17,11],[1024,768],[16384,9216]])for(const tile of [64,512])for(const sx of [.25,.5,.75])for(const sy of [.25,.5,.75])for(const [tx,ty,lx,ly] of [[0,0,0,0],[tile,0,3,5],[tile*2,tile,7,11]]){if(tx+lx>=W||ty+ly>=H)continue;const gx=tx+lx+sx,gy=ty+ly+sy,fullX=(gx-.5*W)/W,fullY=(.5*H-gy)/W,tiledX=((tx+lx+sx)-.5*W)/W,tiledY=(.5*H-(ty+ly+sy))/W;must(Object.is(fullX,tiledX)&&Object.is(fullY,tiledY),'tile mapping seam')}
const maxFramePixels=8*1048576,fieldBytes=maxFramePixels*4;must(fieldBytes<=128*1024*1024,'default storage binding budget exceeded');
const oldQueueBytes=maxFramePixels*4,newCounterBytes=16;must(newCounterBytes<oldQueueBytes/100000,'unresolved queue was not reduced to a counter');
for(const [cw,ch] of [[390,844],[1920,1080],[1080,1920]]){const aspect=ch/cw,w0=16384,h0=Math.round(w0*aspect),h=Math.min(16384,h0),w=h0>16384?Math.max(64,Math.round(h/aspect)):w0;must(w<=16384&&h<=16384,'export dimension clamp failed')}
console.log(JSON.stringify({status:'pass',pixelContract:'centered',exportTile:512,strictRetryPass:false,unresolvedCounterBytes:newCounterBytes,maxExportSide:16384},null,2));

View file

@ -0,0 +1 @@
import fs from 'node:fs/promises';const h=await fs.readFile(new URL('../index.html',import.meta.url),'utf8');const req=['id="view"','id="processMode"','保守的 (Strict)','id="exportPrecision"','gpu-kernels.js','script.js'];for(const x of req)if(!h.includes(x))throw new Error('missing '+x);if(h.includes('<script src="kernels.js"></script>'))throw new Error('legacy kernels.js loaded');console.log(JSON.stringify({status:'pass',checks:req.length+1},null,2));

View file

@ -0,0 +1,40 @@
import assert from 'node:assert/strict';
import {inflateSync} from 'node:zlib';
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
class StreamingPng{
constructor(w,h){this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
async rows(filteredRows){await this.writer.write(filteredRows)}
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
}
assert.equal(typeof CompressionStream,'function','CompressionStream required for this model');
const p=new StreamingPng(2,2);
const row0=Uint8Array.from([255,0,0,255, 0,255,0,255]);
const row1=Uint8Array.from([0,0,255,255, 255,255,255,128]);
const filtered=new Uint8Array(18);filtered.set(row0,1);filtered.set(row1,10);await p.rows(filtered);
const bytes=new Uint8Array(await (await p.finish()).arrayBuffer());
assert.deepEqual(Array.from(bytes.subarray(0,8)),[137,80,78,71,13,10,26,10]);
let off=8, width=0, height=0, idats=[], seenIend=false;
while(off<bytes.length){
const dv=new DataView(bytes.buffer,bytes.byteOffset+off);
const len=dv.getUint32(0,false); const type=new TextDecoder().decode(bytes.subarray(off+4,off+8));
const data=bytes.subarray(off+8,off+8+len); const got=dv.getUint32(8+len,false);
assert.equal(got,crc32Parts([bytes.subarray(off+4,off+8),data]),`CRC ${type}`);
if(type==='IHDR'){const h=new DataView(data.buffer,data.byteOffset,data.byteLength);width=h.getUint32(0,false);height=h.getUint32(4,false);assert.equal(data[8],8);assert.equal(data[9],6)}
if(type==='IDAT')idats.push(data);
if(type==='IEND')seenIend=true;
off += 12+len;
}
assert.equal(width,2); assert.equal(height,2); assert.ok(idats.length>=1); assert.ok(seenIend);
const packed=Buffer.concat(idats.map(x=>Buffer.from(x)));
const raw=new Uint8Array(inflateSync(packed));
assert.equal(raw.length,2*(1+2*4));
assert.equal(raw[0],0); assert.deepEqual(Array.from(raw.subarray(1,9)),Array.from(row0));
assert.equal(raw[9],0); assert.deepEqual(Array.from(raw.subarray(10,18)),Array.from(row1));
const aborted=new StreamingPng(1,1); await aborted.rows(Uint8Array.from([0,0,0,0,255])); await aborted.abort(new Error('cancelled'));
console.log(JSON.stringify({status:'pass',png:{width,height,idatChunks:idats.length,rawBytes:raw.length},abort:'settled'},null,2));

View file

@ -0,0 +1,3 @@
import fs from 'node:fs/promises';import vm from 'node:vm';import {performance} from 'node:perf_hooks';
const source=await fs.readFile(new URL('../script.js',import.meta.url),'utf8'),prefix="function referenceWorkerSource(){return String.raw`",start=source.indexOf(prefix)+prefix.length,end=source.indexOf("`}\nclass ReferenceService",start);if(start<prefix.length||end<0)throw new Error('worker source not found');const workerSource=source.slice(start,end);let message=null;const self={postMessage:m=>{message=m},onmessage:null};const ctx={self,postMessage:(...a)=>self.postMessage(...a),performance,Math,Number,BigInt,ArrayBuffer,DataView,Float32Array,Float64Array,Uint32Array,Set,String,Error};vm.createContext(ctx);vm.runInContext(workerSource,ctx,{timeout:5000});if(typeof self.onmessage!=='function')throw new Error('worker handler missing');
self.onmessage({data:{type:'build',id:1,key:'test',bits:256,re:(-3n*(1n<<256n)/4n).toString(),im:'0',iter:800}});if(!message)throw new Error('worker produced no message');if(message.type==='error')throw new Error(message.error);if(message.checkpointMismatch)throw new Error('reference +64 guard checkpoint mismatch');if(message.refLen<3||message.refs.byteLength!==(message.refLen+1)*16)throw new Error('reference packing invalid');for(const forbidden of ['nodes','levels','nodeCount','levelCount','invalidNodes'])if(forbidden in message)throw new Error('BLA payload leaked from quarantined reference worker: '+forbidden);const primary=message;message=null;const b=1536,R=(-3n*(1n<<BigInt(b))/4n).toString();self.onmessage({data:{type:'build',id:2,key:'e400',bits:b,re:R,im:'0',iter:120}});if(!message||message.type==='error')throw new Error(message?.error||'e400 worker produced no message');if(message.checkpointMismatch||message.refs.byteLength!==(message.refLen+1)*16)throw new Error('e400 reference contract failed');console.log(JSON.stringify({status:'pass',refLen:primary.refLen,precisionBits:primary.precisionBits,checkpointCount:primary.checkpointCount,buildMs:primary.buildMs,productionBla:false,e400:{checkpointMismatch:message.checkpointMismatch,refLen:message.refLen}},null,2));

View file

@ -0,0 +1,32 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url),script=await fs.readFile(new URL('script.js',root),'utf8'),kernels=await fs.readFile(new URL('gpu-kernels.js',root),'utf8'),html=await fs.readFile(new URL('index.html',root),'utf8');
const must=(ok,msg)=>{if(!ok)throw new Error(msg)};
must(/const VERSION=24/.test(script),'renderer version is not 24');
must(/navigator\.gpu\.requestAdapter/.test(script),'WebGPU adapter path missing');
must(/guarded rescaled perturbation/.test(script),'guarded deep WebGPU path missing');
must(/referenceWorkerSource/.test(script)&&/BigInt\(d\.re\)/.test(script),'BigInt reference worker missing');
must(!/function buildBla\(/.test(script)&&!/BlaNode/.test(kernels)&&!/useBla/.test(kernels),'unsafe BLA path is not fully quarantined');
must(/F32_U/.test(kernels)&&/safe_abs_error/.test(kernels)&&/errAbs/.test(kernels),'deep error-bound guard missing');
must(/struct UnresolvedHead/.test(kernels)&&/atomicAdd\(&unresolved\.remaining/.test(kernels),'unresolved counter missing');
must(!/strict_queue|QUEUE_FINALIZE_WGSL/.test(kernels)&&!/dispatchWorkgroupsIndirect|qitems|strict-indirect/.test(script),'redundant strict retry queue remains');
must(/texture_storage_2d<rgba8unorm,write>/.test(kernels),'GPU color field path missing');
must(/AA_RESOLVE_WGSL/.test(kernels)&&/renderTileRGBA2x/.test(script),'GPU 2x2 export resolve missing');
must(/class StreamingPng/.test(script)&&/CompressionStream\('deflate'\)/.test(script)&&/async abort\(reason\)/.test(script),'streaming PNG export/abort missing');
must(/PRESENT_WGSL/.test(kernels),'GPU reprojection/presentation missing');
must(/recolorPending/.test(script)&&!/const token=\+\+state\.token,stateSnap/.test(script),'recolor race fix missing');
must(/this\.context=null/.test(script)&&/webgpuCanvasClaimed=true;this\.configure\(\)/.test(script)&&/if\(webgpuCanvasClaimed\)return null/.test(script),'delayed WebGPU canvas claim/fallback guard missing');
must(/このズーム深度はWebGPUが必要です/.test(script),'deep fallback rejection missing');
must(!/Math\.min\(340/.test(script),'deep exact-coordinate formatter still capped at 340 digits');
must(/uncapturederror/.test(script)&&/pushErrorScope/.test(script),'WebGPU error capture missing');
must(/gpu-kernels\.js/.test(html)&&!/src="kernels\.js"/.test(html),'index is not WebGPU-only');
must(/numericParams/.test(script)&&/colorParams/.test(script),'persistent frame parameter buffers missing');
must(/ensureExportWorkspace/.test(script)&&/exportWorkspaceDestroy/.test(script)&&/export tile exceeds reusable workspace/.test(script),'reusable export GPU workspace missing');
must(/if\(!adapter\)adapter=await navigator\.gpu\.requestAdapter\(\)/.test(script),'adapter fallback request missing');
must(/gpuInitFailed/.test(script)&&/WebGPU初期化失敗/.test(script),'shader/pipeline failure guard missing');
must(/if\(state\.gpuInitFailed\)return null/.test(script),'failed shader initialization can be retried on every render');
must(/if\(!r\)\{if\(state\.gpuInitFailed\)/.test(script),'shader failure still enters CPU fallback');
must(/return 524288/.test(script)&&/1572864/.test(script),'responsive screen pixel budgets missing');
must(/gpuUnavailable/.test(script)&&/return 262144/.test(script),'lightweight no-adapter CPU fallback budget missing');
for(const banned of ['DEEP_SIMD_B64','DEEP_SCALAR_B64','BLA_SIMD_B64','BLA_SCALAR_B64','deepPool','deepWasm','highPrecisionDirectPixelAsync','renderDeepAdaptive','render_perturb_rebase_rect','WebAssembly.Instance'])must(!script.includes(banned),'legacy production deep dependency remains: '+banned);
must(!/Validated direct/.test(html),'obsolete Validated UI remains');
console.log(JSON.stringify({status:'pass',rendererVersion:24,shaderVersion:'24.1.3',checks:28},null,2));

View file

@ -0,0 +1,5 @@
import fs from 'node:fs/promises';
const root=new URL('../',import.meta.url);const exists=async p=>{try{await fs.stat(new URL(p,root));return true}catch{return false}};
for(const banned of ['kernels.js','src/deep_kernel.c','src/bla_kernel_v18.c','src/color_kernel.c','src/shallow_kernel.c','build/wasm-v23'])if(await exists(banned))throw new Error('legacy renderer asset remains: '+banned);
for(const required of ['index.html','script.js','gpu-kernels.js','tests/webgpu-acceptance.html','tests/webgpu-acceptance.js'])if(!await exists(required))throw new Error('required v24 asset missing: '+required);
console.log(JSON.stringify({status:'pass',legacyDeepAssets:0,requiredAssets:5},null,2));

View file

@ -0,0 +1,21 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
const root=new URL('../',import.meta.url);
const src=await fs.readFile(new URL('gpu-kernels.js',root),'utf8');
const context={};context.globalThis=context;vm.runInNewContext(src,context,{filename:'gpu-kernels.js'});
const kernels=context.MANDEL_WEBGPU_KERNELS;
if(!kernels)throw new Error('kernel bundle did not initialize');
// WGSL 16.2 Reserved Words. A module must not contain one of these tokens.
const reserved=`NULL Self abstract active alignas alignof as asm asm_fragment async attribute auto await become cast catch class co_await co_return co_yield coherent column_major common compile compile_fragment concept const_cast consteval constexpr constinit crate debugger decltype delete demote demote_to_helper do dynamic_cast enum explicit export extends extern external fallthrough filter final finally friend from fxgroup get goto groupshared highp impl implements import inline instanceof interface layout lowp macro macro_rules match mediump meta mod module move mut mutable namespace new nil noexcept noinline nointerpolation non_coherent noncoherent noperspective null nullptr of operator package packoffset partition pass patch pixelfragment precise precision premerge priv protected pub public readonly ref regardless register reinterpret_cast require resource restrict self set shared sizeof smooth snorm static static_assert static_cast std subroutine super target template this thread_local throw trait try type typedef typeid typename typeof union unless unorm unsafe unsized use using varying virtual volatile wgsl where with writeonly yield`.split(/\s+/);
const strip=s=>s.replace(/\/\*[\s\S]*?\*\//g,' ').replace(/\/\/.*$/gm,' ');
const failures=[];
for(const [name,code] of Object.entries(kernels)){
if(typeof code!=='string'||!name.endsWith('_WGSL'))continue;
const clean=strip(code);
for(const word of reserved){
const re=new RegExp(`\\b${word}\\b`);
if(re.test(clean))failures.push(`${name}: reserved token ${word}`);
}
}
if(failures.length)throw new Error(failures.join('\n'));
console.log(JSON.stringify({status:'pass',check:'wgsl-reserved-words',shaderVersion:kernels.version,kernels:Object.keys(kernels).filter(k=>k.endsWith('_WGSL')).length},null,2));

View file

@ -0,0 +1,4 @@
<!doctype html><meta charset="utf-8"><title>v24 WebGPU acceptance</title>
<style>body{font:13px ui-monospace,monospace;background:#09101d;color:#e9efff;margin:16px}iframe{width:320px;height:200px;border:1px solid #445}pre{white-space:pre-wrap}</style>
<h1>v24 WebGPU acceptance</h1><iframe id="app" src="../index.html"></iframe><pre id="out">starting…</pre>
<script src="webgpu-acceptance.js"></script>

View file

@ -0,0 +1,35 @@
(()=>{'use strict';
const out=document.querySelector('#out'),frame=document.querySelector('#app');
const scenes=[
['z0','-0.5','0','3.4',120],
['period2-cusp-z14','-0.75','0','3.4e-14',1200],
['swirly-seahorses-z12','-0.7453983606667815','0.1125046349959942','3.52e-12',2000],
['period2-cusp-z20','-0.75','0','3.4e-20',1800],
['period2-cusp-z100','-0.75','0','3.4e-100',2400],
['period3-interior','-0.122561166876653619975245551820735654052','0.744861766619744236593170428604392367240','1e-8',1200],
['period2-cusp-e280','-0.75','0','1e-280',2400],
['period2-cusp-e400','-0.75','0','1e-400',1600]
];
function roundDiv(v,d){const neg=v<0n,a=neg?-v:v,q=(a+d/2n)/d;return neg?-q:q}
function align(v,a,b){const d=b-a;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,q=(a+(1n<<(BigInt(b)-1n)))>>BigInt(b);return neg?-q:q}
function pixel(st,x,y,bits=st.bits){const re=align(BigInt(st.re),st.bits,bits),im=align(BigInt(st.im),st.bits,bits),span=align(BigInt(st.span),st.bits,bits),den=BigInt(2*st.width);return[re+roundDiv(span*BigInt(2*x+1-st.width),den),im+roundDiv(span*BigInt(st.height-2*y-1),den)]}
function orbit(cr,ci,bits,limit){const bail=4n<<BigInt(bits);let zr=0n,zi=0n;for(let n=0;n<limit;n++){const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+ci;zr=zr2-zi2+cr;if(roundShift(zr*zr,bits)+roundShift(zi*zi,bits)>bail)return n+1}return limit}
function points(w,h){const xs=[0,.25,.5,.75,1].map(t=>Math.min(w-1,Math.max(0,Math.round(t*(w-1))))),ys=[0,.25,.5,.75,1].map(t=>Math.min(h-1,Math.max(0,Math.round(t*(h-1)))));return ys.flatMap(y=>xs.map(x=>[x,y]))}
async function waitApp(){if(frame.contentWindow&&frame.contentWindow.__MANDEL_TEST__)return;await new Promise(r=>frame.addEventListener('load',r,{once:true}));const t=performance.now();while(!frame.contentWindow.__MANDEL_TEST__&&performance.now()-t<15000)await new Promise(r=>setTimeout(r,50));if(!frame.contentWindow.__MANDEL_TEST__)throw new Error('test hook not available')}
async function checkScene(api,id,re,im,span,iter,mode){
const r=await api.setView({re,im,span,baseIter:iter,adaptive:false,processMode:mode}),diag=r.diag;
if(diag.backend!=='webgpu'||diag.webgpuError)throw new Error(id+'/'+mode+': WebGPU backend unavailable/error: '+(diag.webgpuError||diag.backend));
const st=api.state(),ps=points(st.width,st.height),meta=await api.sampleMeta(ps);let guardMismatch=0,falseEscaped=0,falseBounded=0,escapeIterationMismatch=0,unknown=0;
for(let i=0;i<ps.length;i++){const [x,y]=ps[i],p=pixel(st,x,y),pg=pixel(st,x,y,st.bits+64),a=orbit(p[0],p[1],st.bits,st.iter),g=orbit(pg[0],pg[1],st.bits+64,st.iter);if(a!==g){guardMismatch++;continue}const m=meta[i],cls=(m>>>28)&3,n=m&0x0fffffff;if(cls===0){unknown++;continue}if(cls===1&&a===st.iter)falseEscaped++;if(cls!==1&&a<st.iter)falseBounded++;if(cls===1&&a<st.iter&&n!==a)escapeIterationMismatch++}
const row={id,mode,width:st.width,height:st.height,iter:st.iter,deep:diag.deep,guardMismatch,falseEscaped,falseBounded,escapeIterationMismatch,unknown,known:ps.length-unknown,reference:diag.reference};
if(guardMismatch||falseEscaped||falseBounded)throw new Error(id+'/'+mode+' numeric gate failed: '+JSON.stringify(row));
if(id==='swirly-seahorses-z12'){if(row.known<12)throw new Error(id+'/'+mode+': excessive UNKNOWN rate: '+JSON.stringify(row));}
else if(row.unknown!==0)throw new Error(id+'/'+mode+': stable corpus scene produced UNKNOWN samples: '+JSON.stringify(row));
return row;
}
async function run(){await waitApp();const api=frame.contentWindow.__MANDEL_TEST__,report=[];for(const [id,re,im,span,iter] of scenes){for(const mode of (id==='z0'?['standard']:['standard','validate'])){out.textContent='running '+id+' / '+mode+'…\n'+JSON.stringify(report,null,2);report.push(await checkScene(api,id,re,im,span,iter,mode));}}
await api.setView({re:'-0.7453983606667815',im:'0.1125046349959942',span:'3.52e-12',baseIter:2000,adaptive:false,processMode:'validate'});const denseState={...api.state(),width:61,height:39},denseMeta=await api.probeMeta({w:61,h:39,strict:true}),densePoints=[[25,12],[26,15],[27,25]],denseRegression=[];for(const[x,y]of densePoints){const p=pixel(denseState,x,y),pg=pixel(denseState,x,y,denseState.bits+64),a=orbit(p[0],p[1],denseState.bits,denseState.iter),g=orbit(pg[0],pg[1],denseState.bits+64,denseState.iter);if(a!==g)throw new Error('dense guard mismatch '+x+','+y);const m=denseMeta[y*61+x],cls=(m>>>28)&3,n=m&0x0fffffff;if(cls===1&&a===denseState.iter)throw new Error('dense false escape '+JSON.stringify({x,y,a,cls,n}));if(cls!==0&&cls!==1&&a<denseState.iter)throw new Error('dense false bounded '+JSON.stringify({x,y,a,cls,n}));denseRegression.push({x,y,oracle:a,cls,n})}
await api.setView({re:'-0.75',im:'0',span:'3.4e-20',baseIter:1000,adaptive:false,processMode:'validate'});const exportSmoke=await api.smokeExportTile({w:48,h:32,strict:true,ss:1}),exportSmokeAA=await api.smokeExportTile({w:48,h:32,strict:true,ss:2});if(exportSmoke.length!==exportSmoke.expected||exportSmokeAA.length!==exportSmokeAA.expected)throw new Error('GPU export tile readback length mismatch');if(exportSmoke.unresolved!==0||exportSmokeAA.unresolved!==0)throw new Error('GPU export smoke left unresolved samples: '+JSON.stringify({exportSmoke,exportSmokeAA}));const diag=frame.contentWindow.__MANDEL_DIAG__.snapshot();if(diag.uncapturedErrors&&diag.uncapturedErrors.length)throw new Error('uncaptured WebGPU errors: '+JSON.stringify(diag.uncapturedErrors));const result={status:'pass',kind:'real-webgpu-acceptance',date:new Date().toISOString(),shaderVersion:diag.shaderVersion,adapter:diag.adapter,limits:diag.limits,exportSmoke,exportSmokeAA,denseRegression,report};out.textContent=JSON.stringify(result,null,2);document.title='PASS v24.1 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result}
run().catch(e=>{const result={status:'fail',error:String(e&&e.stack||e)};out.textContent=JSON.stringify(result,null,2);document.title='FAIL v24.1 WebGPU acceptance';globalThis.__WEBGPU_ACCEPTANCE__=result});
})();