Compare commits

...

2 commits

Author SHA1 Message Date
de177e8896 e 2026-08-23 17:12:29 +09:00
caa90775f9 2026-08-22 20:55:27 +09:00
125 changed files with 8234 additions and 51 deletions

206
AUDIT.md Normal file
View file

@ -0,0 +1,206 @@
# v18 数値・性能監査
> **Archive notice:** この監査はv18時点の履歴資料です。現行renderer v23の受入値ではありません。v23のsource/配布baselineは`audit/v23-source-baseline.json`、実行時browser baselineは`audit/v23-browser-baseline.json`へ分離します。
## 目的
v7〜v17で積み重なった補正処理を「残っているから残す」のではなく、正しさ・速度・相互作用を測定して再判定した。
絶対時間は実行環境に依存するため、**誤判定率・同条件での比率・処理の計算量**を主な判断材料とする。
## 結論
| 処理 | v18判断 | 理由 |
|---|---|---|
| Arbitrary precision + Reference Orbit | 維持 | deep zoomの座標精度と基準軌道に必須 |
| Perturbation + Rebasing | 維持 | 全pixelをBigIntで直接計算するより圧倒的に有利 |
| BLA | 維持・監視強化 | deep pixel kernelの主高速化手段。ただし適切なεは画面依存 |
| Persistent Worker / Reference | 維持 | warm stateの価値が大きい |
| 固定距離によるReference維持 | 改廃 | 距離だけではrecenterの損得を判定できない |
| Pilot render | 縮小して維持 | コスト予測専用なら有用。画素の代用には不正確 |
| 数学的iteration上限を途中で切るwork cap | **廃止** | 黒/外部分類を壊す |
| Pilotのnearest-neighborで未解決pixelを補完 | **廃止** | 境界で誤差が大きい |
| coarse periodic interior mask | **廃止** | 効く画面が偏り、pilotコストも増える |
| 全strip strict verification | **廃止** | deep zoomで隠れた巨大ボトルネック |
| 1点の誤差で全画面strict再描画 | **廃止** | 局所誤差に対して再計算範囲が過大 |
| 複数の解像度feedback controller | **統合** | controller同士が競合して粗いまま固定されることがあった |
| Dither | **廃止** | detail detectorの勾配を人工的に増やし、余計な高精細tileを誘発 |
| Adaptive detail refinement/cache | 維持 | idle時へ遅延すれば費用対効果が良い |
| Autoの黒/色境界追従 | 維持 | kernelと独立で軽く、誤航行抑制に有用 |
| Synthetic Worker Wisdom | **廃止** | 実際のWASM+BLA負荷と相関しにくい |
| Real BLA Wisdom | 維持 | 実kernelを使ってWorker数を選べる |
## 1. v17のiteration work capは数学的に不正確
v17はdeep base描画で、表示用global iterationより小さい`computeIter`を使い、そこまで発散しなければ事実上黒候補として扱っていた。
Seahorse付近、48×30、global 3490 / cap 2870をBigInt直接計算で照合した。
- capまで残ったpixel: **1440 / 1440**
- その後3490までに発散したpixel: **1066**
- cap survivor中のlate escape: **74.03%**
つまり「2870で未発散だから黒」とみなすのは、この画面では4分の3近くを誤分類する。
**v18:** `computeIter = colorIter`へ戻した。負荷制限は数学的iteration数ではなく、BLA operation / perturbation operationの回数に対して行う。上限到達は`UNRESOLVED`であり黒ではない。少数なら局所再計算、多ければそのstripだけ無制限BLAへ戻す。
Raw: `audit/raw/workcap_truth_v17.json`
## 2. Pilot画像による未解決pixel補完は不正確
128×78のfull BLAと56×34 pilotのnearest-neighbor補完を比較。
- 黒/外部分類の不一致: **451 / 9984 = 4.52%**
- escape iteration差が8超: **43.71%**
境界付近では低解像度pilotを「計算結果」として使えない。
**v18:** pilotはコスト推定にだけ使う。画面へ表示せず、未解決pixelの色・黒判定にも使わない。
Raw: `audit/raw/pilot_fill_audit.json`
## 3. Interior detectionは「追加反復」ではなく早期終了で入れる
以前の黒専用look-aheadは、黒pixelへ追加iterationを課すため重かった。v18では逆に、内部と判定できたpixelを**早く終了**する。
実装:
- main cardioid / period-2 bulbの解析判定
- 軌道微分の収縮判定
- BLA jump時はBLAの線形係数で微分も更新
96×72、2500 iterationでInterior detection OFF/ONを比較。黒/外部分類不一致は全テスト0。
| 場所 | OFF | ON | 分類不一致 |
|---|---:|---:|---:|
| period-3 wide | 766.7 ms | 6.0 ms | 0 |
| period-3 mid | 438.7 ms | 2.3 ms | 0 |
| period-2近傍 | 364.7 ms | 1.0 ms | 0 |
| mixed interior/exterior | 282.8 ms | 16.2 ms | 0 |
| cardioid cusp近傍 | 329.9 ms | 1.8 ms | 0 |
これは「黒を正しくするために重くする処理」ではなく、黒の多い画面を軽くする処理として残す価値が高い。
Raw: `audit/raw/interior_safety_v18.json`
## 4. BLA εは単一固定値にできない
Seahorse z≈14、160×98で保守的BLAを基準に比較。
| ε | 時間 | class mismatch | iteration差>1 |
|---|---:|---:|---:|
| 2^-23 | 約299 ms | 1.00% | 5.23% |
| 2^-32 | 約786 ms | 0.383% | 1.16% |
| 2^-40 | 約1218 ms | 0.083% | 0.198% |
一方、同じSeahorseでもz≈20では2^-23が基準と一致したテストがある。したがって「常に厳密」「常に緩い」の両方が非効率。
**v18:** fast BLAを使い、1フレームあたり固定少数点だけsafe BLAと照合する。誤差が出たstripだけsafe BLAで再計算する。
- 通常の検証はstrict perturbationではない
- 1stripごとに何点もstrict計算しない
- 1点の不一致で全画面をstrict再描画しない
Raw: `audit/raw/audit_v18_kernel.txt`
## 5. per-strip strict verificationは隠れたボトルネックだった
旧設計では各stripのBLA後にstrict perturbationを複数点実行していた。非常に深い場所ではBLA本体が数msでも、strict検証が毎strip積み重なりwall timeを支配する。
v18の設計:
1. fast BLAでstrip計算
2. **フレーム全体で固定個数**だけsafe BLAと比較
3. mismatchがあれば**そのstripだけ**safe BLAで再計算
4. status failureの少数pixelだけexact fallback
これで検証コストが「strip数 × deep iteration」に増えにくくなった。
## 6. Reference recenterは距離だけで決めない
z≈14ではrecenterが有利なケースが多かった。
- 0.8画面横移動: first-frameで約**91 ms節約**
- 0.8横+0.5縦: 約**40 ms節約**
しかしz≈20では、0.5画面横移動でrecenterが約**14.5 ms損**するケースもあった。
同じ「0.5画面離れた」でも損得が逆転するため、固定距離閾値は合理的でない。
**v18:**
- Referenceは基本的に維持
- 実測kernel MPPが基準より悪化したときだけ、予測損失とReference rebuild EMAを比較
- rebuildの方が安いと予測できる場合だけrecenter
- 極端に離れた場合だけhard safety limitを使う
Raw: `audit/raw/reference_recenter_audit.json`, `audit/raw/reference_recenter_audit_z20.json`
## 7. Resolution controlを一本化
過去版では以下が同時に解像度へ作用していた。
- pilot推定
- renderPerf MPP
- deepScale
- Device WisdomのresScale
- 品質段階ごとのmin/max width
複数feedbackが同時に動くため、負荷変化後に過剰縮小したり、逆に最低幅に張り付いたりしやすかった。
**v18:** deep base resolutionは一つの時間予算モデルで決定する。pilotまたは最近の実測MPPを入力にし、detail tileは別のidle品質レイヤーとして扱う。
## 8. Ditherをdetail判定の前に入れない
Ditherはグラデーションbandingには効くが、画像のRGB勾配を人工的に増やす。detail detectorの入力へ先に適用すると「存在しない細部」を検出して追加tileを計算する。
**v18:** runtime ditherは削除。Canvasのhigh-quality scalingは維持。再導入するならdetail選定後の表示/export段階だけにする。
## 9. Real BLA Wisdomは残す
旧Synthetic Wisdomは単純なJS浮動小数点loopでWorker数を選んでいた。実際の負荷はWASM、BLA table、reference memory、postMessageを含むため別物。
**v18:** 長時間idle時のみ実際のBLA mini renderを1〜4 Workerで測定し、実スループットからWorker数を決める。解像度feedbackとは切り離した。
## 10. v17→v18 end-to-end確認
同一のclean Chromium 144 headless、780×441 viewportで、UI操作に相当するdeep base passを比較した。動的解像度を含む**実際のbase-frame pipeline比較**であり、pixel-for-pixel kernel benchmarkではない。
| Scene | v17 | v18 | 改善 |
|---|---:|---:|---:|
| Seahorse z≈14 | 1059 ms | 288 ms | **3.68×** |
| Seahorse z≈20 | 1226 ms | 238 ms | **5.15×** |
| Seahorse z≈100 | 983 ms | 474 ms | **2.07×** |
| period-3 z≈20 | 1931 ms | 781 ms | **2.47×** |
v18のclean runではruntime exception 0。
Raw: `audit/raw/v17-final-browser-benchmark.json`, `audit/raw/v18-final-browser-benchmark.json`, `audit/benchmarks.json`
## 11. 残した処理
次はデータ上、削る理由が弱いので維持した。
- BigInt fixed-point coordinate
- Reference Orbit cache + precision promotion
- Perturbation / Rebasing
- BLA
- Series Approximation主にfallback側
- Persistent Worker
- smooth coloring LUT
- Auto black/colour boundary tracking + safe rollback
- world-space HQ tile cache
- progressive detail refinement
- high-quality Canvas scaling
## 12. 今後のボトルネック
v18ではpixel hot-loopの補正処理を大幅に整理した。さらに深い地点で残る主候補はReference OrbitのBigInt生成である。
特にReferenceが長く、かつ高bit精度になるケースでは、次の候補は:
1. Reference Orbit専用Worker
2. 次Referenceの先読み / double buffer
3. Reference orbitの圧縮・周期利用
ただしz≈20程度ではReference生成が常に主因ではないため、常時複雑化するよりtelemetryで必要なときだけ有効化する方がよい。

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上修正済みです。

51
BUILD_REPRODUCIBILITY.md Normal file
View file

@ -0,0 +1,51 @@
# WASM build reproducibility
v23では、`src/`を監査可能なkernel source、`src/abi.json`を公開ABI、
`toolchain.lock.json`を固定toolchain契約として扱います。
## 互換payloadを展開する
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/extract-wasm.ps1
```
現在配布している8 payloadを`dist/wasm/`へ展開し、byte数とSHA-256を
`manifest.json`へ記録します。COLOR SIMD/scalarがbyte単位で同一ならStandaloneでは
Base64文字列を1本へ自動集約し、異なる場合は各payloadを保持します。どちらの場合もmanifest上の2 ABI名は維持します。
## sourceから再生成する
clang 17.0.6をPATHへ置き、次を実行します。
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-wasm.ps1
```
生成先は`build/wasm-v23/`です。`src/shallow_kernel.c`
`src/deep_kernel.c``src/bla_kernel_v18.c``src/color_kernel.c`をそれぞれ
SIMD/scalarでcompileし、timestampを含まないsource manifestを生成します。
配布payloadへ昇格するときだけ`-Promote`を指定します。昇格前に
`tests/kernel-golden.mjs`が、画素中心座標、escape iteration、magnitude、
smooth補正、SIMD/scalar同値性を検査します。golden失敗時はコピーも
`kernels.js`再生成も行いません。
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-wasm.ps1 -Promote
```
## 信頼境界
clang 17.0.6とNode.js v22.18.0を作業領域内で使用し、復元sourceのcompile、
generated-WASM golden、SIMD/scalar同値性、画素中心契約を実行済みです。tool配布archive自体の
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`へ記録しています。
省略なしの再検証は、toolchainをPATHへ追加した状態で次を実行します。
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/test-all.ps1 -RequireToolchain
```

55
COMPLETION_AUDIT.md Normal file
View file

@ -0,0 +1,55 @@
# Mandelbrot Deep Zoom v23 完了監査
## 判定
`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」と判定する。
## 要件トレーサビリティ
| 提案 | 実装結果 | 主な証拠 |
|---|---|---|
| 6.1 イベント駆動 | 常時RAFを廃止。paintstatsをinvalidate時だけ予約し、非表示時はscheduler・render・refine・Exportを停止 | `script.js``scheduleFrame``requestScheduler``stopSchedulers``visibilitychange``tests/source-contract.ps1` |
| 6.2 Canvas計算grid分離 | 処理モード別Screen pixel budgetとeffective DPRを導入。Previewは110ms予算、cold deepは幅48px以下。標準shallowは2〜4MP、標準deepは実測MPPから約1.4秒予算へ調整、精細は4〜8MP | `screenPixelBudget``adaptStandardDeepBudget``resize``targetSize``RENDER_PROFILE` |
| 6.3 gesture取消 | wheel 110ms、pointer 90ms settle。操作中は再投影。省電力はPreviewで停止し、標準はCovered後の追加処理なし。精細の追加反復はescape境界候補だけをdeep 384shallow 4,096点まで実行。非共有環境は短いtilechunk | gesture handlers、`nextQualityDue``scheduleUnknownContinuation` |
| 6.4 deep遅延生成 | main deep WASM、Module、Worker、referenceを数値条件付近まで生成しない。1 Workerから実測増員。実job wisdomを端末・kernel keyで永続化 | `prewarmDeepAssets``prepareDeepModules``addDeepWorker``deepWisdomStorageKey` |
| 6.5 WASMmemory | compile済みdeepBLAcolor Moduleを一度だけ作りWorkerへclone。field buffer返却、byte ledger、Worker退役を実装。1 module1 shared memory統合はPhase 3Bの条件付き項目 | `compileKernelPair`、Worker `init``memoryLedger``retireDeepAssets``tests/module-clone.mjs` |
| 6.6 byte cache | fixed countを廃止し、mobile 96 MiBdesktop 192 MiBのmanaged byte ledgerとLRUへ変更 | `rendererMemoryBudget``detailCacheBudget``trimDetailCache` |
| 6.7 HostedStandalone | content hash外部WASMstreaming compileのHosted、3ファイル直開きのStandaloneを生成。deep requestは深部到達前0の契約 | `hosted-loader.js``scripts/build-hosted.ps1``scripts/build-standalone.ps1``dist/` |
| 6.8 再現build | 4 kernel source、ABI、固定clang flags、SHA-256 manifest、SIMDscalar goldenを整備 | `src/``toolchain.lock.json``BUILD_REPRODUCIBILITY.md``build/wasm-v23/manifest.json` |
| 7.1 画素中心 | JS、BigInt、shallowdeepBLA WASM、detail、Exportを`x+0.5,y+0.5`へ統一しrenderer v23化 | `tests/pixel-mapping.mjs` 851 sample、`tests/pixel-contract.mjs` 1,440 sample |
| 7.2 adaptive AA | 全域sample後、数値fieldの分類・分散・confidenceでquadtree 2×4× subsample。linear RGBでresolve | `scoreDetailTiles``resolveSubsampleField` |
| 7.3 fieldcolor分離 | state、smooth、iteration、confidenceを保持。境界subsample fieldも保持。palette変更は再着色だけ | `makeField``colorizeField``recolorCurrentField` |
| 7.4 BLAdetail精度 | Covereddetailで同一precision profile。高scoreまたはValidated detailはguarded direct。同一world座標の数値gateと視覚gateを分離 | `blaProfile``sendDeepDetailRect`、pixel tests |
| 7.5 未確定分離 | `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を分離。反復を局所継続し、未確定数を表示・Export metadataへ記録 | field constants、`scheduleUnknownContinuation``runValidation` |
| 7.6 誤差ベース精度 | pixel stepULPorbit telemetryhysteresisでengine選択。round-to-nearest fixed point。PP+64 directとreference checkpointを照合し、不一致時はglobal reference再構築 | `deepEngineNeeded``roundShift``highPrecisionDirectPixelAsync``verifyReferenceCheckpoints` |
| 7.7 超深部 | 上限と未確定を診断表示。scaled BLA、multi-reference、可変参照長はPhase 4のtelemetry条件付き研究 | 診断UI、下記「条件付き項目」 |
| 7.8 Export | Quick snapshotと独立tiled Exportを分離。1×2×4×custom、AA、BalancedValidated、進捗、取消、全tile後encode、JSON sidecar | `runExport``tests/browser-benchmark.html` |
| 89 改廃・UX | 連続`q`、固定DPR/HQ幅、RGB detail、固定deep閾値、eager pool、件数cacheを廃止。ViewSpec、history、正確座標、初回UI、mobile status、Advanced、accessibilityを実装 | `index.html``script.js``tests/source-contract.ps1` |
## Gate結果
| Gate | 結果 |
|---|---|
| JavaScript本体生成Workerbrowser harness構文、mock DOM初期化 | pass |
| deepBLAcolor `WebAssembly.Module` structured clone・instantiate | pass |
| 256320-bit direct agreement、analytic interior整数証明 | pass |
| reference PP+64 checkpoint agreement・故意の不一致検出 | pass |
| Node固定scene計測予算式契約 | pass。shallow 2 sceneと、出典付きのswirly-seahorses混在境界sceneでdeep BLAを測定し、deep標準を固定画素数から実測時間適応へ変更。保存値は端末負荷に依存する特性値であり、時間上限そのものをassertするgateではない |
| 全backend pixel mapping・2×4× tile seam | pass、851 sample |
| 配布shallow SIMD pixel contract | pass、1,440 sample、mismatch 0 |
| kernel sourceABI、payload checksum、HostedStandalone build | pass |
| 実ブラウザ performancevisualinteractionExportaccessibility | not-run。Browser bindingなし |
機械可読なsource基準値は`audit/v23-source-baseline.json`、未実施browser gateは`audit/v23-browser-baseline.json`に記録した。
追加のbrowser非依存監査は`audit/v23-browserless-baseline.json`へ保存した。固定Node.js v22.18.0で7 executable tests、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を要求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が揃った時点で固定実機の最終受入を判定できる。

92
IMPLEMENTATION_REPORT.md Normal file
View file

@ -0,0 +1,92 @@
# Mandelbrot Deep Zoom v23 実装報告
## 結論
`IMPROVEMENT_PROPOSAL.md`のv22向け設計判断をrenderer v23へ反映し、ローカル数値検証、WASM再現build、HostedStandalone成果物の生成まで実施した。提案は履歴文書であり、修正版のcoverageExport受入契約を現行基準とする。実ブラウザでのみ測定できる性能・表示・操作・download検証は、今回の環境にin-app browser bindingがないため未実行である。したがって、実装と確認済みの非ブラウザgate、未確認のbrowser acceptanceを分けて扱う。
## 実装済み
- 常時RAFを廃止し、invalidate時だけ動くevent-driven schedulerへ変更した。
- wheelpinchpan中は再投影だけを行い、settle後に計算を1回開始する。
- Preview時間予算を110msへ下げ、cold deep Previewは幅48px以下から開始する。以後は実測MPPでPreview寸法を決める。
- 標準deepのCoveredは実測MPPから約1.4秒のpixel budgetを算出する。未計測・再投影のみの場合は保守的MPPを使い、DPR下限は固定比率ではなく長辺64pxとするため4Kでも予算が効く。精細・精度優先ではこの時間適応を行わない。
- 処理モードの自動到達点を固定し、省電力はPreview、標準はCovered、精細はRefined、精度優先はValidatedで停止する。標準では境界AAと未確定追加反復を既定で実行しない。
- 精細の未確定追加反復は全未確定pixelではなくescape境界候補に限定し、1 viewあたりdeep 384点shallow 4,096点を上限にする。
- `Reprojected → Preview → Covered → Refined → Validated検証未完了`を分離した。
- Coveredはeffective DPRの表示Canvas全域をpixel中心`(x + 0.5, y + 0.5)`で計算する。
- escape field、分類、confidenceと彩色を分離し、palette変更時の反復再計算を廃止した。
- fieldへ各sampleの実escape iterationを保持し、再着色・未確定継続・Export監査で近似値ではなく同じ数値結果を使う。
- 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で永続化し、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未確定補修率を加え、精度昇格時は低精度で作った基準軌道を破棄して再構築する。
- 高精度directを約6msでyieldする非同期処理へ変更し、取消可能にした。
- 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を実装した。
- Hosted版はcontent-hashed外部WASMとstreaming compile、Standalone版は3ファイル直開きを維持した。
- cacheは件数ではなくmobile 96 MiBdesktop 192 MiBのlogical byte ledgerで管理する。
## 再現buildと配布
- `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を固定した。
- 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を削除する。
生成先:
- Hosted: `dist/hosted/``dist/wasm/`
- Standalone: `dist/standalone/`
- 再現build: `build/wasm-v23/`
現在の非圧縮基準値は、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
- source contract: pass
- kernel sourceABI contract: pass
- fixed-point precision 256320 bit: pass
- 基準軌道PP+64 checkpoint一致と故意の不一致検出: pass
- analytic interior integer proof 256320 bit: pass
- pixel mapping 851 samples、shallowdeepBLA、2×4× tile seam: pass
- 配布shallow WASM pixel contract 1,440 samples、mismatch 0: pass
- generated-WASM shallowcolordeepBLA golden: pass
- SIMDscalar結果比較: pass
- 本体、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`。browser時間目標の達成を意味しない
- HostedStandalone buildと全payload SHA-256: pass
toolchainを利用できる環境では次で省略なしに再実行できる。
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File scripts/test-all.ps1 -RequireToolchain
```
## Browser acceptance gate
`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`のままである。
残る実機確認:
- mobiledesktop4Kの表示画像とnative coverage
- 操作反映p95、PreviewCoveredRefined所要時間
- idle時の予約RAFtimerbackground jobCanvas・DOM write 0
- browserGPUを含むobserved peak memory
- Export寸法、完了前downloadなし、取消、PNGJSON内容
- 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の再利用として明示している。

566
IMPROVEMENT_PROPOSAL.md Normal file
View file

