diff --git a/README.md b/README.md index 8c9bc18..98d9915 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,7 @@ The optional real-browser WebGPU test is `tests/browser_smoke.mjs`; it requires - `docs/removal-pass/integrated-bench.json` — post-removal CPU/surrogate benchmark Historical investigation records remain under `docs/performance-cost-audit/` and `docs/integrated-improvements/`. + +## Reported deep-view artifact fix (2026-09-29) + +A reported deep URL exposed an unsafe 4th-order series jump and premature publication of FAST numerical failures. Production series acceleration is disabled pending a propagated error-bound redesign, and FAST provisional publication now occurs only after numerical failure repair. See `docs/reported-view-bugfix/IMPLEMENTATION.md`. diff --git a/docs/reported-view-bugfix/IMPLEMENTATION.md b/docs/reported-view-bugfix/IMPLEMENTATION.md new file mode 100644 index 0000000..3a64d9a --- /dev/null +++ b/docs/reported-view-bugfix/IMPLEMENTATION.md @@ -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. diff --git a/docs/reported-view-bugfix/README.md b/docs/reported-view-bugfix/README.md new file mode 100644 index 0000000..cb07f1e --- /dev/null +++ b/docs/reported-view-bugfix/README.md @@ -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. diff --git a/docs/reported-view-bugfix/build-summary.json b/docs/reported-view-bugfix/build-summary.json new file mode 100644 index 0000000..d8bed96 --- /dev/null +++ b/docs/reported-view-bugfix/build-summary.json @@ -0,0 +1,9 @@ +{ + "beforeBytes": 224559, + "afterBytes": 224955, + "deltaBytes": 396, + "beforeGzip": 57744, + "afterGzip": 57958, + "beforeSha256": "e69b91ead027927d977a065d27d9e27629110122df20aa470eab877e23f77ac7", + "afterSha256": "c53c0dc4cb9fa2d2046aaa12ef03cc79c61a710b0c8db36dea1a87be2d9aedd8" +} \ No newline at end of file diff --git a/docs/reported-view-bugfix/index.before.html b/docs/reported-view-bugfix/index.before.html new file mode 100644 index 0000000..9fdef0a --- /dev/null +++ b/docs/reported-view-bugfix/index.before.html @@ -0,0 +1,1970 @@ + + + + + + + +Mandelbrot Deep Zoom v24.6.0 + + + + +
MANDELBROT DEEP ZOOM333の部屋に戻る
+
+
+
+
+
+
0.0080
+
.18
+
+
+
起動中…
+
中心 …
+
倍率 … · 幅 … · 起動中…
+
+
+
+
+ +
+ +

高解像度 PNG 出力

