This commit is contained in:
33333-33333 2026-09-29 23:45:12 +09:00
commit f5fc9b76ce
73 changed files with 32628 additions and 376 deletions

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,123 @@
# Reported deep-view artifact investigation and fix
Date: 2026-09-29
## Purpose
Investigate the reported URL state that displayed large yellow/black geometric regions and sometimes an almost entirely black frame. The production source before this pass is preserved as `index.before.html`. The fix is intentionally narrow: do not restore previously removed expensive work unless evidence points to it.
Reported state:
- bits: 273
- re: `-15958919693715511752069626580195447581643016686947107473855747766272418724092005269`
- im: `3884297345146583343999140717464554120497735725078805729405733239132420430333375719`
- span: `35679807704506851074208413824041445358585910697267232113437882789120793`
- palette: 0
- cycle: 0.008
- shift: 0.18
- base iteration: 350, adaptive enabled, quality high
At 1108×1582, the adaptive initial target for this region is approximately 1536 iterations and the renderer takes the `fast-extended` path.
## Root cause 1: unsafe series jump
The integrated fourth-order series approximation was enabled for the reported view. The fast-reference worker selected:
- reference pixel ≈ `(69.5, 922.5)`
- reference length: 1285
- series jump: 773
- fourth-order coefficient components up to approximately `2.35e38`, close to the maximum finite f32 magnitude
The series validity test checked truncation terms in unscaled orbit space, but did not bound the *intermediate scaled f32 polynomial products* used by WGSL. At screen-domain deltas away from the selected reference, `a4 * d^4` can overflow before the later `ldexp` rescales the term.
The reproducible probe in `experiments/reported-view/analyze.mjs` evaluates the exact worker output from the preserved pre-fix source. On a 32×32 viewport sample grid:
- pre-fix: 290 / 1024 series starts produce a non-finite fourth-order intermediate
- fixed: 0 / 1024, because series acceleration is disabled
This is sufficient to generate `RANGE` failures and black unknown pixels. Separate CPU surrogate comparison also showed large escape-iteration deviations in this state, so merely adding an overflow clamp would not make the current jump trustworthy enough for publication.
### Fix
`SERIES_APPROXIMATION_ENABLED=false` now disables production series construction. The code remains present for future work, but the fast-reference worker emits `jump=0` and zero coefficients.
Re-enabling series should require at minimum:
1. viewport/reference-relative bounds on all scaled polynomial intermediates,
2. a propagated truncation/rounding error bound carried into the perturbation path,
3. a corpus test that checks false escapes, not only absolute approximation error,
4. strict handling when the selected reference is off-center.
## Root cause 2: publishing numerical failures before repair
The FAST primary frame was colored and presented immediately after the primary perturbation pass, before `recoverNumericalCostOrdered()` repaired `REFERENCE_END`, `RANGE`, and other numerical failures.
`COLOR_WGSL` writes alpha zero for `FIELD_UNKNOWN`. Therefore an early provisional frame can appear:
- black when there is no usable history,
- mixed with reprojected old colors when stable history exists,
- as large geometric regions reflecting reference validity / numerical-failure domains rather than Mandelbrot geometry.
### Fix
For `fast-extended`:
- primary colorization is deferred (`deferPrimaryColor = deferColor || fastExtended`),
- unresolved statistics are read,
- numerical failures are repaired,
- only then is the first provisional recolor/presentation allowed.
Direct rendering keeps its existing provisional behavior.
## What was *not* reverted
The previous FAST coverage-repair dispatch was not restored. The existing full-frame unknown reduction and final invariant still detect untouched reason-0 pixels, and prior A/B coverage tests found no holes. The reported state produced a concrete series failure mechanism, so restoring an O(pixels) repair pass would treat the symptom without addressing the cause.
Other removal-pass optimizations are unchanged.
## Verification
### Regression
`node tests/regression.mjs`
Result: PASS, 19 WGSL kernels.
Additional regression pins were added for:
- `SERIES_APPROXIMATION_ENABLED=false`
- FAST primary-color deferral via `deferPrimaryColor=deferColor||fastExtended`
### Reported-view worker probe
`node experiments/reported-view/analyze.mjs`
See `results.json`.
Key result:
| Metric | Before | After |
|---|---:|---:|
| reference length | 1285 | 1285 |
| series jump | 773 | 0 |
| non-finite series-start samples | 290 / 1024 | 0 / 1024 |
### Existing integrated CPU benchmark
`node experiments/integrated_bench.mjs`
Completed successfully. The series section now reports jump 0 at all tested spans, while periodicity, sparse active state, multi-reference, and CPU fallback tests continue to run.
### Browser/WebGPU smoke
`tests/browser_smoke.mjs` could not run because its expected external Chromium remote-debug endpoint at `127.0.0.1:9333` was not available in this environment. This is an environment limitation, not a test assertion failure. Actual WebGPU visual confirmation of the supplied URL remains the most important local-device check.
## Build summary
See `build-summary.json`.
- before SHA-256: `e69b91ead027927d977a065d27d9e27629110122df20aa470eab877e23f77ac7`
- fixed SHA-256: `c53c0dc4cb9fa2d2046aaa12ef03cc79c61a710b0c8db36dea1a87be2d9aedd8`
- HTML size delta: +396 bytes (comments/guard and publication-order logic)
The WGSL kernels themselves were not changed in this pass, so kernel hashes remain pinned to the prior 19-kernel set.