@ -0,0 +1,566 @@
# マンデルブロ集合ビューワー 軽量・高精細化 改善提案
- 対象baseline: Mandelbrot ∞ Zoom v22
- 文書status: v22に対する実装前提案。採用項目はrenderer v23へ実装済みであり、現行状態と残存gateは`IMPLEMENTATION_REPORT.md``COMPLETION_AUDIT.md`を正とする。
- Document status: historical-v22 / implemented-v23
- 証拠境界: v22成果物とGit snapshotは現workspaceに残っていないため、v22の行番号・bytesは履歴記録であり再監査可能なbaselineではない。以後の比較基準はversion付きJSONとSHA-256を必須とする。
- 作成日: 2026-08-22
- 対象ファイル: `index.html`, `script.js`, `kernels.js`, `src/bla_kernel_v18.c`
- 性格: 設計判断を保存する履歴文書兼、修正版の品質契約。効果値は、version付き監査JSONの引用を除き検証すべき目標または仮説である。
## 1. 結論
現行実装は、任意精度座標、基準軌道、摂動法、リベース、BLA、SIMD、局所フォールバックまで備えている。数値エンジンの骨格を捨てて全面的にGPU化するより、次の順で周辺設計を改める方が、軽量性と高精細を両立しやすい。
1. 待機中も続く全画面再描画を止め、イベント駆動にする。
2. 描画品質を曖昧な数値 `q` ではなく、Reprojected / Preview / Covered / Refined / Validated の完了条件で管理する。
3. 画面全域を、確定した表示pixel gridまでタイル描画してから、境界だけ適応的アンチエイリアスする。通常はnative DPRとし、画素予算でDPRを制限した場合は明示する。
4. 計算結果と彩色を分離し、色変更でフラクタル計算をやり直さない。
5. 深部WorkerとWASMを遅延生成し、固定最大長・Workerごとの重複メモリを減らす。
6. 画素中心、BLA精度、未解決画素、出力完了条件を明文化し、「高精細」の意味を計測・監査できる状態へ変える。
最優先はカーネルの細かな命令最適化ではない。現状の配布物は合計138,831 bytesとすでに小さい一方、待機中の常時描画、巨大なCanvas、Workerごとの固定配列は継続的なCPU/GPU・メモリ負荷になるためである。
## 2. 「軽量」と「高精細」の定義
本提案では、目標を次のように分ける。
| 軸 | 意味 | 主な指標 |
|---|---|---|
| 配布の軽さ | 初回ロード・解析・WASMコンパイルが小さい | 転送bytes、起動時に取得/compileする資産 |
| 実行の軽さ | 操作中と待機中にCPU/GPUを浪費しない | long task、idle CPU、Canvas draw回数、消費電力 |
| メモリの軽さ | DPR・Worker数・参照長が増えても予算内に収める | peak memory、cache bytes、Workerごとの固定領域 |
| ラスター精細度 | 表示ピクセルを実サンプルで覆い、境界を適切にAAする | sample coverage、resolved coverage、samples/pixel、継ぎ目 |
| 数値精度 | 座標・反復・BLA近似の誤差を検出し、未解決を黒と混同しない | 高精度referenceとの差、unresolved数、検証モード |
| 出力精細度 | 表示プレビューとは独立して、指定寸法を完了後に保存する | 出力寸法、AA、再現性、未完了pixel数 |
「Canvasが4Kサイズである」だけでは高精細とはみなさない。低解像度フレームを4K Canvasへ拡大した状態と、4K分のサンプルを実計算した状態を区別する。
本書のcoverageは次の2値へ分ける。sample coverageは宣言したiteration/precision policyで新ViewSpecのsampleを計算済みのtarget pixel比率であり、`UNRESOLVED`も計算済みsampleとして含む。resolved coverageは`ESCAPED`またはpolicy上の`INTERIOR_PROVEN`へ確定した比率である。Coveredはsample coverage 100%、Validatedはsample coverage 100%に加えてunresolved 0を要求する。
## 3. 現状評価
### 3.1 残す価値が高い部分
- BigInt固定小数点による中心・表示幅と、段階的な精度昇格 (`script.js:58-109`)
- 可変精度BigInt固定小数で生成し、Float64表現を摂動/BLAへ渡す基準軌道 (`script.js:608-705`)
- SIMDからscalar、さらにJS/BigIntへのフォールバック (`script.js:208-227`, `script.js:551-685`)
- 常駐Workerのwarm cache、少数検証、strip単位の局所補修 (`script.js:296-390`)
- main cardioid / period-2 bulbと収縮判定による内部早期終了 (`src/bla_kernel_v18.c:94-172`)
- 操作中の既存フレーム再投影 (`script.js:154-184`, `script.js:393-424`)
- smooth coloring、world-space detail cacheという考え方 (`script.js:187-206`, `script.js:393-531`)
既存のv18監査では、v17比のbase-frame改善が2.07〜5.15倍で、テストした内部早期終了は黒/外部分類不一致0だった (`AUDIT.md:60-82`, `AUDIT.md:163-176`)。この実績があるため、BLAや内部早期終了を一律に廃止する理由はない。
提案作成時の現行コードはv22だったのに、性能監査と最終ベンチの表題・データはv18だった。既存値は設計判断の参考には使えるが、v22の受入基準にはできない。当初は同じsceneのv22再計測を必要条件としたが、現在は不変なv22成果物が残っていないため比較を`not-verifiable`とし、version固定したv23以後のbaselineだけを回帰判定へ使う。
### 3.2 主要なギャップ
| 優先度 | ギャップ | 根拠 | 影響 |
|---|---|---|---|
| P0 | 静止中も永久に`requestAnimationFrame`を回す | `script.js:722-729` | idle CPU/GPU、電池、発熱 |
| P0 | 1フレームで全Canvasと全detail cacheを再合成し、DOM統計も更新する | `script.js:413-423`, `script.js:713-716` | 画面が変わらなくても継続負荷。ドラッグ中は同一RAF内で二重paintになる |
| P0 | 解像度制御が端末実測を使っていない | `renderPerf`は記録のみ。`targetSize()`は固定tier (`script.js:42-44`, `script.js:426-439`, `script.js:534-537`) | 小画面では過剰計算、大画面では不足 |
| P0 | HQ全画面は最大2100×2200で、選抜領域だけ2倍 | `script.js:429-461` | 4Kでは要求gridを全域計算しない。細線を基礎画像で見失うと精細化されない |
| P0 | 確認できるJS/direct/BLA経路が、画素中心ではなく整数格子点をサンプルする | `script.js:516-518`, `script.js:570-573`, `script.js:672-674`, `src/bla_kernel_v18.c:184-190` | 非対称なsample grid、解像度間対応の非標準化、backend差のリスク |
| P0 | detail fast BLAがbase HQより緩い | base HQはe-32、detailは通常e-28。`strictBoundary`は現在常にfalse (`script.js:350-359`, `script.js:504-525`) | 「精細化した領域の方が数値的に粗い」可能性 |
| P0 | PNGが現在の表示Canvasを即時保存する | `script.js:739` | 再投影中・低解像度・detail未完了でも高解像度出力に見える |
| P1 | 起動180ms後にdeep poolを必ず作る | `script.js:296-307`, `script.js:750` | 浅部だけ見る場合もWorker、参照配列、LUTを確保 |
| P1 | Workerごとに基準軌道とBLA表を重複保持・構築する | `script.js:249-259`, `src/bla_kernel_v18.c:3-23` | 深部使用後のpeak memoryが大きい |
| P1 | WorkerでstripごとにRGBAを新規確保し、transfer後にmain全画面配列とImageDataへ再コピーする | `script.js:265-269`, `script.js:378-382`, `script.js:398-400` | memory bandwidth・一時領域。transfer自体はzero-copy |
| P1 | palette/cycle/shift変更で数値計算まで再実行する | `script.js:397`, `script.js:468`, `script.js:744-747` | 深部の色調整が不必要に重い |
| P1 | deep切替が固定ズーム指数11.5 | `script.js:20`, `script.js:707-709` | 中心値、DPR、1画素幅に対し過剰または不足 |
| P1 | 有限反復表示として`maxIter`到達を黒にするため、精度優先時も証明済み内部と未確定を区別できない | `script.js:116-119`, `script.js:561-564`, `script.js:666-670` | Validated/Exportで確定度を説明できない。v17の途中work cap不具合とは別問題 |
| P2 | BLAが通常doubleのspanに依存し、約10^-280以深で無効になる | `script.js:364`, `script.js:508`, `src/bla_kernel_v18.c:175-190` | 超深部で性能が崖状に低下 |
### 3.3 メモリ上の具体例
`src/bla_kernel_v18.c`の固定配列だけを合計すると、1つのBLA WASM instanceあたり約15.6 MiBになる。
- 出力: `counts[65536]` + `mags[65536]` ≈ 0.75 MiB
- 参照軌道: `refs_r[150001]` + `refs_i[150001]` ≈ 2.29 MiB
- BLA double配列5本 × 300100 ≈ 11.45 MiB
- BLA length配列 × 300100 ≈ 1.14 MiB
深部job後に4 WorkerがBLA instanceを持てば、この固定領域だけで約62.5 MiBとなる。さらに各WorkerにはJS側の`RR/RI`、3パレットのLUT、deep/color WASM、出力scratchがある。
また、Worker内部の`RR/RI`とLUTはWASMより先に生成されるため、起動直後に4 Workerを作るだけでもtyped arrayだけで概算13 MiB超を確保する。固定最大配列を現在の`refLen`へ合わせる効果は大きい。
### 3.4 配布サイズ上の具体例
- `index.html`: 6,618 bytes
- `script.js`: 80,077 bytes
- `kernels.js`: 52,136 bytes
- 実行に必要な3ファイル合計: 138,831 bytes
- Base64部分: 51,808文字、復号後WASM合計38,845 bytes
絶対量はすでに小さい。Base64と復号後の約13KB差は非圧縮表現上の値であり、Brotli転送量が同量減るわけではない。外出しの主効果は独立cache、遅延取得、streaming compileであるため、先にidle負荷とメモリ重複を直す方がよい。なお`COLOR_SIMD_B64``COLOR_SCALAR_B64`は同一内容であり、現状は同じ2,510-byteバイナリを二重に内包している。
## 4. 推奨する新しい品質仕様
内部の連続値`q`を、利用者と実装の双方が判定できる状態機械へ置き換える。現状は初回`render(.3)`に対し分岐が`q < .3`なので、意図した最軽量tierではなく次のtierへ入る (`script.js:426-437`, `script.js:751`)。enum化はこの種の境界ずれも防ぐ。
| 状態 | 開始条件 | 解像度・計算 | 完了条件 | UI表示例 |
|---|---|---|---|---|
| Reprojected | wheel / pinch / pan継続中 | 既存フレームのtransformのみ | 入力停止待ち | プレビュー |
| Preview | 入力停止80〜120ms後 | 時間予算内の低解像度。近似BLA可 | 最優先tileを表示 | 高速描画 |
| Covered | Preview後 | 選択した処理モードのtarget gridを全域tile計算 | sample coverage 100%、再投影pixel 0。未確定数は別表示 | 全域描画 68% |
| Refined | Covered後のidle | 境界・不確実tileだけ2×/4× adaptive AA | 対象tile完了 | 境界AA完了 |
| Validated | 精度優先時またはValidated Export | 宣言した有限反復・精度policyを局所照合 | checks合格かつunresolved 0。残る場合は別状態「検証未完了・未確定N」 | 検証完了 / 検証未完了 |
重要な契約は次の通り。
- Previewは粗くてよいが、新しい未解決画素を「内部」と断定して黒にしない。
- Coveredはズーム深度に関係なく、確定したScreen Canvas gridを新しいViewSpecの実sampleだけで全域計算する。再投影した旧frameは描画中のpresentationにだけ使い、sample coverageへ数えない。計算済み`UNRESOLVED`はsample coverageへ数えるがresolved coverageへは数えない。
- native DPRは上限であり、標準・精細とも明示したpixel/memory budget内で使う。精細は時間適応で解像度を下げないが、memory/pixel上限は維持する。DPRを制限した場合はeffective DPRと実解像度を表示する。
- Refinedは単なる2倍補間ではなく、複数の実サンプルをlinear-lightでresolveする。
- Validatedは数学的なマンデルブロ集合所属証明ではなく、宣言した有限反復・precision policyの照合完了を表す。全sampleへ誤差境界または証明手続きを適用する将来モードだけを`Certified`と呼ぶ。
- 「高精細」と「数値検証完了」を同じチェックボックスにしない。
- Exportは表示Canvasとは独立したjobであり、view、寸法、反復方針、AA、precision tier、乱数seedを固定してから開始する。Balanced ExportとValidated Exportを区別し、前者は未確定数をmetadataへ記録して完了可能、後者だけをunresolved 0必須とする。
処理モードと描画状態は分ける。
| 処理モード | Coveredのtarget | 自動到達点 |
|---|---|---|
| 省電力 | 自動Coveredなし。Previewは最大約1MP | Preview |
| 標準 | shallowはmobile低memory約2MP、desktop約4MP。deepはPreview実測から約1.4秒のCovered予算へeffective DPRを下げる | Covered。AA・未確定追加反復は自動実行しない |
| 精細 | mobile低memory約4MP、desktop約8MP。上限時は実解像度を明示 | Refined。未確定追加反復は境界候補だけを上限付きで行う |
| 精度優先 | 指定gridとprecision policy | Validated。将来はCertifiedを選択可 |
Exportは処理モードではなく独立jobである。Balanced / Validatedのprecision tierを選び、それぞれRefined相当 / Validated相当の完了条件を適用する。
## 5. 推奨アーキテクチャ
```text
入力
ViewStateBigInt座標・履歴・version
Render Schedulergesture、優先度、時間/メモリ予算、取消)
├─ f64 SIMD backend
├─ deep BLA / perturbation backend
└─ high-precision verifier / fallback
Field Cacheescaped / interior likely / interior proven / unresolved、smooth value、confidence
├─ Colorizer → Screen Compositor
└─ Adaptive AA / Export
```
責務を次の単位へ分ける。
- `view-model`: BigInt座標、pixel-to-world規約、URL version、戻る/進む
- `scheduler`: gesture settle、tile priority、generation cancel、visibility、品質状態
- `backend`: f64 / deep / verifierを同じtile protocolで実装
- `field-cache`: palette非依存のpacked数値結果、未解決tileだけの継続状態、byte-budget LRU
- `colorizer`: palette、tone mapping、最終ditherだけを担当
- `presenter`: 再投影、tile合成、coverage表示
- `exporter`: 指定寸法の独立tile render、進捗、取消、metadata
Workerへ送るjobは、最低限`viewVersion`, `tile`, `samplePattern`, `iterationPolicy`, `precisionPolicy`を持たせる。古いversionの結果は合成せず、1 tileを8〜12ms程度へ収めて取消後の滞留を限定する。
## 6. 軽量化の具体策
### 6.1 イベント駆動描画へ変更する — P0
現在の`loop()`は静止中も毎フレーム`paintFrame()``updateStats()`、次のRAF予約を行う。次へ変更する。
- `needsPaint` / `needsStats` / `scheduled`を設け、invalidate時だけRAFを1回予約する。
- pointermove/wheelはイベントごとに描かず、最新transformを1 RAFに集約する。
- render/tile完了時だけ該当rectを合成する。
- statsは状態変化時のみ、操作中でも最大4〜10Hzに制限する。
- `document.hidden`ではdetail、Wisdom、未表示tileを停止する。
- 静止完了後はRAF、Canvas write、DOM writeを0回/秒にする。
これは実装難度が低く、画質を落とさず、待機時負荷を大きく下げる最初の変更である。
### 6.2 表示Canvasと計算解像度を分離する — P0
現状は表示Canvasを`innerWidth × min(DPR, 2.5)`で確保する一方、frameは最大2100×2200である。4K・高DPRでは巨大な表示バッファへ小さいframeを拡大するだけになり得る。
極端な例として、3840×2160 CSS px・DPR 2.5では表示Canvasだけで9600×5400×4 bytes、約198 MiBになる。画素数上限がないため、source frameより大幅に大きいsurfaceを常時再描画し得る。
- Screen Canvasには処理モード別に約1〜8MPのbyte/pixel budgetを設ける。標準shallowは2〜4MP、精細・精度優先は4〜8MPを初期値とする。標準deepはcold Previewを極小gridで測り、実測MPPから約1.4秒以内を狙うpixel budgetへ下げる。effective DPRと実寸を表示し、精細を選んだ場合はこの時間適応を使わない。
- `effectiveDpr = min(deviceDpr, pixelBudget由来上限)`とする。
- Covered計算はタイルで選択モードのtarget Canvas gridを100%覆う。
- 画面より大きい4K/8K出力は表示Canvasを巨大化せず、ExporterのOffscreenCanvasまたはtile encoderで作る。
- `renderPerf`を本当に使い、Previewだけ`targetPixels = budgetMs / EMA(msPerPixel)`で決める。
- EMAはshallow/deepだけでなく、iteration帯、BLA有無、warm/coldを区別する。
Previewの目安は80〜120ms、停止後baseは400〜700msとし、その後はtileで漸進する。端末差を吸収するため、固定pixel幅ではなく時間予算から決める。
### 6.3 操作中の無駄な計算を止める — P0
- wheelをpointerと同じgestureとして扱い、入力停止80〜120msまでは再投影だけにする。
- pinch / wheel / +を同じsettle処理へ統合する。
- SharedArrayBufferを使えるHosted版では、WASM hot loopが定期的に読むatomic cancel flagを設ける。
- SharedArrayBufferが使えない構成では、tile/chunkを小さくして取消後の最悪滞留時間を制限する。
- Preview中にwork capへ到達した画素は`UNRESOLVED`のまま残す。現状は低品質passで未解決が2%を超えると同stripを即時無制限再計算する (`script.js:265-266`) が、field/statusを導入するPhase 2以降はCoveredで継続し、Validatedでは必要に応じ高精度経路へ上げる。
- 浅部の`.58 → .84 → 1.0`という最大3回の全面再計算は、実測が遅い端末ではPreview → Coveredの2段へ減らす。
### 6.4 deep資産を遅延生成する — P0/P1
- Hosted版の初期表示ではshallow SIMDだけを取得・compileする。Standalone版は全Base64文字列をparseするが、deep payloadの復号・compileとWorker生成は遅延する。
- deep main-thread WASM、deep worker source、Worker poolは、数値条件が閾値へ近づいた時点で準備する。
- Workerは最初1〜2個だけ作り、実jobのthroughputから必要時だけ増やす。
- 現在のReal Wisdomは深部HQへ到達した各ページセッションのidle時に1回、最大4 Workerなら合計10回のmini renderを追加する (`script.js:333-348`, `script.js:698`)。通常jobから受動学習し、能動ベンチは診断時または不確実時だけにする。
- 学習結果はkernel versionと最小限の端末帯域特性をkeyに永続化する。schema versionと保存時刻を持たせ、既定TTL 30日で失効させる。storage拒否時はsession内学習だけで動作し、raw timing履歴や過剰なfingerprint属性を保存しない。
### 6.5 WASMとメモリを統合・動的化する — P1
- deep、BLA、smooth/colorを1 module・1 memoryへ統合する。
- `MAX_REF``MAX_BLA`の固定最大配列をやめ、現在の`refLen`に合わせて初期容量を決め、必要時だけ`memory.grow`する。linear memoryは縮まないため、大scene後の回収には容量bucket別instance poolの退役・再生成を使う。
- JS `RR/RI` → deep WASM → BLA WASMという重複copyを1回へ減らす。
- BLA tableは参照長`n`に対して概ね`2n`要素を動的確保する。
- Hosted版ではCOOP/COEPを設定する。共有対象はbuild完了後immutableなreference、key別BLA table、tileごとに非重複なfield領域へ限定する。table公開にはready barrierを設け、scratch・統計はWorker別offsetまたは別memoryにしてdata raceを避ける。
- 非共有版ではbuffer poolを使い、transferしたArrayBufferをWorkerへ返却して再利用する。
- `mags`をcolor WASMへcopyする経路も統合し、kernelからpacked fieldまたはRGBAを一度だけ出す。
### 6.6 キャッシュを件数ではなくbytesで管理する — P1
`DETAIL_TILE_LIMIT=96`は、tile寸法、DPR、Canvas実装の裏側を考慮しない。次へ置き換える。
- mobile 96 MiB、desktop 192 MiBを、JSから管理できるtyped array / WASM / cacheの暫定論理予算として計測開始する。
- frame、numeric field、tile canvasの推定bytes、reference、BLA、scratchをmemory ledgerへ登録する。ブラウザ内部のCanvas/GPU backing storeは正確に台帳化できないため、固定環境のprocess/UA memory観測を別指標にする。
- LRUはbytes、現viewportからの距離、再利用見込みでevictする。
- pan開始時の全frame cloneを避け、gesture中はimmutableな現frameを直接transformする。
- 論理予算超過時はcacheを捨てる。ブラウザ固有のmemory-pressure通知へ依存せず、最終解像度を黙って落とさない。
### 6.7 配布をHosted版とStandalone版へ分ける — P2
推奨する標準はHosted版である。
- `.wasm`とmodule Workerをhash付き外部資産にする。
- `compileStreaming` / `instantiateStreaming`、Brotli、immutable cacheを使う。
- SIMD版を先に取得し、失敗時だけscalar版を取得する。
- deep/BLA/colorは深部到達時だけ取得する。
- compile済み`WebAssembly.Module`をWorkerへstructured cloneする。
既存の「3ファイルを直接開ける」はStandalone互換buildとして残し、build時にWASMをinlineする。これにより、軽量な通常配信とオフライン可搬性を両立できる。
WebGPUは最初から必須にしない。まずpalette適用、linear-light resolve、tile合成へ使い、数値演算は低ズームPreviewだけの任意backendとして比較する。fp32を深部の最終結果へ使わない。
### 6.8 WASM buildを再現可能にする — P1
現状の`src/`にはBLA C sourceしかなく、他の埋込WASMに対応するsource、build command、toolchain manifestが見当たらない。バイナリだけでは、画素座標、丸め、SIMD/scalarの同値性を十分に監査できない。
- 全kernel sourceと生成scriptを保存する。
- clang等のversion、compile flags、exports ABIを固定する。
- 生成物のSHA-256をmanifestへ記録する。
- SIMD/scalarについて同じgolden vectorを実行する。
- `kernels.js`またはStandalone bundleは生成物とし、手編集しない。
## 7. 高精細・高精度化の具体策
### 7.1 pixel-to-world規約を画素中心へ統一する — P0
確認できるJS/direct/BLA実装では整数格子式が使われている。埋込WASMを含む全backend、detail、exportで、次の規約を共有する。
```text
cr = centerRe + span * ((x + 0.5) / width - 0.5)
ci = centerIm + span * ((height / 2 - (y + 0.5)) / width)
```
BigIntでは次の有理式にし、早期のdouble化を避ける。
```text
cr = centerRe + span * (2*x + 1 - width) / (2*width)
ci = centerIm + span * (height - 2*y - 1) / (2*width)
```
canonical座標は上の有理数とする。fixed-point化では分子の符号と対称なround-to-nearest、exact halfはaway-from-zeroへ丸める`roundDiv`をABI契約とし、積を先に計算してから1回だけ除算する。JS f64、shallow WASM、deep WASM、BLA、direct fallback、detail、exportについて、同一pixelがcanonical座標を各backendの表現可能範囲で再現するcross-backend testを追加する。「backend差0」は整数のfixed-point表現または分類・iteration契約に対して用い、f64座標は宣言ULP以内を基準にする。整数格子式だけで現在のtile seamが実証されたわけではないが、規約を統一して将来のbackend差を防ぐ。この変更は既存URLのview中心を変えない一方、画像sampleは半画素分変化し得るため、renderer/golden image versionを上げる。
### 7.2 Covered完了後にadaptive AAする — P0/P1
現行のRGBA勾配選別は、基礎サンプルに現れなかった細いフィラメントや微小島を検出できない。また、Canvasの`imageSmoothingQuality`は再拡大補間であり、真のフラクタルAAではない。
新しい流れは次の通り。
1. 画面全域を1 sample / target screen pixelで計算する。
2. kernelからescape iteration、smooth potential、内部/外部/未解決、confidence、可能なら距離推定値を出す。
3. 分類境界、値の分散、距離推定、tile中央・四隅probeからquadtreeを細分化する。
4. 必要なpixelだけ4/8/16 sampleへ増やす。
5. sampleをlinear RGBで平均し、最後にsRGB/P3へencodeする。
これにより、全面4倍SSAAより少ない計算量で、ジャギー、細線、微小島を改善できる。detail選定はpalette非依存となる。ただし中央・四隅probeや距離推定も、任意に細い未サンプル構造を完全には保証しない。Refinedは誤差推定に基づくadaptive AAであり、全pixel面積の数学的被覆証明ではない。Certifiedを名乗る場合は、別途保守的なdistance/error boundが必要になる。
### 7.3 数値fieldと色を分離する — P1
全画面について保持するのは、少なくとも次のpacked fieldに限定する。
- state: `ESCAPED`, `INTERIOR_LIKELY`, `INTERIOR_PROVEN`, `UNRESOLVED`
- smooth escape valueまたは固定小数へpackした値
- confidence / precision tier
`zr/zi`、参照位置、微分などの継続状態は全画面に持たず、未解決pixelまたは未解決tileだけに保持する。8MP全域へ完全な継続状態を持つと、memory budgetを容易に超えるためである。
adaptive AAを再着色可能にするには、境界pixelのsubsampleごとのstate/smooth値とsample patternを保持する。単一の平均smooth値だけでは、非線形paletteを正しく再resolveできない。palette、cycle、shift変更時はこのfieldから安価なcolor/AA resolveだけを再実行し、フラクタル反復はやり直さない。detail cache keyからstyleを外し、geometry/field cacheとcolor cacheを分離する。
現状の3 palette × 2048 phase × 64 mix LUTはmainと各Workerで約1.1 MiBずつ使う。1D palette LUT + 計算式または補間へ変えれば小さくできる。色はlinear RGBまたはOKLabで生成し、最後だけ8bit化する。
「昼夜」paletteは周期の先頭色と末尾色が一致していない (`script.js:189-197`)。phase wrap位置に筋が出ないよう、循環paletteはendpointを一致させ、非循環paletteはping-pong mappingにする。banding対策のditherを戻す場合は、detail/AA選定後の最終表示またはExportだけに固定seedで適用する。
### 7.4 BLA/detailの精度逆転をなくす — P0
- Covered/Refinedのfast εは、少なくともbase HQと同等にする。
- 境界scoreまたはconfidenceが悪いtileではεを厳しくするか、BLAを切ってstrict perturbation / double-doubleへ上げる。
- 現行detail検証の「黒分類不一致3.5%以下、平均RGB差48以下」 (`script.js:476-487`) は、品質保証ではなく視覚的coherence gateにすぎない。視覚gateと数値correctness gateを分離し、後者は同一world座標を高精度referenceで再評価して比較する。low/high rasterの異なるsample位置を直接0 mismatch要件にしない。
- fast BLAとsafe BLAは同じdouble参照軌道を使うため、「safe」を数学的厳密性の意味で使わない。
- Balancedでは少数probe方式を残せるが、Validatedでは疑わしいtileを独立した高精度reference実装またはdouble-doubleと比較する。少数probe合格だけで未検査pixelを保証済みとは呼ばない。
### 7.5 未解決を黒から分離し、反復を継続可能にする — P1
通常の有限反復表示で`maxIter`到達を黒にすること自体は一般的な表示規約であり、v17の「global iterationより手前で打ち切ったwork cap」とは別である。本提案では、Validated/Exportの確定度を説明するため、内部表現を次へ分ける。
- `INTERIOR_PROVEN`: 誤差境界付き解析判定、または周期吸引を証明できたもの
- `INTERIOR_LIKELY`: 現行のf64 cardioid/bulb判定や収縮heuristicなど、高信頼だが証明扱いしないもの
- `ESCAPED`: 発散が確認されたもの
- `UNRESOLVED`: iteration/work/precision上限へ達したもの
Coveredの描画中は未解決領域へ中立色または直前結果をpresentationとして重ねられる。旧frameの再投影はsample coverageへ数えないが、新ViewSpecでpolicy上限まで計算した`UNRESOLVED`はsample coverageへ数え、resolved coverageと未確定数で別に監査する。精細では未解決の境界tileだけ反復を段階的に継続する。これなら、旧work capを復活させずに計算量を局所化できる。
収縮による内部早期終了は既存監査で有効だったためBalancedでは`INTERIOR_LIKELY`として維持する。Validatedでは判定根拠をconfidenceへ記録し、policyが要求する場合は誤差境界付き解析判定・周期証明へ上げられないものを`UNRESOLVED`へ戻す。
### 7.6 engine選択とguard bitsを誤差ベースにする — P1
固定`DEEP_ZOOM_THRESHOLD=11.5`ではなく、少なくとも軸ごとに次を評価する。
- 1 pixelのworld-space幅
- `centerRe`, `centerIm`付近のf64 ULP
- 隣接pixel座標が別のdoubleになるか
- 直近orbitの誤差・glitch率
`pixelStep > safety × ulp(center)`を満たす間はf64を使い、境界付近にはhysteresisを置く。これにより、不要なdeep移行を減らしつつ量子化を防げる。
座標のBigInt精度と基準軌道の計算精度も分ける。基準軌道をP bitとP+32/64 bitのcheckpointで比較する。単一global referenceを使う間はreference単位で精度を上げる。tile単位で上げるにはlocal referenceを新設するか、そのtileだけdirect/double-double摂動へ昇格する。全画面を常時多倍長化しない。
`exactDeepPixel`は固定小数積を右shiftで量子化する高精度direct計算であり、数学的なexactではない (`script.js:602-605`)。正値は切り捨て、負値は算術shiftによる負方向丸めになる。`highPrecisionDirectPixel`へ改名し、round-to-nearestを使う。Certifiedでは区間演算でescapeを証明し、内部には別の周期吸引証明を使う。
### 7.7 超深部の性能崖を段階的に解消する — P2
- BLA APIのspan、offset、cMaxを通常doubleではなくmantissa + exponent bucketへする。
- 係数も必要に応じてscaled complexまたはdouble-doubleへ昇格する。
- glitch tileを分割し、tile中心にlocal referenceを作るmulti-reference perturbationを研究する。
- reference orbitをchunk化し、固定150,001点・最大140,000反復の上限をmemory budgetへ置き換える。
- 上限へ達した場合は「∞」を暗黙に名乗らず、現在の反復/参照/精度上限と未解決数を表示する。
multi-referenceやdouble-doubleは高難度である。version固定した現rendererのbrowser計測で、reference生成、glitch fallback、10^-280以深のどれが実際に支配的か確認してから実装する。v22欠落分を現renderer値で遡及的に代用しない。
### 7.8 Exportを独立pipelineにする — P0/P1
`PNG`を次の2操作へ分ける。
- Quick snapshot: 現在見えているCanvasを保存。Previewである可能性を明示する。
- High-quality export: 1× / 2× / 4× / custom、AA、Balanced / Validated、進捗、取消を指定する。Balancedは未確定sampleをmetadataへ記録して完了可能、Validatedはchecks合格かつunresolved 0だけをfinalとして出す。Certifiedは誤差境界経路を実装した後に追加する。
High-quality exportでは、固定したViewSpecをtile描画し、全tile完了後にだけencodeする。開始前に最大辺、sample数、予約bytesをpreflightし、予算超過・allocation失敗は解像度を黙って下げず明示失敗する。座標、span、bits、iteration policy、palette、AA、precision tier、backend、kernel hash、SIMD/scalar、色空間、encoder、renderer versionをPNG metadataまたはsidecar JSONへ記録する。同じ条件では規定許容差内で再現し、bit単位再現が必要な場合はscalar mathとencoderまで固定する。
## 8. 既存仕様の改廃
| 現行仕様 | 判断 | 新仕様 |
|---|---|---|
| 自動ズームを削除 | 維持 | 自動航法は戻さず、手動探索を軽く保つ |
| 深度だけで解像度を下げない | 趣旨を維持 | Coveredは深度非依存でtarget grid 100%。Previewだけ時間予算で縮小可 |
| `q`による固定4段階解像度 | 廃止 | 描画状態をReprojected / Preview / Covered / Refined / Validatedへ変更 |
| DPR 2.5、HQ 2100×2200、direct fallback幅390/620 | 置換 | 処理モード別pixel budgetを表示。Covered/Exportは選択したgridをtileで完遂 |
| 整数格子pixel座標 | 破壊的変更 | `(x+0.5, y+0.5)`へ統一し、renderer versionとgolden imageを更新 |
| RGB勾配で最大36枚だけ2× | 置換 | 数値field/confidence/distanceベースのadaptive quadtree AA |
| HQチェック1個 | 分割 | 表示品質、省電力、数値検証を別設定へ |
| palette変更で全再計算 | 廃止 | field cacheから即時再着色 |
| 固定z=11.5でdeep切替 | 廃止 | pixel step / ULP / orbit errorで切替 |
| 起動直後にdeep pool生成 | 廃止 | 数値的必要時に段階生成 |
| 深部HQ後、各ページセッションでReal Wisdomを能動測定 | 既定では廃止 | 実jobから受動学習。診断時だけ能動測定 |
| detail cache 96件固定 | 廃止 | 管理可能領域のbyte budget LRU |
| 表示Canvasの即時PNGだけ | 変更 | Quick snapshot + 独立High-quality export |
| Base64 WASM 3ファイル直開き | 互換buildへ | Hosted版を標準、Standalone版を併売 |
| SIMD → scalar → JS/BigInt fallback | 維持 | 全backendを同じtile/pixel-center contractへ統一 |
| Reference / perturbation / rebase / BLA | 維持・強化 | 局所精度昇格、動的memory、将来multi-reference |
| world-space cache | 維持・再設計 | style非依存field cache、quadtree key、byte budget |
| base iteration + adaptive slider | 変更 | Auto policyは維持。生値はAdvancedへ移し、未確定数を表示 |
| view hash / URL共有 | 強化 | version付きViewSpec、gesture settle保存、戻る/進む復元 |
| 「∞ Zoom」表記 | 廃止 | 有限の参照長・iteration・precision上限を持つため「Deep Zoom」とし、現在の上限と未確定数を併記 |
| UI初期非表示 | 変更 | 初回だけ操作ヒントを表示し、以後は利用者設定を復元 |
低優先度の整理対象として、未使用の`MILLION``last``sx/sy`、未使用`pilot`引数、常にfalseの`strictBoundary`、v22時点で同一内容だったCOLOR SIMD/scalar payloadがある。性能改善というより、仕様とコードのずれを減らすためにテスト後に削除する。実装後の再現buildではCOLOR variantsが異なるため、generatorは同一時だけ自動deduplicateする。
## 9. UX・再現性の変更
- 一般statusは「プレビュー / 全域描画 68% / 境界AA中 / 完了 / 未確定あり」とする。Covered等の内部用語は診断drawerだけに出す。
- bit数、BLA ε、Worker数、MPPは診断drawerへ移す。
- 深部座標表示を17桁固定 (`script.js:114`) にせず、spanから必要桁数を求める。画面上は短縮し、コピー時は正確な値を出す。
- URL hashをversion付きにし、見た目を再現するiteration profileも保存する。端末依存の解像度やWorker数は保存しない。
- gesture settleごとに`replaceState`、明示shareで`pushState``popstate/hashchange`でviewを復元する。座標/spanの正確値入力・コピーとUndo/Redoも同じViewSpecを使う。
- `user-scalable=no`を外し、bodyはpage zoom可能にする。Canvasだけに必要なtouch-actionを限定し、アプリのpinch zoomとブラウザpage zoomの範囲を分ける。
- rangeとlabelを関連付け、toast/statusへ`aria-live`、UI toggleへ`aria-expanded`、visible focus、44px以上のtarget、`prefers-reduced-motion/transparency`対応を付ける。
- 初回は操作ヒントを表示し、2回目以降はUI表示状態を保存する。
- 通常利用者には「省電力 / 標準 / 精細」を提示し、生の反復回数はAdvancedへ移す。
- 700px以下でも進捗、未確定数、Export状態へアクセスできるcompact status sheetを出す。現行のようにstatsとhintをすべて消さない (`index.html:21`)。
## 10. 実装ロードマップ
P0/P1/P2は利用者影響のseverityであり、実装順ではない。各項目はphase、依存先、mandatory / telemetry-conditional、未通過gateを追跡する。後段実装を先行できても、前段gateを通過したとは扱わない。
### Phase 0A — browser非依存の証拠基準化
- shallow/deep/colorを含む全kernel sourceを回収または復元し、固定toolchain、compile flags、生成script、payload checksumを揃える。
- z0 / z14 / z20 / z100 / period-3 interior / boundary / 10^-280付近をversion付き固定sceneにする。中心座標はspan相応のguard digitsを持つdecimalかexact rationalとし、precision kind、oracle、期待分類、iteration policyをscene schemaへ保存する。
- v22成果物が残っている場合は全対象hashとbaselineをimmutableに保存する。残っていない場合は「v22比較不能」と機械可読に記録し、後からv23値で代用しない。
- 現行`exactDeepPixel`とは独立した、複数精度一致と誤差上限を持つ高精度reference実装で、分類、iteration、smooth valueを比較する。
- source、ABI、pixel mapping、tile seam、SIMD/scalar、解析的内部証明、Module clone、配布物hash、文書内の生成値を同じcommandで検証する。
- v18監査はarchiveと明記し、現renderer baselineを別JSONにする。
Exit gate: 全kernelをsourceから再現buildでき、version付きsceneとoracleで数値baselineを再生成でき、全成果物hashと文書内の生成値が一致する。過去baseline欠落は明示的な`not-verifiable`でありpassにしない。
### Phase 0B — 固定browser baseline
- desktop、mobile相当、4Kでcold / warmを測る。
- first preview、Covered完了、Refined完了、idle job/draw、peak memory、long taskを記録する。
- Hosted / Standalone、SIMD / scalar、Worker / Workerなしを対象構成matrixに従って分けて実行する。
- viewportのCSS寸法だけでDPRを模擬しない。mobile / desktop / 4Kは要求DPRを持つ別browser contextまたは別実機runとし、各runの実viewport・実DPR・browser build・OS・電源・試行数を保存する。
- visualはraw pixelまたはlinear-light fieldのversion付きgoldenと比較し、hash文字列が生成できただけではpassにしない。
Exit gate: 対象profileごとのbrowser acceptanceが合格し、cold / warm、操作p95、visual、idle、long task、observed memory、Export、accessibilityのraw結果を保存する。binding不在などで未実施の場合、後続実装は進められるがbrowser性能・操作・表示の達成を主張しない。
### Phase 1A — Runtime quick wins
- event-driven paint/stats
- wheel gesture settle
- `visibilitychange`停止
- deep pool遅延生成
- Screen Canvas pixel budget
- 初回`q=.3`境界の修正と、既存`q`のままPreview時間予算へ`renderPerf`を接続
- Quick snapshotに品質状態を表示し、描画中の誤解を防止
- 固定件数cacheをbyte budgetへ置換
Exit gate: idle時に予約RAF/timer/background job 0、wheel中full render 0、浅部だけではdeep compile 0、Canvas/管理可能memoryが予算内。
### Phase 1B — Pixel contract
- 全kernelを画素中心規約へ変更し、cross-backend testを追加する。
- renderer/golden image versionを上げる。
- detail BLAをbase HQ以上へ修正する。
Exit gate: canonical rationalに対するfixed-point整数一致とf64の宣言ULP基準、tile境界回帰0、既存URLのview中心不変を確認する。
### Phase 2 — Tile/field pipeline
- packed field形式、subsample形式、論理byte budgetを先に固定する。
- buffer poolを導入し、field追加による一時的なpeak memory悪化を防ぐ。
- shallowもWorker化し、全backendを共通tile protocolへする。
- Reprojected / Preview / Covered / Refined状態機械を導入する。
- palette非依存field bufferと即時再着色を導入する。
- 処理モード別sample coverage 100%とresolved coverage未確定数の分離を実装する。
- ULPベースのshallow/deep切替を導入する。
- `ESCAPED / INTERIOR_LIKELY / INTERIOR_PROVEN / UNRESOLVED`を導入し、局所反復継続を実装する。
Exit gate: sample coverage 100%、再投影pixelの誤加算0、未確定数の独立計上、false `INTERIOR_PROVEN` 0、logical memory budget内。
### Phase 3A — Refine / Export
- Hosted / Standalone buildを分ける。
- 独立High-quality export、進捗、取消、metadataを実装する。
- adaptive AAとlinear-light resolveを導入する。
Exit gate: 指定寸法、完了待ち、取消、metadata、許容差内の再現性、Refinedのsample accountingが合格する。
### Phase 3B — 計測で必要ならWASM memory再編
- deep/BLA/color WASM統合と動的初期容量
- immutable reference / key別BLA table共有、Worker別scratch
- shared framebufferまたはbuffer poolによるcopy削減
- 大容量instanceの退役・再生成
Exit gate: 対象deep sceneで速度改善を確認し、data race 0、logical/observed peak memoryが予算内。
### Phase 4 — 計測後の高精度研究
- reference orbit専用Workerと次referenceの先読み
- double-double局所昇格
- error ball / interval verification
- scaled BLA
- multi-reference perturbation
- WebGPUによるcolor/resolve/composition、必要なら浅部Preview演算
Phase 4は、telemetryで支配コストまたは誤差が確認された項目だけ実施する。
## 11. 受入基準
browser時間値は、profileごとに固定したbrowser build・OS・viewport・実DPR・電源条件で、warmup後30回以上を採取してp95を算出する。単発値、同期event handler時間、異なるsceneを混ぜた分位点は代用しない。Visual golden、数値oracle、threshold policyには独立したversion IDを与え、threshold欠落時はpassにしない。
| 分野 | 基準案 |
|---|---|
| Idle | 最終更新2秒後に予約RAF/timer/background job 0、Canvas/DOM write 0回/秒。CPUは同環境blank baselineとの差を補助指標にする |
| 操作 | 最後の実入力eventから、その入力を反映した再投影frameのpaint完了までのp95 < 16ms、計測区間のmain thread 50ms超long task 0、継続wheel中に新規full renderを開始しない |
| Preview | settle期限到達からPreview paint完了まで、profile・sceneごとの30回以上でp95 < 120ms |
| Covered | target Screen Canvasのsample coverage 100%、effective DPRを表示し、再投影pixelをsample coverageへ数えない。resolved coverageとunresolved数を別記録。scene別時間budgetはPhase 0B後に確定 |
| Refined | Fine modeで実際に`REFINED`へ到達し、対象pixelは記録済み実sampleのlinear-light resolve。subsample accountingとversion付きgoldenを比較し、Canvas補間だけをAAと数えない |
| 数値 | version付き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 |
| 配布 | v22 baseline欠落のため初回比較は`not-verifiable`とする。v23の同一request集合・Content-Encoding・cache条件を新baselineとして固定し、以後の取得bytes・compile timeを回帰判定する。Hostedではdeep到達前のdeep request 0 |
| Accessibility | keyboard-onlyで全操作を実行し、visible focus、44px target、label/name、live status、page zoomとCanvas pinchの共存、reduced motion/transparencyの実動作が合格。静的属性の存在だけではpassにしない |
| Regression | version付き現行scene corpusをCIで実行し、速度・memory・分類・例外を履歴化する。各metricのwarn/fail閾値をpolicyへ保存する |
端末依存の時間値は、対象端末を固定して初めて合否に使う。絶対時間だけでなく、同一scene・同一pixel数の比率とcorrectnessを主要指標にする。browser非依存gateはsourceABIhash数値oraclepixel mappingscene精度document consistencyまでを証明し、paint、DPR、GPU、入力、download、実accessibilityの代替とはみなさない。
## 12. 推奨する最初の実装単位
最初の変更セットは、次の範囲に限定すると効果を測りやすい。
1. v22 baselineの有無を機械可読に確定し、現renderer baselineと全WASMの再現buildを用意する。
2. 常時RAFと毎frame DOM更新を廃止する。
3. wheelをgestureとしてデバウンスし、非表示時のjobを止める。
4. Screen Canvasへpixel budgetを設ける。
5. deep poolを必要時まで作らない。
6. 既存`q`のままPreview時間予算へ`renderPerf`を接続する。
7. version付き現renderer用のidle・gesture・startup・memory回帰テストを追加する。
この段階ではBLAカーネルやpixel contractをまだ変えない。まずidle負荷、入力応答、Canvas/初期memory、Preview時間を改善する。画素中心変更は、全kernelを再現buildできるPhase 1Bで行う。
## 付録A: 根拠データ
- 現行仕様: `README.md`
- 数値・性能監査: `AUDIT.md`
- end-to-end記録: `audit/final-browser-benchmark.json`, `audit/benchmarks.json`
- UI/スケジューラ/renderer: `index.html`, `script.js`
- BLA固定配列・演算: `src/bla_kernel_v18.c`
既存監査から今後も守るべき知見は次の通り。
- 数学的iterationを途中で切って黒扱いしない。
- pilot画像を未解決pixelの計算結果として使わない。
- 全stripを毎回strict検証しない。
- 1点の不一致で全画面を再計算せず、tile/strip単位で補修する。
- ditherはdetail検出前へ入れず、最終表示段だけにする。
- Reference recenterは固定距離でなく、実測損失とbuild費を比較する。
## 付録B: 用語
| 用語 | 本文での意味 |
|---|---|
| BLA | Bilinear Approximation。基準軌道上の複数反復を近似的にまとめて進める高速化 |
| perturbation | 高精度な基準軌道との差分だけを低精度で追跡する摂動法 |
| rebase | 差分が不安定になったとき、基準軌道上の別位置へ基準を移す処理 |
| DPR / effective DPR | OS/browserが示すdevice pixel比 / pixel budget適用後に実際に使う比率 |
| sample / resolved coverage | 選択したScreen Canvas gridのうち、新ViewSpecでpolicy上限まで実計算済みの割合 / `ESCAPED`またはpolicy上の`INTERIOR_PROVEN`へ確定した割合 |
| AA | Anti-Aliasing。1 pixel内の複数sampleをresolveして境界のジャギーを減らす処理 |
| SIMD | 1命令で複数データを処理するWASM最適化 |
| ULP | その浮動小数値付近で表現できる隣接値の間隔 |
| EMA / MPP | 指数移動平均 / millisecond per pixel。時間予算の推定に使う |
| cold / warm | 初回compile・cache未構築 / Worker・reference・BLA cache構築後 |
| confidence | 近似・検証・内部判定の根拠と確からしさを表すmetadata |
| Validated / Certified | 宣言した有限policyを照合済み / 全sampleへ誤差境界または証明手続きを適用済み |
## 付録C: 互換構成
| 構成 | 動作方針 | 主な制限 |
|---|---|---|
| Hosted + SIMD + Worker | 推奨構成。外部WASM、遅延取得、並列tile | Shared memoryは別途COOP/COEPが必要 |
| Hosted、SIMDなし | scalar WASMを必要時に取得 | 速度低下。数値policyは同一 |
| SharedArrayBufferなし | Transferable buffer poolと短いchunkを使用 | hot loop中のatomic cancel、read-only table共有なし |
| Workerなし | main-thread WASMを時間sliceして使用 | 並列なし。Covered/Exportの完了が遅い |
| OffscreenCanvasなし | Workerはfield/RGBA bufferを返し、main Canvasへ合成 | Export encodeと合成の一部がmain thread |
| Standalone / `file://` | 埋込WASM互換build。deepの復号・compileは遅延 | streaming、HTTP cache、COOP/COEP、Service Workerなし |
最低対応browser versionとoffline cache方針は、Phase 0Bで実機matrixを作って確定する。

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の代替ではありません。