+
+ + + + +
+ +
+
+
+ + + + diff --git a/docs/reported-view-bugfix/index.diff b/docs/reported-view-bugfix/index.diff new file mode 100644 index 0000000..9711c14 --- /dev/null +++ b/docs/reported-view-bugfix/index.diff @@ -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;n0||!!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} + } diff --git a/docs/reported-view-bugfix/integrated-bench.json b/docs/reported-view-bugfix/integrated-bench.json new file mode 100644 index 0000000..180b6e7 --- /dev/null +++ b/docs/reported-view-bugfix/integrated-bench.json @@ -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" + } +} diff --git a/docs/reported-view-bugfix/results.json b/docs/reported-view-bugfix/results.json new file mode 100644 index 0000000..8e1e580 --- /dev/null +++ b/docs/reported-view-bugfix/results.json @@ -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 + } +} diff --git a/experiments/reported-view/analyze.mjs b/experiments/reported-view/analyze.mjs new file mode 100644 index 0000000..acf1859 --- /dev/null +++ b/experiments/reported-view/analyze.mjs @@ -0,0 +1,47 @@ +import fs from 'node:fs/promises'; +import vm from 'node:vm'; +import {createHash} from 'node:crypto'; + +const beforeUrl=new URL('../../docs/reported-view-bugfix/index.before.html',import.meta.url); +const afterUrl=new URL('../../index.html',import.meta.url); +const view={ + bits:273, + re:'-15958919693715511752069626580195447581643016686947107473855747766272418724092005269', + im:'3884297345146583343999140717464554120497735725078805729405733239132420430333375719', + span:'35679807704506851074208413824041445358585910697267232113437882789120793', + width:1108,height:1582,iter:1536, +}; + +function extract(html){return [...html.matchAll(/]*>([\s\S]*?)<\/script>/g)].map(m=>m[1])} +function appInternal(html){ + const scripts=extract(html);const kc={};vm.runInNewContext(scripts[0],kc);const kernels=kc.MANDEL_WEBGPU_KERNELS; + const element={width:view.width,height:view.height,clientWidth:view.width,clientHeight:view.height,style:{},hidden:false,value:'',textContent:'',disabled:false,addEventListener(){},classList:{toggle(){}},setAttribute(){},removeAttribute(){},showModal(){},close(){},toBlob(){},getBoundingClientRect(){return{left:0,top:0,width:view.width,height:view.height}}}; + const context={MANDEL_WEBGPU_KERNELS:kernels,document:{querySelector:()=>element,getElementById:()=>element,documentElement:{classList:{toggle(){}}},body:{classList:{toggle(){}},appendChild(){}}},navigator:{hardwareConcurrency:4,deviceMemory:8},location:{search:'',hash:''},history:{replaceState(){}},performance,URLSearchParams,URL:{createObjectURL:()=>`blob:test`,revokeObjectURL(){}},TextEncoder,Blob:class{},console,addEventListener(){},requestAnimationFrame:()=>1,cancelAnimationFrame(){},setTimeout,clearTimeout,setInterval,clearInterval,innerWidth:view.width,innerHeight:view.height,devicePixelRatio:1,localStorage:{getItem(){return null},setItem(){},removeItem(){}}}; + const cut=scripts[1].indexOf('// ── boot / teardown'); + const code=scripts[1].slice(0,cut)+`\nglobalThis.internal={fastReferenceWorkerSource,pixelPrecisionBits};})();`; + vm.runInNewContext(code,context);return context.internal; +} +function runWorker(source,message){let result;const wc={self:{},postMessage:r=>{result=r},performance};vm.runInNewContext(source,wc);wc.self.onmessage({data:message});if(!result)throw new Error('worker returned nothing');if(result.type==='error')throw new Error(result.error);return result} +function bitLen(v){v=v<0n?-v:v;return v===0n?0:v.toString(2).length} +function fixedLog2Abs(v,bits){v=v<0n?-v:v;if(v===0n)return-Infinity;const bl=bitLen(v),take=Math.min(53,bl),sh=bl-take,top=Number(v>>BigInt(sh));return Math.log2(top)+sh-bits} +function fixedRatio(a,b){if(!b||!a)return 0;let neg=a<0n;if(neg)a=-a;const q=(a<<52n)/b,v=Number(q)/4503599627370496;return neg?-v:v} +function spanMantExp(span,bits){const l=fixedLog2Abs(span,bits),exp=Math.floor(l);return{mant:Math.pow(2,l-exp),exp}} +function complexMul(ar,ai,br,bi){return [Math.fround(Math.fround(ar*br)-Math.fround(ai*bi)),Math.fround(Math.fround(ar*bi)+Math.fround(ai*br))]} +function nonFiniteSeriesStarts(series,refX,refY,spanMant,width,height,cols=32,rows=32){ + if(!series?.jump)return 0;let bad=0;const c=series.coeffs.map(Math.fround); + for(let jy=0;jy0,baselineHasNonFiniteSeriesStarts:before.nonFiniteSeriesStartSamples>0,patchedSeriesDisabled:after.series.jump===0&&after.nonFiniteSeriesStartSamples===0}}; +console.log(JSON.stringify(output,null,2)); diff --git a/index.html b/index.html index 9fdef0a..6ece015 100644 --- a/index.html +++ b/index.html @@ -973,8 +973,13 @@ function cadd(a,b){return[a[0]+b[0],a[1]+b[1]]} 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;n0||!!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} } diff --git a/tests/regression.mjs b/tests/regression.mjs index 78d3e5c..a7209d0 100644 --- a/tests/regression.mjs +++ b/tests/regression.mjs @@ -43,6 +43,8 @@ assert.match(kernels.DIRECT_F32_WGSL,/candPeriod/); assert.match(kernels.DIRECT_F32_WGSL,/FIELD_INTERIOR_HEURISTIC/); assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/pack_meta\(n,FIELD_INTERIOR_HEURISTIC\)/); assert.match(kernels.FAST_PERTURB_WGSL,/series_start/); +assert.match(prodHtml,/SERIES_APPROXIMATION_ENABLED=false/); +assert.match(prodHtml,/deferPrimaryColor=deferColor\|\|fastExtended/); assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/unknownOnly==2u/); assert.match(prodHtml,/knownInteriorViewportMayOverlap/); assert.match(prodHtml,/numericHistoryScaleEligible/);