View file

@ -0,0 +1,8 @@
# Reported-view bugfix artifacts
- `IMPLEMENTATION.md` — diagnosis, changes, verification, and limitations.
- `results.json` — reproducible before/after fast-reference worker analysis for the reported view.
- `integrated-bench.json` — existing CPU-side integrated benchmark after the fix.
- `build-summary.json` — file sizes and SHA-256 hashes.
- `index.diff` — exact production source diff.
- `index.before.html` — preserved pre-fix production source.

View file

@ -0,0 +1,9 @@
{
"beforeBytes": 224559,
"afterBytes": 224955,
"deltaBytes": 396,
"beforeGzip": 57744,
"afterGzip": 57958,
"beforeSha256": "e69b91ead027927d977a065d27d9e27629110122df20aa470eab877e23f77ac7",
"afterSha256": "c53c0dc4cb9fa2d2046aaa12ef03cc79c61a710b0c8db36dea1a87be2d9aedd8"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
--- /mnt/data/mandelbrot_bugfix_reported_view/docs/reported-view-bugfix/index.before.html 2026-09-29 10:14:53.060514133 +0000
+++ /mnt/data/mandelbrot_bugfix_reported_view/index.html 2026-09-29 10:15:44.235560513 +0000
@@ -973,8 +973,13 @@
function cmuln(a,b){return[a[0]*b[0]-a[1]*b[1],a[0]*b[1]+a[1]*b[0]]}
function cscale(a,k){return[a[0]*k,a[1]*k]}
function clog2(a){const m=Math.hypot(a[0],a[1]);return m>0?Math.log2(m):-Infinity}
+const SERIES_APPROXIMATION_ENABLED=false;
function seriesForOrbit(rr,ri,refLen,span,bits,w,h){
- if(!span||refLen<16)return{jump:0,coeffs:Array(8).fill(0),errorLog2:0};
+ // Disabled after a reported deep-view failure: the current 4th-order jump
+ // has no per-pixel propagated error bound and can overflow scaled f32
+ // polynomial intermediates near viewport edges. Keep the implementation
+ // available for future guarded reintroduction, but never publish it now.
+ if(!SERIES_APPROXIMATION_ENABLED||!span||refLen<16)return{jump:0,coeffs:Array(8).fill(0),errorLog2:0};
const aspect=Math.max(1e-9,h/Math.max(1,w)),logDelta=fixedLog2Abs(BigInt(span),bits)+Math.log2(Math.hypot(1,aspect));
let a=Array.from({length:7},()=>[0,0]),best={jump:0,coeffs:Array(8).fill(0),errorLog2:0},limit=Math.min(refLen,4096);
for(let n=0;n<limit;n++){
@@ -1776,8 +1781,8 @@
if(proof.pixels||historySeed){certified=await r.certifyInterior(snap,token,true,proof,historySeed);if(token!==state.token)return null;preserveInterior=certified>0||!!historySeed}
if(certified===canvas.width*canvas.height){decision={backend:'exact-interior',fastExtended:false,reason:'exact-full-interior'};const stats={total:0,heuristicInterior:0,reasons:{errorBound:0,escapeUncertain:0,referenceEnd:0,rebaseGap:0,range:0,operationLimit:0}};state.unresolved=0;state.unknownReasons=stats.reasons;state.heuristicInterior=0;const painted=await r.recolor(token,iter);if(!painted||token!==state.token)return null;const frontier={stats,iter,minimumIter:iter,rounds:0,processed:0,escaped:0,converged:true,finiteBudgetComplete:true,policy:'exact-interior-proof',membershipCertified:true,applied:true};state.frameView=snap;state.fieldView={...snap,iter,frontier,w:canvas.width,h:canvas.height,fastExtended:false,backend:'exact-interior',referenceKey:'',referenceReused:false,complete:true,membershipCertified:true};r.captureColorSource(snap,state.colorAuto||state.recolorPending);r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});return{decision,referenceReused:false,referencePixel:null,certified}}
let fastCtx=null,referencePixel=null,referenceReused=false;if(fastExtended){const reuse=reusableFastReference(snap,iter,canvas.width,canvas.height);if(reuse){fastCtx=reuse.ctx;referencePixel=reuse.pixel;referenceReused=true}else{fastCtx=await fastRefs.request(snap,iter,canvas.width,canvas.height);if(token!==state.token)return null;referencePixel=referencePixelForSource(fastCtx.source,snap,canvas.width,canvas.height)}}
- const deferColor=!!(r.historyReady&&canUseStableReprojection());state.deferNumericPublish=deferColor;const ok=await r.computeFrame(snap,iter,token,referencePixel,fastExtended,fastCtx,preserveInterior,!!historySeed,deferColor);if(!ok||token!==state.token){state.deferNumericPublish=false;return null}if(!deferColor&&!await presentProvisional(r,snap,iter,token,true)){state.deferNumericPublish=false;return null}
- let stats=await r.readUnresolvedStats();if(token!==state.token)return null;if(fastExtended){const initialFailed=numericalFailureCount(stats);if(initialFailed>0){const ordered=await recoverNumericalCostOrdered(r,snap,iter,token,stats);if(!ordered||token!==state.token){state.deferNumericPublish=false;return null}stats=ordered.stats}state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.heuristicInterior=stats.heuristicInterior||0;if(initialFailed>0&&!deferColor){const painted=await r.recolor(token,iter);if(!painted||token!==state.token){state.deferNumericPublish=false;return null}}}else{state.unresolved=stats.total;state.unknownReasons=stats.reasons}
+ const deferColor=!!(r.historyReady&&canUseStableReprojection()),deferPrimaryColor=deferColor||fastExtended;state.deferNumericPublish=deferColor;const ok=await r.computeFrame(snap,iter,token,referencePixel,fastExtended,fastCtx,preserveInterior,!!historySeed,deferPrimaryColor);if(!ok||token!==state.token){state.deferNumericPublish=false;return null}if(!fastExtended&&!deferColor&&!await presentProvisional(r,snap,iter,token,true)){state.deferNumericPublish=false;return null}
+ let stats=await r.readUnresolvedStats();if(token!==state.token)return null;if(fastExtended){const initialFailed=numericalFailureCount(stats);if(initialFailed>0){const ordered=await recoverNumericalCostOrdered(r,snap,iter,token,stats);if(!ordered||token!==state.token){state.deferNumericPublish=false;return null}stats=ordered.stats}state.unresolved=stats.total;state.unknownReasons=stats.reasons;state.heuristicInterior=stats.heuristicInterior||0;if(!deferColor&&!await presentProvisional(r,snap,iter,token,true)){state.deferNumericPublish=false;return null}}else{state.unresolved=stats.total;state.unknownReasons=stats.reasons}
const frontier=await refinePixelFrontier(r,snap,iter,token,{total:state.unresolved,reasons:{...(state.unknownReasons||{})}},certified);if(!frontier||token!==state.token){state.deferNumericPublish=false;return null}const effectiveIter=frontier.iter;if(!frontier.converged)throw new Error('全画素反復が安全上限 '+PIXEL_FRONTIER_MAX_ITER+' までに収束しなかったためframeを公開しません');if(frontier.applied&&!deferColor){const painted=await r.recolor(token,effectiveIter);if(!painted||token!==state.token){state.deferNumericPublish=false;return null}}if(deferColor){const painted=await r.recolor(token,effectiveIter);if(!painted||token!==state.token){state.deferNumericPublish=false;return null}}
const remaining=numericalFailureCount({reasons:state.unknownReasons||{}}),unprocessed=Math.max(0,state.unresolved-remaining-(state.unknownReasons?.operationLimit||0));if(unprocessed)throw new Error('未計算画素が '+unprocessed+' px 残っています');if(remaining>0)throw new Error('画素精度補修後も数値未確定が '+remaining+' px 残ったためframeを公開しません');state.frameView=snap;state.fieldView={...snap,iter:effectiveIter,frontier,w:canvas.width,h:canvas.height,fastExtended,backend:frontier.applied?'pixel-frontier-'+decision.backend:decision.backend,referenceKey:fastCtx?.key||'',referenceReused,complete:true,membershipCertified:frontier.membershipCertified};r.captureColorSource(snap,state.colorAuto||state.recolorPending);r.presentFrame({scaleX:1,scaleY:1,offsetX:0,offsetY:0});state.deferNumericPublish=false;return{decision,referenceReused,referencePixel,certified}
}