245
audit/benchmarks.json Normal file
View file

@ -0,0 +1,245 @@
{
"environment_note": "Absolute timings are container/Chromium specific. Use ratios and correctness rates as the main evidence.",
"end_to_end_dynamic_base_comparison": [
{
"scene": "seahorse_z14",
"v17_base_ms": 1059.0,
"v18_base_ms": 288.0,
"speedup_x": 3.677
},
{
"scene": "seahorse_z20",
"v17_base_ms": 1226.0,
"v18_base_ms": 238.0,
"speedup_x": 5.151
},
{
"scene": "seahorse_z100",
"v17_base_ms": 983.0,
"v18_base_ms": 474.0,
"speedup_x": 2.074
},
{
"scene": "period3_z20",
"v17_base_ms": 1931.0,
"v18_base_ms": 781.0,
"speedup_x": 2.472
}
],
"v17_work_cap_correctness": {
"w": 48,
"h": 30,
"pixels": 1440,
"globalIter": 3490,
"capIter": 2870,
"capped": 1440,
"late": 1066,
"insideGlobal": 374,
"lateEscapeRateAmongCapped": 0.7402777777777778,
"elapsedMs": 6344.968723999999
},
"pilot_nearest_fill_accuracy": {
"full": [
128,
78
],
"pilot": [
56,
34
],
"fullMs": 1356.671812,
"pilotMs": 169.93951399999992,
"classMismatch": 451,
"classRate": 0.04517227564102564,
"escapeIterDiffGt8": 4364,
"diffRate": 0.437099358974359
},
"intrinsic_interior_detection": [
{
"name": "period3 wide",
"re": "-0.1225611668766536",
"im": "0.7448617666197442",
"span": 0.1,
"offMs": 766.7149390000001,
"onMs": 5.955513999999994,
"interior": 6912,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "period3 mid",
"re": "-0.1225611668766536",
"im": "0.7448617666197442",
"span": 0.03,
"offMs": 438.65803500000015,
"onMs": 2.334043000000065,
"interior": 6912,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "period2 boundary neighborhood",
"re": "-0.75",
"im": "0",
"span": 0.1,
"offMs": 364.72781799999984,
"onMs": 1.0177550000000792,
"interior": 6806,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "mixed exterior/interior",
"re": "-0.5",
"im": "0.5",
"span": 0.1,
"offMs": 282.849144,
"onMs": 16.219244000000117,
"interior": 5338,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "cardioid cusp neighborhood",
"re": "0.25",
"im": "0",
"span": 0.04,
"offMs": 329.88844100000006,
"onMs": 1.7636029999998755,
"interior": 6403,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
}
],
"reference_recenter_z14": {
"oldRefBuildMs": 29.320617,
"tests": [
{
"sx": 0.5,
"sy": 0,
"oldMs": 691.804547,
"newMs": 640.2659849999995,
"newRefBuildMs": 9.508285999999998,
"firstFrameNetSavingMs": 42.03027600000041,
"steadySavingMs": 51.53856200000041
},
{
"sx": 0.8,
"sy": 0,
"oldMs": 756.002915,
"newMs": 655.9519419999997,
"newRefBuildMs": 8.81105800000023,
"firstFrameNetSavingMs": 91.23991500000011,
"steadySavingMs": 100.05097300000034
},
{
"sx": 0,
"sy": 0.8,
"oldMs": 730.6417070000007,
"newMs": 671.6106240000008,
"newRefBuildMs": 8.391966000001048,
"firstFrameNetSavingMs": 50.639116999998805,
"steadySavingMs": 59.03108299999985
},
{
"sx": 0.8,
"sy": 0.5,
"oldMs": 787.9931480000014,
"newMs": 740.047842,
"newRefBuildMs": 7.513370000000577,
"firstFrameNetSavingMs": 40.43193600000086,
"steadySavingMs": 47.94530600000144
},
{
"sx": 1.2,
"sy": 0,
"oldMs": 705.7068249999975,
"newMs": 689.7542820000017,
"newRefBuildMs": 7.101795999999013,
"firstFrameNetSavingMs": 8.850746999996773,
"steadySavingMs": 15.952542999995785
},
{
"sx": 1.5,
"sy": 0.8,
"oldMs": 793.3721589999986,
"newMs": 733.093237000001,
"newRefBuildMs": 7.395623000000342,
"firstFrameNetSavingMs": 52.88329899999735,
"steadySavingMs": 60.27892199999769
}
]
},
"reference_recenter_z20": {
"oldRefBuildMs": 23.925890999999993,
"tests": [
{
"sx": 0.5,
"sy": 0,
"oldMs": 100.18383699999998,
"newMs": 105.04222300000004,
"newRefBuildMs": 9.637189000000006,
"firstFrameNetSavingMs": -14.49557500000006,
"steadySavingMs": -4.858386000000053
},
{
"sx": 0.8,
"sy": 0,
"oldMs": 106.89347099999986,
"newMs": 93.24754299999995,
"newRefBuildMs": 8.222010999999952,
"firstFrameNetSavingMs": 5.42391699999996,
"steadySavingMs": 13.645927999999913
},
{
"sx": 0,
"sy": 0.8,
"oldMs": 110.3922050000001,
"newMs": 98.40486799999985,
"newRefBuildMs": 7.7547400000000835,
"firstFrameNetSavingMs": 4.232597000000169,
"steadySavingMs": 11.987337000000252
},
{
"sx": 0.8,
"sy": 0.5,
"oldMs": 138.4319349999996,
"newMs": 102.46810800000003,
"newRefBuildMs": 8.225470999999743,
"firstFrameNetSavingMs": 27.73835599999984,
"steadySavingMs": 35.96382699999958
},
{
"sx": 1.2,
"sy": 0,
"oldMs": 111.11085400000002,
"newMs": 93.51345999999967,
"newRefBuildMs": 8.248458000000028,
"firstFrameNetSavingMs": 9.348936000000322,
"steadySavingMs": 17.59739400000035
},
{
"sx": 1.5,
"sy": 0.8,
"oldMs": 126.42733499999986,
"newMs": 95.50466299999971,
"newRefBuildMs": 8.381738999999925,
"firstFrameNetSavingMs": 22.540933000000223,
"steadySavingMs": 30.922672000000148
}
]
},
"code_metrics": {
"v17_script_lines": 1067,
"v17_script_bytes": 143149,
"v18_script_lines": 805,
"v18_script_bytes": 83427,
"v18_kernels_lines": 12,
"v18_kernels_bytes": 52136
}
}

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

@ -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

@ -0,0 +1,168 @@
[
{
"z": 40,
"global": 7752,
"comp": 4300,
"refBuildMs": 18.262529999999998,
"refLen": 3246,
"fullBlaMs": 5.190414000000004,
"blaBuildMs": 2.432442000000009,
"strictMs": 0,
"blaStrict": {
"valid": 41340,
"classMismatch": 0,
"classRate": 0,
"diffGt1": 0,
"diffRate": 0,
"bad": 0,
"maxDiff": 0
},
"work": {
"unresolved": 0,
"unresolvedRate": 0,
"lateEscape": 0,
"lateEscapeRate": 0,
"pilotClassMismatch": 0,
"pilotClassMismatchRate": 0,
"meanIterErr": 0,
"maxIterErr": 0
},
"safe": {
"safeRatio": 0,
"periodicMs": 0.08044799999998986,
"blackRatio": 0,
"interiorRatio": 0
},
"safeAcc": {
"skipped": 0,
"ratio": 0,
"falseEscaped": 0,
"falseRate": 0
},
"safeRun": {
"buildMs": 0.15029499999999985,
"renderMs": 1.6046879999999817,
"pixels": 41340,
"tasks": 34
},
"eps": [
{
"e": 18,
"ms": 1.3821410000000185,
"build": 0.14597599999999034
},
{
"e": 20,
"ms": 1.4494080000000054,
"build": 0.09218199999997978
},
{
"e": 23,
"ms": 1.6922059999999988,
"build": 0.09046299999999974
},
{
"e": 27,
"ms": 1.456985000000003,
"build": 0.09038200000000529
},
{
"e": 32,
"ms": 1.3537540000000092,
"build": 0.0893779999999822
}
],
"recenter": {
"oldRefLen": 3246,
"newRefLen": 3246,
"newBuildMs": 10.052744000000018,
"oldRenderMs": 1.324105999999972,
"newRenderMs": 1.4144129999999961,
"benefitMs": -0.09030700000002412,
"netFirstFrame": -10.143051000000042
}
},
{
"z": 100,
"global": 17085,
"comp": 7600,
"refBuildMs": 9.734955000000014,
"refLen": 3246,
"fullBlaMs": 1.3740439999999978,
"blaBuildMs": 0.10230300000000625,
"strictMs": 0,
"blaStrict": {
"valid": 41340,
"classMismatch": 0,
"classRate": 0,
"diffGt1": 0,
"diffRate": 0,
"bad": 0,
"maxDiff": 0
},
"work": {
"unresolved": 0,
"unresolvedRate": 0,
"lateEscape": 0,
"lateEscapeRate": 0,
"pilotClassMismatch": 0,
"pilotClassMismatchRate": 0,
"meanIterErr": 0,
"maxIterErr": 0
},
"safe": {
"safeRatio": 0,
"periodicMs": 0.06870000000000687,
"blackRatio": 0,
"interiorRatio": 0
},
"safeAcc": {
"skipped": 0,
"ratio": 0,
"falseEscaped": 0,
"falseRate": 0
},
"safeRun": {
"buildMs": 0.11969400000000974,
"renderMs": 1.3758139999999912,
"pixels": 41340,
"tasks": 34
},
"eps": [
{
"e": 18,
"ms": 1.3486680000000035,
"build": 0.08054500000000075
},
{
"e": 20,
"ms": 1.3468280000000163,
"build": 0.09072599999998943
},
{
"e": 23,
"ms": 1.3401210000000106,
"build": 0.0852779999999882
},
{
"e": 27,
"ms": 1.6213699999999847,
"build": 0.08418900000000917
},
{
"e": 32,
"ms": 1.4460180000000094,
"build": 0.08714600000001838
}
],
"recenter": {
"oldRefLen": 3246,
"newRefLen": 3246,
"newBuildMs": 8.042026000000021,
"oldRenderMs": 1.3196079999999881,
"newRenderMs": 1.3709360000000004,
"benefitMs": -0.05132800000001225,
"netFirstFrame": -8.093354000000033
}
}
]

View file

@ -0,0 +1,142 @@
seahorse-z14 iter 3490 ref 3246 build 21.53 baseline 1807.84
{ e: 23, b: 0, p: 0, int: 0 } ms 299.14 {
bla: 3043732,
ptb: 11197405,
rebase: 72197,
interior: 0,
unresolved: 0,
fail: 0
} cmp {
cls: 157,
clsRate: 0.010012755102040817,
diff: 820,
diffRate: 0.05229591836734694,
bad: 0
}
{ e: 23, b: 1600, p: 2200, int: 1 } ms 373.69 {
bla: 3043732,
ptb: 11197405,
rebase: 72197,
interior: 0,
unresolved: 0,
fail: 0
} cmp {
cls: 157,
clsRate: 0.010012755102040817,
diff: 820,
diffRate: 0.05229591836734694,
bad: 0
}
{ e: 23, b: 800, p: 1200, int: 1 } ms 398.19 {
bla: 3043729,
ptb: 11189659,
rebase: 71797,
interior: 0,
unresolved: 107,
fail: 0
} cmp {
cls: 131,
clsRate: 0.008354591836734693,
diff: 747,
diffRate: 0.04764030612244898,
bad: 107
}
{ e: 32, b: 1600, p: 2200, int: 1 } ms 785.73 {
bla: 3016859,
ptb: 26792628,
rebase: 72089,
interior: 0,
unresolved: 54,
fail: 0
} cmp {
cls: 60,
clsRate: 0.003826530612244898,
diff: 182,
diffRate: 0.011607142857142858,
bad: 54
}
{ e: 40, b: 0, p: 0, int: 1 } ms 1217.85 {
bla: 1091101,
ptb: 44427881,
rebase: 72170,
interior: 0,
unresolved: 0,
fail: 0
} cmp {
cls: 13,
clsRate: 0.0008290816326530612,
diff: 31,
diffRate: 0.0019770408163265305,
bad: 0
}
seahorse-z20 iter 4509 ref 3246 build 14.47 baseline 383.51
{ e: 23, b: 0, p: 0, int: 0 } ms 52.83 {
bla: 605601,
ptb: 2085586,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 1600, p: 2200, int: 1 } ms 60.84 {
bla: 605601,
ptb: 2085586,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 800, p: 1200, int: 1 } ms 61.15 {
bla: 605601,
ptb: 2085586,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 32, b: 1600, p: 2200, int: 1 } ms 138.45 {
bla: 1253140,
ptb: 4763652,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 40, b: 0, p: 0, int: 1 } ms 253.84 {
bla: 2443606,
ptb: 8374654,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
period2-z20 iter 4509 ref 4509 build 4.04 baseline 1552.21
{ e: 23, b: 0, p: 0, int: 0 } ms 1507.19 {
bla: 0,
ptb: 70701120,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 1600, p: 2200, int: 1 } ms 0.20 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 800, p: 1200, int: 1 } ms 0.12 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 32, b: 1600, p: 2200, int: 1 } ms 0.12 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 40, b: 0, p: 0, int: 1 } ms 0.16 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
main-z20 iter 4509 ref 4509 build 9.77 baseline 5.36
{ e: 23, b: 0, p: 0, int: 0 } ms 2.83 {
bla: 141120,
ptb: 15680,
rebase: 0,
interior: 0,
unresolved: 0,
fail: 0
} cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 1600, p: 2200, int: 1 } ms 0.15 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 23, b: 800, p: 1200, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 32, b: 1600, p: 2200, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }
{ e: 40, b: 0, p: 0, int: 1 } ms 0.10 { bla: 0, ptb: 0, rebase: 0, interior: 15680, unresolved: 0, fail: 0 } cmp { cls: 0, clsRate: 0, diff: 0, diffRate: 0, bad: 0 }

View file

@ -0,0 +1,132 @@
[
{
"name": "seahorse-z14",
"results": [
{
"r": false,
"last": 693.8000000007451,
"e": "WASM SIMD ×2 · BLA e-28 + rebase · 局所補修 6",
"f": 1,
"px": 12288,
"k": 690.1000000052154,
"rep": 0.3,
"un": 0,
"int": 0,
"bla": 162.36328125,
"ptb": 1876.836181640625,
"bits": 512,
"q": 0.58
},
{
"r": false,
"last": 1999.9000000022352,
"e": "WASM SIMD ×2 · BLA e-32 + rebase · 局所補修 12",
"f": 2,
"px": 36300,
"k": 2172.9000000059605,
"rep": 0.36363636363636365,
"un": 0,
"int": 0,
"bla": 135.99895316804407,
"ptb": 2132.273305785124,
"bits": 512,
"q": 1
}
]
},
{
"name": "seahorse-z20",
"results": [
{
"r": false,
"last": 388.79999999701977,
"e": "WASM SIMD ×2 · BLA e-23 + rebase",
"f": 3,
"px": 72463,
"k": 330.9000000022352,
"rep": 0,
"un": 0,
"int": 0,
"bla": 38.68238963333011,
"ptb": 133.7588424437299,
"bits": 512,
"q": 0.58
},
{
"r": false,
"last": 1732.699999999255,
"e": "WASM SIMD ×2 · BLA e-32 + rebase",
"f": 4,
"px": 134091,
"k": 1531.5999999977648,
"rep": 0,
"un": 0,
"int": 0,
"bla": 82.81766114056872,
"ptb": 306.63208567316224,
"bits": 512,
"q": 1
}
]
},
{
"name": "period3-z20",
"results": [
{
"r": false,
"last": 174.90000000223517,
"e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%",
"f": 5,
"px": 202800,
"k": 67.5,
"rep": 0,
"un": 0,
"int": 1,
"bla": 2,
"ptb": 8,
"bits": 512,
"q": 0.58
}
]
},
{
"name": "period2-z20",
"results": [
{
"r": false,
"last": 49.19999999925494,
"e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%",
"f": 6,
"px": 202800,
"k": 4.399999998509884,
"rep": 0,
"un": 0,
"int": 1,
"bla": 0,
"ptb": 0,
"bits": 512,
"q": 0.58
}
]
},
{
"name": "cardioid-z20",
"results": [
{
"r": false,
"last": 33,
"e": "WASM SIMD ×2 · BLA e-23 + rebase · 内部早期終了 100%",
"f": 7,
"px": 43200,
"k": 0.5999999977648258,
"rep": 0,
"un": 0,
"int": 1,
"bla": 0,
"ptb": 0,
"bits": 512,
"q": 0.58
}
]
}
]

View file

@ -0,0 +1,77 @@
[
{
"z": 14,
"runs": [
{
"r": false,
"last": 528.9000000022352,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-28 + rebase \u00b7 \u5c40\u6240\u88dc\u4fee 1",
"f": 1,
"px": 12288,
"k": 531.7000000104308,
"pilot": 111.89999999850988,
"rep": 0.05
},
{
"r": false,
"last": 332.6000000014901,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-28 + rebase",
"f": 2,
"px": 12288,
"k": 472.99999998882413,
"pilot": 111.89999999850988,
"rep": 0
}
]
},
{
"z": 20,
"runs": [
{
"r": false,
"last": 322.1000000014901,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
"f": 3,
"px": 56444,
"k": 340.30000000447035,
"pilot": 6.699999999254942,
"rep": 0
},
{
"r": false,
"last": 216.40000000223517,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
"f": 4,
"px": 45510,
"k": 218.69999999925494,
"pilot": 6.699999999254942,
"rep": 0
}
]
},
{
"z": 100,
"runs": [
{
"r": false,
"last": 33.70000000298023,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
"f": 5,
"px": 202800,
"k": 16.599999994039536,
"pilot": 0.09999999776482582,
"rep": 0
},
{
"r": false,
"last": 46.099999997764826,
"e": "WASM SIMD \u00d72 \u00b7 BLA e-23 + rebase",
"f": 6,
"px": 202800,
"k": 10.899999998509884,
"pilot": 0.09999999776482582,
"rep": 0
}
]
}
]

View file

@ -0,0 +1,44 @@
[
{
"z": 20,
"runs": [
{
"r": false,
"last": 1422.0999999977648,
"f": 1,
"px": 35371,
"k": 170.7000000141561,
"pilot": 10.700000002980232
},
{
"r": false,
"last": 1080.1000000014901,
"f": 2,
"px": 12288,
"k": 57.79999999701977,
"pilot": 10.700000002980232
}
]
},
{
"z": 100,
"runs": [
{
"r": false,
"last": 3249.7000000029802,
"f": 3,
"px": 202800,
"k": 17.099999982863665,
"pilot": 0.19999999925494194
},
{
"r": false,
"last": 1368.800000000745,
"f": 4,
"px": 16170,
"k": 2.2000000029802322,
"pilot": 0.19999999925494194
}
]
}
]

View file

@ -0,0 +1,62 @@
[
{
"name": "period3 wide",
"re": "-0.1225611668766536",
"im": "0.7448617666197442",
"span": 0.1,
"offMs": 766.7149390000001,
"onMs": 5.955513999999994,
"interior": 6912,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "period3 mid",
"re": "-0.1225611668766536",
"im": "0.7448617666197442",
"span": 0.03,
"offMs": 438.65803500000015,
"onMs": 2.334043000000065,
"interior": 6912,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "period2 boundary neighborhood",
"re": "-0.75",
"im": "0",
"span": 0.1,
"offMs": 364.72781799999984,
"onMs": 1.0177550000000792,
"interior": 6806,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "mixed exterior/interior",
"re": "-0.5",
"im": "0.5",
"span": 0.1,
"offMs": 282.849144,
"onMs": 16.219244000000117,
"interior": 5338,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
},
{
"name": "cardioid cusp neighborhood",
"re": "0.25",
"im": "0",
"span": 0.04,
"offMs": 329.88844100000006,
"onMs": 1.7636029999998755,
"interior": 6403,
"classMismatch": 0,
"diffGt1": 0,
"total": 6912
}
]

View file

@ -0,0 +1,98 @@
[
{
"exp": 14,
"setup": {
"bits": 768,
"z": 14,
"iter": 3490
},
"base": {
"wall": 7315.300000000745,
"lastRender": 7292.300000000745,
"w": 260,
"h": 159,
"pilotMs": 0,
"kernelMPP": 0,
"interiorSkip": 0,
"black": 0,
"bad": 0,
"engine": "WASM SIMD · main-thread rebase · skip 998",
"z": 14,
"iter": 3490,
"compute": 2870
},
"safe": null
},
{
"exp": 20,
"setup": {
"bits": 768,
"z": 20.00000000000001,
"iter": 4509
},
"base": {
"wall": 3576.60000000149,
"lastRender": 3555.39999999851,
"w": 260,
"h": 159,
"pilotMs": 0,
"kernelMPP": 0,
"interiorSkip": 0,
"black": 0,
"bad": 0,
"engine": "WASM SIMD · main-thread rebase · skip 998",
"z": 20.00000000000001,
"iter": 4509,
"compute": 3200
},
"safe": null
},
{
"exp": 40,
"setup": {
"bits": 768,
"z": 40.00000000000002,
"iter": 7752
},
"base": {
"wall": 3800.199999999255,
"lastRender": 3782.599999997765,
"w": 260,
"h": 159,
"pilotMs": 0,
"kernelMPP": 0,
"interiorSkip": 0,
"black": 0,
"bad": 0,
"engine": "WASM SIMD · main-thread rebase · skip 998",
"z": 40.00000000000002,
"iter": 7752,
"compute": 4300
},
"safe": null
},
{
"exp": 100,
"setup": {
"bits": 768,
"z": 100,
"iter": 17085
},
"base": {
"wall": 3633.7999999970198,
"lastRender": 3622,
"w": 260,
"h": 159,
"pilotMs": 0,
"kernelMPP": 0,
"interiorSkip": 0,
"black": 0,
"bad": 0,
"engine": "WASM SIMD · main-thread rebase · skip 998",
"z": 100,
"iter": 17085,
"compute": 7600
},
"safe": null
}
]

View file

@ -0,0 +1,16 @@
{
"full": [
128,
78
],
"pilot": [
56,
34
],
"fullMs": 1356.671812,
"pilotMs": 169.93951399999992,
"classMismatch": 451,
"classRate": 0.04517227564102564,
"escapeIterDiffGt8": 4364,
"diffRate": 0.437099358974359
}

View file

@ -0,0 +1,59 @@
{
"oldRefBuildMs": 29.320617,
"tests": [
{
"sx": 0.5,
"sy": 0,
"oldMs": 691.804547,
"newMs": 640.2659849999995,
"newRefBuildMs": 9.508285999999998,
"firstFrameNetSavingMs": 42.03027600000041,
"steadySavingMs": 51.53856200000041
},
{
"sx": 0.8,
"sy": 0,
"oldMs": 756.002915,
"newMs": 655.9519419999997,
"newRefBuildMs": 8.81105800000023,
"firstFrameNetSavingMs": 91.23991500000011,
"steadySavingMs": 100.05097300000034
},
{
"sx": 0,
"sy": 0.8,
"oldMs": 730.6417070000007,
"newMs": 671.6106240000008,
"newRefBuildMs": 8.391966000001048,
"firstFrameNetSavingMs": 50.639116999998805,
"steadySavingMs": 59.03108299999985
},
{
"sx": 0.8,
"sy": 0.5,
"oldMs": 787.9931480000014,
"newMs": 740.047842,
"newRefBuildMs": 7.513370000000577,
"firstFrameNetSavingMs": 40.43193600000086,
"steadySavingMs": 47.94530600000144
},
{
"sx": 1.2,
"sy": 0,
"oldMs": 705.7068249999975,
"newMs": 689.7542820000017,
"newRefBuildMs": 7.101795999999013,
"firstFrameNetSavingMs": 8.850746999996773,
"steadySavingMs": 15.952542999995785
},
{
"sx": 1.5,
"sy": 0.8,
"oldMs": 793.3721589999986,
"newMs": 733.093237000001,
"newRefBuildMs": 7.395623000000342,
"firstFrameNetSavingMs": 52.88329899999735,
"steadySavingMs": 60.27892199999769
}
]
}

View file

