This commit is contained in:
33333-33333 2026-09-29 19:37:53 +09:00
commit f65054da0c
11 changed files with 2648 additions and 3 deletions

View file

@ -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 - `docs/removal-pass/integrated-bench.json` — post-removal CPU/surrogate benchmark
Historical investigation records remain under `docs/performance-cost-audit/` and `docs/integrated-improvements/`. 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`.

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

View file

@ -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(/<script[^>]*>([\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;jy<rows;jy++)for(let ix=0;ix<cols;ix++){
const gx=(ix+.5)*width/cols,gy=(jy+.5)*height/rows,dx=Math.fround((gx-refX)/width),dy=Math.fround((refY-gy)/width),d=[Math.fround(spanMant*dx),Math.fround(spanMant*dy)];
const d2=complexMul(d[0],d[1],d[0],d[1]),d4=complexMul(d2[0],d2[1],d2[0],d2[1]);
const t4=complexMul(c[6],c[7],d4[0],d4[1]);
if(!Number.isFinite(t4[0])||!Number.isFinite(t4[1]))bad++;
}
return bad;
}
async function analyze(path){
const html=await fs.readFile(path,'utf8'),a=appInternal(html);const snap={bits:view.bits,re:BigInt(view.re),im:BigInt(view.im),span:BigInt(view.span)};const targetBits=a.pixelPrecisionBits(snap,view.width,40);
const result=runWorker(a.fastReferenceWorkerSource(),{type:'build',id:1,key:'reported-view',sourceBits:view.bits,targetBits,re:view.re,im:view.im,span:view.span,width:view.width,height:view.height,iter:view.iter});
const refRe=BigInt(result.referenceRe),refIm=BigInt(result.referenceIm),span=BigInt(view.span),refX=view.width*.5+fixedRatio(refRe-BigInt(view.re),span)*view.width,refY=view.height*.5-fixedRatio(refIm-BigInt(view.im),span)*view.width,se=spanMantExp(span,view.bits);
return {sha256:createHash('sha256').update(html).digest('hex'),targetBits,refLen:result.refLen,selectionEscape:result.selectionEscape,referencePixel:{x:refX,y:refY},series:result.series,nonFiniteSeriesStartSamples:nonFiniteSeriesStarts(result.series,refX,refY,se.mant,view.width,view.height),seriesSampleCount:32*32};
}
const before=await analyze(beforeUrl),after=await analyze(afterUrl);const output={view,before,after,checks:{baselineSeriesActive:before.series.jump>0,baselineHasNonFiniteSeriesStarts:before.nonFiniteSeriesStartSamples>0,patchedSeriesDisabled:after.series.jump===0&&after.nonFiniteSeriesStartSamples===0}};
console.log(JSON.stringify(output,null,2));

View file

@ -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 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 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} 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){ 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)); 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); 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++){ for(let n=0;n<limit;n++){
@ -1776,8 +1781,8 @@ async function renderFinalFrame(r,snap,iter,token){
if(proof.pixels||historySeed){certified=await r.certifyInterior(snap,token,true,proof,historySeed);if(token!==state.token)return null;preserveInterior=certified>0||!!historySeed} 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}} 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)}} 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} 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(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} 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 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} 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

@ -43,6 +43,8 @@ assert.match(kernels.DIRECT_F32_WGSL,/candPeriod/);
assert.match(kernels.DIRECT_F32_WGSL,/FIELD_INTERIOR_HEURISTIC/); assert.match(kernels.DIRECT_F32_WGSL,/FIELD_INTERIOR_HEURISTIC/);
assert.doesNotMatch(kernels.FAST_PERTURB_WGSL,/pack_meta\(n,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(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.doesNotMatch(kernels.FAST_PERTURB_WGSL,/unknownOnly==2u/);
assert.match(prodHtml,/knownInteriorViewportMayOverlap/); assert.match(prodHtml,/knownInteriorViewportMayOverlap/);
assert.match(prodHtml,/numericHistoryScaleEligible/); assert.match(prodHtml,/numericHistoryScaleEligible/);