View file

@ -0,0 +1,381 @@
{
"date": "2026-09-29T10:16:09.784Z",
"periodicity": {
"w": 96,
"h": 64,
"maxIter": 8192,
"oracleIter": 65536,
"rows": [
{
"view": "overview",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 884420,
"heuristicWork": 133404,
"workReduction": 0.849162162773343,
"candidates": 96,
"falseCandidates": 0,
"baseLimit": 104,
"heuristicLimit": 8,
"diagnosticIfForcedDirect": null,
"ms": 73.54993999999999
},
{
"view": "period3-bulb",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 50331648,
"heuristicWork": 460156,
"workReduction": 0.9908575216929117,
"candidates": 6144,
"falseCandidates": 0,
"baseLimit": 6144,
"heuristicLimit": 0,
"diagnosticIfForcedDirect": null,
"ms": 2914.8620730000002
},
{
"view": "period3-core",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 50331648,
"heuristicWork": 442395,
"workReduction": 0.991210401058197,
"candidates": 6144,
"falseCandidates": 0,
"baseLimit": 6144,
"heuristicLimit": 0,
"diagnosticIfForcedDirect": null,
"ms": 2906.390313
},
{
"view": "seahorse-boundary",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 410111,
"heuristicWork": 410111,
"workReduction": 0,
"candidates": 0,
"falseCandidates": 0,
"baseLimit": 0,
"heuristicLimit": 0,
"diagnosticIfForcedDirect": null,
"ms": 18.368617999999515
},
{
"view": "seahorse-deep",
"productionBackend": "fast-extended",
"pixels": 6144,
"baseWork": 36579905,
"heuristicWork": 6167354,
"workReduction": null,
"candidates": 0,
"falseCandidates": 0,
"baseLimit": 4304,
"heuristicLimit": 4304,
"diagnosticIfForcedDirect": {
"workReduction": 0.8314004916087125,
"candidates": 4125,
"falseCandidates": 12
},
"ms": 2205.721136
},
{
"view": "exterior",
"productionBackend": "direct",
"pixels": 6144,
"baseWork": 686432,
"heuristicWork": 137063,
"workReduction": 0.8003254510279241,
"candidates": 72,
"falseCandidates": 0,
"baseLimit": 79,
"heuristicLimit": 7,
"diagnosticIfForcedDirect": null,
"ms": 38.21316800000022
}
],
"note": "Production DIRECT_F32 rule with two attracting-cycle confirmations. FAST/perturbation periodic early-stop is intentionally disabled."
},
"series": {
"rows": [
{
"span": 0.00001,
"jump": 0,
"errorLog2": 0,
"maxAbsError": 0,
"maxRelativeError": 0,
"refLen": 1200,
"buildMs": 9.239160999999513
},
{
"span": 1e-7,
"jump": 0,
"errorLog2": 0,
"maxAbsError": 0,
"maxRelativeError": 0,
"refLen": 1200,
"buildMs": 6.685975000000326
},
{
"span": 1e-9,
"jump": 0,
"errorLog2": 0,
"maxAbsError": 0,
"maxRelativeError": 0,
"refLen": 1200,
"buildMs": 7.990567000000738
},
{
"span": 1e-11,
"jump": 0,
"errorLog2": 0,
"maxAbsError": 0,
"maxRelativeError": 0,
"refLen": 1200,
"buildMs": 7.234717999999702
}
],
"note": "Production fast-reference worker output. Error compares packed 4th-order series against direct perturbation over 8 screen-domain deltas."
},
"sparseActiveState": {
"rows": [
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 1,
"capacity": 524288,
"oldBytes": 23068672,
"newBytes": 23134208,
"reduction": -0.0028409090909091717
},
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 0.25,
"capacity": 164096,
"oldBytes": 23068672,
"newBytes": 7285760,
"reduction": 0.6841708096590908
},
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 0.1,
"capacity": 65792,
"oldBytes": 23068672,
"newBytes": 2960384,
"reduction": 0.8716708096590909
},
{
"preset": "fast",
"pixels": 524288,
"activeRatio": 0.01,
"capacity": 6810,
"oldBytes": 23068672,
"newBytes": 365176,
"reduction": 0.9841700467196378
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 1,
"capacity": 1048576,
"oldBytes": 46137344,
"newBytes": 46268416,
"reduction": -0.0028409090909091717
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 0.25,
"capacity": 327936,
"oldBytes": 46137344,
"newBytes": 14560256,
"reduction": 0.6844149502840908
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 0.1,
"capacity": 131328,
"oldBytes": 46137344,
"newBytes": 5909504,
"reduction": 0.8719149502840909
},
{
"preset": "standard",
"pixels": 1048576,
"activeRatio": 0.01,
"capacity": 13364,
"oldBytes": 46137344,
"newBytes": 719088,
"reduction": 0.9844141873446378
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 1,
"capacity": 4194304,
"oldBytes": 184549376,
"newBytes": 185073664,
"reduction": -0.0028409090909091717
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 0.25,
"capacity": 1310976,
"oldBytes": 184549376,
"newBytes": 58207232,
"reduction": 0.6845980557528408
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 0.1,
"capacity": 524544,
"oldBytes": 184549376,
"newBytes": 23604224,
"reduction": 0.8720980557528409
},
{
"preset": "high",
"pixels": 4194304,
"activeRatio": 0.01,
"capacity": 52685,
"oldBytes": 184549376,
"newBytes": 2842428,
"reduction": 0.9845980080691251
}
],
"note": "Production allocation: 32 B state + 2x4 B queues + 4 B pixel map per active slot, plus 1-bit membership bitmap. Capacity adds 25% + 256 headroom."
},
"multiReference": {
"maxGuidedPasses": 3,
"w": 64,
"h": 40,
"maxIter": 1200,
"rows": [
{
"view": "seahorse-1e-5",
"grid": 1,
"references": 1,
"total": 2560,
"refEnd": 0,
"mismatchClass": 3,
"mismatchIter": 155,
"mismatchRate": 0.001171875
},
{
"view": "seahorse-1e-5",
"grid": 2,
"references": 5,
"total": 2560,
"refEnd": 0,
"mismatchClass": 3,
"mismatchIter": 155,
"mismatchRate": 0.001171875
},
{
"view": "seahorse-1e-5",
"grid": 3,
"references": 9,
"total": 2560,
"refEnd": 0,
"mismatchClass": 3,
"mismatchIter": 155,
"mismatchRate": 0.001171875
},
{
"view": "seahorse-1e-7",
"grid": 1,
"references": 1,
"total": 2560,
"refEnd": 0,
"mismatchClass": 66,
"mismatchIter": 74,
"mismatchRate": 0.02578125
},
{
"view": "seahorse-1e-7",
"grid": 2,
"references": 5,
"total": 2560,
"refEnd": 0,
"mismatchClass": 37,
"mismatchIter": 41,
"mismatchRate": 0.014453125
},
{
"view": "seahorse-1e-7",
"grid": 3,
"references": 9,
"total": 2560,
"refEnd": 0,
"mismatchClass": 50,
"mismatchIter": 67,
"mismatchRate": 0.01953125
}
],
"note": "CPU f32 perturbation locality surrogate. Production selects up to three additional references from failure clusters instead of fixed grids."
},
"cpuFallback": {
"w": 640,
"h": 360,
"iter": 350,
"threads": 4,
"one": {
"medianMs": 92.0384610000001,
"times": [
92.0384610000001,
92.44781700000021,
93.7905269999992,
88.7301699999989,
99.79122599999937,
82.51707699999861,
82.31785099999979
],
"work": 2658084,
"escaped": 176648,
"operationLimit": 3394,
"heuristicInterior": 1654
},
"multi": {
"medianMs": 35.13209400000051,
"times": [
90.21825200000058,
47.29191700000047,
35.13209400000051,
34.57788200000141,
32.72723099999894,
35.70634799999971,
32.441507999999885
],
"work": 2658084,
"escaped": 176648,
"operationLimit": 3394,
"heuristicInterior": 1654
},
"speedup": 2.6197829540134663,
"workMatch": true,
"escapedMatch": true,
"operationLimitMatch": true,
"heuristicMatch": true,
"note": "Production CPU fallback Worker source executed under node:worker_threads shim."
},
"sourceAudit": {
"heuristicClass": true,
"strictDisablesPeriodicity": true,
"seriesBinding": true,
"compactPixelMap": true,
"multiReferencePasses": true,
"cpuFallback": true
},
"environment": {
"node": "v22.16.0",
"cpus": 5,
"platform": "linux",
"arch": "x64"
}
}