@ -0,0 +1,59 @@
{
"oldRefBuildMs": 23.925890999999993,
"tests": [
{
"sx": 0.5,
"sy": 0,
"oldMs": 100.18383699999998,
"newMs": 105.04222300000004,
"newRefBuildMs": 9.637189000000006,
"firstFrameNetSavingMs": -14.49557500000006,
"steadySavingMs": -4.858386000000053
},
{
"sx": 0.8,
"sy": 0,
"oldMs": 106.89347099999986,
"newMs": 93.24754299999995,
"newRefBuildMs": 8.222010999999952,
"firstFrameNetSavingMs": 5.42391699999996,
"steadySavingMs": 13.645927999999913
},
{
"sx": 0,
"sy": 0.8,
"oldMs": 110.3922050000001,
"newMs": 98.40486799999985,
"newRefBuildMs": 7.7547400000000835,
"firstFrameNetSavingMs": 4.232597000000169,
"steadySavingMs": 11.987337000000252
},
{
"sx": 0.8,
"sy": 0.5,
"oldMs": 138.4319349999996,
"newMs": 102.46810800000003,
"newRefBuildMs": 8.225470999999743,
"firstFrameNetSavingMs": 27.73835599999984,
"steadySavingMs": 35.96382699999958
},
{
"sx": 1.2,
"sy": 0,
"oldMs": 111.11085400000002,
"newMs": 93.51345999999967,
"newRefBuildMs": 8.248458000000028,
"firstFrameNetSavingMs": 9.348936000000322,
"steadySavingMs": 17.59739400000035
},
{
"sx": 1.5,
"sy": 0.8,
"oldMs": 126.42733499999986,
"newMs": 95.50466299999971,
"newRefBuildMs": 8.381738999999925,
"firstFrameNetSavingMs": 22.540933000000223,
"steadySavingMs": 30.922672000000148
}
]
}

View file

@ -0,0 +1,82 @@
{
"environment": "Chromium 144 headless, 780x441 viewport, container-local",
"scenes": [
{
"name": "seahorse_z14",
"bits": 288,
"first": {
"render": "835 ms / 3490 回 · BLA 8.034ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 0.9532754421234131
},
"base": {
"render": "1059 ms / 3490 回 · BLA 9.226ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.098907470703125
},
"engine": "WASM SIMD ×2 常駐Worker · BLA e-23 + リベース · スキップ 998 / 288 bit",
"renderText": "1059 ms / 3490 回 · BLA 9.226ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z20",
"bits": 320,
"first": {
"render": "1574 ms / 4509 回 · BLA 1.891ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.7402434349060059
},
"base": {
"render": "1226 ms / 4509 回 · BLA 2.126ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.3763623237609863
},
"engine": "WASM SIMD ×2 常駐Worker · BLA e-25 + リベース · スキップ 3231 / 320 bit",
"renderText": "1226 ms / 4509 回 · BLA 2.126ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "seahorse_z100",
"bits": 576,
"first": {
"render": "1224 ms / 17085 回 · BLA 0.104ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.4651060104370117
},
"base": {
"render": "983 ms / 17085 回 · BLA 0.097ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.1331260204315186
},
"engine": "WASM SIMD ×2 常駐Worker · BLA e-23 + リベース · スキップ 3245 / 576 bit",
"renderText": "983 ms / 17085 回 · BLA 0.097ms/kpx · 内部省略 0%",
"canvas": "780x441"
},
{
"name": "period3_z20",
"bits": 320,
"first": {
"render": "1675 ms / 4509 回 · BLA 53.047ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 1.8480825424194336
},
"base": {
"render": "1931 ms / 4509 回 · BLA 52.519ms/kpx · 内部省略 0%",
"badge": "深部リベース",
"saw": true,
"wall": 2.0063276290893555
},
"engine": "WASM SIMD ×2 常駐Worker · BLA e-25 + リベース · スキップ 4508 / 320 bit",
"renderText": "1931 ms / 4509 回 · BLA 52.519ms/kpx · 内部省略 0%",
"canvas": "780x441"
}
],
"errorCount": 0
}

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

@ -0,0 +1,12 @@
{
"w": 48,
"h": 30,
"pixels": 1440,
"globalIter": 3490,
"capIter": 2870,
"capped": 1440,
"late": 1066,
"insideGlobal": 374,
"lateEscapeRateAmongCapped": 0.7402777777777778,
"elapsedMs": 6344.968723999999
}

19
audit/smoke-test.json Normal file
View file

@ -0,0 +1,19 @@
{
"values": {
"title": "Mandelbrot ∞ Zoom v18",
"engine": "WebAssembly f64 / 256 bit",
"render": "281 ms / 350 回",
"badge": "準備完了",
"canvas": "780x441",
"black": {
"n": 6035,
"b": 1358,
"uniq": 1215
},
"autoText": "❚❚ 自動",
"badge2": "自動 · 境界追従",
"render2": "53 ms / 381 回"
},
"errorCount": 0,
"errors": []
}

View file

@ -0,0 +1,18 @@
{
"format": "mandelbrot-browser-baseline-v23",
"status": "not-run",
"reason": "No in-app browser binding was available in this environment.",
"lastCheckedUtc": "2026-08-22T10:48:56.1197927Z",
"connectionAudit": {
"consecutiveGoalTurns": 3,
"auditCycle": "resumed-after-performance-fix",
"availableBrowsers": 0,
"outcome": "blocked",
"recovery": "Connect an in-app Browser, then rerun tests/browser-benchmark.html. Unrelated browser-control backends are intentionally not substituted. Node runtime-budget coverage is available in audit/v23-node-performance.json but does not replace the browser gate."
},
"runner": "tests/browser-benchmark.html?profile=<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

@ -0,0 +1,69 @@
{
"format": "mandelbrot-node-runtime-budget-v23",
"generatedUtc": "2026-08-22T10:48:04.607Z",
"node": "v24.16.0",
"contracts": {
"previewBudget110": true,
"modeTargets": true,
"screenBudgets": true,
"boundedContinuation": true,
"coldDeepCap": true,
"measuredDeepBudget": true,
"viewportIndependentFloor": true
},
"shallow": [
{
"id": "overview-350",
"width": 640,
"height": 400,
"iterations": 350,
"pixels": 256000,
"medianMs": 86.21379999999999,
"msPerMegapixel": 336.77265624999995,
"meanIterations": 78.3253515625,
"samplesMs": [
83.7123,
87.21029999999999,
86.21379999999999,
87.21250000000003,
85.40730000000002
]
},
{
"id": "boundary-900",
"width": 512,
"height": 320,
"iterations": 900,
"pixels": 163840,
"medianMs": 109.8057,
"msPerMegapixel": 670.2008056640625,
"meanIterations": 154.49669189453124,
"samplesMs": [
109.8057,
110.24979999999994,
109.15139999999997,
108.90229999999997,
110.59719999999993
]
}
],
"deep": {
"id": "seahorse-bla-2000",
"width": 256,
"height": 144,
"iterations": 2000,
"pixels": 36864,
"blaEntries": 12,
"buildMs": 0.4078999999999269,
"medianMs": 722.5364,
"msPerMegapixel": 19600.054253472223,
"unresolved": 0,
"samplesMs": [
692.4027999999998,
695.0712000000003,
773.1455000000001,
722.5364,
863.2840000000006
]
}
}

View file

@ -0,0 +1,109 @@
{
"format": "mandelbrot-source-baseline-v23",
"generatedUtc": "2026-08-22T12:30:30.4538464Z",
"rendererVersion": 23,
"contract": {
"status": "pass",
"rendererVersion": 23,
"wasmPayloads": 8,
"scriptBytes": 129807,
"htmlBytes": 11395
},
"hosted": {
"firstViewFiles": [
{
"file": "dist/hosted/index.html",
"bytes": 11388,
"sha256": "e92c68d4b3c36a438675aee5fff4349c75ebd8ba8bdbc85369bfb3f55d07fd73"
},
{
"file": "dist/hosted/hosted-loader.js",
"bytes": 2338,
"sha256": "43804e07f0603d57a4c4d210cfbfb02e5fa7adc13e2dc53c1a15e05d1d74f4c1"
},
{
"file": "dist/hosted/script.js",
"bytes": 129807,
"sha256": "6ffebe51532fb8a141e9b1939e586d76b6c79db18786c06713e67ffe8d4ca78a"
},
{
"file": "dist/wasm/wasm-simd.d19b26c04e1b2f59.wasm",
"bytes": 511,
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
}
],
"firstViewUncompressedBytes": 144044,
"deepRequestContractBeforeDeepView": 0,
"deepRequestMeasurementStatus": "not-run",
"note": "The zero is a static loader contract, not a measured request count. Transfer compression, request count, parse, compile, and runtime timings require the fixed Hosted browser benchmark."
},
"standalone": {
"files": [
{
"file": "dist/standalone/index.html",
"bytes": 11395,
"sha256": "b2017e3aac73d22d1178e2a28bb5cb1ea0035cbcae5a4ed422dbe4ff7eb951fa"
},
{
"file": "dist/standalone/script.js",
"bytes": 129807,
"sha256": "6ffebe51532fb8a141e9b1939e586d76b6c79db18786c06713e67ffe8d4ca78a"
},
{
"file": "dist/standalone/kernels.js",
"bytes": 31389,
"sha256": "e97726c09af138da92b331c376766c1128c10aa3c361b83750356824262ea1a0"
}
],
"uncompressedBytes": 172591
},
"sourceBuild": {
"files": [
{
"file": "src/shallow_kernel.c",
"bytes": 1512,
"sha256": "ec1067a7a425e180eef7bebf250730de1144e63783735c9ed22084856da096d5"
},
{
"file": "src/deep_kernel.c",
"bytes": 5076,
"sha256": "c484aa9255a7a33eb4ce4ab90c74ec45865885b9a04fd263ce230838d3ce5137"
},
{
"file": "src/bla_kernel_v18.c",
"bytes": 8744,
"sha256": "66724b9afee313899110380f427ba6abbf5340af5c9af1b715eb24b8cff901a9"
},
{
"file": "src/color_kernel.c",
"bytes": 1284,
"sha256": "a40c7bd511dba94b096bf5e075f1fb95bb9f98b5154bac712f5e2bdeb1432a09"
},
{
"file": "src/abi.json",
"bytes": 899,
"sha256": "747f6dd6a0240684e9b502ec19a90458956e2d4fc583084bfe0b3ac0e879a0da"
},
{
"file": "toolchain.lock.json",
"bytes": 381,
"sha256": "d51122cff57d4304880db660be37988403c44770e209e51fe33e5ba30c9a7f38"
}
],
"compiler": "clang 17.0.6",
"node": "v22.18.0",
"compiledInThisEnvironment": true,
"goldenStatus": "pass",
"generatedManifest": {
"file": "build/wasm-v23/manifest.json",
"bytes": 4193,
"sha256": "cc388d664f642181d45a110ea5b44c47524050f8bf904ebd115e8e24c0f58a18"
},
"reason": "Verified generated manifest reused; fixed toolchain was removed after the successful gate."
},
"browserBenchmark": {
"status": "not-run",
"report": "audit/v23-browser-baseline.json",
"reason": "No in-app browser binding was available in this environment."
}
}

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
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,85 @@
{
"format": "mandelbrot-source-wasm-manifest-v23",
"toolchain": {
"format": "mandelbrot-wasm-toolchain-v1",
"compiler": "clang",
"version": "17.0.6",
"target": "wasm32-unknown-unknown",
"commonFlags": [
"-O3",
"-nostdlib",
"-ffreestanding",
"-fno-builtin",
"-Wl,--no-entry",
"-Wl,--export-memory",
"-Wl,--strip-all"
],
"scalarFlags": [
"-mno-simd128",
"-fno-vectorize",
"-fno-slp-vectorize"
],
"simdFlags": [
"-msimd128"
]
},
"abi": "src/abi.json",
"payloads": [
{
"symbol": "WASM_SIMD_B64",
"file": "wasm-simd.wasm",
"bytes": 511,
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b",
"source": "src/shallow_kernel.c"
},
{
"symbol": "WASM_SCALAR_B64",
"file": "wasm-scalar.wasm",
"bytes": 511,
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b",
"source": "src/shallow_kernel.c"
},
{
"symbol": "DEEP_SIMD_B64",
"file": "deep-simd.wasm",
"bytes": 4866,
"sha256": "d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214",
"source": "src/deep_kernel.c"
},
{
"symbol": "DEEP_SCALAR_B64",
"file": "deep-scalar.wasm",
"bytes": 5246,
"sha256": "d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2",
"source": "src/deep_kernel.c"
},
{
"symbol": "BLA_SIMD_B64",
"file": "bla-simd.wasm",
"bytes": 4959,
"sha256": "f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e",
"source": "src/bla_kernel_v18.c"
},
{
"symbol": "BLA_SCALAR_B64",
"file": "bla-scalar.wasm",
"bytes": 4593,
"sha256": "4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f",
"source": "src/bla_kernel_v18.c"
},
{
"symbol": "COLOR_SIMD_B64",
"file": "color-simd.wasm",
"bytes": 1387,
"sha256": "8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03",
"source": "src/color_kernel.c"
},
{
"symbol": "COLOR_SCALAR_B64",
"file": "color-scalar.wasm",
"bytes": 632,
"sha256": "aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90",
"source": "src/color_kernel.c"
}
]
}

Binary file not shown.

Binary file not shown.

11
dist/_headers vendored Normal file
View file

@ -0,0 +1,11 @@
/*
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

3
dist/hosted/_headers vendored Normal file
View file

@ -0,0 +1,3 @@
/*
X-Content-Type-Options: nosniff
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
});
})();

36
dist/hosted/hosted-loader.js vendored Normal file
View file

@ -0,0 +1,36 @@
const wasmRoot = new URL('../wasm/', import.meta.url);
async function compileAsset(name) {
const url = new URL(name, wasmRoot);
const response = await fetch(url, { cache: 'force-cache' });
if (!response.ok) throw new Error(`WASM fetch failed: ${name} (${response.status})`);
if (WebAssembly.compileStreaming) {
try { return await WebAssembly.compileStreaming(Promise.resolve(response.clone())); }
catch { /* A proxy may have supplied the wrong MIME type; use bytes below. */ }
}
return WebAssembly.compile(await response.arrayBuffer());
}
let shallowModule, shallowSimd = true;
try { shallowModule = await compileAsset('wasm-simd.d19b26c04e1b2f59.wasm'); }
catch { shallowModule = await compileAsset('wasm-scalar.d19b26c04e1b2f59.wasm'); shallowSimd = false; }
const asset = name => new URL(name, wasmRoot).href;
globalThis.MANDEL_KERNELS = Object.freeze({
// The compiled module is structured-cloned to the shallow Worker. No Base64
// conversion or second compile is needed in the hosted build.
WASM_SIMD_B64: shallowModule,
WASM_SCALAR_B64: shallowModule,
DEEP_SIMD_B64: asset('deep-simd.d49385f06e4a8f55.wasm'),
DEEP_SCALAR_B64: asset('deep-scalar.d133bbecc8f6dbdf.wasm'),
BLA_SIMD_B64: asset('bla-simd.f80f136e9fb676ce.wasm'),
BLA_SCALAR_B64: asset('bla-scalar.4f06913487044823.wasm'),
COLOR_SIMD_B64: asset('color-simd.8cf83374ed0c680b.wasm'),
COLOR_SCALAR_B64: asset('color-scalar.aa2914ec1acacaa2.wasm')
});
globalThis.MANDEL_KERNEL_META = Object.freeze({"WASM_SIMD_B64":"d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b","WASM_SCALAR_B64":"d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b","DEEP_SIMD_B64":"d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214","DEEP_SCALAR_B64":"d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2","BLA_SIMD_B64":"f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e","BLA_SCALAR_B64":"4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f","COLOR_SIMD_B64":"8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03","COLOR_SCALAR_B64":"aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90"});
globalThis.MANDEL_HOSTED_SHALLOW_SIMD = shallowSimd;
const app = document.createElement('script');
app.src = './script.js';
document.body.appendChild(app);

69
dist/hosted/index.html vendored Normal file
View file

@ -0,0 +1,69 @@
<!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 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}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}#palette{color:#111;background:#f4f5f8}#palette option{color:#111;background:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.exact-grid{display:grid;grid-template-columns:54px 1fr;gap:6px;margin-top:8px}.exact-grid input{min-width:0;width:100%;min-height:44px;border:1px solid var(--line);border-radius:8px;background:#070c19;color:var(--text);padding:6px;font:11px ui-monospace,monospace}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.diagnostics{margin-top:8px;color:var(--muted);font:10.5px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.exact-grid input{user-select:text;-webkit-user-select:text}
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}
#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord"></span></div><div class="row"><span class="muted">倍率</span> <span id="zoom"></span> <span class="muted">表示幅</span> <span id="span"></span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render"></span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div 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">保守的 (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"> 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>
<details><summary>診断情報</summary><div class="diagnostics"><div id="diagEngine">engine: …</div><div id="diagNumeric">numeric: …</div><div id="diagMemory">memory: …</div></div></details>
</div>
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
<div class="hint">ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。</div>
</div>
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI</button>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="exportDialog" aria-labelledby="exportTitle">
<h2 id="exportTitle">高解像度 PNG 出力</h2>
<div class="export-grid">
<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="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="gpu-kernels.js"></script>
<script src="script.js"></script>
</body>
</html>

248
dist/hosted/script.js vendored Normal file
View file

@ -0,0 +1,248 @@
(()=>{'use strict';
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
const $=s=>document.querySelector(s),canvas=$('#view');
const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0};
// ── exact fixed-point view state ─────────────────────────────────────────
function one(bits=state.bits){return 1n<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){const stepLog=log2FixedAt(snap.span,snap.bits)-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp)));return !Number.isFinite(ratio)||ratio<96||stepLog<-120}
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
// ── iteration / quality policy ───────────────────────────────────────────
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
'use strict';
const MAX_REF=150001,MAX_LEVELS=20;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class ReferenceService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=null;this.failed=false}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache=d;p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.1'].join(':');if(this.cache&&this.cache.key===key)return Promise.resolve(this.cache);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
}
const refs=new ReferenceService();
// ── WebGPU renderer ──────────────────────────────────────────────────────
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
class WebGpuRenderer{
constructor(adapter,device){
this.adapter=adapter;this.device=device;
const ai=adapter.info||{};
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,xm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
webgpuCanvasClaimed=true;this.configure();
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
ensureFrame(w,h){
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,64,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
}
ensureExportWorkspace(){
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved'),pbufs:Array.from({length:4},(_,i)=>buf(d,64,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+16,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
}
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','unresolved','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){const se=spanMantExp(snap);return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,0,0,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,Math.fround(se.mant),true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true)})}
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(4));
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
}
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolved(){if(!this.frame)return 0;const d=this.device,B=GPUBufferUsage,r=buf(d,16,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,16);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const value=new Uint32Array(r.getMappedRange().slice(0))[0];r.unmap();destroy(r);return value}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false}){
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,16,B.STORAGE|B.COPY_DST),pbuf=buf(d,64,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}
const read=buf(d,n*4,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return out;
}
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolved,pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),p=encoder.beginComputePass();p.setPipeline(this.deep);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),p=encoder.beginComputePass();p.setPipeline(this.direct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
}
async renderTileRGBA2x({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,forceStrict=false}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolved,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si];if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();}
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext()}
}
// ── GPU startup / rendering orchestration ────────────────────────────────
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
async function renderFrame(){const token=++state.token,snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,canvas.width),t0=performance.now();state.rendering=true;state.dirty=false;state.drawState='COVERING';runtime.renderStarts++;updateStats();try{const r=renderer||await initRenderer();if(token!==state.token)return;if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}let ctx=null;if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded rescaled perturbation'}else state.lastEngine='WebGPU · f32 direct';const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false;state.unresolved=0;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}else r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();if(deep)r.readUnresolved().then(q=>{if(token===state.token){state.unresolved=q||0;updateStats()}}).catch(()=>{})}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}}
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback深部非対応';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI':'UI';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
const exportJob={active:false,cancelled:false};
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
class StreamingPng{
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
async rows(filteredRows){await this.writer.write(filteredRows)}
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
}
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
async function runExport(){
if(exportJob.active)return;
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w),strict=$('#exportPrecision').value==='strict';
let ctx=null;
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
let done=0,unresolvedSamples=0;
try{
for(let y=0;y<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'sidecar参照':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
// ── diagnostics ──────────────────────────────────────────────────────────
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(deepNeeded()?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx;$('#diagEngine').textContent='engine: '+state.lastEngine+' | WebGPU '+(renderer?'ready':'unavailable')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height}
globalThis.__MANDEL_TEST__={
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
async probeMeta({w,h,strict=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict}))},
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
};
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:deepNeeded(),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
})();

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
});
})();

69
dist/standalone/index.html vendored Normal file
View file

@ -0,0 +1,69 @@
<!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 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}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}#palette{color:#111;background:#f4f5f8}#palette option{color:#111;background:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.exact-grid{display:grid;grid-template-columns:54px 1fr;gap:6px;margin-top:8px}.exact-grid input{min-width:0;width:100%;min-height:44px;border:1px solid var(--line);border-radius:8px;background:#070c19;color:var(--text);padding:6px;font:11px ui-monospace,monospace}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.diagnostics{margin-top:8px;color:var(--muted);font:10.5px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.exact-grid input{user-select:text;-webkit-user-select:text}
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}
#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord"></span></div><div class="row"><span class="muted">倍率</span> <span id="zoom"></span> <span class="muted">表示幅</span> <span id="span"></span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render"></span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div 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">保守的 (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"> 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>
<details><summary>診断情報</summary><div class="diagnostics"><div id="diagEngine">engine: …</div><div id="diagNumeric">numeric: …</div><div id="diagMemory">memory: …</div></div></details>
</div>
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
<div class="hint">ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。</div>
</div>
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI</button>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="exportDialog" aria-labelledby="exportTitle">
<h2 id="exportTitle">高解像度 PNG 出力</h2>
<div class="export-grid">
<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="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="gpu-kernels.js"></script>
<script src="script.js"></script>
</body>
</html>

22
dist/standalone/kernels.js vendored Normal file

File diff suppressed because one or more lines are too long

248
dist/standalone/script.js vendored Normal file
View file

@ -0,0 +1,248 @@
(()=>{'use strict';
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
const $=s=>document.querySelector(s),canvas=$('#view');
const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0};
// ── exact fixed-point view state ─────────────────────────────────────────
function one(bits=state.bits){return 1n<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){const stepLog=log2FixedAt(snap.span,snap.bits)-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp)));return !Number.isFinite(ratio)||ratio<96||stepLog<-120}
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
// ── iteration / quality policy ───────────────────────────────────────────
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
'use strict';
const MAX_REF=150001,MAX_LEVELS=20;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class ReferenceService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=null;this.failed=false}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache=d;p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.1'].join(':');if(this.cache&&this.cache.key===key)return Promise.resolve(this.cache);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
}
const refs=new ReferenceService();
// ── WebGPU renderer ──────────────────────────────────────────────────────
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
class WebGpuRenderer{
constructor(adapter,device){
this.adapter=adapter;this.device=device;
const ai=adapter.info||{};
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,xm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
webgpuCanvasClaimed=true;this.configure();
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
ensureFrame(w,h){
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,64,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
}
ensureExportWorkspace(){
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved'),pbufs:Array.from({length:4},(_,i)=>buf(d,64,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+16,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
}
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','unresolved','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){const se=spanMantExp(snap);return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,0,0,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,Math.fround(se.mant),true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true)})}
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(4));
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
}
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolved(){if(!this.frame)return 0;const d=this.device,B=GPUBufferUsage,r=buf(d,16,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,16);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const value=new Uint32Array(r.getMappedRange().slice(0))[0];r.unmap();destroy(r);return value}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false}){
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,16,B.STORAGE|B.COPY_DST),pbuf=buf(d,64,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}
const read=buf(d,n*4,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return out;
}
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolved,pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),p=encoder.beginComputePass();p.setPipeline(this.deep);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),p=encoder.beginComputePass();p.setPipeline(this.direct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
}
async renderTileRGBA2x({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,forceStrict=false}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolved,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si];if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();}
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext()}
}
// ── GPU startup / rendering orchestration ────────────────────────────────
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
async function renderFrame(){const token=++state.token,snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,canvas.width),t0=performance.now();state.rendering=true;state.dirty=false;state.drawState='COVERING';runtime.renderStarts++;updateStats();try{const r=renderer||await initRenderer();if(token!==state.token)return;if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}let ctx=null;if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded rescaled perturbation'}else state.lastEngine='WebGPU · f32 direct';const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false;state.unresolved=0;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}else r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();if(deep)r.readUnresolved().then(q=>{if(token===state.token){state.unresolved=q||0;updateStats()}}).catch(()=>{})}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}}
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback深部非対応';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI':'UI';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
const exportJob={active:false,cancelled:false};
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
class StreamingPng{
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
async rows(filteredRows){await this.writer.write(filteredRows)}
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
}
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
async function runExport(){
if(exportJob.active)return;
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w),strict=$('#exportPrecision').value==='strict';
let ctx=null;
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
let done=0,unresolvedSamples=0;
try{
for(let y=0;y<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'sidecar参照':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
// ── diagnostics ──────────────────────────────────────────────────────────
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(deepNeeded()?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx;$('#diagEngine').textContent='engine: '+state.lastEngine+' | WebGPU '+(renderer?'ready':'unavailable')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height}
globalThis.__MANDEL_TEST__={
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
async probeMeta({w,h,strict=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict}))},
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
};
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:deepNeeded(),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
})();

Binary file not shown.

BIN
dist/wasm/bla-scalar.wasm vendored Normal file

Binary file not shown.

BIN
dist/wasm/bla-simd.f80f136e9fb676ce.wasm vendored Normal file

Binary file not shown.

BIN
dist/wasm/bla-simd.wasm vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
dist/wasm/color-scalar.wasm vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
dist/wasm/color-simd.wasm vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
dist/wasm/deep-scalar.wasm vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
dist/wasm/deep-simd.wasm vendored Normal file

Binary file not shown.

55
dist/wasm/manifest.json vendored Normal file
View file

@ -0,0 +1,55 @@
{
"format": "mandelbrot-wasm-manifest-v1",
"generatedUtc": "2026-08-22T12:30:26.3193366Z",
"input": "kernels.js",
"payloads": [
{
"symbol": "WASM_SIMD_B64",
"file": "wasm-simd.wasm",
"bytes": 511,
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
},
{
"symbol": "WASM_SCALAR_B64",
"file": "wasm-scalar.wasm",
"bytes": 511,
"sha256": "d19b26c04e1b2f59ee1e9c8f6df0ceb9d6b2a56d08b906c0f3b8d9e10903460b"
},
{
"symbol": "DEEP_SIMD_B64",
"file": "deep-simd.wasm",
"bytes": 4866,
"sha256": "d49385f06e4a8f55c82fc8b4ecf7beb26313624dbcc038301a33a3eaa3465214"
},
{
"symbol": "DEEP_SCALAR_B64",
"file": "deep-scalar.wasm",
"bytes": 5246,
"sha256": "d133bbecc8f6dbdf4fdce2b400044ae50e34589c3cb1d612f4c8d644cbddffc2"
},
{
"symbol": "BLA_SIMD_B64",
"file": "bla-simd.wasm",
"bytes": 4959,
"sha256": "f80f136e9fb676ce65b2ae53be4ccfdc65712e623a5a06312645a025428e0c1e"
},
{
"symbol": "BLA_SCALAR_B64",
"file": "bla-scalar.wasm",
"bytes": 4593,
"sha256": "4f0691348704482331e48cb0739500fc2e716bc1f6a5c3b8f10ac5f2d54e985f"
},
{
"symbol": "COLOR_SIMD_B64",
"file": "color-simd.wasm",
"bytes": 1387,
"sha256": "8cf83374ed0c680b9f0cd3619c736689680fbc8c3b5475c0aa20b45073b01d03"
},
{
"symbol": "COLOR_SCALAR_B64",
"file": "color-scalar.wasm",
"bytes": 632,
"sha256": "aa2914ec1acacaa29ac2408118c320235657a7b807b9a60fd7abb49ba3424d90"
}
]
}

Binary file not shown.

BIN
dist/wasm/wasm-scalar.wasm vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
dist/wasm/wasm-simd.wasm vendored Normal file

Binary file not shown.

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
});
})();

11
hosted-headers.txt Normal file
View file

@ -0,0 +1,11 @@
/*
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

36
hosted-loader.js Normal file
View file

@ -0,0 +1,36 @@
const wasmRoot = new URL('../wasm/', import.meta.url);
async function compileAsset(name) {
const url = new URL(name, wasmRoot);
const response = await fetch(url, { cache: 'force-cache' });
if (!response.ok) throw new Error(`WASM fetch failed: ${name} (${response.status})`);
if (WebAssembly.compileStreaming) {
try { return await WebAssembly.compileStreaming(Promise.resolve(response.clone())); }
catch { /* A proxy may have supplied the wrong MIME type; use bytes below. */ }
}
return WebAssembly.compile(await response.arrayBuffer());
}
let shallowModule, shallowSimd = true;
try { shallowModule = await compileAsset('wasm-simd.wasm'); }
catch { shallowModule = await compileAsset('wasm-scalar.wasm'); shallowSimd = false; }
const asset = name => new URL(name, wasmRoot).href;
globalThis.MANDEL_KERNELS = Object.freeze({
// The compiled module is structured-cloned to the shallow Worker. No Base64
// conversion or second compile is needed in the hosted build.
WASM_SIMD_B64: shallowModule,
WASM_SCALAR_B64: shallowModule,
DEEP_SIMD_B64: asset('deep-simd.wasm'),
DEEP_SCALAR_B64: asset('deep-scalar.wasm'),
BLA_SIMD_B64: asset('bla-simd.wasm'),
BLA_SCALAR_B64: asset('bla-scalar.wasm'),
COLOR_SIMD_B64: asset('color-simd.wasm'),
COLOR_SCALAR_B64: asset('color-scalar.wasm')
});
globalThis.MANDEL_KERNEL_META = Object.freeze(/*__KERNEL_META__*/{});
globalThis.MANDEL_HOSTED_SHALLOW_SIMD = shallowSimd;
const app = document.createElement('script');
app.src = './script.js';
document.body.appendChild(app);

69
index.html Normal file
View file

@ -0,0 +1,69 @@
<!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 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}
#view{position:fixed;inset:0;width:100%;height:100%;display:block;background:#050813;image-rendering:auto;touch-action:none}
.top{position:fixed;z-index:5;top:max(10px,env(safe-area-inset-top));left:10px;right:10px;display:flex;gap:8px;pointer-events:none}.brand,.stats,.panel,.toast{backdrop-filter:blur(18px) saturate(130%);-webkit-backdrop-filter:blur(18px) saturate(130%)}
.brand{pointer-events:auto;background:var(--panel);border:1px solid var(--line);border-radius:16px;padding:10px 14px;font-weight:850;letter-spacing:.04em;font-size:13px;box-shadow:0 12px 40px rgba(0,0,0,.32)}.brand small{display:block;margin-top:2px;color:var(--muted);font-size:10px;font-weight:600;letter-spacing:0}
.stats{margin-left:auto;max-width:min(560px,65vw);padding:9px 12px;border:1px solid var(--line);border-radius:14px;background:var(--panel);font:11px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace;overflow:hidden}.row{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.muted{color:var(--muted)}
.panel{position:fixed;z-index:6;right:10px;bottom:max(10px,env(safe-area-inset-bottom));width:min(380px,calc(100vw - 20px));padding:11px;border:1px solid var(--line);border-radius:19px;background:var(--panel);box-shadow:0 18px 58px rgba(0,0,0,.44)}
.toolbar{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}select{appearance:auto;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:4px 8px;border-radius:10px;font-size:12px}#palette{color:#111;background:#f4f5f8}#palette option{color:#111;background:#fff}button{appearance:none;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.07);color:var(--text);min-height:44px;padding:7px 5px;border-radius:12px;font-size:12px;font-weight:760;cursor:pointer}button:active{transform:translateY(1px)}button.primary{background:linear-gradient(135deg,rgba(64,215,236,.25),rgba(139,78,255,.22));border-color:rgba(97,219,233,.48)}button.on{outline:1px solid rgba(97,219,233,.8)}button:focus-visible,select:focus-visible,input:focus-visible,#view:focus-visible{outline:3px solid #fff;outline-offset:2px}
.group{margin-top:10px;padding-top:9px;border-top:1px solid rgba(255,255,255,.08)}.line{display:grid;grid-template-columns:98px 1fr 48px;align-items:center;gap:8px;margin:7px 0}.line label{font-size:12px;color:#dce1ef}.line output{text-align:right;color:var(--muted);font:11px ui-monospace,monospace}input[type=range]{width:100%;min-height:44px;accent-color:var(--accent)}.checks{display:flex;gap:12px;flex-wrap:wrap;margin-top:8px;color:#dce1ef;font-size:12px}.checks label{display:flex;align-items:center;min-height:44px;gap:6px}
details{margin-top:9px;border-top:1px solid rgba(255,255,255,.08);padding-top:8px}summary{display:flex;align-items:center;min-height:44px;cursor:pointer;color:var(--muted);font-size:12px}.exact-grid{display:grid;grid-template-columns:54px 1fr;gap:6px;margin-top:8px}.exact-grid input{min-width:0;width:100%;min-height:44px;border:1px solid var(--line);border-radius:8px;background:#070c19;color:var(--text);padding:6px;font:11px ui-monospace,monospace}.mini-actions{display:flex;gap:6px;margin-top:7px}.mini-actions button{flex:1}
.diagnostics{margin-top:8px;color:var(--muted);font:10.5px/1.5 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.exact-grid input{user-select:text;-webkit-user-select:text}
.bottom{display:flex;align-items:center;justify-content:space-between;gap:8px}.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.07);font-size:10px;color:#d9dfed}.dot{width:7px;height:7px;border-radius:50%;background:#61dbe9;box-shadow:0 0 12px #61dbe9}.hint{margin-top:8px;color:var(--muted);font-size:10.5px;line-height:1.45}
.toast{position:fixed;z-index:10;left:50%;bottom:24px;transform:translate(-50%,16px);opacity:0;transition:.18s;pointer-events:none;padding:9px 12px;border:1px solid var(--line);border-radius:12px;background:rgba(7,12,25,.95);font-size:12px}.toast.show{opacity:1;transform:translate(-50%,0)}
dialog{width:min(430px,calc(100vw - 24px));border:1px solid var(--line);border-radius:18px;background:#0b1120;color:var(--text);padding:16px;box-shadow:0 24px 80px #000}dialog::backdrop{background:rgba(0,0,0,.65)}dialog h2{font-size:16px;margin:0 0 12px}.export-grid{display:grid;grid-template-columns:130px 1fr;gap:10px;align-items:center}.export-grid label{font-size:12px}.export-grid input,.export-grid select{width:100%}.export-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:14px}progress{width:100%;margin-top:12px}
#uiToggle{position:fixed;z-index:20;left:max(10px,env(safe-area-inset-left));bottom:max(10px,env(safe-area-inset-bottom));min-width:52px;min-height:44px;padding:8px 12px;border-radius:999px;background:rgba(7,12,25,.78);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);box-shadow:0 8px 30px rgba(0,0,0,.3)}body.ui-hidden .top,body.ui-hidden .panel{display:none}body.ui-hidden #uiToggle{background:rgba(7,12,25,.7)}
.compact-status{display:none;position:fixed;z-index:4;right:8px;top:max(8px,env(safe-area-inset-top));max-width:58vw;padding:7px 10px;border:1px solid var(--line);border-radius:999px;background:var(--panel);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@media(max-width:700px){.stats{display:none}.brand small{display:none}.panel{left:8px;right:8px;bottom:max(8px,env(safe-area-inset-bottom));width:auto;padding:10px;touch-action:pan-x pan-y pinch-zoom}.line{grid-template-columns:82px 1fr 42px}.hint{display:none}.compact-status{display:block}button,select{min-height:44px}}
@media(prefers-reduced-motion:reduce){.toast{transition:none}button:active{transform:none}}
@media(prefers-reduced-transparency:reduce){.brand,.stats,.panel,.toast,#uiToggle,.compact-status{backdrop-filter:none;-webkit-backdrop-filter:none;background:#0b1120}}
</style>
</head>
<body>
<canvas id="view" tabindex="0" role="img" aria-label="マンデルブロ集合。矢印キーで移動、Enterで拡大、Shift+Enterで縮小できます"></canvas>
<div class="top"><div class="brand">MANDELBROT DEEP ZOOM</div><div class="stats" role="status" aria-live="polite" aria-atomic="true"><div class="row"><span class="muted">中心</span> <span id="coord"></span></div><div class="row"><span class="muted">倍率</span> <span id="zoom"></span> <span class="muted">表示幅</span> <span id="span"></span></div><div class="row"><span class="muted">計算</span> <span id="engine">起動中…</span> <span class="muted">描画</span> <span id="render"></span></div></div></div>
<div id="compactStatus" class="compact-status" role="status" aria-live="polite">起動中</div>
<div 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">保守的 (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"> 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>
<details><summary>診断情報</summary><div class="diagnostics"><div id="diagEngine">engine: …</div><div id="diagNumeric">numeric: …</div><div id="diagMemory">memory: …</div></div></details>
</div>
<div class="group bottom"><span class="badge"><span class="dot"></span><span id="badge">起動中</span></span><div style="display:flex;gap:6px"><button id="share">URL共有</button></div></div>
<div class="hint">ホイール / ピンチでズーム、ドラッグで移動。HキーでUI表示を切り替え。</div>
</div>
<button id="uiToggle" title="UIを隠す / 表示" aria-controls="controls" aria-expanded="true">UI</button>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<dialog id="exportDialog" aria-labelledby="exportTitle">
<h2 id="exportTitle">高解像度 PNG 出力</h2>
<div class="export-grid">
<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="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="gpu-kernels.js"></script>
<script src="script.js"></script>
</body>
</html>

22
kernels.js Normal file

File diff suppressed because one or more lines are too long

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"
}
}

248
script.js Normal file
View file

@ -0,0 +1,248 @@
(()=>{'use strict';
const G=globalThis.MANDEL_WEBGPU_KERNELS;if(!G)throw new Error('gpu-kernels.js が読み込まれていません');
const $=s=>document.querySelector(s),canvas=$('#view');
const VERSION=24,INITIAL_BITS=256,MIN_SPAN_BITS=224,TARGET_SPAN_BITS=240,RATIO_DEN=4503599627370496n;
const FIELD_UNKNOWN=0,FIELD_ESCAPED=1,FIELD_INTERIOR_LIKELY=2,FIELD_INTERIOR_PROVEN=3;
const state={bits:INITIAL_BITS,re:0n,im:0n,span:0n,baseIter:350,adaptive:true,hq:false,processMode:'standard',palette:0,cycle:.008,shift:.18,token:0,rendering:false,recoloring:false,recolorPending:false,dirty:true,lastRender:0,lastEngine:'起動中',drawState:'REPROJECTED',frameView:null,fieldView:null,pointerActive:false,wheelActive:false,effectiveDpr:1,screenPixelBudget:0,unresolved:0,gpuError:'',gpuInitFailed:false,gpuUnavailable:false,lastInteraction:performance.now(),focusX:.5,focusY:.5,uiHidden:false};
let renderer=null,rendererInitPromise=null,fallbackCtx=null,webgpuCanvasClaimed=false,raf=0,settleTimer=0,lastWrittenHash='',navigationHash='';
const viewHistory=[];let viewHistoryIndex=-1;
const runtime={renderStarts:0,deviceLosses:0,referenceBuilds:0,gpuFrames:0,gpuRecolors:0,exports:0};
// ── exact fixed-point view state ─────────────────────────────────────────
function one(bits=state.bits){return 1n<<BigInt(bits)}
function fromFrac(n,d=1n){return n*one()/d}
function fromDec(s){s=String(s).trim();let neg=s.startsWith('-');if(neg)s=s.slice(1);if(s.startsWith('+'))s=s.slice(1);const p=s.toLowerCase().split('e'),mant=p[0],exp=p[1]?parseInt(p[1],10):0,a=mant.split('.'),i=a[0]||'0',f=a[1]||'';let digits=(i+f).replace(/^0+(?=\d)/,'')||'0',places=f.length-exp;if(places<0){digits+='0'.repeat(-places);places=0}const den=10n**BigInt(places),v=(BigInt(digits)*one()+den/2n)/den;return neg?-v:v}
function decimalRequiredBits(s){s=String(s).trim().replace(/^[+-]/,'');const p=s.toLowerCase().split('e'),f=(p[0].split('.')[1]||'').length,e=p[1]?parseInt(p[1],10):0;return Math.max(64,Math.ceil(Math.max(0,f-e)*Math.log2(10))+32)}
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function align(v,fromBits,toBits){const d=toBits-fromBits;return d===0?v:d>0?v<<BigInt(d):v>>BigInt(-d)}
function fixedNum(v,bits=state.bits){if(v===0n)return 0;const neg=v<0n;if(neg)v=-v;const bl=bitLen(v),keep=52;let top,exp;if(bl>keep){const sh=BigInt(bl-keep);top=Number(v>>sh);exp=bl-keep-bits}else{top=Number(v);exp=-bits}const x=top*Math.pow(2,exp);return neg?-x:x}
function log2FixedAt(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits}
function log2Fixed(v){return log2FixedAt(v,state.bits)}
function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v}
function mulRatio(v,f){const n=BigInt(Math.max(1,Math.round(f*Number(RATIO_DEN))));return v*n/RATIO_DEN}
function promoteState(shift){const s=BigInt(shift);state.re<<=s;state.im<<=s;state.span<<=s;if(state.frameView){state.frameView={...state.frameView,bits:state.frameView.bits+shift,re:state.frameView.re<<s,im:state.frameView.im<<s,span:state.frameView.span<<s}}state.bits+=shift}
function ensurePrecision(){const bl=bitLen(state.span);if(bl<MIN_SPAN_BITS)promoteState(TARGET_SPAN_BITS-bl)}
function fmtFixed(v,d=17){let neg=v<0n;if(neg)v=-v;const scale=10n**BigInt(d),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(d+1,'0');s=s.slice(0,-d)+'.'+s.slice(-d);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function fmtFixedExact(v){let neg=v<0n;if(neg)v=-v;const maxD=state.bits,scale=10n**BigInt(maxD),q=v*scale>>BigInt(state.bits);let s=q.toString().padStart(maxD+1,'0');s=s.slice(0,-maxD)+'.'+s.slice(-maxD);s=s.replace(/(\.\d*?)0+$/,'$1').replace(/\.$/,'');return(neg?'-':'')+s}
function snapshot(){return{bits:state.bits,re:state.re,im:state.im,span:state.span}}
function zoomExp(){return Math.max(0,Math.log10(3.4)-log2Fixed(state.span)/Math.log2(10))}
function fmtSpan(){const l=log2Fixed(state.span)/Math.log2(10);if(l>-4)return fmtFixed(state.span,12);const e=Math.floor(l),m=Math.pow(10,l-e);return m.toFixed(7)+'e'+e}
function spanMantExp(snap){const l=log2FixedAt(snap.span,snap.bits);if(!Number.isFinite(l))return{mant:0,exp:0};const exp=Math.floor(l),mant=Math.pow(2,l-exp);return{mant,exp}}
function f32Ulp(x){x=Math.fround(Math.abs(x));if(!Number.isFinite(x))return Infinity;if(x===0)return 2**-149;const e=Math.floor(Math.log2(x));return 2**(e-23)}
function deepNeeded(snap=snapshot(),width=Math.max(1,canvas.width)){const stepLog=log2FixedAt(snap.span,snap.bits)-Math.log2(width),cr=fixedNum(snap.re,snap.bits),ci=fixedNum(snap.im,snap.bits),ulp=Math.max(f32Ulp(cr),f32Ulp(ci),2**-149),ratio=Math.pow(2,Math.min(1024,stepLog-Math.log2(ulp)));return !Number.isFinite(ratio)||ratio<96||stepLog<-120}
function currentViewSpec(){return{bits:state.bits,re:state.re,im:state.im,span:state.span,palette:state.palette,cycle:state.cycle,shift:state.shift,baseIter:state.baseIter,adaptive:state.adaptive}}
function viewSpecKey(v){return[v.bits,v.re,v.im,v.span,v.palette,v.cycle,v.shift,v.baseIter,v.adaptive].join(':')}
function recordView(){const v=currentViewSpec(),k=viewSpecKey(v);if(viewHistoryIndex>=0&&viewSpecKey(viewHistory[viewHistoryIndex])===k)return;viewHistory.splice(viewHistoryIndex+1);viewHistory.push(v);if(viewHistory.length>80)viewHistory.shift();viewHistoryIndex=viewHistory.length-1;syncHistoryButtons()}
function restoreView(v){if(!v)return;Object.assign(state,{bits:v.bits,re:v.re,im:v.im,span:v.span,palette:v.palette,cycle:v.cycle,shift:v.shift,baseIter:v.baseIter,adaptive:v.adaptive});ensurePrecision();syncControls();saveHash(false);markDirty()}
// ── iteration / quality policy ───────────────────────────────────────────
function maxIter(){if(!state.adaptive)return state.baseIter;const z=zoomExp(),bonus=Math.max(0,Math.floor(70*Math.sqrt(z)+15*z));return Math.min(150000,Math.max(state.baseIter,state.baseIter+bonus))}
function pixelBudget(){const low=Number(navigator.deviceMemory||8)<=4,small=matchMedia('(max-width:700px)').matches;if(!navigator.gpu||state.gpuUnavailable)return 262144;if(state.processMode==='power')return 524288;if(state.processMode==='fine')return(low||small?1572864:3145728);if(state.processMode==='validate')return(low||small?1048576:2097152);return(low||small?786432:1572864)}
function resize(){const cssW=Math.max(1,innerWidth),cssH=Math.max(1,innerHeight),budget=pixelBudget(),native=Math.max(1,devicePixelRatio||1),bd=Math.sqrt(budget/(cssW*cssH));let dpr=Math.max(Math.min(1,64/Math.max(cssW,cssH)),Math.min(native,bd));if(renderer){const md=Math.max(2,renderer.adapterLimits.maxTextureDimension2D||8192);dpr=Math.min(dpr,md/cssW,md/cssH)}const w=Math.max(2,Math.round(cssW*dpr)),h=Math.max(2,Math.round(cssH*dpr));state.effectiveDpr=dpr;state.screenPixelBudget=budget;if(canvas.width!==w||canvas.height!==h){canvas.width=w;canvas.height=h;if(renderer)renderer.configure();markDirty(false)}}
// ── high precision reference worker ─────────────────────────────────────
function referenceWorkerSource(){return String.raw`
'use strict';
const MAX_REF=150001,MAX_LEVELS=20;
function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length}
function roundShift(v,b){const neg=v<0n,a=neg?-v:v,half=1n<<(BigInt(b)-1n),q=(a+half)>>BigInt(b);return neg?-q:q}
function fixedNum(v,b){if(v===0n)return 0;let neg=v<0n;if(neg)v=-v;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh)),n=top*Math.pow(2,sh-b);return neg?-n:n}
function orbit(bits,re,im,iter){const B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE;let zr=0n,zi=0n,escape=0,n=0;const rr=new Float64Array(iter+1),ri=new Float64Array(iter+1);for(;n<iter&&!escape;n++){rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+im;zr=zr2-zi2+re;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}rr[n]=fixedNum(zr,bits);ri[n]=fixedNum(zi,bits);return{rr,ri,refLen:escape||iter,escape}}
function verify(baseBits,re,im,ref,refLen){const bits=baseBits+64,R=re<<64n,I=im<<64n,B=BigInt(bits),ONE=1n<<B,BAIL=16n*ONE,targets=new Set([0,refLen]);for(let n=1;n<refLen;n*=2)targets.add(n);const stride=Math.max(1,Math.floor(refLen/32));for(let n=stride;n<refLen;n+=stride)targets.add(n);let zr=0n,zi=0n,escape=0,mismatch=false,checked=0;for(let n=0;n<=refLen&&!escape&&!mismatch;n++){if(targets.has(n)){checked++;if(!Object.is(fixedNum(zr,bits),ref.rr[n])||!Object.is(fixedNum(zi,bits),ref.ri[n]))mismatch=true}if(n===refLen)break;const zr2=roundShift(zr*zr,bits),zi2=roundShift(zi*zi,bits);zi=roundShift(2n*zr*zi,bits)+I;zr=zr2-zi2+R;const mag=roundShift(zr*zr,bits)+roundShift(zi*zi,bits);if(mag>BAIL)escape=n+1}return{mismatch,checked}}
function packRefs(rr,ri,refLen){const buf=new ArrayBuffer((refLen+1)*16),dv=new DataView(buf);for(let i=0;i<=refLen;i++){const hr=Math.fround(rr[i]),hi=Math.fround(ri[i]),lr=Math.fround(rr[i]-hr),li=Math.fround(ri[i]-hi),o=i*16;dv.setFloat32(o,hr,true);dv.setFloat32(o+4,hi,true);dv.setFloat32(o+8,lr,true);dv.setFloat32(o+12,li,true)}return buf}
self.onmessage=e=>{const d=e.data;if(!d||d.type!=='build')return;const t0=performance.now();try{const bits=d.bits+64,re=BigInt(d.re)<<64n,im=BigInt(d.im)<<64n,ref=orbit(bits,re,im,Math.min(MAX_REF-1,d.iter)),v=verify(bits,re,im,ref,ref.refLen),refs=packRefs(ref.rr,ref.ri,ref.refLen);postMessage({type:'built',id:d.id,key:d.key,refLen:ref.refLen,escape:ref.escape,precisionBits:bits,checkpointMismatch:v.mismatch,checkpointCount:v.checked,buildMs:performance.now()-t0,refs},[refs])}catch(error){postMessage({type:'error',id:d.id,error:String(error&&error.stack||error)})}}
`}
class ReferenceService{
constructor(){this.worker=null;this.url='';this.serial=0;this.pending=new Map();this.cache=null;this.failed=false}
ensure(){if(this.worker)return true;if(this.failed||typeof Worker==='undefined'||typeof Blob==='undefined')return false;try{this.url=URL.createObjectURL(new Blob([referenceWorkerSource()],{type:'text/javascript'}));this.worker=new Worker(this.url);this.worker.onmessage=e=>{const d=e.data,p=this.pending.get(d.id);if(!p)return;this.pending.delete(d.id);if(d.type==='error')p.reject(new Error(d.error));else{runtime.referenceBuilds++;this.cache=d;p.resolve(d)}};this.worker.onerror=e=>{this.failed=true;for(const p of this.pending.values())p.reject(new Error(e.message||'reference worker error'));this.pending.clear();this.destroy()};return true}catch{this.failed=true;return false}}
request(snap,iter){const key=[snap.bits,snap.re,snap.im,iter,'guarded-perturb-v24.1'].join(':');if(this.cache&&this.cache.key===key)return Promise.resolve(this.cache);if(this.pending.size)this.cancelPending('superseded reference request');if(!this.ensure())return Promise.reject(new Error('Reference Workerを作成できません'));const id=++this.serial;return new Promise((resolve,reject)=>{this.pending.set(id,{resolve,reject});this.worker.postMessage({type:'build',id,key,bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),iter})})}
cancelPending(reason='cancelled'){if(!this.pending.size)return;for(const p of this.pending.values())p.reject(new Error(reason));this.pending.clear();if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
destroy(){this.cancelPending('destroyed');if(this.worker){try{this.worker.terminate()}catch{}this.worker=null}if(this.url){try{URL.revokeObjectURL(this.url)}catch{}this.url=''}}
}
const refs=new ReferenceService();
// ── WebGPU renderer ──────────────────────────────────────────────────────
function buf(device,size,usage,label){return device.createBuffer({label,size:Math.max(4,Math.ceil(size/4)*4),usage})}
function destroy(x){if(x&&x.destroy)try{x.destroy()}catch{}}
function writeU32F32(size,writer){const a=new ArrayBuffer(size),d=new DataView(a);writer(d);return a}
class WebGpuRenderer{
constructor(adapter,device){
this.adapter=adapter;this.device=device;
const ai=adapter.info||{};
this.adapterInfo={vendor:ai.vendor||'',architecture:ai.architecture||'',device:ai.device||'',description:ai.description||''};
this.adapterLimits={maxBufferSize:Number(adapter.limits.maxBufferSize),maxStorageBufferBindingSize:Number(adapter.limits.maxStorageBufferBindingSize),maxComputeWorkgroupsPerDimension:Number(adapter.limits.maxComputeWorkgroupsPerDimension),maxTextureDimension2D:Number(adapter.limits.maxTextureDimension2D)};
this.context=null;this.format=navigator.gpu.getPreferredCanvasFormat();
this.frame=null;this.deepCtx=null;this.exportWs=null;this.compilation=[];this.uncapturedErrors=[];this.lossReason='';this.sampler=device.createSampler({magFilter:'linear',minFilter:'linear'});
device.addEventListener?.('uncapturederror',e=>{const msg=String(e.error&&e.error.message||e.error||'WebGPU uncaptured error');this.uncapturedErrors.push(msg);state.gpuError=msg;console.error(e.error||e)});
this.ready=this.initPipelines();
device.lost.then(info=>{this.lossReason=info.message||info.reason||'device lost';runtime.deviceLosses++;state.gpuError=this.lossReason;renderer=null;markDirty(false);initRenderer()});
}
configure(){if(this.context)this.context.configure({device:this.device,format:this.format,alphaMode:'opaque'})}
async module(label,code){const m=this.device.createShaderModule({label,code});if(m.getCompilationInfo){const info=await m.getCompilationInfo();const errs=info.messages.filter(x=>x.type==='error');this.compilation.push({label,messages:info.messages.map(x=>({type:x.type,line:x.lineNum,message:x.message}))});if(errs.length)throw new Error(label+': '+errs.map(x=>x.message).join('\n'))}return m}
async initPipelines(){
this.device.pushErrorScope?.('validation');
try{
const [dm,xm,cm,am,pm]=await Promise.all([this.module('direct',G.DIRECT_F32_WGSL),this.module('deep',G.DEEP_PERTURB_WGSL),this.module('color',G.COLOR_WGSL),this.module('aa-resolve',G.AA_RESOLVE_WGSL),this.module('present',G.PRESENT_WGSL)]);
this.direct=this.device.createComputePipeline({layout:'auto',compute:{module:dm,entryPoint:'main'}});
this.deep=this.device.createComputePipeline({layout:'auto',compute:{module:xm,entryPoint:'main'}});
this.color=this.device.createComputePipeline({layout:'auto',compute:{module:cm,entryPoint:'main'}});
this.aaResolve=this.device.createComputePipeline({layout:'auto',compute:{module:am,entryPoint:'main'}});
this.present=this.device.createRenderPipeline({layout:'auto',vertex:{module:pm,entryPoint:'vs'},fragment:{module:pm,entryPoint:'fs',targets:[{format:this.format}]},primitive:{topology:'triangle-list'}});
this.context=canvas.getContext('webgpu');
if(!this.context)throw new Error('WebGPU canvas contextを取得できません');
webgpuCanvasClaimed=true;this.configure();
}finally{if(this.device.popErrorScope){const error=await this.device.popErrorScope();if(error)throw error}}
}
frameDestroy(){if(!this.frame)return;for(const k of ['meta','smooth','unresolved','numericParams','colorParams','presentParams','front','back'])destroy(this.frame[k]);this.frame=null}
ensureFrame(w,h){
const n=w*h;if(this.frame&&this.frame.w===w&&this.frame.h===h)return this.frame;this.frameDestroy();const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage;
this.frame={w,h,n,meta:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST,'field-smooth'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'unresolved-count'),numericParams:buf(d,64,B.UNIFORM|B.COPY_DST,'numeric-params'),colorParams:buf(d,32,B.UNIFORM|B.COPY_DST,'color-params'),presentParams:buf(d,16,B.UNIFORM|B.COPY_DST,'present-params'),front:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'front-color'}),back:d.createTexture({size:[w,h],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING|T.COPY_SRC,label:'back-color'})};return this.frame;
}
ensureExportWorkspace(){
if(this.exportWs)return this.exportWs;const d=this.device,B=GPUBufferUsage,T=GPUTextureUsage,S=512,n=S*S,bpr=S*4,pixelBytes=bpr*S;
this.exportWs={size:S,meta:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-meta'),smooth:buf(d,n*4,B.STORAGE|B.COPY_DST,'export-smooth'),unresolved:buf(d,16,B.STORAGE|B.COPY_SRC|B.COPY_DST,'export-unresolved'),pbufs:Array.from({length:4},(_,i)=>buf(d,64,B.UNIFORM|B.COPY_DST,'export-numeric-'+i)),cbuf:buf(d,32,B.UNIFORM|B.COPY_DST,'export-color'),samples:Array.from({length:4},(_,i)=>d.createTexture({label:'export-sample-'+i,size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.TEXTURE_BINDING})),tex:d.createTexture({label:'export-resolve',size:[S,S],format:'rgba8unorm',usage:T.STORAGE_BINDING|T.COPY_SRC}),read:buf(d,pixelBytes+16,B.COPY_DST|B.MAP_READ,'export-readback')};return this.exportWs;
}
exportWorkspaceDestroy(){if(!this.exportWs)return;for(const k of ['meta','smooth','unresolved','cbuf','tex','read'])destroy(this.exportWs[k]);for(const b of this.exportWs.pbufs)destroy(b);for(const t of this.exportWs.samples)destroy(t);this.exportWs=null}
setDeepContext(ctx){if(this.deepCtx&&this.deepCtx.key===ctx.key)return;this.destroyDeepContext();const d=this.device,B=GPUBufferUsage,refsB=buf(d,ctx.refs.byteLength,B.STORAGE|B.COPY_DST,'reference-orbit');d.queue.writeBuffer(refsB,0,ctx.refs);this.deepCtx={...ctx,refsB}}
destroyDeepContext(){if(this.deepCtx)destroy(this.deepCtx.refsB);this.deepCtx=null}
directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,strict].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(32,Math.fround(fixedNum(snap.re,snap.bits)),true);d.setFloat32(36,Math.fround(fixedNum(snap.im,snap.bits)),true);d.setFloat32(40,Math.fround(fixedNum(snap.span,snap.bits)),true);d.setFloat32(44,sx,true);d.setFloat32(48,sy,true)})}
deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sx=.5,sy=.5,strict=0){const se=spanMantExp(snap);return writeU32F32(64,d=>{[w,h,fullW,fullH,tileX,tileY,iter,this.deepCtx.refLen,strict,0,0,0].forEach((v,i)=>d.setUint32(i*4,v,true));d.setFloat32(48,Math.fround(se.mant),true);d.setInt32(52,se.exp,true);d.setFloat32(56,sx,true);d.setFloat32(60,sy,true)})}
colorParamsData(w,h){return writeU32F32(32,d=>{d.setUint32(0,w,true);d.setUint32(4,h,true);d.setUint32(8,state.palette,true);d.setUint32(12,state.hq?1:0,true);d.setFloat32(16,state.cycle,true);d.setFloat32(20,state.shift,true)})}
async computeFrame(snap,iter,deep,deepContext,token,forceStrict=false){
await this.ready;const f=this.ensureFrame(canvas.width,canvas.height),d=this.device;if(deep)this.setDeepContext(deepContext);d.queue.writeBuffer(f.unresolved,0,new Uint32Array(4));
d.queue.writeBuffer(f.numericParams,0,deep?this.deepParams(f.w,f.h,f.w,f.h,0,0,iter,snap,.5,.5,forceStrict?1:0):this.directParams(f.w,f.h,f.w,f.h,0,0,iter,snap));
d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const encoder=d.createCommandEncoder({label:'mandelbrot-frame'});
if(deep){const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:f.meta}},{binding:3,resource:{buffer:f.smooth}},{binding:4,resource:{buffer:f.unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
else{const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.numericParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));pass.end();}
const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));cp.end();
d.queue.submit([encoder.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuFrames++;return true;
}
async recolor(token){await this.ready;if(!this.frame)return false;const f=this.frame,d=this.device;d.queue.writeBuffer(f.colorParams,0,this.colorParamsData(f.w,f.h));const e=d.createCommandEncoder(),bg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:f.colorParams}},{binding:1,resource:{buffer:f.meta}},{binding:2,resource:{buffer:f.smooth}},{binding:3,resource:f.back.createView()}]}),p=e.beginComputePass();p.setPipeline(this.color);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(f.w/8),Math.ceil(f.h/8));p.end();d.queue.submit([e.finish()]);await d.queue.onSubmittedWorkDone();if(token!==state.token)return false;[f.front,f.back]=[f.back,f.front];runtime.gpuRecolors++;return true}
presentTransform(view=state.frameView){if(!this.frame||!view)return{scaleX:1,scaleY:1,offsetX:0,offsetY:0};const cur=snapshot(),b=Math.max(cur.bits,view.bits),cs=align(cur.span,cur.bits,b),ps=align(view.span,view.bits,b),dr=align(cur.re,cur.bits,b)-align(view.re,view.bits,b),di=align(cur.im,cur.bits,b)-align(view.im,view.bits,b),scale=fixedRatio(cs,ps);return{scaleX:scale,scaleY:scale,offsetX:fixedRatio(dr,ps),offsetY:-fixedRatio(di,ps)*this.frame.w/Math.max(1,this.frame.h)}}
presentFrame(transform=this.presentTransform()){if(!this.frame)return;const d=this.device,pb=this.frame.presentParams;d.queue.writeBuffer(pb,0,new Float32Array([transform.scaleX,transform.scaleY,transform.offsetX,transform.offsetY]));const bg=d.createBindGroup({layout:this.present.getBindGroupLayout(0),entries:[{binding:0,resource:this.sampler},{binding:1,resource:this.frame.front.createView()},{binding:2,resource:{buffer:pb}}]}),e=d.createCommandEncoder(),pass=e.beginRenderPass({colorAttachments:[{view:this.context.getCurrentTexture().createView(),clearValue:{r:.0196,g:.0314,b:.0745,a:1},loadOp:'clear',storeOp:'store'}]});pass.setPipeline(this.present);pass.setBindGroup(0,bg);pass.draw(3);pass.end();d.queue.submit([e.finish()])}
async readMeta(indices){if(!this.frame||!indices.length)return new Uint32Array();const d=this.device,B=GPUBufferUsage,r=buf(d,indices.length*4,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();for(let i=0;i<indices.length;i++)e.copyBufferToBuffer(this.frame.meta,indices[i]*4,r,i*4,4);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const out=new Uint32Array(r.getMappedRange().slice(0));r.unmap();destroy(r);return out}
async readUnresolved(){if(!this.frame)return 0;const d=this.device,B=GPUBufferUsage,r=buf(d,16,B.COPY_DST|B.MAP_READ),e=d.createCommandEncoder();e.copyBufferToBuffer(this.frame.unresolved,0,r,0,16);d.queue.submit([e.finish()]);await r.mapAsync(GPUMapMode.READ);const value=new Uint32Array(r.getMappedRange().slice(0))[0];r.unmap();destroy(r);return value}
async renderTileMeta({snap,iter,deep,deepContext,fullW,fullH,tileX=0,tileY=0,w,h,sampleX=.5,sampleY=.5,forceStrict=false}){
await this.ready;if(deep)this.setDeepContext(deepContext);const d=this.device,B=GPUBufferUsage,n=w*h,meta=buf(d,n*4,B.STORAGE|B.COPY_SRC|B.COPY_DST),smooth=buf(d,n*4,B.STORAGE|B.COPY_DST),unresolved=buf(d,16,B.STORAGE|B.COPY_DST),pbuf=buf(d,64,B.UNIFORM|B.COPY_DST),encoder=d.createCommandEncoder({label:'numeric-probe'});d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}
const read=buf(d,n*4,B.COPY_DST|B.MAP_READ);encoder.copyBufferToBuffer(meta,0,read,0,n*4);d.queue.submit([encoder.finish()]);await read.mapAsync(GPUMapMode.READ);const out=new Uint32Array(read.getMappedRange().slice(0));read.unmap();[meta,smooth,unresolved,pbuf,read].forEach(destroy);return out;
}
async renderTileRGBA({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,sampleX=.5,sampleY=.5,edgeAA=false,forceStrict=false}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolved,pbuf=ws.pbufs[0],tex=ws.tex,encoder=d.createCommandEncoder();d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),p=encoder.beginComputePass();p.setPipeline(this.deep);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),p=encoder.beginComputePass();p.setPipeline(this.direct);p.setBindGroup(0,bg);p.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));p.end();}
const ca=this.colorParamsData(w,h),cd=new DataView(ca);cd.setUint32(12,edgeAA?1:0,true);d.queue.writeBuffer(ws.cbuf,0,ca);const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:tex.createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
}
async renderTileRGBA2x({snap,iter,deep,deepContext,fullW,fullH,tileX,tileY,w,h,forceStrict=false}){
await this.ready;if(w>512||h>512)throw new Error('export tile exceeds reusable workspace');if(deep)this.setDeepContext(deepContext);const d=this.device,ws=this.ensureExportWorkspace(),meta=ws.meta,smooth=ws.smooth,unresolved=ws.unresolved,encoder=d.createCommandEncoder({label:'export-aa2x'}),offsets=[[.25,.25],[.75,.25],[.25,.75],[.75,.75]],ca=this.colorParamsData(w,h);new DataView(ca).setUint32(12,0,true);d.queue.writeBuffer(ws.cbuf,0,ca);d.queue.writeBuffer(unresolved,0,new Uint32Array(4));
for(let si=0;si<4;si++){const [sampleX,sampleY]=offsets[si],pbuf=ws.pbufs[si];if(deep){d.queue.writeBuffer(pbuf,0,this.deepParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY,forceStrict?1:0));const bg=d.createBindGroup({layout:this.deep.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:this.deepCtx.refsB}},{binding:2,resource:{buffer:meta}},{binding:3,resource:{buffer:smooth}},{binding:4,resource:{buffer:unresolved}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.deep);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}else{d.queue.writeBuffer(pbuf,0,this.directParams(w,h,fullW,fullH,tileX,tileY,iter,snap,sampleX,sampleY));const bg=d.createBindGroup({layout:this.direct.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:pbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}}]}),pass=encoder.beginComputePass();pass.setPipeline(this.direct);pass.setBindGroup(0,bg);pass.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));pass.end();}const cbg=d.createBindGroup({layout:this.color.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:ws.cbuf}},{binding:1,resource:{buffer:meta}},{binding:2,resource:{buffer:smooth}},{binding:3,resource:ws.samples[si].createView()}]}),cp=encoder.beginComputePass();cp.setPipeline(this.color);cp.setBindGroup(0,cbg);cp.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));cp.end();}
const abg=d.createBindGroup({layout:this.aaResolve.getBindGroupLayout(0),entries:[{binding:0,resource:ws.samples[0].createView()},{binding:1,resource:ws.samples[1].createView()},{binding:2,resource:ws.samples[2].createView()},{binding:3,resource:ws.samples[3].createView()},{binding:4,resource:ws.tex.createView()}]}),ap=encoder.beginComputePass();ap.setPipeline(this.aaResolve);ap.setBindGroup(0,abg);ap.dispatchWorkgroups(Math.ceil(w/8),Math.ceil(h/8));ap.end();
const bpr=Math.ceil(w*4/256)*256,pixelBytes=bpr*h;encoder.copyTextureToBuffer({texture:ws.tex},{buffer:ws.read,bytesPerRow:bpr,rowsPerImage:h},{width:w,height:h});encoder.copyBufferToBuffer(unresolved,0,ws.read,pixelBytes,16);d.queue.submit([encoder.finish()]);await ws.read.mapAsync(GPUMapMode.READ,0,pixelBytes+16);const raw=new Uint8Array(ws.read.getMappedRange(0,pixelBytes+16)),out=new Uint8ClampedArray(w*h*4);for(let y=0;y<h;y++)out.set(raw.subarray(y*bpr,y*bpr+w*4),y*w*4);const unresolvedCount=new DataView(raw.buffer,raw.byteOffset+pixelBytes,16).getUint32(0,true);ws.read.unmap();return{rgba:out,unresolved:unresolvedCount};
}
destroy(){this.frameDestroy();this.exportWorkspaceDestroy();this.destroyDeepContext()}
}
// ── GPU startup / rendering orchestration ────────────────────────────────
async function initRenderer(){if(renderer)return renderer;if(rendererInitPromise)return rendererInitPromise;if(state.gpuInitFailed)return null;if(!navigator.gpu){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU非対応';ensureFallback();return null}rendererInitPromise=(async()=>{try{let adapter=await navigator.gpu.requestAdapter({powerPreference:'high-performance'});if(!adapter)adapter=await navigator.gpu.requestAdapter();if(!adapter){state.gpuInitFailed=false;state.gpuUnavailable=true;state.gpuError='WebGPU adapterがありません';ensureFallback();return null}const device=await adapter.requestDevice();const r=new WebGpuRenderer(adapter,device);await r.ready;renderer=r;state.gpuInitFailed=false;state.gpuUnavailable=false;state.gpuError='';resize();markDirty(false);return r}catch(e){state.gpuInitFailed=true;state.gpuError='WebGPU初期化失敗: '+String(e&&e.message||e);updateStats();return null}finally{rendererInitPromise=null}})();return rendererInitPromise}
function ensureFallback(){if(fallbackCtx)return fallbackCtx;if(webgpuCanvasClaimed)return null;try{fallbackCtx=canvas.getContext('2d',{alpha:false})}catch{}return fallbackCtx}
function cancelRender(){state.token++;state.rendering=false;state.recolorPending=false;refs.cancelPending('render cancelled')}
function markDirty(cancel=true){if(cancel)cancelRender();state.dirty=true;state.lastInteraction=performance.now();state.drawState=state.frameView?'REPROJECTED':'PREVIEW';schedule()}
function schedule(){if(!raf)raf=requestAnimationFrame(loop)}
async function renderFrame(){const token=++state.token,snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,canvas.width),t0=performance.now();state.rendering=true;state.dirty=false;state.drawState='COVERING';runtime.renderStarts++;updateStats();try{const r=renderer||await initRenderer();if(token!==state.token)return;if(!r){if(state.gpuInitFailed){state.rendering=false;state.drawState='ERROR';state.lastEngine='WebGPU shader/pipeline error';updateStats();return}renderFallback(token,snap,iter);return}let ctx=null;if(deep){state.lastEngine='WebGPU · reference準備';updateStats();ctx=await refs.request(snap,iter);if(token!==state.token)return;if(ctx.checkpointMismatch)throw new Error('reference guard checkpoint mismatch');state.lastEngine='WebGPU · guarded rescaled perturbation'}else state.lastEngine='WebGPU · f32 direct';const ok=await r.computeFrame(snap,iter,deep,ctx,token,state.processMode==='validate');if(!ok)return;state.frameView=snap;state.fieldView={...snap,iter,w:canvas.width,h:canvas.height,deep};state.drawState=state.hq?'REFINED':'COVERED';state.lastRender=performance.now()-t0;state.rendering=false;state.unresolved=0;const pendingColor=state.recolorPending;if(pendingColor){state.recolorPending=false;recolor()}else r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});updateStats();if(deep)r.readUnresolved().then(q=>{if(token===state.token){state.unresolved=q||0;updateStats()}}).catch(()=>{})}catch(e){if(token!==state.token)return;state.rendering=false;state.gpuError=String(e&&e.message||e);state.lastEngine='WebGPU error';updateStats();console.error(e)}}
function renderFallback(token,snap,iter){const ctx=ensureFallback();if(!ctx){state.rendering=false;return}const w=canvas.width,h=canvas.height;if(deepNeeded(snap,w)){state.rendering=false;state.gpuError='このズーム深度はWebGPUが必要です';state.lastEngine='Fallback · deep unsupported';updateStats();return}const img=ctx.createImageData(w,h),out=img.data,cre=fixedNum(snap.re,snap.bits),cim=fixedNum(snap.im,snap.bits),sp=fixedNum(snap.span,snap.bits),scale=sp/w;let y=0;function slice(){if(token!==state.token)return;const end=performance.now()+8;while(y<h&&performance.now()<end){for(let x=0;x<w;x++){const cr=cre+(x+.5-w*.5)*scale,ci=cim+(h*.5-y-.5)*scale;let zr=0,zi=0,n=0,mag=0;while(n<iter&&mag<=4){const zr2=zr*zr,zi2=zi*zi;zi=2*zr*zi+ci;zr=zr2-zi2+cr;mag=zr*zr+zi*zi;n++}const o=(y*w+x)*4;if(n>=iter){out[o]=out[o+1]=out[o+2]=0}else{const t=(n+1-Math.log2(.5*Math.log2(Math.max(4.0001,mag))))*.008+state.shift;out[o]=255*(.3+.7*(.5+.5*Math.cos(6.28318*t)));out[o+1]=255*(.25+.75*(.5+.5*Math.cos(6.28318*(t+.33))));out[o+2]=255*(.2+.8*(.5+.5*Math.cos(6.28318*(t+.67))))}out[o+3]=255}y++}if(y<h)requestAnimationFrame(slice);else{ctx.putImageData(img,0,0);state.frameView=snap;state.rendering=false;state.lastRender=0;state.lastEngine='JavaScript f64 fallback深部非対応';state.drawState='COVERED';updateStats()}}requestAnimationFrame(slice)}
async function recolor(){state.recolorPending=true;if(!renderer||!state.fieldView||state.rendering||state.recoloring)return false;state.recoloring=true;let painted=false;try{while(state.recolorPending&&!state.rendering&&renderer&&state.fieldView){state.recolorPending=false;const token=state.token,ok=await renderer.recolor(token);if(!ok||token!==state.token)continue;renderer.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});painted=true;updateStats()}return painted}catch(e){console.error(e);return false}finally{state.recoloring=false;if(state.recolorPending&&!state.rendering)queueMicrotask(recolor)}}
function loop(){raf=0;if(state.pointerActive||state.wheelActive){if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform());updateStats();return}if(state.dirty&&!state.rendering)renderFrame();else if(renderer&&state.frameView)renderer.presentFrame(renderer.presentTransform())}
// ── interaction / view history ──────────────────────────────────────────
function viewRect(){return canvas.getBoundingClientRect()}
function updateFocus(x,y){const r=viewRect();state.focusX=Math.max(0,Math.min(1,(x-r.left)/Math.max(1,r.width)));state.focusY=Math.max(0,Math.min(1,(y-r.top)/Math.max(1,r.height)))}
function zoomAt(x,y,factor){const r=viewRect(),fx=(x-r.left)/Math.max(1,r.width)-.5,fy=(y-r.top)/Math.max(1,r.height)-.5;factor=Math.max(.01,Math.min(100,factor));const old=state.span,neu=mulRatio(old,factor),dx=BigInt(Math.round(fx*1e9)),dy=BigInt(Math.round(fy*1e9));state.re+=(old-neu)*dx/1000000000n;const oldY=old*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width)),newY=neu*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im-=(oldY-newY)*dy/1000000000n;state.span=neu;ensurePrecision();state.dirty=true;schedule()}
function pan(dx,dy){const w=Math.max(1,canvas.clientWidth),h=Math.max(1,canvas.clientHeight);state.re-=state.span*BigInt(Math.round(dx*1e6))/BigInt(Math.round(w*1e6));const ys=state.span*BigInt(canvas.height)/BigInt(Math.max(1,canvas.width));state.im+=ys*BigInt(Math.round(dy*1e6))/BigInt(Math.round(h*1e6));ensurePrecision();state.dirty=true;schedule()}
function reset(){state.bits=INITIAL_BITS;state.re=-fromFrac(1n,2n);state.im=0n;state.span=fromFrac(34n,10n);ensurePrecision();markDirty();saveHash(false)}
const pts=new Map();let lx=0,ly=0,pinch=0;
canvas.addEventListener('wheel',e=>{e.preventDefault();updateFocus(e.clientX,e.clientY);if(!state.wheelActive){cancelRender();state.wheelActive=true}zoomAt(e.clientX,e.clientY,Math.exp(e.deltaY*.00125));clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.wheelActive=false;recordView();saveHash(false);markDirty()},110)},{passive:false});
canvas.addEventListener('pointerdown',e=>{updateFocus(e.clientX,e.clientY);try{canvas.setPointerCapture(e.pointerId)}catch{};if(!pts.size){cancelRender();state.pointerActive=true}pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){lx=e.clientX;ly=e.clientY}else{const a=[...pts.values()];pinch=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1])}});
canvas.addEventListener('pointermove',e=>{if(!pts.has(e.pointerId))return;updateFocus(e.clientX,e.clientY);pts.set(e.pointerId,[e.clientX,e.clientY]);if(pts.size===1){const dx=e.clientX-lx,dy=e.clientY-ly;pan(dx,dy);lx=e.clientX;ly=e.clientY}else if(pts.size===2){const a=[...pts.values()],d=Math.hypot(a[0][0]-a[1][0],a[0][1]-a[1][1]);if(pinch>0&&d>0)zoomAt((a[0][0]+a[1][0])/2,(a[0][1]+a[1][1])/2,pinch/d);pinch=d}});
function endPointer(e){pts.delete(e.pointerId);pinch=0;if(pts.size)return;clearTimeout(settleTimer);settleTimer=setTimeout(()=>{state.pointerActive=false;recordView();saveHash(false);markDirty()},90)}canvas.addEventListener('pointerup',endPointer);canvas.addEventListener('pointercancel',endPointer);
// ── URL / controls ───────────────────────────────────────────────────────
function saveHash(push){const p=new URLSearchParams();p.set('v',String(VERSION));p.set('b',String(state.bits));p.set('re',state.re.toString());p.set('im',state.im.toString());p.set('sp',state.span.toString());p.set('pal',String(state.palette));p.set('cy',String(state.cycle));p.set('sh',String(state.shift));p.set('it',String(state.baseIter));p.set('ad',state.adaptive?'1':'0');const h='#'+p.toString();lastWrittenHash=h;try{push?history.pushState(null,'',h):history.replaceState(null,'',h)}catch{location.hash=h}}
function loadHash(){const p=new URLSearchParams(location.hash.slice(1));if(!p.has('b'))return false;try{const b=Number(p.get('b')),re=BigInt(p.get('re')),im=BigInt(p.get('im')),sp=BigInt(p.get('sp'));if(!Number.isInteger(b)||b<64||sp<=0n)return false;state.bits=b;state.re=re;state.im=im;state.span=sp;if(p.has('pal'))state.palette=Math.max(0,Math.min(2,Number(p.get('pal'))|0));if(p.has('cy'))state.cycle=Math.max(.001,Math.min(.05,Number(p.get('cy'))||.008));if(p.has('sh'))state.shift=Math.max(0,Math.min(1,Number(p.get('sh'))||0));if(p.has('it'))state.baseIter=Math.max(100,Math.min(2500,Number(p.get('it'))||350));if(p.has('ad'))state.adaptive=p.get('ad')!=='0';ensurePrecision();return true}catch{return false}}
function syncCoordinateInputs(){$('#coordReInput').value=fmtFixedExact(state.re);$('#coordImInput').value=fmtFixedExact(state.im);$('#coordSpanInput').value=fmtFixedExact(state.span)}
function syncHistoryButtons(){$('#undoView').disabled=viewHistoryIndex<=0;$('#redoView').disabled=viewHistoryIndex<0||viewHistoryIndex>=viewHistory.length-1}
function syncControls(){$('#processMode').value=state.processMode;$('#palette').value=String(state.palette);$('#cycle').value=String(state.cycle);$('#cycleO').textContent=state.cycle.toFixed(4);$('#shift').value=String(state.shift);$('#shiftO').textContent=state.shift.toFixed(2);$('#iters').value=String(state.baseIter);$('#itersO').textContent=String(state.baseIter);$('#adaptive').checked=state.adaptive;$('#hq').checked=state.hq;syncCoordinateInputs();syncHistoryButtons()}
function toast(s){const e=$('#toast');e.textContent=s;e.classList.add('show');setTimeout(()=>e.classList.remove('show'),1500)}
function applyUi(){document.body.classList.toggle('ui-hidden',state.uiHidden);$('#uiToggle').textContent=state.uiHidden?'UI':'UI';$('#uiToggle').setAttribute('aria-expanded',state.uiHidden?'false':'true')}
$('#uiToggle').onclick=()=>{state.uiHidden=!state.uiHidden;try{localStorage.setItem('mandelbrot.uiHidden',state.uiHidden?'1':'0')}catch{}applyUi()};
$('#zin').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,.5);recordView();saveHash(false);markDirty()};$('#zout').onclick=()=>{const r=viewRect();zoomAt(r.left+r.width/2,r.top+r.height/2,2);recordView();saveHash(false);markDirty()};$('#reset').onclick=()=>{reset();recordView();syncControls()};
$('#share').onclick=async()=>{saveHash(true);try{await navigator.clipboard.writeText(location.href);toast('共有URLをコピーしました')}catch{toast('URLを更新しました')}};
$('#coordApply').onclick=()=>{try{const values=[$('#coordReInput').value,$('#coordImInput').value,$('#coordSpanInput').value],required=Math.max(...values.map(decimalRequiredBits));if(required>state.bits)promoteState(Math.ceil((required-state.bits)/64)*64);const re=fromDec(values[0]),im=fromDec(values[1]),span=fromDec(values[2]);if(span<=0n)throw new Error('表示幅は正数にしてください');state.re=re;state.im=im;state.span=span;ensurePrecision();recordView();saveHash(false);markDirty()}catch(e){toast('座標を適用できません: '+String(e&&e.message||e))}};
$('#coordCopy').onclick=async()=>{const value=JSON.stringify({rendererVersion:VERSION,bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),decimal:{re:fmtFixedExact(state.re),im:fmtFixedExact(state.im),span:fmtFixedExact(state.span)}});try{await navigator.clipboard.writeText(value);toast('正確な座標をコピーしました')}catch{toast('コピーできませんでした')}};
$('#undoView').onclick=()=>{if(viewHistoryIndex>0){viewHistoryIndex--;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};$('#redoView').onclick=()=>{if(viewHistoryIndex<viewHistory.length-1){viewHistoryIndex++;restoreView(viewHistory[viewHistoryIndex]);syncHistoryButtons()}};
$('#palette').onchange=e=>{state.palette=Math.max(0,Math.min(2,Number(e.target.value)|0));recolor()};$('#cycle').oninput=e=>{state.cycle=Number(e.target.value);$('#cycleO').textContent=state.cycle.toFixed(4);recolor()};$('#shift').oninput=e=>{state.shift=Number(e.target.value);$('#shiftO').textContent=state.shift.toFixed(2);recolor()};
$('#iters').oninput=e=>{state.baseIter=Number(e.target.value);$('#itersO').textContent=String(state.baseIter);markDirty()};$('#adaptive').onchange=e=>{state.adaptive=e.target.checked;markDirty()};$('#hq').onchange=e=>{state.hq=e.target.checked;recolor()};
$('#processMode').onchange=e=>{state.processMode=/^(power|standard|fine|validate)$/.test(e.target.value)?e.target.value:'standard';state.hq=state.processMode==='fine'||state.processMode==='validate';$('#hq').checked=state.hq;resize();markDirty();try{localStorage.setItem('mandelbrot.processMode',state.processMode)}catch{}};
addEventListener('resize',()=>{resize();markDirty()});addEventListener('keydown',e=>{if(/^(INPUT|SELECT|TEXTAREA|BUTTON)$/.test(e.target.tagName))return;let ok=true;if(e.key==='h'||e.key==='H')$('#uiToggle').click();else if(e.key==='r'||e.key==='R')$('#reset').click();else if(e.key==='+'||e.key==='='||e.key==='Enter'&&!e.shiftKey)$('#zin').click();else if(e.key==='-'||e.key==='Enter'&&e.shiftKey)$('#zout').click();else if(e.key==='ArrowLeft')pan(innerWidth*.08,0);else if(e.key==='ArrowRight')pan(-innerWidth*.08,0);else if(e.key==='ArrowUp')pan(0,innerHeight*.08);else if(e.key==='ArrowDown')pan(0,-innerHeight*.08);else ok=false;if(ok){e.preventDefault();recordView();saveHash(false);markDirty()}});
addEventListener('hashchange',()=>{if(location.hash===lastWrittenHash){lastWrittenHash='';return}if(location.hash===navigationHash)return;navigationHash=location.hash;setTimeout(()=>navigationHash='',0);if(loadHash()){syncControls();recordView();markDirty()}});
// ── export: GPU tiled + streaming PNG, optional GPU 2x2 supersampling ──────
const exportJob={active:false,cancelled:false};
function downloadBlob(blob,name){const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),1000)}
const CRC_TABLE=(()=>{const t=new Uint32Array(256);for(let n=0;n<256;n++){let c=n;for(let k=0;k<8;k++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;t[n]=c>>>0}return t})();
function crc32Parts(parts){let c=0xffffffff;for(const part of parts)for(const b of part)c=CRC_TABLE[(c^b)&255]^(c>>>8);return(c^0xffffffff)>>>0}
function pngChunk(type,data=new Uint8Array()){const tb=new TextEncoder().encode(type),out=new Uint8Array(12+data.length),dv=new DataView(out.buffer);dv.setUint32(0,data.length,false);out.set(tb,4);out.set(data,8);dv.setUint32(8+data.length,crc32Parts([tb,data]),false);return out}
class StreamingPng{
constructor(w,h){if(typeof CompressionStream==='undefined')throw new Error('このブラウザはストリーミングPNG出力に必要なCompressionStreamへ対応していません');this.w=w;this.h=h;this.cs=new CompressionStream('deflate');this.writer=this.cs.writable.getWriter();this.compressed=(async()=>{const r=this.cs.readable.getReader(),chunks=[];for(;;){const q=await r.read();if(q.done)break;chunks.push(q.value)}return chunks})()}
async rows(filteredRows){await this.writer.write(filteredRows)}
async finish(){await this.writer.close();const chunks=await this.compressed,ihdr=new Uint8Array(13),dv=new DataView(ihdr.buffer);dv.setUint32(0,this.w,false);dv.setUint32(4,this.h,false);ihdr[8]=8;ihdr[9]=6;const parts=[new Uint8Array([137,80,78,71,13,10,26,10]),pngChunk('IHDR',ihdr)];for(const c of chunks)parts.push(pngChunk('IDAT',c));parts.push(pngChunk('IEND'));return new Blob(parts,{type:'image/png'})}
async abort(reason){try{await this.writer.abort(reason)}catch{}try{await this.compressed}catch{}}
}
function exportDimensions(){const scale=Number($('#exportScale').value),aspect=canvas.height/Math.max(1,canvas.width),requested=Math.max(64,Math.round(scale?canvas.width*scale:Number($('#exportWidth').value)||canvas.width));let w=Math.min(16384,requested),h=Math.max(1,Math.round(w*aspect));if(h>16384){h=16384;w=Math.max(64,Math.round(h/Math.max(1e-12,aspect)))}return{w:Math.min(16384,w),h:Math.min(16384,h)}}
async function runExport(){
if(exportJob.active)return;
const r=renderer||await initRenderer();if(!r){$('#exportStatus').textContent='WebGPUが必要です。';return}
const{w,h}=exportDimensions(),ss=Math.max(1,Math.min(2,Number($('#exportAA').value)||1)),snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w),strict=$('#exportPrecision').value==='strict';
let ctx=null;
if(deep){$('#exportStatus').textContent='高精度参照軌道を準備中…';ctx=await refs.request(snap,iter);if(ctx.checkpointMismatch){$('#exportStatus').textContent='参照軌道検証に失敗しました。';return}}
const tile=512,totalTiles=Math.ceil(w/tile)*Math.ceil(h/tile),png=new StreamingPng(w,h),sampleCount=ss===2?4:1;
exportJob.active=true;exportJob.cancelled=false;$('#exportProgress').hidden=false;$('#exportProgress').value=0;$('#exportStart').disabled=true;
let done=0,unresolvedSamples=0;
try{
for(let y=0;y<h;y+=tile){
const th=Math.min(tile,h-y),rowStride=1+w*4,band=new Uint8Array(rowStride*th);
for(let x=0;x<w;x+=tile){
if(exportJob.cancelled)throw new Error('cancelled');const tw=Math.min(tile,w-x);
const result=ss===1?await r.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}):await r.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:x,tileY:y,w:tw,h:th,forceStrict:strict});
const data=result.rgba;unresolvedSamples+=result.unresolved||0;
for(let row=0;row<th;row++)band.set(data.subarray(row*tw*4,(row+1)*tw*4),row*rowStride+1+x*4);
done++;$('#exportProgress').value=done/totalTiles;$('#exportStatus').textContent='GPUタイル生成 '+Math.round(100*done/totalTiles)+'%'+(unresolvedSamples?' · 未確定sample '+unresolvedSamples:'');
}
if(exportJob.cancelled)throw new Error('cancelled');await png.rows(band);await new Promise(requestAnimationFrame);
}
if(exportJob.cancelled)throw new Error('cancelled');$('#exportStatus').textContent='PNGストリームを確定中…';
const blob=await png.finish(),stamp=Date.now(),base='mandelbrot-'+stamp,meta={format:'mandelbrot-view-v24',rendererVersion:VERSION,backend:'webgpu',numericEngine:deep?'bigint-reference + guarded-rescaled-f32-perturbation':'f32-direct',membershipCertified:false,precisionPolicy:strict?'strict-gpu':'balanced-gpu',pixelContract:'centered',width:w,height:h,supersampling:ss,numericSamples:w*h*sampleCount,unresolvedSamples,exportPipeline:ss===2?'gpu-4sample-resolve + single-readback-per-tile + streaming-png':'gpu-tile + streaming-png',iterationPolicy:{adaptive:state.adaptive,base:state.baseIter,effective:iter},view:{bits:snap.bits,re:snap.re.toString(),im:snap.im.toString(),span:snap.span.toString()},palette:{id:state.palette,cycle:state.cycle,shift:state.shift},reference:ctx?{precisionBits:ctx.precisionBits,checkpointCount:ctx.checkpointCount,checkpointMismatch:ctx.checkpointMismatch,blaEnabled:false}:null,shaderVersion:G.version};
downloadBlob(blob,base+'.png');downloadBlob(new Blob([JSON.stringify(meta,null,2)],{type:'application/json'}),base+'.json');runtime.exports++;
$('#exportStatus').textContent=unresolvedSamples?'保存しました · 未確定sample '+unresolvedSamples+'sidecar参照':'PNGと座標メタデータを保存しました。';
}catch(e){await png.abort(e);$('#exportStatus').textContent=String(e.message)==='cancelled'?'出力を中止しました。':'出力失敗: '+String(e&&e.message||e)}
finally{exportJob.active=false;$('#exportStart').disabled=false}
}
$('#png').onclick=()=>{const d=$('#exportDialog');$('#exportWidth').value=String(canvas.width);$('#exportScale').value='1';$('#exportProgress').hidden=true;$('#exportStatus').textContent='';d.showModal?d.showModal():d.setAttribute('open','')};$('#exportScale').onchange=e=>{const s=Number(e.target.value);if(s)$('#exportWidth').value=String(exportDimensions().w)};$('#exportStart').onclick=runExport;$('#exportCancel').onclick=()=>{if(exportJob.active){exportJob.cancelled=true;$('#exportStatus').textContent='中止しています…'}else $('#exportDialog').close()};$('#exportQuick').onclick=()=>canvas.toBlob(blob=>{if(blob)downloadBlob(blob,'mandelbrot-'+Date.now()+'.png')},'image/png');
// ── diagnostics ──────────────────────────────────────────────────────────
function updateStats(){const z=zoomExp(),digits=Math.max(8,Math.min(80,Math.ceil(z)+8));$('#coord').textContent=fmtFixed(state.re,digits)+' '+(state.im<0n?'':'+')+' '+fmtFixed(state.im<0n?-state.im:state.im,digits)+'i';$('#zoom').textContent=z<4?Math.pow(10,z).toFixed(1)+'×':'≈ 10^'+z.toFixed(2);$('#span').textContent=fmtSpan();$('#engine').textContent=renderer?(deepNeeded()?'WebGPU 深部':'WebGPU 標準'):(state.gpuInitFailed?'WebGPU エラー':state.gpuError?'Fallback':'起動中');$('#render').textContent=state.rendering?'描画中…':state.lastRender?state.lastRender.toFixed(0)+' ms':'準備完了';let status=state.drawState==='ERROR'?'描画停止':state.drawState==='REPROJECTED'?'再投影':state.drawState==='COVERING'?'GPU描画中':state.drawState==='REFINED'?'GPU境界平滑化':state.drawState==='COVERED'?'全域描画 完了':'準備中';if(state.unresolved)status+=' · 未確定 '+state.unresolved;if(state.gpuError){const ge=state.gpuError.length>120?state.gpuError.slice(0,117)+'…':state.gpuError;status+=' · '+ge;}$('#badge').textContent=status;$('#compactStatus').textContent=status;const d=renderer&&renderer.deepCtx;$('#diagEngine').textContent='engine: '+state.lastEngine+' | WebGPU '+(renderer?'ready':'unavailable')+' | shader '+G.version;$('#diagNumeric').textContent='numeric: view '+state.bits+' bit | iter '+maxIter()+(d?' | ref '+d.precisionBits+' bit':'');const frameBytes=renderer&&renderer.frame?renderer.frame.n*16:0,deepBytes=d?d.refs.byteLength:0;$('#diagMemory').textContent='GPU managed est: '+((frameBytes+deepBytes)/1048576).toFixed(1)+' MiB | canvas '+canvas.width+'×'+canvas.height}
globalThis.__MANDEL_TEST__={
async setView({re,im,span,bits,baseIter=350,adaptive=false,processMode='standard'}){cancelRender();if(bits){state.bits=bits}else{state.bits=Math.max(256,decimalRequiredBits(re),decimalRequiredBits(im),decimalRequiredBits(span))}state.re=fromDec(re);state.im=fromDec(im);state.span=fromDec(span);state.baseIter=baseIter;state.adaptive=adaptive;state.processMode=processMode;state.hq=false;ensurePrecision();resize();markDirty(false);const start=performance.now();while((state.dirty||state.rendering)&&performance.now()-start<120000){schedule();await new Promise(r=>setTimeout(r,20))}if(state.dirty||state.rendering)throw new Error('test render timeout');return{width:canvas.width,height:canvas.height,diag:globalThis.__MANDEL_DIAG__.snapshot()}},
async sampleMeta(points){if(!renderer||!renderer.frame)throw new Error('GPU field unavailable');const idx=points.map(([x,y])=>y*renderer.frame.w+x);const m=await renderer.readMeta(idx);return Array.from(m)},
state:()=>({bits:state.bits,re:state.re.toString(),im:state.im.toString(),span:state.span.toString(),width:canvas.width,height:canvas.height,iter:maxIter()}),
async probeMeta({w,h,strict=true}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);let ctx=null;if(deep)ctx=await refs.request(snap,iter);return Array.from(await renderer.renderTileMeta({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,w,h,forceStrict:strict}))},
async smokeExportTile({w=48,h=32,strict=true,ss=1}={}){if(!renderer)throw new Error('WebGPU renderer unavailable');const snap=snapshot(),iter=maxIter(),deep=deepNeeded(snap,w);let ctx=null;if(deep)ctx=await refs.request(snap,iter);const result=ss===2?await renderer.renderTileRGBA2x({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,forceStrict:strict}):await renderer.renderTileRGBA({snap,iter,deep,deepContext:ctx,fullW:w,fullH:h,tileX:0,tileY:0,w,h,sampleX:.5,sampleY:.5,edgeAA:false,forceStrict:strict}),data=result.rgba;let checksum=2166136261>>>0;for(const v of data){checksum^=v;checksum=Math.imul(checksum,16777619)>>>0}return{length:data.length,expected:w*h*4,checksum,deep,strict,ss,unresolved:result.unresolved||0}}
};
globalThis.__MANDEL_DIAG__={snapshot:()=>({rendererVersion:VERSION,backend:renderer?'webgpu':'fallback',shaderVersion:G.version,webgpuError:state.gpuError,pixelContract:'centered',drawState:state.drawState,rendering:state.rendering,zoom:zoomExp(),deep:deepNeeded(),bits:state.bits,iteration:maxIter(),unresolved:state.unresolved,screen:{width:canvas.width,height:canvas.height,effectiveDpr:state.effectiveDpr,pixelBudget:state.screenPixelBudget},reference:renderer&&renderer.deepCtx?{key:renderer.deepCtx.key,precisionBits:renderer.deepCtx.precisionBits,refLen:renderer.deepCtx.refLen,checkpointMismatch:renderer.deepCtx.checkpointMismatch,checkpointCount:renderer.deepCtx.checkpointCount,blaEnabled:false}:null,runtime:{...runtime},adapter:renderer?renderer.adapterInfo:null,limits:renderer?renderer.adapterLimits:null,compilation:renderer?renderer.compilation:null,uncapturedErrors:renderer?renderer.uncapturedErrors.slice():[]})};
// ── boot / teardown ──────────────────────────────────────────────────────
addEventListener('visibilitychange',()=>{if(document.hidden){cancelRender();exportJob.cancelled=true}else markDirty(false)});addEventListener('pagehide',()=>{cancelRender();refs.destroy();if(renderer)renderer.destroy()},{once:true});
try{state.uiHidden=localStorage.getItem('mandelbrot.uiHidden')==='1';const m=localStorage.getItem('mandelbrot.processMode');if(/^(power|standard|fine|validate)$/.test(m)){state.processMode=m;state.hq=m==='fine'||m==='validate'}}catch{}applyUi();resize();if(!loadHash())reset();recordView();syncControls();updateStats();initRenderer().then(()=>{resize();markDirty(false)});schedule();
})();

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