View file

@ -0,0 +1,68 @@
{
"view": {
"bits": 273,
"re": "-15958919693715511752069626580195447581643016686947107473855747766272418724092005269",
"im": "3884297345146583343999140717464554120497735725078805729405733239132420430333375719",
"span": "35679807704506851074208413824041445358585910697267232113437882789120793",
"width": 1108,
"height": 1582,
"iter": 1536
},
"before": {
"sha256": "e69b91ead027927d977a065d27d9e27629110122df20aa470eab877e23f77ac7",
"targetBits": 96,
"refLen": 1285,
"selectionEscape": 1285,
"referencePixel": {
"x": 69.5,
"y": 922.4999999999999
},
"series": {
"jump": 773,
"coeffs": [
40452732,
196035184,
-3191889952015122400,
2277982484077478000,
-7.321407650201077e+27,
3.7598990810493897e+28,
-2.345861669341488e+38,
2.300172499347822e+38
],
"errorLog2": -27.69705218182358
},
"nonFiniteSeriesStartSamples": 290,
"seriesSampleCount": 1024
},
"after": {
"sha256": "c53c0dc4cb9fa2d2046aaa12ef03cc79c61a710b0c8db36dea1a87be2d9aedd8",
"targetBits": 96,
"refLen": 1285,
"selectionEscape": 1285,
"referencePixel": {
"x": 69.5,
"y": 922.4999999999999
},
"series": {
"jump": 0,
"coeffs": [
0,
0,
0,
0,
0,
0,
0,
0
],
"errorLog2": 0
},
"nonFiniteSeriesStartSamples": 0,
"seriesSampleCount": 1024
},
"checks": {
"baselineSeriesActive": true,
"baselineHasNonFiniteSeriesStarts": true,
"patchedSeriesDisabled": true
}
}