43
scripts/build-hosted.ps1 Normal file
View file

@ -0,0 +1,43 @@
param([string]$OutputDirectory = (Join-Path $PSScriptRoot '..\dist\hosted'))
$ErrorActionPreference = 'Stop'
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
$output = [System.IO.Path]::GetFullPath($OutputDirectory)
if (-not $output.StartsWith($workspace, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "OutputDirectory must stay inside the workspace: $output"
}
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'extract-wasm.ps1') | Out-Null
if ($LASTEXITCODE -ne 0) { throw "WASM extraction failed with exit code $LASTEXITCODE." }
New-Item -ItemType Directory -Path $output -Force | Out-Null
$manifestPath = Join-Path $workspace 'dist\wasm\manifest.json'
$manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
$html = Get-Content -LiteralPath (Join-Path $workspace 'index.html') -Raw -Encoding UTF8
$html = [regex]::Replace($html, '<script src="kernels\.js"></script>\s*<script src="script\.js"></script>', '<script type="module" src="./hosted-loader.js"></script>')
Set-Content -LiteralPath (Join-Path $output 'index.html') -Value $html -Encoding UTF8
Copy-Item -LiteralPath (Join-Path $workspace 'script.js') -Destination (Join-Path $output 'script.js') -Force
$loader = Get-Content -LiteralPath (Join-Path $workspace 'hosted-loader.js') -Raw -Encoding UTF8
$kernelMeta = [ordered]@{}
foreach ($payload in $manifest.payloads) { $kernelMeta[$payload.symbol] = $payload.sha256 }
$loader = $loader.Replace('/*__KERNEL_META__*/{}', ($kernelMeta | ConvertTo-Json -Compress))
$expectedHashed = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($payload in $manifest.payloads) {
$extension = [System.IO.Path]::GetExtension($payload.file)
$stem = [System.IO.Path]::GetFileNameWithoutExtension($payload.file)
$hashed = "$stem.$($payload.sha256.Substring(0,16))$extension"
[void]$expectedHashed.Add($hashed)
Copy-Item -LiteralPath (Join-Path (Split-Path $manifestPath) $payload.file) -Destination (Join-Path (Split-Path $manifestPath) $hashed) -Force
$loader = $loader.Replace($payload.file, $hashed)
}
$wasmDirectory = (Resolve-Path -LiteralPath (Split-Path $manifestPath)).Path
if (-not $wasmDirectory.StartsWith($workspace + [System.IO.Path]::DirectorySeparatorChar, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "Refusing to prune hashed assets outside the workspace: $wasmDirectory"
}
foreach ($asset in Get-ChildItem -LiteralPath $wasmDirectory -File) {
if ($asset.Name -match '^[a-z0-9-]+\.[0-9a-f]{16}\.wasm$' -and -not $expectedHashed.Contains($asset.Name)) {
Remove-Item -LiteralPath $asset.FullName -Force
}
}
[System.IO.File]::WriteAllText((Join-Path $output 'hosted-loader.js'), $loader, [System.Text.UTF8Encoding]::new($false))
Copy-Item -LiteralPath (Join-Path $workspace 'hosted-headers.txt') -Destination (Join-Path (Split-Path $output) '_headers') -Force
Write-Output $output

50
scripts/build-kernels.ps1 Normal file
View file

@ -0,0 +1,50 @@
param(
[string]$ManifestPath = (Join-Path $PSScriptRoot '..\dist\wasm\manifest.json'),
[string]$OutputPath = (Join-Path $PSScriptRoot '..\kernels.js')
)
$ErrorActionPreference = 'Stop'
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
$output = [System.IO.Path]::GetFullPath($OutputPath)
if (-not $output.StartsWith($workspace, [System.StringComparison]::OrdinalIgnoreCase)) { throw 'OutputPath must stay inside the workspace.' }
$manifest = Get-Content -LiteralPath $ManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
$values = @{}
foreach ($payload in $manifest.payloads) {
$path = Join-Path (Split-Path $ManifestPath) $payload.file
$values[$payload.symbol] = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($path))
}
foreach ($required in @('WASM_SIMD_B64','WASM_SCALAR_B64','DEEP_SIMD_B64','DEEP_SCALAR_B64','BLA_SIMD_B64','BLA_SCALAR_B64','COLOR_SIMD_B64','COLOR_SCALAR_B64')) {
if (-not $values.ContainsKey($required)) { throw "Missing payload: $required" }
}
$colorDeduplicated = $values.COLOR_SIMD_B64 -eq $values.COLOR_SCALAR_B64
$lines = @(
"'use strict';"
'// Generated from dist/wasm/manifest.json. Run scripts/extract-wasm.ps1 before replacing compatibility inputs.'
)
if ($colorDeduplicated) { $lines += "const COLOR_B64='$($values.COLOR_SIMD_B64)';" }
$lines += @(
'globalThis.MANDEL_KERNELS=Object.freeze({'
" WASM_SIMD_B64:'$($values.WASM_SIMD_B64)',"
" WASM_SCALAR_B64:'$($values.WASM_SCALAR_B64)',"
" DEEP_SIMD_B64:'$($values.DEEP_SIMD_B64)',"
" DEEP_SCALAR_B64:'$($values.DEEP_SCALAR_B64)',"
" BLA_SIMD_B64:'$($values.BLA_SIMD_B64)',"
" BLA_SCALAR_B64:'$($values.BLA_SCALAR_B64)',"
)
if ($colorDeduplicated) {
$lines += @(' COLOR_SIMD_B64:COLOR_B64,', ' COLOR_SCALAR_B64:COLOR_B64')
} else {
$lines += @(" COLOR_SIMD_B64:'$($values.COLOR_SIMD_B64)',", " COLOR_SCALAR_B64:'$($values.COLOR_SCALAR_B64)'")
}
$lines += @(
'});'
'globalThis.MANDEL_KERNEL_META=Object.freeze({'
)
for ($index = 0; $index -lt $manifest.payloads.Count; $index++) {
$payload = $manifest.payloads[$index]
$comma = if ($index -lt $manifest.payloads.Count - 1) { ',' } else { '' }
$lines += " '$($payload.symbol)':'$($payload.sha256)'$comma"
}
$lines += '});'
[System.IO.File]::WriteAllText($output, ($lines -join [Environment]::NewLine) + [Environment]::NewLine, [System.Text.UTF8Encoding]::new($false))
Write-Output $output

View file

@ -0,0 +1,13 @@
param([string]$OutputDirectory = (Join-Path $PSScriptRoot '..\dist\standalone'))
$ErrorActionPreference = 'Stop'
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
$output = [System.IO.Path]::GetFullPath($OutputDirectory)
if (-not $output.StartsWith($workspace, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "OutputDirectory must stay inside the workspace: $output"
}
New-Item -ItemType Directory -Path $output -Force | Out-Null
foreach ($name in @('index.html','script.js','kernels.js')) {
Copy-Item -LiteralPath (Join-Path $workspace $name) -Destination (Join-Path $output $name) -Force
}
Write-Output $output

64
scripts/build-wasm.ps1 Normal file
View file

@ -0,0 +1,64 @@
param(
[string]$Compiler = 'clang',
[string]$OutputDirectory = (Join-Path $PSScriptRoot '..\build\wasm-v23'),
[switch]$Promote
)
$ErrorActionPreference = 'Stop'
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
$output = [System.IO.Path]::GetFullPath($OutputDirectory)
if (-not $output.StartsWith($workspace, [System.StringComparison]::OrdinalIgnoreCase)) { throw 'OutputDirectory must stay inside the workspace.' }
$lock = Get-Content -LiteralPath (Join-Path $workspace 'toolchain.lock.json') -Raw -Encoding UTF8 | ConvertFrom-Json
$versionLine = (& $Compiler --version | Select-Object -First 1)
if ($versionLine -notmatch [regex]::Escape($lock.version)) { throw "Expected clang $($lock.version), got: $versionLine" }
New-Item -ItemType Directory -Path $output -Force | Out-Null
$common = @('--target=wasm32-unknown-unknown') + @($lock.commonFlags)
$variants = @(
[pscustomobject]@{ name='simd'; flags=@($lock.simdFlags) },
[pscustomobject]@{ name='scalar'; flags=@($lock.scalarFlags) }
)
$modules = @(
[pscustomobject]@{ symbol='WASM'; name='wasm'; source='shallow_kernel.c'; memory=2097152 },
[pscustomobject]@{ symbol='DEEP'; name='deep'; source='deep_kernel.c'; memory=8388608 },
[pscustomobject]@{ symbol='BLA'; name='bla'; source='bla_kernel_v18.c'; memory=33554432 },
[pscustomobject]@{ symbol='COLOR'; name='color'; source='color_kernel.c'; memory=2097152 }
)
$payloads = @()
foreach ($module in $modules) {
foreach ($variant in $variants) {
$file = "$($module.name)-$($variant.name).wasm"
$path = Join-Path $output $file
$args = $common + @($variant.flags) + @("-Wl,--initial-memory=$($module.memory)", "-Wl,--max-memory=$($module.memory)", '-o', $path, (Join-Path $workspace "src\$($module.source)") )
& $Compiler @args
if ($LASTEXITCODE -ne 0) { throw "clang failed for $file" }
$payloads += [ordered]@{
symbol = "$($module.symbol)_$($variant.name.ToUpperInvariant())_B64"
file = $file
bytes = (Get-Item -LiteralPath $path).Length
sha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
source = "src/$($module.source)"
}
}
}
$manifest = [ordered]@{
format = 'mandelbrot-source-wasm-manifest-v23'
toolchain = $lock
abi = 'src/abi.json'
payloads = $payloads
}
$manifestPath = Join-Path $output 'manifest.json'
$manifest | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
if ($Promote) {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'tests\kernel-golden.ps1') -GeneratedDirectory $output
if ($LASTEXITCODE -ne 0) { throw 'Generated-WASM golden gate failed; payloads were not promoted.' }
$generatedKernels = Join-Path $output 'kernels.js'
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'scripts\build-kernels.ps1') -ManifestPath $manifestPath -OutputPath $generatedKernels
if ($LASTEXITCODE -ne 0) { throw 'kernels.js generation failed; payloads were not promoted.' }
$dist = Join-Path $workspace 'dist\wasm'
foreach ($payload in $payloads) { Copy-Item -LiteralPath (Join-Path $output $payload.file) -Destination (Join-Path $dist $payload.file) -Force }
Copy-Item -LiteralPath $manifestPath -Destination (Join-Path $dist 'manifest.json') -Force
Copy-Item -LiteralPath $generatedKernels -Destination (Join-Path $workspace 'kernels.js') -Force
}
Write-Output $manifestPath

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));

56
scripts/extract-wasm.ps1 Normal file
View file

@ -0,0 +1,56 @@
param(
[string]$KernelFile = (Join-Path $PSScriptRoot '..\kernels.js'),
[string]$OutputDirectory = (Join-Path $PSScriptRoot '..\dist\wasm')
)
$ErrorActionPreference = 'Stop'
$kernelPath = (Resolve-Path -LiteralPath $KernelFile).Path
$workspacePath = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
$resolvedOutput = [System.IO.Path]::GetFullPath($OutputDirectory)
if (-not $resolvedOutput.StartsWith($workspacePath, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "OutputDirectory must stay inside the workspace: $resolvedOutput"
}
$source = Get-Content -LiteralPath $kernelPath -Raw -Encoding UTF8
$kernelObject = [regex]::Match($source, 'MANDEL_KERNELS\s*=\s*Object\.freeze\(\{(?<body>[\s\S]*?)\}\);')
if (-not $kernelObject.Success) { throw 'MANDEL_KERNELS payload object was not found.' }
$payloadSource = $kernelObject.Groups['body'].Value
$entries = [ordered]@{}
foreach ($match in [regex]::Matches($payloadSource, '([A-Z0-9_]+_B64)\s*:\s*''([^'']+)''')) {
$entries[$match.Groups[1].Value] = $match.Groups[2].Value
}
# Generated kernels.js may store byte-identical color variants once.
# Keep both public symbols in the manifest so hosted assets and compatibility
# checks remain stable when the standalone source uses the shared blob form.
$sharedColor = [regex]::Match($source, 'const\s+COLOR_B64\s*=\s*''([^'']+)''')
if ($sharedColor.Success) {
foreach ($symbol in @('COLOR_SIMD_B64', 'COLOR_SCALAR_B64')) {
if ($payloadSource -match ([regex]::Escape($symbol) + '\s*:\s*COLOR_B64')) {
$entries[$symbol] = $sharedColor.Groups[1].Value
}
}
}
if ($entries.Count -lt 8) { throw "Expected at least 8 embedded WASM payload symbols; found $($entries.Count)." }
New-Item -ItemType Directory -Path $resolvedOutput -Force | Out-Null
$manifest = [ordered]@{
format = 'mandelbrot-wasm-manifest-v1'
generatedUtc = [DateTime]::UtcNow.ToString('o')
input = $kernelPath.Substring($workspacePath.Length).TrimStart('\').Replace('\','/')
payloads = @()
}
foreach ($entry in $entries.GetEnumerator()) {
$symbol = $entry.Key
$bytes = [Convert]::FromBase64String($entry.Value)
$name = ($symbol -replace '_B64$','').ToLowerInvariant().Replace('_','-') + '.wasm'
$path = Join-Path $resolvedOutput $name
[System.IO.File]::WriteAllBytes($path, $bytes)
$hash = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
$manifest.payloads += [ordered]@{ symbol=$symbol; file=$name; bytes=$bytes.Length; sha256=$hash }
}
$manifestPath = Join-Path $resolvedOutput 'manifest.json'
$manifest | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
Write-Output $manifestPath

View file

@ -0,0 +1,99 @@
$ErrorActionPreference = 'Stop'
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'build-hosted.ps1') | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Hosted build failed with exit code $LASTEXITCODE." }
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'build-standalone.ps1') | Out-Null
if ($LASTEXITCODE -ne 0) { throw "Standalone build failed with exit code $LASTEXITCODE." }
$contract = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'tests\source-contract.ps1') | ConvertFrom-Json
if ($LASTEXITCODE -ne 0 -or $contract.status -ne 'pass') { throw 'Source contract failed; baseline was not written.' }
$wasmManifest = Get-Content -LiteralPath (Join-Path $workspace 'dist\wasm\manifest.json') -Raw -Encoding UTF8 | ConvertFrom-Json
$shallowPayload = $wasmManifest.payloads | Where-Object symbol -eq 'WASM_SIMD_B64' | Select-Object -First 1
$shallowHashed = ([System.IO.Path]::GetFileNameWithoutExtension($shallowPayload.file) + '.' + $shallowPayload.sha256.Substring(0,16) + [System.IO.Path]::GetExtension($shallowPayload.file))
function File-Info([string]$Path) {
$item = Get-Item -LiteralPath $Path
[pscustomobject][ordered]@{ file=$item.FullName.Substring($workspace.Length).TrimStart('\').Replace('\','/'); bytes=$item.Length; sha256=(Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant() }
}
$node = Get-Command node -ErrorAction SilentlyContinue
$clang = Get-Command clang -ErrorAction SilentlyContinue
$nodeVersion = if ($node) { (& $node.Source --version | Select-Object -First 1) } else { $null }
$sourceBuildVerified = $false
$sourceBuildReason = 'Pinned clang 17.0.6 and Node CLI were not available.'
if ($node -and $clang -and ((& $clang.Source --version | Select-Object -First 1) -match '17\.0\.6')) {
$generatedManifest = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'scripts\build-wasm.ps1') -Compiler $clang.Source
if ($LASTEXITCODE -ne 0) { throw 'Source-WASM build failed while recording the baseline.' }
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'tests\kernel-golden.ps1') -GeneratedDirectory (Split-Path $generatedManifest)
if ($LASTEXITCODE -ne 0) { throw 'Generated-WASM golden failed while recording the baseline.' }
$sourceBuildVerified = $true
$sourceBuildReason = $null
} else {
$previousPath = Join-Path $workspace 'audit\v23-source-baseline.json'
$candidateManifest = Join-Path $workspace 'build\wasm-v23\manifest.json'
if ((Test-Path -LiteralPath $previousPath) -and (Test-Path -LiteralPath $candidateManifest)) {
$previous = Get-Content -LiteralPath $previousPath -Raw -Encoding UTF8 | ConvertFrom-Json
$candidateHash = (Get-FileHash -LiteralPath $candidateManifest -Algorithm SHA256).Hash.ToLowerInvariant()
$sourceInputsMatch = $true
foreach ($record in $previous.sourceBuild.files) {
$recordPath = Join-Path $workspace ($record.file.Replace('/', [System.IO.Path]::DirectorySeparatorChar))
if (-not (Test-Path -LiteralPath $recordPath) -or (Get-FileHash -LiteralPath $recordPath -Algorithm SHA256).Hash.ToLowerInvariant() -ne $record.sha256) { $sourceInputsMatch = $false; break }
}
if ($sourceInputsMatch -and $previous.sourceBuild.compiledInThisEnvironment -and $previous.sourceBuild.goldenStatus -eq 'pass' -and $previous.sourceBuild.generatedManifest.sha256 -eq $candidateHash) {
$generatedManifest = $candidateManifest
$sourceBuildVerified = $true
$nodeVersion = $previous.sourceBuild.node
$sourceBuildReason = 'Verified generated manifest reused; fixed toolchain was removed after the successful gate.'
}
}
}
$hostedFiles = @(
File-Info (Join-Path $workspace 'dist\hosted\index.html')
File-Info (Join-Path $workspace 'dist\hosted\hosted-loader.js')
File-Info (Join-Path $workspace 'dist\hosted\script.js')
File-Info (Join-Path $workspace "dist\wasm\$shallowHashed")
)
$standaloneFiles = @(
File-Info (Join-Path $workspace 'dist\standalone\index.html')
File-Info (Join-Path $workspace 'dist\standalone\script.js')
File-Info (Join-Path $workspace 'dist\standalone\kernels.js')
)
$kernelSources = @(
File-Info (Join-Path $workspace 'src\shallow_kernel.c')
File-Info (Join-Path $workspace 'src\deep_kernel.c')
File-Info (Join-Path $workspace 'src\bla_kernel_v18.c')
File-Info (Join-Path $workspace 'src\color_kernel.c')
File-Info (Join-Path $workspace 'src\abi.json')
File-Info (Join-Path $workspace 'toolchain.lock.json')
)
$baseline = [ordered]@{
format = 'mandelbrot-source-baseline-v23'
generatedUtc = [DateTime]::UtcNow.ToString('o')
rendererVersion = 23
contract = $contract
hosted = [ordered]@{
firstViewFiles = $hostedFiles
firstViewUncompressedBytes = ($hostedFiles | Measure-Object bytes -Sum).Sum
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
uncompressedBytes = ($standaloneFiles | Measure-Object bytes -Sum).Sum
}
sourceBuild = [ordered]@{
files=$kernelSources
compiler='clang 17.0.6'
node=$nodeVersion
compiledInThisEnvironment=$sourceBuildVerified
goldenStatus=if ($sourceBuildVerified) { 'pass' } else { 'not-run' }
generatedManifest=if ($sourceBuildVerified) { File-Info $generatedManifest } else { $null }
reason=$sourceBuildReason
}
browserBenchmark = [ordered]@{ status='not-run'; report='audit/v23-browser-baseline.json'; reason='No in-app browser binding was available in this environment.' }
}
$path = Join-Path $workspace 'audit\v23-source-baseline.json'
$baseline | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $path -Encoding UTF8
Write-Output $path

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));

41
scripts/test-all.ps1 Normal file
View file

@ -0,0 +1,41 @@
param([switch]$RequireToolchain)
$ErrorActionPreference = 'Stop'
$workspace = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
function Run-CheckedPowerShell([string]$Path) {
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $Path
if ($LASTEXITCODE -ne 0) { throw "PowerShell test failed: $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) {
foreach ($test in @('js-syntax.mjs','module-clone.mjs','precision-reference.mjs','analytic-interior.mjs','pixel-mapping.mjs','pixel-contract.mjs','runtime-budget.mjs')) {
& $node.Source (Join-Path $workspace "tests\$test")
if ($LASTEXITCODE -ne 0) { throw "Node regression test failed: $test" }
}
} elseif ($RequireToolchain) { throw 'Node.js is required by -RequireToolchain.' }
else { Write-Warning 'Node.js is unavailable; executable JavaScript/WASM tests were skipped.' }
$clang = Get-Command clang -ErrorAction SilentlyContinue
$sourceGolden = $false
if ($clang -and $node -and ((& $clang.Source --version | Select-Object -First 1) -match '17\.0\.6')) {
$generated = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'scripts\build-wasm.ps1') -Compiler $clang.Source
if ($LASTEXITCODE -ne 0) { throw 'Source-WASM build failed.' }
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $workspace 'tests\kernel-golden.ps1') -GeneratedDirectory (Split-Path $generated)
if ($LASTEXITCODE -ne 0) { throw 'Generated-WASM golden failed.' }
$sourceGolden = $true
} 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.' }
$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

23
src/abi.json Normal file
View file

@ -0,0 +1,23 @@
{
"format": "mandelbrot-kernel-abi-v23",
"pixelContract": "sample centers at x + 0.5, y + 0.5",
"limits": { "outputPixels": 65536, "referenceSamples": 150001 },
"modules": {
"shallow": {
"source": "shallow_kernel.c",
"exports": ["memory", "counts_ptr", "mags_ptr", "render_rows"]
},
"deep": {
"source": "deep_kernel.c",
"exports": ["memory", "counts_ptr", "mags_ptr", "refs_r_ptr", "refs_i_ptr", "render_perturb_rebase_rect"]
},
"bla": {
"source": "bla_kernel_v18.c",
"exports": ["memory", "counts_ptr", "mags_ptr", "refs_r_ptr", "refs_i_ptr", "build_bla", "render_bla_rect", "render_bla_rect_v2", "stat_bla_steps", "stat_ptb_steps", "stat_rebases", "stat_interior", "stat_unresolved", "stat_fail"]
},
"color": {
"source": "color_kernel.c",
"exports": ["memory", "mags_ptr", "corr_ptr", "smooth_batch"]
}
}
}

204
src/bla_kernel_v18.c Normal file
View file

@ -0,0 +1,204 @@
#include <stdint.h>
#define MAX_REF 150001
#define MAX_BLA 300100
#define OUT_MAX 65536
#define MAX_LEVELS 20
#define STATUS_FAIL 0xffffffffu
#define STATUS_UNRESOLVED 0xfffffffeu
void *memset(void *dst,int c,unsigned long n){unsigned char *p=(unsigned char*)dst;for(unsigned long i=0;i<n;i++)p[i]=(unsigned char)c;return dst;}
static uint32_t counts[OUT_MAX];
static double mags[OUT_MAX];
static double refs_r[MAX_REF];
static double refs_i[MAX_REF];
static double bla_ar[MAX_BLA];
static double bla_ai[MAX_BLA];
static double bla_br[MAX_BLA];
static double bla_bi[MAX_BLA];
static double bla_rad[MAX_BLA];
static int32_t bla_len[MAX_BLA];
static int32_t level_off[MAX_LEVELS];
static int32_t level_cnt[MAX_LEVELS];
static int32_t level_num = 0;
static int32_t built_ref_len = 0;
static uint32_t st_bla=0,st_ptb=0,st_rebase=0,st_interior=0,st_unresolved=0,st_fail=0;
static uint64_t st_iter=0;
__attribute__((export_name("counts_ptr"))) uintptr_t counts_ptr(void){ return (uintptr_t)counts; }
__attribute__((export_name("mags_ptr"))) uintptr_t mags_ptr(void){ return (uintptr_t)mags; }
__attribute__((export_name("refs_r_ptr"))) uintptr_t refs_r_ptr(void){ return (uintptr_t)refs_r; }
__attribute__((export_name("refs_i_ptr"))) uintptr_t refs_i_ptr(void){ return (uintptr_t)refs_i; }
__attribute__((export_name("stat_bla_steps"))) uint32_t stat_bla_steps(void){ return st_bla; }
__attribute__((export_name("stat_ptb_steps"))) uint32_t stat_ptb_steps(void){ return st_ptb; }
__attribute__((export_name("stat_rebases"))) uint32_t stat_rebases(void){ return st_rebase; }
__attribute__((export_name("stat_interior"))) uint32_t stat_interior(void){ return st_interior; }
__attribute__((export_name("stat_unresolved"))) uint32_t stat_unresolved(void){ return st_unresolved; }
__attribute__((export_name("stat_fail"))) uint32_t stat_fail(void){ return st_fail; }
__attribute__((export_name("stat_iter_lo"))) uint32_t stat_iter_lo(void){ return (uint32_t)st_iter; }
__attribute__((export_name("stat_iter_hi"))) uint32_t stat_iter_hi(void){ return (uint32_t)(st_iter>>32); }
static inline double hypot2(double x,double y){ return __builtin_sqrt(x*x+y*y); }
static inline int finite2(double x){ return __builtin_isfinite(x); }
__attribute__((export_name("build_bla"))) int build_bla(int ref_len, double cmax, double eps){
if(ref_len < 3 || ref_len > MAX_REF) { level_num=0; return 0; }
int n = ref_len - 1;
int off = 0;
if(n > MAX_BLA) { level_num=0; return 0; }
level_off[0]=0; level_cnt[0]=n; level_num=1;
for(int j=0;j<n;j++){
int m=j+1;
double ar=2.0*refs_r[m], ai=2.0*refs_i[m];
double am=hypot2(ar,ai);
bla_ar[j]=ar; bla_ai[j]=ai; bla_br[j]=1.0; bla_bi[j]=0.0; bla_len[j]=1;
double r=0.0;
if(am>0.0 && finite2(am)){
r=eps*am-cmax/am;
if(r<0.0 || !finite2(r)) r=0.0;
}
bla_rad[j]=r;
}
int prev_off=0, prev_n=n;
while(prev_n>1 && level_num<MAX_LEVELS){
int cur_n=(prev_n+1)>>1;
off=prev_off+prev_n;
if(off+cur_n>MAX_BLA){ level_num=0; return 0; }
level_off[level_num]=off; level_cnt[level_num]=cur_n;
for(int j=0;j<cur_n;j++){
int x=prev_off+(j<<1), z=off+j;
if((j<<1)+1>=prev_n){
bla_ar[z]=bla_ar[x]; bla_ai[z]=bla_ai[x]; bla_br[z]=bla_br[x]; bla_bi[z]=bla_bi[x]; bla_rad[z]=bla_rad[x]; bla_len[z]=bla_len[x];
continue;
}
int y=x+1;
double axr=bla_ar[x], axi=bla_ai[x], bxr=bla_br[x], bxi=bla_bi[x], rx=bla_rad[x];
double ayr=bla_ar[y], ayi=bla_ai[y], byr=bla_br[y], byi=bla_bi[y], ry=bla_rad[y];
double azr=ayr*axr-ayi*axi, azi=ayr*axi+ayi*axr;
double bzr=ayr*bxr-ayi*bxi+byr, bzi=ayr*bxi+ayi*bxr+byi;
double am=hypot2(axr,axi), bm=hypot2(bxr,bxi);
double v = am>0.0 ? (ry-bm*cmax)/am : -1.0;
double rz = rx < v ? rx : v;
if(rz<0.0) rz=0.0;
if(!finite2(azr)||!finite2(azi)||!finite2(bzr)||!finite2(bzi)||!finite2(rz)) rz=0.0;
bla_ar[z]=azr; bla_ai[z]=azi; bla_br[z]=bzr; bla_bi[z]=bzi; bla_rad[z]=rz; bla_len[z]=bla_len[x]+bla_len[y];
}
prev_off=off; prev_n=cur_n; level_num++;
}
built_ref_len=ref_len;
return level_num;
}
static inline int analytic_interior(double cr,double ci){
double y2=ci*ci;
double x=cr-0.25, q=x*x+y2;
if(q*(q+x) < 0.25*y2-1e-15) return 1;
x=cr+1.0;
if(x*x+y2 < 0.0625-1e-15) return 1;
return 0;
}
/*
The work limits count low-precision *operations*, not Mandelbrot iterations.
A BLA may skip thousands of mathematical iterations in one operation.
max_* <= 0 means unlimited. STATUS_UNRESOLVED means "retry locally", never
"interior". Derivative contraction is a cheap attracting-cycle detector.
*/
static inline int render_one_v2(
double dc_r,double dc_i,double base_cr,double base_ci,
int ref_len,int iter,int max_bla_steps,int max_ptb_steps,int interior_on,
uint32_t *out_n,double *out_m
){
double zr=0.0,zi=0.0;
double dr=1.0,di=0.0;
int dvalid=0;
int n=0,m=0,bla_steps=0,ptb_steps=0;
if(interior_on && analytic_interior(base_cr+dc_r,base_ci+dc_i)){
*out_n=(uint32_t)iter;*out_m=0.0;st_interior++;return 1;
}
while(n<iter){
if(m>ref_len){st_fail++;return 0;}
double vr=refs_r[m]+zr, vi=refs_i[m]+zi;
double mag=vr*vr+vi*vi;
if(mag>4.0){ *out_n=(uint32_t)n; *out_m=mag; st_iter+=(uint64_t)n; return 1; }
if(interior_on && dvalid && n>=12){
double dm=dr*dr+di*di;
if(finite2(dm) && dm<1e-6){*out_n=(uint32_t)iter;*out_m=0.0;st_interior++;st_iter+=(uint64_t)n;return 1;}
}
if(m>0 && mag < zr*zr+zi*zi){ zr=vr; zi=vi; m=0; st_rebase++; continue; }
int used=0;
if(m>=1 && m<ref_len && level_num>0){
int rel=m-1;
double z2=zr*zr+zi*zi;
int kmax = rel==0 ? level_num-1 : __builtin_ctz((unsigned)rel);
if(kmax>=level_num) kmax=level_num-1;
for(int k=kmax;k>=0;k--){
int idx=rel>>k;
if(idx>=level_cnt[k]) continue;
int bi=level_off[k]+idx;
int skip=bla_len[bi];
double rad=bla_rad[bi];
if(skip<2 || m+skip>ref_len || n+skip>iter || !(z2<rad*rad)) continue;
if(max_bla_steps>0 && bla_steps>=max_bla_steps){st_unresolved++;*out_n=STATUS_UNRESOLVED;*out_m=0.0;st_iter+=(uint64_t)n;return 2;}
double nr=bla_ar[bi]*zr-bla_ai[bi]*zi+bla_br[bi]*dc_r-bla_bi[bi]*dc_i;
double ni=bla_ar[bi]*zi+bla_ai[bi]*zr+bla_br[bi]*dc_i+bla_bi[bi]*dc_r;
if(!finite2(nr)||!finite2(ni)) continue;
if(interior_on && dvalid){
double nd=bla_ar[bi]*dr-bla_ai[bi]*di;
double ndi=bla_ar[bi]*di+bla_ai[bi]*dr;
if(finite2(nd)&&finite2(ndi)){dr=nd;di=ndi;} else dvalid=0;
}
zr=nr;zi=ni;m+=skip;n+=skip;bla_steps++;st_bla++;used=1;break;
}
}
if(used) continue;
if(m>=ref_len){st_fail++;return 0;}
if(max_ptb_steps>0 && ptb_steps>=max_ptb_steps){st_unresolved++;*out_n=STATUS_UNRESOLVED;*out_m=0.0;st_iter+=(uint64_t)n;return 2;}
if(interior_on){
if(n==0){dr=1.0;di=0.0;dvalid=1;}
else if(dvalid){
double nd=2.0*(vr*dr-vi*di), ndi=2.0*(vr*di+vi*dr);
if(finite2(nd)&&finite2(ndi)){dr=nd;di=ndi;} else dvalid=0;
}
}
double Rr=refs_r[m],Ri=refs_i[m];
double nr=2.0*(Rr*zr-Ri*zi)+(zr*zr-zi*zi)+dc_r;
double ni=2.0*(Rr*zi+Ri*zr)+2.0*zr*zi+dc_i;
if(!finite2(nr)||!finite2(ni)){st_fail++;return 0;}
zr=nr;zi=ni;m++;n++;ptb_steps++;st_ptb++;
}
*out_n=(uint32_t)iter; *out_m=0.0; st_iter+=(uint64_t)n; return 1;
}
__attribute__((export_name("render_bla_rect_v2"))) int render_bla_rect_v2(
double span,double off_r,double off_i,double base_cr,double base_ci,
int ref_len,int w,int h,int x0,int y0,int rw,int rh,int iter,
int max_bla_steps,int max_ptb_steps,int interior_on
){
if(ref_len!=built_ref_len || level_num<=0 || w<=0 || h<=0 || rw<=0 || rh<=0) return 0;
int total=rw*rh; if(total>OUT_MAX) return 0;
st_bla=st_ptb=st_rebase=st_interior=st_unresolved=st_fail=0;st_iter=0;
int k=0;
for(int yy=0;yy<rh;yy++){
int y=y0+yy;
double dc_i=off_i+span*((0.5*(double)h-(double)y)/(double)w);
for(int xx=0;xx<rw;xx++,k++){
int x=x0+xx;
double dc_r=off_r+span*((double)x/(double)w-0.5);
uint32_t n; double mm;
int ok=render_one_v2(dc_r,dc_i,base_cr,base_ci,ref_len,iter,max_bla_steps,max_ptb_steps,interior_on,&n,&mm);
if(ok==0){ counts[k]=STATUS_FAIL; mags[k]=0.0; }
else { counts[k]=n; mags[k]=mm; }
}
}
return total;
}
/* Compatibility entry point used by old probes/fallback paths. */
__attribute__((export_name("render_bla_rect"))) int render_bla_rect(
double span,double off_r,double off_i,int ref_len,int w,int h,int x0,int y0,int rw,int rh,int iter
){
return render_bla_rect_v2(span,off_r,off_i,0.0,0.0,ref_len,w,h,x0,y0,rw,rh,iter,0,0,0);
}

37
src/color_kernel.c Normal file
View file

@ -0,0 +1,37 @@
#include <stdint.h>
#define OUT_MAX 65536
static double mags[OUT_MAX];
static float corrections[OUT_MAX];
__attribute__((export_name("mags_ptr"))) uintptr_t mags_ptr(void) { return (uintptr_t)mags; }
__attribute__((export_name("corr_ptr"))) uintptr_t corr_ptr(void) { return (uintptr_t)corrections; }
/* Freestanding log2: range reduction plus an odd atanh series for ln(m). */
static double log2_local(double x) {
union { double d; uint64_t u; } v = { x };
int exponent = (int)((v.u >> 52) & 0x7ffu) - 1023;
v.u = (v.u & UINT64_C(0x000fffffffffffff)) | UINT64_C(0x3ff0000000000000);
const double y = (v.d - 1.0) / (v.d + 1.0);
const double y2 = y * y;
double term = y, sum = term;
term *= y2; sum += term / 3.0;
term *= y2; sum += term / 5.0;
term *= y2; sum += term / 7.0;
term *= y2; sum += term / 9.0;
term *= y2; sum += term / 11.0;
return (double)exponent + (2.0 * sum) * 1.4426950408889634074;
}
__attribute__((export_name("smooth_batch"))) int smooth_batch(int count) {
if (count < 0) return 0;
if (count > OUT_MAX) count = OUT_MAX;
for (int i = 0; i < count; i++) {
double m = mags[i];
if (!(m > 4.0)) m = 4.0000001;
const double u = log2_local(m);
corrections[i] = (float)(1.0 - log2_local(0.5 * u));
}
return count;
}

112
src/deep_kernel.c Normal file
View file

@ -0,0 +1,112 @@
#include <stdint.h>
#define MAX_REF 150001
#define OUT_MAX 65536
#define NEG_BUCKET (-1000000000)
#define STATUS_FAIL UINT32_C(0xffffffff)
static uint32_t counts[OUT_MAX];
static double mags[OUT_MAX];
static double refs_r[MAX_REF];
static double refs_i[MAX_REF];
__attribute__((export_name("counts_ptr"))) uintptr_t counts_ptr(void) { return (uintptr_t)counts; }
__attribute__((export_name("mags_ptr"))) uintptr_t mags_ptr(void) { return (uintptr_t)mags; }
__attribute__((export_name("refs_r_ptr"))) uintptr_t refs_r_ptr(void) { return (uintptr_t)refs_r; }
__attribute__((export_name("refs_i_ptr"))) uintptr_t refs_i_ptr(void) { return (uintptr_t)refs_i; }
typedef struct { double r, i; int bucket; } scaled_complex;
static scaled_complex normalize(scaled_complex z) {
double ar = z.r < 0.0 ? -z.r : z.r;
double ai = z.i < 0.0 ? -z.i : z.i;
double m = ar > ai ? ar : ai;
if (m == 0.0) return (scaled_complex){0.0, 0.0, NEG_BUCKET};
while (m > 3.402823669209385e38) { z.r *= 8.636168555094445e-78; z.i *= 8.636168555094445e-78; z.bucket++; m *= 8.636168555094445e-78; }
while (m < 2.938735877055719e-39) { z.r *= 1.157920892373162e77; z.i *= 1.157920892373162e77; z.bucket--; m *= 1.157920892373162e77; }
return z;
}
static scaled_complex add_scaled(scaled_complex a, scaled_complex b) {
if (a.bucket == NEG_BUCKET) return b;
if (b.bucket == NEG_BUCKET) return a;
int bucket = a.bucket > b.bucket ? a.bucket : b.bucket;
double ar = 0.0, ai = 0.0;
if (a.bucket == bucket) { ar += a.r; ai += a.i; }
else if (a.bucket == bucket - 1) { ar += a.r * 8.636168555094445e-78; ai += a.i * 8.636168555094445e-78; }
if (b.bucket == bucket) { ar += b.r; ai += b.i; }
else if (b.bucket == bucket - 1) { ar += b.r * 8.636168555094445e-78; ai += b.i * 8.636168555094445e-78; }
return normalize((scaled_complex){ar, ai, bucket});
}
static scaled_complex multiply_scaled(scaled_complex a, scaled_complex b) {
if (a.bucket == NEG_BUCKET || b.bucket == NEG_BUCKET) return (scaled_complex){0.0, 0.0, NEG_BUCKET};
return normalize((scaled_complex){a.r*b.r-a.i*b.i, a.r*b.i+a.i*b.r, a.bucket+b.bucket});
}
static scaled_complex multiply_reference(scaled_complex z, double rr, double ri) {
if (z.bucket == NEG_BUCKET) return z;
return normalize((scaled_complex){2.0*(rr*z.r-ri*z.i), 2.0*(rr*z.i+ri*z.r), z.bucket});
}
static double scale_bucket(double x, int bucket) {
if (bucket > 3) return x < 0.0 ? -1.0e308 : 1.0e308;
if (bucket < -3) return 0.0;
while (bucket > 0) { x *= 1.157920892373162e77; bucket--; }
while (bucket < 0) { x *= 8.636168555094445e-78; bucket++; }
return x;
}
static int render_one(scaled_complex dc, int ref_len, int max_iter, uint32_t *out_n, double *out_mag) {
scaled_complex delta = {0.0, 0.0, NEG_BUCKET};
int ref_index = 0;
for (int n = 0; n < max_iter; n++) {
if (ref_index >= ref_len) { *out_n = STATUS_FAIL; *out_mag = 0.0; return 0; }
const double dr = scale_bucket(delta.r, delta.bucket);
const double di = scale_bucket(delta.i, delta.bucket);
const double rr = refs_r[ref_index], ri = refs_i[ref_index];
scaled_complex linear = multiply_reference(delta, rr, ri);
scaled_complex square = multiply_scaled(delta, delta);
delta = add_scaled(add_scaled(linear, square), dc);
ref_index++;
const double zr = refs_r[ref_index] + scale_bucket(delta.r, delta.bucket);
const double zi = refs_i[ref_index] + scale_bucket(delta.i, delta.bucket);
const double mag = zr*zr + zi*zi;
if (mag > 4.0) { *out_n = (uint32_t)(n + 1); *out_mag = mag; return 1; }
const double dm = dr*dr + di*di;
if (ref_index > 0 && dm > mag) {
delta = normalize((scaled_complex){zr, zi, 0});
ref_index = 0;
}
}
*out_n = (uint32_t)max_iter; *out_mag = 0.0; return 1;
}
__attribute__((export_name("render_perturb_rebase_rect"))) int render_perturb_rebase_rect(
double span_mant, int span_bucket, double off_r, double off_i, int off_bucket,
int ref_len, int width, int height, double cx, double cy,
int x0, int y0, int rect_width, int rows, int max_iter,
int skip, double ar, double ai, int a_bucket,
double br, double bi, int b_bucket
) {
(void)skip; (void)ar; (void)ai; (void)a_bucket; (void)br; (void)bi; (void)b_bucket;
if (ref_len < 1 || ref_len >= MAX_REF || width <= 0 || rect_width <= 0 || rows <= 0) return 0;
const int total = rect_width * rows;
if (total > OUT_MAX) return 0;
const scaled_complex offset = normalize((scaled_complex){off_r, off_i, off_bucket});
int out = 0;
for (int yy = 0; yy < rows; yy++) {
const int y = y0 + yy;
for (int xx = 0; xx < rect_width; xx++, out++) {
const int x = x0 + xx;
scaled_complex pixel = {span_mant * ((double)x - cx) / (double)width,
span_mant * (cy - (double)y) / (double)width,
span_bucket};
scaled_complex dc = add_scaled(offset, normalize(pixel));
uint32_t n; double mag;
render_one(dc, ref_len, max_iter, &n, &mag);
counts[out] = n; mags[out] = mag;
}
}
return total;
}

44
src/shallow_kernel.c Normal file
View file

@ -0,0 +1,44 @@
#include <stdint.h>
#define OUT_MAX 65536
static uint32_t counts[OUT_MAX];
static double mags[OUT_MAX];
__attribute__((export_name("counts_ptr"))) uintptr_t counts_ptr(void) { return (uintptr_t)counts; }
__attribute__((export_name("mags_ptr"))) uintptr_t mags_ptr(void) { return (uintptr_t)mags; }
/*
* center_re/center_im are already shifted by half a pixel by the JavaScript
* adapter. This preserves the historical ABI while making every sample land
* at (x + 0.5, y + 0.5) in the renderer-v23 contract.
*/
__attribute__((export_name("render_rows"))) int render_rows(
double center_re, double center_im, double span,
int width, int height, int y0, int rows, int max_iter
) {
if (width <= 0 || height <= 0 || rows <= 0 || max_iter <= 0) return 0;
int total = width * rows;
if (total > OUT_MAX) return 0;
const double scale = span / (double)width;
int out = 0;
for (int yy = 0; yy < rows; yy++) {
const int y = y0 + yy;
const double ci = center_im + scale * (0.5 * (double)height - (double)y);
for (int x = 0; x < width; x++, out++) {
const double cr = center_re + scale * ((double)x - 0.5 * (double)width);
double zr = 0.0, zi = 0.0, zr2 = 0.0, zi2 = 0.0;
int n = 0;
while (n < max_iter && zr2 + zi2 <= 4.0) {
zi = 2.0 * zr * zi + ci;
zr = zr2 - zi2 + cr;
zr2 = zr * zr;
zi2 = zi * zi;
n++;
}
counts[out] = (uint32_t)n;
mags[out] = n < max_iter ? zr2 + zi2 : 0.0;
}
}
return total;
}

View file

@ -0,0 +1,29 @@
const fromFraction = (numerator, denominator, bits) => numerator * (1n << BigInt(bits)) / denominator;
function fixedAnalyticInterior(cr, ci, bits) {
const scale = 1n << BigInt(bits);
const x = cr - (scale >> 2n), y = ci, q = x * x + y * y;
if (4n * q * (q + x * scale) <= y * y * scale * scale) return true;
const d = cr + scale;
return 16n * (d * d + y * y) <= scale * scale;
}
const cases = [
{ id: 'origin-cardioid', re: [0n, 1n], im: [0n, 1n], expected: true },
{ id: 'cardioid-cusp', re: [1n, 4n], im: [0n, 1n], expected: true },
{ id: 'period2-center', re: [-1n, 1n], im: [0n, 1n], expected: true },
{ id: 'period2-boundary', re: [-5n, 4n], im: [0n, 1n], expected: true },
{ id: 'right-exterior', re: [13n, 50n], im: [0n, 1n], expected: false },
{ id: 'period3-not-analytic', re: [-123n, 1000n], im: [745n, 1000n], expected: false },
{ id: 'far-exterior', re: [1n, 1n], im: [1n, 1n], expected: false }
];
let checked = 0;
for (const bits of [256, 320]) for (const sample of cases) {
const cr = fromFraction(sample.re[0], sample.re[1], bits);
const ci = fromFraction(sample.im[0], sample.im[1], bits);
const actual = fixedAnalyticInterior(cr, ci, bits);
if (actual !== sample.expected) throw new Error(`${sample.id} at ${bits} bit: expected ${sample.expected}, got ${actual}`);
checked++;
}
console.log(JSON.stringify({ status: 'pass', checked, precisions: [256, 320], proof: 'integer cardioid/period-2 inequalities' }));

View file

@ -0,0 +1,323 @@
<!doctype html>
<meta charset="utf-8">
<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');
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

157
tests/js-syntax.mjs Normal file
View file

@ -0,0 +1,157 @@
import fs from 'node:fs/promises';
import vm from 'node:vm';
const root = new URL('../', import.meta.url);
const kernels = await fs.readFile(new URL('kernels.js', root), 'utf8');
const appSource = await fs.readFile(new URL('script.js', root), 'utf8');
let app = appSource;
const browserHarness = await fs.readFile(new URL('tests/browser-benchmark.html', root), 'utf8');
const browserModule = browserHarness.match(/<script type="module">([\s\S]*?)<\/script>/)?.[1];
if (!browserModule) throw new Error('Browser benchmark module not found.');
new vm.Script(browserModule, { filename: 'browser-benchmark-module.js' });
app = app.replace(/\}\)\(\);\s*$/, `
const __fieldProbe = makeField(2, 100);
putField(__fieldProbe, 0, 7, 16, FIELD_ESCAPED);
putField(__fieldProbe, 1, 100, 0, FIELD_UNKNOWN);
globalThis.__MANDEL_FIELD_PROBE__ = {
iterations: Array.from(__fieldProbe.iterations),
classes: Array.from(__fieldProbe.classes)
};
globalThis.__MANDEL_WORKER_SOURCES__ = {
shallow: shallowWorkerSource(),
deep: deepWorkerSource()
};
globalThis.__MANDEL_MODE_PROBE__ = {
targets: Object.fromEntries(Object.keys(MODE_TARGET).map(mode => [mode, modeTarget(mode)])),
coldDeepPreview: targetSize(RENDER_PROFILE[RENDER_PASS.PREVIEW], true)
};
globalThis.__MANDEL_REFERENCE_PROBE__ = async () => {
const bits = 256, refLen = 40, rr = new Float64Array(refLen + 1), ri = new Float64Array(refLen + 1);
const ref = { bits, re: 0n, im: 0n, rr, ri, escape: 0, version: 1, checkpointVersion: 0, checkpointBits: 0, checkpointCount: 0, checkpointMismatch: false };
const run = () => new Promise(resolve => verifyReferenceCheckpoints(ref, refLen, state.token, ok => resolve(ok)));
const agreement = await run();
ref.rr[16] = 1; ref.checkpointVersion = 0; ref.checkpointMismatch = false;
const catchesMismatch = !(await run());
return { agreement, catchesMismatch, count: ref.checkpointCount, bits: ref.checkpointBits };
};
})();`);
const controls = new Map();
function makeContext2d() {
return {
fillStyle: '', imageSmoothingEnabled: true, imageSmoothingQuality: 'high',
fillRect() {}, drawImage() {}, putImageData() {}, save() {}, restore() {},
setTransform() {}, translate() {}, scale() {},
createImageData(width, height) {
return { width, height, data: new Uint8ClampedArray(width * height * 4) };
},
getImageData(_x, _y, width, height) {
return { width, height, data: new Uint8ClampedArray(width * height * 4) };
}
};
}
function makeElement(id = '') {
return {
id, value: '', checked: false, disabled: false, hidden: false,
width: 800, height: 600, clientWidth: 800, clientHeight: 600,
textContent: '', innerHTML: '', style: {}, dataset: {},
classList: { add() {}, remove() {}, toggle() {} },
addEventListener() {}, setAttribute() {}, click() {}, close() {}, showModal() {},
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }),
getContext: () => makeContext2d(),
toBlob(callback) { callback(new Blob()); }
};
}
function element(id) {
if (!controls.has(id)) controls.set(id, makeElement(id));
return controls.get(id);
}
Object.assign(element('processMode'), { value: 'standard' });
Object.assign(element('palette'), { value: '0' });
Object.assign(element('cycle'), { value: '.008' });
Object.assign(element('shift'), { value: '.18' });
Object.assign(element('iters'), { value: '350' });
Object.assign(element('adaptive'), { checked: true });
Object.assign(element('hq'), { checked: true });
Object.assign(element('exportScale'), { value: '1' });
Object.assign(element('exportAA'), { value: '1' });
Object.assign(element('exportPrecision'), { value: 'balanced' });
let rafId = 0;
const sandbox = {
console, WebAssembly, BigInt, Blob, URL, URLSearchParams,
Uint8Array, Uint8ClampedArray, Uint32Array, Float32Array, Float64Array,
ArrayBuffer, Map, Set, Math, Date, JSON, Promise, performance,
atob, btoa,
innerWidth: 800, innerHeight: 600, devicePixelRatio: 1,
navigator: { hardwareConcurrency: 4, deviceMemory: 8, clipboard: { writeText: async () => {} } },
location: { protocol: 'http:', origin: 'http://localhost', hash: '', href: 'http://localhost/' },
history: { pushState() {}, replaceState() {} },
localStorage: { getItem: () => null, setItem() {} },
matchMedia: () => ({ matches: false }),
requestAnimationFrame: () => ++rafId,
cancelAnimationFrame() {}, requestIdleCallback: () => 1,
setTimeout: () => 1, clearTimeout() {}, queueMicrotask() {},
addEventListener() {}, removeEventListener() {},
document: {
hidden: false,
body: makeElement('body'),
querySelector(selector) { return element(selector.replace(/^#/, '')); },
createElement() { return makeElement(); }
}
};
sandbox.window = sandbox;
sandbox.globalThis = sandbox;
const context = vm.createContext(sandbox);
new vm.Script(kernels, { filename: 'kernels.js' }).runInContext(context);
new vm.Script(app, { filename: 'script.js' }).runInContext(context);
const sources = sandbox.__MANDEL_WORKER_SOURCES__;
if (!sources?.shallow || !sources?.deep) throw new Error('Worker source extraction failed.');
new vm.Script(sources.shallow, { filename: 'shallow-worker.js' });
new vm.Script(sources.deep, { filename: 'deep-worker.js' });
let deepReady = null;
const workerSandbox = {
WebAssembly, Uint8Array, Uint8ClampedArray, Uint32Array, Float32Array, Float64Array,
ArrayBuffer, Map, Set, Math, Date, JSON, Promise, performance,
postMessage(message) { deepReady = message; }
};
workerSandbox.self = workerSandbox;
const workerContext = vm.createContext(workerSandbox);
new vm.Script(sources.deep, { filename: 'deep-worker.js' }).runInContext(workerContext);
const moduleFiles = { deep: 'deep-simd.wasm', bla: 'bla-simd.wasm', color: 'color-simd.wasm' };
const modules = {};
for (const [key, file] of Object.entries(moduleFiles)) {
const bytes = await fs.readFile(new URL(`dist/wasm/${file}`, root));
modules[key] = { module: structuredClone(await WebAssembly.compile(bytes)), simd: true };
}
await workerSandbox.onmessage({ data: { type: 'init', modules } });
if (deepReady?.type !== 'ready' || deepReady.error) throw new Error(`Deep Worker module init failed: ${deepReady?.error || 'no reply'}`);
const diagnostics = sandbox.__MANDEL_DIAG__?.snapshot();
if (diagnostics?.rendererVersion !== 23) throw new Error('Application initialization failed.');
if (diagnostics.lastPass !== 'preview') throw new Error('Discrete preview profile did not initialize.');
if (diagnostics.automaticTarget !== 'COVERED') throw new Error('Standard mode must stop automatically at Covered.');
if (JSON.stringify(sandbox.__MANDEL_MODE_PROBE__?.targets) !== JSON.stringify({ power: 'PREVIEW', standard: 'COVERED', fine: 'REFINED', validate: 'VALIDATED' })) throw new Error('Processing-mode completion targets failed.');
if (sandbox.__MANDEL_MODE_PROBE__?.coldDeepPreview?.[0] > 48) throw new Error('Cold deep Preview exceeded its conservative width cap.');
if (sandbox.__MANDEL_FIELD_PROBE__?.iterations?.join(',') !== '7,100') throw new Error('Field iteration channel failed.');
sandbox.requestAnimationFrame = callback => { queueMicrotask(() => callback(performance.now())); return ++rafId; };
const referenceCheckpoints = await sandbox.__MANDEL_REFERENCE_PROBE__();
if (!referenceCheckpoints.agreement || !referenceCheckpoints.catchesMismatch || referenceCheckpoints.bits !== 320) throw new Error('P/P+64 reference checkpoint verification failed.');
console.log(JSON.stringify({
status: 'pass', rendererVersion: diagnostics.rendererVersion, lastPass: diagnostics.lastPass, automaticTarget: diagnostics.automaticTarget,
fieldIterations: sandbox.__MANDEL_FIELD_PROBE__.iterations,
modeProbe: sandbox.__MANDEL_MODE_PROBE__,
referenceCheckpoints,
deepWorkerModuleInit: true,
browserHarnessSyntax: true,
sources: {
appBytes: Buffer.byteLength(appSource),
shallowWorkerCharacters: sources.shallow.length,
deepWorkerCharacters: sources.deep.length
}
}));

68
tests/kernel-golden.mjs Normal file
View file

@ -0,0 +1,68 @@
import fs from 'node:fs/promises';
import path from 'node:path';
const root = path.resolve(process.argv[2] || 'build/wasm-v23');
const load = async name => {
const bytes = await fs.readFile(path.join(root, name));
return (await WebAssembly.instantiate(bytes, {})).instance.exports;
};
const assert = (ok, message) => { if (!ok) throw new Error(message); };
const close = (a, b, tolerance) => Math.abs(a - b) <= tolerance * Math.max(1, Math.abs(a), Math.abs(b));
function direct(cr, ci, limit) {
let zr=0, zi=0, zr2=0, zi2=0, n=0;
while (n < limit && zr2 + zi2 <= 4) {
zi = 2*zr*zi + ci; zr = zr2 - zi2 + cr; zr2 = zr*zr; zi2 = zi*zi; n++;
}
return [n, n < limit ? zr2 + zi2 : 0];
}
async function shallowGolden() {
const cores = await Promise.all(['wasm-simd.wasm','wasm-scalar.wasm'].map(load));
const width=19, height=11, iter=320, re=-0.5, im=0, span=3.4, scale=span/width;
let baseline;
for (const [variant, ex] of [['simd',cores[0]],['scalar',cores[1]]]) {
const npx=ex.render_rows(re+scale*.5,im-scale*.5,span,width,height,0,height,iter);
assert(npx===width*height, `shallow ${variant}: output size`);
const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),npx);
const mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),npx);
const copy=Uint32Array.from(counts); if (!baseline) baseline=copy;
for(let y=0;y<height;y++)for(let x=0;x<width;x++){
const i=y*width+x,shiftedRe=re+scale*.5,shiftedIm=im-scale*.5,cr=shiftedRe+scale*(x-width*.5),ci=shiftedIm+scale*(height*.5-y),[n,m]=direct(cr,ci,iter);
assert(counts[i]===n, `shallow ${variant}: count mismatch at ${x},${y}`);
if(n<iter)assert(close(mags[i],m,1e-12),`shallow ${variant}: magnitude mismatch at ${x},${y}: wasm=${mags[i]} direct=${m}`);
assert(copy[i]===baseline[i],`shallow SIMD/scalar mismatch at ${i}`);
}
}
}
async function colorGolden() {
const cores=await Promise.all(['color-simd.wasm','color-scalar.wasm'].map(load));
const inputs=[4.0000001,4.1,8,32,1e4,1e100]; let baseline;
for(const [variant,ex] of [['simd',cores[0]],['scalar',cores[1]]]){
const mags=new Float64Array(ex.memory.buffer,ex.mags_ptr(),65536),corr=new Float32Array(ex.memory.buffer,ex.corr_ptr(),65536);mags.set(inputs);assert(ex.smooth_batch(inputs.length)===inputs.length,`color ${variant}: output size`);const copy=Float32Array.from(corr.subarray(0,inputs.length));if(!baseline)baseline=copy;
for(let i=0;i<inputs.length;i++){const expected=1-Math.log2(.5*Math.log2(inputs[i]));assert(close(copy[i],expected,3e-5),`color ${variant}: correction ${i}`);assert(close(copy[i],baseline[i],1e-7),`color SIMD/scalar mismatch ${i}`)}
}
}
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 deepGolden(){
const deep=await Promise.all(['deep-simd.wasm','deep-scalar.wasm'].map(load));
const blas=await Promise.all(['bla-simd.wasm','bla-scalar.wasm'].map(load));
const width=13,height=9,iter=600,re=-.75,im=.1,span=1e-7,scale=span/width,ref=reference(re,im,iter);
let baseline;
for(const [variant,ex] of [['simd',deep[0]],['scalar',deep[1]]]){
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 npx=ex.render_perturb_rebase_rect(span,0,scale*.5,-scale*.5,0,iter,width,height,width*.5,height*.5,0,0,width,height,iter,0,0,0,0,0,0,0);
assert(npx===width*height,`deep ${variant}: output size`);const counts=Uint32Array.from(new Uint32Array(ex.memory.buffer,ex.counts_ptr(),npx));if(!baseline)baseline=counts;
for(let y=0;y<height;y++)for(let x=0;x<width;x++){const i=y*width+x,cr=re+(x+.5-width*.5)*scale,ci=im+(height*.5-y-.5)*scale,[n]=direct(cr,ci,iter);assert(counts[i]===n,`deep ${variant}: count mismatch at ${x},${y}`);assert(counts[i]===baseline[i],`deep SIMD/scalar mismatch ${i}`)}
}
for(const [variant,ex] of [['simd',blas[0]],['scalar',blas[1]]]){
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);assert(ex.build_bla(iter,Math.hypot(span*.5,span*height/(2*width)),2**-32)>0,`BLA ${variant}: build`);
const npx=ex.render_bla_rect_v2(span,scale*.5,-scale*.5,re,im,iter,width,height,0,0,width,height,iter,0,0,1);assert(npx===width*height,`BLA ${variant}: output size`);const counts=new Uint32Array(ex.memory.buffer,ex.counts_ptr(),npx);for(let i=0;i<npx;i++)assert(counts[i]===baseline[i]||counts[i]===0xfffffffe,`BLA ${variant}: mismatch ${i}`)
}
}
await shallowGolden();await colorGolden();await deepGolden();
console.log(JSON.stringify({status:'pass',generatedDirectory:root,suites:['shallow','color','deep','bla']}));

7
tests/kernel-golden.ps1 Normal file
View file

@ -0,0 +1,7 @@
param([Parameter(Mandatory=$true)][string]$GeneratedDirectory)
$ErrorActionPreference = 'Stop'
$node = Get-Command node -ErrorAction SilentlyContinue
if (-not $node) { throw 'Node.js is required to execute generated WASM golden vectors.' }
$directory = (Resolve-Path -LiteralPath $GeneratedDirectory).Path
& $node.Source (Join-Path $PSScriptRoot 'kernel-golden.mjs') $directory
if ($LASTEXITCODE -ne 0) { throw 'Kernel golden vectors failed.' }

Some files were not shown because too many files have changed in this diff Show more